browser/devtools/profiler/controller.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/profiler/controller.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,411 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +var isJSM = typeof require !== "function";
    1.11 +
    1.12 +// This code is needed because, for whatever reason, mochitest can't
    1.13 +// find any requirejs module so we have to load it old school way. :(
    1.14 +
    1.15 +if (isJSM) {
    1.16 +  var Cu = this["Components"].utils;
    1.17 +  let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils;
    1.18 +  this["loader"] = { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils) };
    1.19 +  this["require"] = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    1.20 +} else {
    1.21 +  var { Cu } = require("chrome");
    1.22 +}
    1.23 +
    1.24 +const { L10N_BUNDLE } = require("devtools/profiler/consts");
    1.25 +
    1.26 +var EventEmitter = require("devtools/toolkit/event-emitter");
    1.27 +
    1.28 +Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
    1.29 +Cu.import("resource://gre/modules/devtools/Console.jsm");
    1.30 +Cu.import("resource://gre/modules/AddonManager.jsm");
    1.31 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    1.32 +
    1.33 +loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
    1.34 +
    1.35 +loader.lazyGetter(this, "gDevTools",
    1.36 +  () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
    1.37 +
    1.38 +loader.lazyGetter(this, "DebuggerServer",
    1.39 +  () => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer);
    1.40 +
    1.41 +/**
    1.42 + * Data structure that contains information that has
    1.43 + * to be shared between separate ProfilerController
    1.44 + * instances.
    1.45 + */
    1.46 +const sharedData = {
    1.47 +  data: new WeakMap(),
    1.48 +  controllers: new WeakMap(),
    1.49 +};
    1.50 +
    1.51 +/**
    1.52 + * Makes a structure representing an individual profile.
    1.53 + */
    1.54 +function makeProfile(name, def={}) {
    1.55 +  if (def.timeStarted == null)
    1.56 +    def.timeStarted = null;
    1.57 +
    1.58 +  if (def.timeEnded == null)
    1.59 +    def.timeEnded = null;
    1.60 +
    1.61 +  return {
    1.62 +    name: name,
    1.63 +    timeStarted: def.timeStarted,
    1.64 +    timeEnded: def.timeEnded,
    1.65 +    fromConsole: def.fromConsole || false
    1.66 +  };
    1.67 +}
    1.68 +
    1.69 +// Three functions below all operate with sharedData
    1.70 +// structure defined above. They should be self-explanatory.
    1.71 +
    1.72 +function addTarget(target) {
    1.73 +  sharedData.data.set(target, new Map());
    1.74 +}
    1.75 +
    1.76 +function getProfiles(target) {
    1.77 +  return sharedData.data.get(target);
    1.78 +}
    1.79 +
    1.80 +/**
    1.81 + * Object to control the JavaScript Profiler over the remote
    1.82 + * debugging protocol.
    1.83 + *
    1.84 + * @param Target target
    1.85 + *        A target object as defined in Target.jsm
    1.86 + */
    1.87 +function ProfilerController(target) {
    1.88 +  if (sharedData.controllers.has(target)) {
    1.89 +    return sharedData.controllers.get(target);
    1.90 +  }
    1.91 +
    1.92 +  this.target = target;
    1.93 +  this.client = target.client;
    1.94 +  this.isConnected = false;
    1.95 +  this.consoleProfiles = [];
    1.96 +  this.reservedNames = {};
    1.97 +
    1.98 +  addTarget(target);
    1.99 +
   1.100 +  // Chrome debugging targets have already obtained a reference
   1.101 +  // to the profiler actor.
   1.102 +  if (target.chrome) {
   1.103 +    this.isConnected = true;
   1.104 +    this.actor = target.form.profilerActor;
   1.105 +  }
   1.106 +
   1.107 +  sharedData.controllers.set(target, this);
   1.108 +  EventEmitter.decorate(this);
   1.109 +};
   1.110 +
   1.111 +ProfilerController.prototype = {
   1.112 +  target:          null,
   1.113 +  client:          null,
   1.114 +  isConnected:     null,
   1.115 +  consoleProfiles: null,
   1.116 +  reservedNames:   null,
   1.117 +
   1.118 +  /**
   1.119 +   * Return a map of profile results for the current target.
   1.120 +   *
   1.121 +   * @return Map
   1.122 +   */
   1.123 +  get profiles() {
   1.124 +    return getProfiles(this.target);
   1.125 +  },
   1.126 +
   1.127 +  /**
   1.128 +   * Checks whether the profile is currently recording.
   1.129 +   *
   1.130 +   * @param object profile
   1.131 +   *        An object made by calling makeProfile function.
   1.132 +   * @return boolean
   1.133 +   */
   1.134 +  isProfileRecording: function PC_isProfileRecording(profile) {
   1.135 +    return profile.timeStarted !== null && profile.timeEnded === null;
   1.136 +  },
   1.137 +
   1.138 +  getProfileName: function PC_getProfileName() {
   1.139 +    let num = 1;
   1.140 +    let name = L10N.getFormatStr("profiler.profileName", [num]);
   1.141 +
   1.142 +    while (this.reservedNames[name]) {
   1.143 +      num += 1;
   1.144 +      name = L10N.getFormatStr("profiler.profileName", [num]);
   1.145 +    }
   1.146 +
   1.147 +    this.reservedNames[name] = true;
   1.148 +    return name;
   1.149 +  },
   1.150 +
   1.151 +  /**
   1.152 +   * A listener that fires whenever console.profile or console.profileEnd
   1.153 +   * is called.
   1.154 +   *
   1.155 +   * @param string type
   1.156 +   *        Type of a call. Either 'profile' or 'profileEnd'.
   1.157 +   * @param object data
   1.158 +   *        Event data.
   1.159 +   */
   1.160 +  onConsoleEvent: function (type, data) {
   1.161 +    let name = data.extra.name;
   1.162 +
   1.163 +    let profileStart = () => {
   1.164 +      if (name && this.profiles.has(name))
   1.165 +        return;
   1.166 +
   1.167 +      // Add profile structure to shared data.
   1.168 +      let profile = makeProfile(name || this.getProfileName(), {
   1.169 +        timeStarted: data.extra.currentTime,
   1.170 +        fromConsole: true
   1.171 +      });
   1.172 +
   1.173 +      this.profiles.set(profile.name, profile);
   1.174 +      this.consoleProfiles.push(profile.name);
   1.175 +      this.emit("profileStart", profile);
   1.176 +    };
   1.177 +
   1.178 +    let profileEnd = () => {
   1.179 +      if (!name && !this.consoleProfiles.length)
   1.180 +        return;
   1.181 +
   1.182 +      if (!name)
   1.183 +        name = this.consoleProfiles.pop();
   1.184 +      else
   1.185 +        this.consoleProfiles.filter((n) => n !== name);
   1.186 +
   1.187 +      if (!this.profiles.has(name))
   1.188 +        return;
   1.189 +
   1.190 +      let profile = this.profiles.get(name);
   1.191 +      if (!this.isProfileRecording(profile))
   1.192 +        return;
   1.193 +
   1.194 +      let profileData = data.extra.profile;
   1.195 +      profileData.threads = profileData.threads.map((thread) => {
   1.196 +        let samples = thread.samples.filter((sample) => {
   1.197 +          return sample.time >= profile.timeStarted;
   1.198 +        });
   1.199 +
   1.200 +        return { samples: samples };
   1.201 +      });
   1.202 +
   1.203 +      profile.timeEnded = data.extra.currentTime;
   1.204 +      profile.data = profileData;
   1.205 +
   1.206 +      this.emit("profileEnd", profile);
   1.207 +    };
   1.208 +
   1.209 +    if (type === "profile")
   1.210 +      profileStart();
   1.211 +
   1.212 +    if (type === "profileEnd")
   1.213 +      profileEnd();
   1.214 +  },
   1.215 +
   1.216 +  /**
   1.217 +   * Connects to the client unless we're already connected.
   1.218 +   *
   1.219 +   * @param function cb
   1.220 +   *        Function to be called once we're connected. If
   1.221 +   *        the controller is already connected, this function
   1.222 +   *        will be called immediately (synchronously).
   1.223 +   */
   1.224 +  connect: function (cb=function(){}) {
   1.225 +    if (this.isConnected) {
   1.226 +      return void cb();
   1.227 +    }
   1.228 +
   1.229 +    // Check if we already have a grip to the listTabs response object
   1.230 +    // and, if we do, use it to get to the profilerActor. Otherwise,
   1.231 +    // call listTabs. The problem is that if we call listTabs twice
   1.232 +    // webconsole tests fail (see bug 872826).
   1.233 +
   1.234 +    let register = () => {
   1.235 +      let data = { events: ["console-api-profiler"] };
   1.236 +
   1.237 +      // Check if Gecko Profiler Addon [1] is installed and, if it is,
   1.238 +      // don't register our own console event listeners. Gecko Profiler
   1.239 +      // Addon takes care of console.profile and console.profileEnd methods
   1.240 +      // and we don't want to break it.
   1.241 +      //
   1.242 +      // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/
   1.243 +
   1.244 +      AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => {
   1.245 +        if (addon && !addon.userDisabled && !addon.softDisabled)
   1.246 +          return void cb();
   1.247 +
   1.248 +        this.request("registerEventNotifications", data, (resp) => {
   1.249 +          this.client.addListener("eventNotification", (type, resp) => {
   1.250 +            let toolbox = gDevTools.getToolbox(this.target);
   1.251 +            if (toolbox == null)
   1.252 +              return;
   1.253 +
   1.254 +            this.onConsoleEvent(resp.subject.action, resp.data);
   1.255 +          });
   1.256 +        });
   1.257 +
   1.258 +        cb();
   1.259 +      });
   1.260 +    };
   1.261 +
   1.262 +    if (this.target.root) {
   1.263 +      this.actor = this.target.root.profilerActor;
   1.264 +      this.isConnected = true;
   1.265 +      return void register();
   1.266 +    }
   1.267 +
   1.268 +    this.client.listTabs((resp) => {
   1.269 +      this.actor = resp.profilerActor;
   1.270 +      this.isConnected = true;
   1.271 +      register();
   1.272 +    });
   1.273 +  },
   1.274 +
   1.275 +  /**
   1.276 +   * Adds actor and type information to data and sends the request over
   1.277 +   * the remote debugging protocol.
   1.278 +   *
   1.279 +   * @param string type
   1.280 +   *        Method to call on the other side
   1.281 +   * @param object data
   1.282 +   *        Data to send with the request
   1.283 +   * @param function cb
   1.284 +   *        A callback function
   1.285 +   */
   1.286 +  request: function (type, data, cb) {
   1.287 +    data.to = this.actor;
   1.288 +    data.type = type;
   1.289 +    this.client.request(data, cb);
   1.290 +  },
   1.291 +
   1.292 +  /**
   1.293 +   * Checks whether the profiler is active.
   1.294 +   *
   1.295 +   * @param function cb
   1.296 +   *        Function to be called with a response from the
   1.297 +   *        client. It will be called with two arguments:
   1.298 +   *        an error object (may be null) and a boolean
   1.299 +   *        value indicating if the profiler is active or not.
   1.300 +   */
   1.301 +  isActive: function (cb) {
   1.302 +    this.request("isActive", {}, (resp) => {
   1.303 +      cb(resp.error, resp.isActive, resp.currentTime);
   1.304 +    });
   1.305 +  },
   1.306 +
   1.307 +  /**
   1.308 +   * Creates a new profile and starts the profiler, if needed.
   1.309 +   *
   1.310 +   * @param string name
   1.311 +   *        Name of the profile.
   1.312 +   * @param function cb
   1.313 +   *        Function to be called once the profiler is started
   1.314 +   *        or we get an error. It will be called with a single
   1.315 +   *        argument: an error object (may be null).
   1.316 +   */
   1.317 +  start: function PC_start(name, cb) {
   1.318 +    if (this.profiles.has(name)) {
   1.319 +      return;
   1.320 +    }
   1.321 +
   1.322 +    let profile = makeProfile(name);
   1.323 +    this.consoleProfiles.push(name);
   1.324 +    this.profiles.set(name, profile);
   1.325 +
   1.326 +    // If profile is already running, no need to do anything.
   1.327 +    if (this.isProfileRecording(profile)) {
   1.328 +      return void cb();
   1.329 +    }
   1.330 +
   1.331 +    this.isActive((err, isActive, currentTime) => {
   1.332 +      if (isActive) {
   1.333 +        profile.timeStarted = currentTime;
   1.334 +        return void cb();
   1.335 +      }
   1.336 +
   1.337 +      let params = {
   1.338 +        entries: 1000000,
   1.339 +        interval: 1,
   1.340 +        features: ["js"],
   1.341 +      };
   1.342 +
   1.343 +      this.request("startProfiler", params, (resp) => {
   1.344 +        if (resp.error) {
   1.345 +          return void cb(resp.error);
   1.346 +        }
   1.347 +
   1.348 +        profile.timeStarted = 0;
   1.349 +        cb();
   1.350 +      });
   1.351 +    });
   1.352 +  },
   1.353 +
   1.354 +  /**
   1.355 +   * Stops the profiler. NOTE, that we don't stop the actual
   1.356 +   * SPS Profiler here. It will be stopped as soon as all
   1.357 +   * clients disconnect from the profiler actor.
   1.358 +   *
   1.359 +   * @param string name
   1.360 +   *        Name of the profile that needs to be stopped.
   1.361 +   * @param function cb
   1.362 +   *        Function to be called once the profiler is stopped
   1.363 +   *        or we get an error. It will be called with a single
   1.364 +   *        argument: an error object (may be null).
   1.365 +   */
   1.366 +  stop: function PC_stop(name, cb) {
   1.367 +    if (!this.profiles.has(name)) {
   1.368 +      return;
   1.369 +    }
   1.370 +
   1.371 +    let profile = this.profiles.get(name);
   1.372 +    if (!this.isProfileRecording(profile)) {
   1.373 +      return;
   1.374 +    }
   1.375 +
   1.376 +    this.request("getProfile", {}, (resp) => {
   1.377 +      if (resp.error) {
   1.378 +        Cu.reportError("Failed to fetch profile data.");
   1.379 +        return void cb(resp.error, null);
   1.380 +      }
   1.381 +
   1.382 +      let data = resp.profile;
   1.383 +      profile.timeEnded = resp.currentTime;
   1.384 +
   1.385 +      // Filter out all samples that fall out of current
   1.386 +      // profile's range.
   1.387 +
   1.388 +      data.threads = data.threads.map((thread) => {
   1.389 +        let samples = thread.samples.filter((sample) => {
   1.390 +          return sample.time >= profile.timeStarted;
   1.391 +        });
   1.392 +
   1.393 +        return { samples: samples };
   1.394 +      });
   1.395 +
   1.396 +      cb(null, data);
   1.397 +    });
   1.398 +  },
   1.399 +
   1.400 +  /**
   1.401 +   * Cleanup.
   1.402 +   */
   1.403 +  destroy: function PC_destroy() {
   1.404 +    this.client = null;
   1.405 +    this.target = null;
   1.406 +    this.actor = null;
   1.407 +  }
   1.408 +};
   1.409 +
   1.410 +if (isJSM) {
   1.411 +  var EXPORTED_SYMBOLS = ["ProfilerController"];
   1.412 +} else {
   1.413 +  module.exports = ProfilerController;
   1.414 +}
   1.415 \ No newline at end of file

mercurial