browser/devtools/profiler/controller.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 var isJSM = typeof require !== "function";
     9 // This code is needed because, for whatever reason, mochitest can't
    10 // find any requirejs module so we have to load it old school way. :(
    12 if (isJSM) {
    13   var Cu = this["Components"].utils;
    14   let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils;
    15   this["loader"] = { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils) };
    16   this["require"] = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    17 } else {
    18   var { Cu } = require("chrome");
    19 }
    21 const { L10N_BUNDLE } = require("devtools/profiler/consts");
    23 var EventEmitter = require("devtools/toolkit/event-emitter");
    25 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
    26 Cu.import("resource://gre/modules/devtools/Console.jsm");
    27 Cu.import("resource://gre/modules/AddonManager.jsm");
    28 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    30 loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
    32 loader.lazyGetter(this, "gDevTools",
    33   () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
    35 loader.lazyGetter(this, "DebuggerServer",
    36   () => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer);
    38 /**
    39  * Data structure that contains information that has
    40  * to be shared between separate ProfilerController
    41  * instances.
    42  */
    43 const sharedData = {
    44   data: new WeakMap(),
    45   controllers: new WeakMap(),
    46 };
    48 /**
    49  * Makes a structure representing an individual profile.
    50  */
    51 function makeProfile(name, def={}) {
    52   if (def.timeStarted == null)
    53     def.timeStarted = null;
    55   if (def.timeEnded == null)
    56     def.timeEnded = null;
    58   return {
    59     name: name,
    60     timeStarted: def.timeStarted,
    61     timeEnded: def.timeEnded,
    62     fromConsole: def.fromConsole || false
    63   };
    64 }
    66 // Three functions below all operate with sharedData
    67 // structure defined above. They should be self-explanatory.
    69 function addTarget(target) {
    70   sharedData.data.set(target, new Map());
    71 }
    73 function getProfiles(target) {
    74   return sharedData.data.get(target);
    75 }
    77 /**
    78  * Object to control the JavaScript Profiler over the remote
    79  * debugging protocol.
    80  *
    81  * @param Target target
    82  *        A target object as defined in Target.jsm
    83  */
    84 function ProfilerController(target) {
    85   if (sharedData.controllers.has(target)) {
    86     return sharedData.controllers.get(target);
    87   }
    89   this.target = target;
    90   this.client = target.client;
    91   this.isConnected = false;
    92   this.consoleProfiles = [];
    93   this.reservedNames = {};
    95   addTarget(target);
    97   // Chrome debugging targets have already obtained a reference
    98   // to the profiler actor.
    99   if (target.chrome) {
   100     this.isConnected = true;
   101     this.actor = target.form.profilerActor;
   102   }
   104   sharedData.controllers.set(target, this);
   105   EventEmitter.decorate(this);
   106 };
   108 ProfilerController.prototype = {
   109   target:          null,
   110   client:          null,
   111   isConnected:     null,
   112   consoleProfiles: null,
   113   reservedNames:   null,
   115   /**
   116    * Return a map of profile results for the current target.
   117    *
   118    * @return Map
   119    */
   120   get profiles() {
   121     return getProfiles(this.target);
   122   },
   124   /**
   125    * Checks whether the profile is currently recording.
   126    *
   127    * @param object profile
   128    *        An object made by calling makeProfile function.
   129    * @return boolean
   130    */
   131   isProfileRecording: function PC_isProfileRecording(profile) {
   132     return profile.timeStarted !== null && profile.timeEnded === null;
   133   },
   135   getProfileName: function PC_getProfileName() {
   136     let num = 1;
   137     let name = L10N.getFormatStr("profiler.profileName", [num]);
   139     while (this.reservedNames[name]) {
   140       num += 1;
   141       name = L10N.getFormatStr("profiler.profileName", [num]);
   142     }
   144     this.reservedNames[name] = true;
   145     return name;
   146   },
   148   /**
   149    * A listener that fires whenever console.profile or console.profileEnd
   150    * is called.
   151    *
   152    * @param string type
   153    *        Type of a call. Either 'profile' or 'profileEnd'.
   154    * @param object data
   155    *        Event data.
   156    */
   157   onConsoleEvent: function (type, data) {
   158     let name = data.extra.name;
   160     let profileStart = () => {
   161       if (name && this.profiles.has(name))
   162         return;
   164       // Add profile structure to shared data.
   165       let profile = makeProfile(name || this.getProfileName(), {
   166         timeStarted: data.extra.currentTime,
   167         fromConsole: true
   168       });
   170       this.profiles.set(profile.name, profile);
   171       this.consoleProfiles.push(profile.name);
   172       this.emit("profileStart", profile);
   173     };
   175     let profileEnd = () => {
   176       if (!name && !this.consoleProfiles.length)
   177         return;
   179       if (!name)
   180         name = this.consoleProfiles.pop();
   181       else
   182         this.consoleProfiles.filter((n) => n !== name);
   184       if (!this.profiles.has(name))
   185         return;
   187       let profile = this.profiles.get(name);
   188       if (!this.isProfileRecording(profile))
   189         return;
   191       let profileData = data.extra.profile;
   192       profileData.threads = profileData.threads.map((thread) => {
   193         let samples = thread.samples.filter((sample) => {
   194           return sample.time >= profile.timeStarted;
   195         });
   197         return { samples: samples };
   198       });
   200       profile.timeEnded = data.extra.currentTime;
   201       profile.data = profileData;
   203       this.emit("profileEnd", profile);
   204     };
   206     if (type === "profile")
   207       profileStart();
   209     if (type === "profileEnd")
   210       profileEnd();
   211   },
   213   /**
   214    * Connects to the client unless we're already connected.
   215    *
   216    * @param function cb
   217    *        Function to be called once we're connected. If
   218    *        the controller is already connected, this function
   219    *        will be called immediately (synchronously).
   220    */
   221   connect: function (cb=function(){}) {
   222     if (this.isConnected) {
   223       return void cb();
   224     }
   226     // Check if we already have a grip to the listTabs response object
   227     // and, if we do, use it to get to the profilerActor. Otherwise,
   228     // call listTabs. The problem is that if we call listTabs twice
   229     // webconsole tests fail (see bug 872826).
   231     let register = () => {
   232       let data = { events: ["console-api-profiler"] };
   234       // Check if Gecko Profiler Addon [1] is installed and, if it is,
   235       // don't register our own console event listeners. Gecko Profiler
   236       // Addon takes care of console.profile and console.profileEnd methods
   237       // and we don't want to break it.
   238       //
   239       // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/
   241       AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => {
   242         if (addon && !addon.userDisabled && !addon.softDisabled)
   243           return void cb();
   245         this.request("registerEventNotifications", data, (resp) => {
   246           this.client.addListener("eventNotification", (type, resp) => {
   247             let toolbox = gDevTools.getToolbox(this.target);
   248             if (toolbox == null)
   249               return;
   251             this.onConsoleEvent(resp.subject.action, resp.data);
   252           });
   253         });
   255         cb();
   256       });
   257     };
   259     if (this.target.root) {
   260       this.actor = this.target.root.profilerActor;
   261       this.isConnected = true;
   262       return void register();
   263     }
   265     this.client.listTabs((resp) => {
   266       this.actor = resp.profilerActor;
   267       this.isConnected = true;
   268       register();
   269     });
   270   },
   272   /**
   273    * Adds actor and type information to data and sends the request over
   274    * the remote debugging protocol.
   275    *
   276    * @param string type
   277    *        Method to call on the other side
   278    * @param object data
   279    *        Data to send with the request
   280    * @param function cb
   281    *        A callback function
   282    */
   283   request: function (type, data, cb) {
   284     data.to = this.actor;
   285     data.type = type;
   286     this.client.request(data, cb);
   287   },
   289   /**
   290    * Checks whether the profiler is active.
   291    *
   292    * @param function cb
   293    *        Function to be called with a response from the
   294    *        client. It will be called with two arguments:
   295    *        an error object (may be null) and a boolean
   296    *        value indicating if the profiler is active or not.
   297    */
   298   isActive: function (cb) {
   299     this.request("isActive", {}, (resp) => {
   300       cb(resp.error, resp.isActive, resp.currentTime);
   301     });
   302   },
   304   /**
   305    * Creates a new profile and starts the profiler, if needed.
   306    *
   307    * @param string name
   308    *        Name of the profile.
   309    * @param function cb
   310    *        Function to be called once the profiler is started
   311    *        or we get an error. It will be called with a single
   312    *        argument: an error object (may be null).
   313    */
   314   start: function PC_start(name, cb) {
   315     if (this.profiles.has(name)) {
   316       return;
   317     }
   319     let profile = makeProfile(name);
   320     this.consoleProfiles.push(name);
   321     this.profiles.set(name, profile);
   323     // If profile is already running, no need to do anything.
   324     if (this.isProfileRecording(profile)) {
   325       return void cb();
   326     }
   328     this.isActive((err, isActive, currentTime) => {
   329       if (isActive) {
   330         profile.timeStarted = currentTime;
   331         return void cb();
   332       }
   334       let params = {
   335         entries: 1000000,
   336         interval: 1,
   337         features: ["js"],
   338       };
   340       this.request("startProfiler", params, (resp) => {
   341         if (resp.error) {
   342           return void cb(resp.error);
   343         }
   345         profile.timeStarted = 0;
   346         cb();
   347       });
   348     });
   349   },
   351   /**
   352    * Stops the profiler. NOTE, that we don't stop the actual
   353    * SPS Profiler here. It will be stopped as soon as all
   354    * clients disconnect from the profiler actor.
   355    *
   356    * @param string name
   357    *        Name of the profile that needs to be stopped.
   358    * @param function cb
   359    *        Function to be called once the profiler is stopped
   360    *        or we get an error. It will be called with a single
   361    *        argument: an error object (may be null).
   362    */
   363   stop: function PC_stop(name, cb) {
   364     if (!this.profiles.has(name)) {
   365       return;
   366     }
   368     let profile = this.profiles.get(name);
   369     if (!this.isProfileRecording(profile)) {
   370       return;
   371     }
   373     this.request("getProfile", {}, (resp) => {
   374       if (resp.error) {
   375         Cu.reportError("Failed to fetch profile data.");
   376         return void cb(resp.error, null);
   377       }
   379       let data = resp.profile;
   380       profile.timeEnded = resp.currentTime;
   382       // Filter out all samples that fall out of current
   383       // profile's range.
   385       data.threads = data.threads.map((thread) => {
   386         let samples = thread.samples.filter((sample) => {
   387           return sample.time >= profile.timeStarted;
   388         });
   390         return { samples: samples };
   391       });
   393       cb(null, data);
   394     });
   395   },
   397   /**
   398    * Cleanup.
   399    */
   400   destroy: function PC_destroy() {
   401     this.client = null;
   402     this.target = null;
   403     this.actor = null;
   404   }
   405 };
   407 if (isJSM) {
   408   var EXPORTED_SYMBOLS = ["ProfilerController"];
   409 } else {
   410   module.exports = ProfilerController;
   411 }

mercurial