browser/devtools/profiler/panel.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 const { Cu, Cc, Ci, components } = require("chrome");
     9 const {
    10   PROFILE_IDLE,
    11   PROFILE_RUNNING,
    12   PROFILE_COMPLETED,
    13   SHOW_PLATFORM_DATA,
    14   L10N_BUNDLE
    15 } = require("devtools/profiler/consts");
    17 const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
    19 var EventEmitter = require("devtools/toolkit/event-emitter");
    20 var Cleopatra    = require("devtools/profiler/cleopatra");
    21 var Sidebar      = require("devtools/profiler/sidebar");
    22 var ProfilerController = require("devtools/profiler/controller");
    23 var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
    25 Cu.import("resource:///modules/devtools/gDevTools.jsm");
    26 Cu.import("resource://gre/modules/devtools/Console.jsm");
    27 Cu.import("resource://gre/modules/Services.jsm");
    28 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    29 Cu.import("resource://gre/modules/osfile.jsm");
    30 Cu.import("resource://gre/modules/NetUtil.jsm");
    32 loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
    34 /**
    35  * Profiler panel. It is responsible for creating and managing
    36  * different profile instances (see cleopatra.js).
    37  *
    38  * ProfilerPanel is an event emitter. It can emit the following
    39  * events:
    40  *
    41  *   - ready:     after the panel is done loading everything,
    42  *                including the default profile instance.
    43  *   - started:   after the panel successfuly starts our SPS
    44  *                profiler.
    45  *   - stopped:   after the panel successfuly stops our SPS
    46  *                profiler and is ready to hand over profiling
    47  *                data
    48  *   - parsed:    after Cleopatra finishes parsing profiling
    49  *                data.
    50  *   - destroyed: after the panel cleans up after itself and
    51  *                is ready to be destroyed.
    52  *
    53  * The following events are used mainly by tests to prevent
    54  * accidential oranges:
    55  *
    56  *   - profileCreated:  after a new profile is created.
    57  *   - profileSwitched: after user switches to a different
    58  *                      profile.
    59  */
    60 function ProfilerPanel(frame, toolbox) {
    61   this.isReady = false;
    62   this.window = frame.window;
    63   this.document = frame.document;
    64   this.target = toolbox.target;
    66   this.profiles = new Map();
    67   this._uid = 0;
    68   this._msgQueue = {};
    70   EventEmitter.decorate(this);
    71 }
    73 ProfilerPanel.prototype = {
    74   isReady:     null,
    75   window:      null,
    76   document:    null,
    77   target:      null,
    78   controller:  null,
    79   profiles:    null,
    80   sidebar:     null,
    82   _uid:        null,
    83   _activeUid:  null,
    84   _runningUid: null,
    85   _browserWin: null,
    86   _msgQueue:   null,
    88   get controls() {
    89     let doc = this.document;
    91     return {
    92       get record() doc.querySelector("#profiler-start"),
    93       get import() doc.querySelector("#profiler-import"),
    94     };
    95   },
    97   get activeProfile() {
    98     return this.profiles.get(this._activeUid);
    99   },
   101   set activeProfile(profile) {
   102     if (this._activeUid === profile.uid)
   103       return;
   105     if (this.activeProfile)
   106       this.activeProfile.hide();
   108     this._activeUid = profile.uid;
   109     profile.show();
   110   },
   112   set recordingProfile(profile) {
   113     let btn = this.controls.record;
   114     this._runningUid = profile ? profile.uid : null;
   116     if (this._runningUid)
   117       btn.setAttribute("checked", true);
   118     else
   119       btn.removeAttribute("checked");
   120   },
   122   get recordingProfile() {
   123     return this.profiles.get(this._runningUid);
   124   },
   126   get browserWindow() {
   127     if (this._browserWin) {
   128       return this._browserWin;
   129     }
   131     let win = this.window.top;
   132     let type = win.document.documentElement.getAttribute("windowtype");
   134     if (type !== "navigator:browser") {
   135       win = Services.wm.getMostRecentWindow("navigator:browser");
   136     }
   138     return this._browserWin = win;
   139   },
   141   get showPlatformData() {
   142     return Services.prefs.getBoolPref(SHOW_PLATFORM_DATA);
   143   },
   145   set showPlatformData(enabled) {
   146     Services.prefs.setBoolPref(SHOW_PLATFORM_DATA, enabled);
   147   },
   149   /**
   150    * Open a debug connection and, on success, switch to the newly created
   151    * profile.
   152    *
   153    * @return Promise
   154    */
   155   open: function PP_open() {
   156     // Local profiling needs to make the target remote.
   157     let target = this.target;
   158     let targetPromise = !target.isRemote ? target.makeRemote() : promise.resolve(target);
   160     return targetPromise
   161       .then((target) => {
   162         let deferred = promise.defer();
   164         this.controller = new ProfilerController(this.target);
   165         this.sidebar = new Sidebar(this.document.querySelector("#profiles-list"));
   167         this.sidebar.on("save", (_, uid) => {
   168           let profile = this.profiles.get(uid);
   170           if (!profile.data)
   171             return void Cu.reportError("Can't save profile because there's no data.");
   173           this.openFileDialog({ mode: "save", name: profile.name }).then((file) => {
   174             if (file)
   175               this.saveProfile(file, profile.data);
   176           });
   177         });
   179         this.sidebar.on("select", (_, uid) => {
   180           let profile = this.profiles.get(uid);
   181           this.activeProfile = profile;
   183           if (profile.isReady) {
   184             return void this.emit("profileSwitched", profile.uid);
   185           }
   187           profile.once("ready", () => {
   188             this.emit("profileSwitched", profile.uid);
   189           });
   190         });
   192         this.controller.connect(() => {
   193           let btn = this.controls.record;
   194           btn.addEventListener("click", () => this.toggleRecording(), false);
   195           btn.removeAttribute("disabled");
   197           let imp = this.controls.import;
   198           imp.addEventListener("click", () => {
   199             this.openFileDialog({ mode: "open" }).then((file) => {
   200               if (file)
   201                 this.loadProfile(file);
   202             });
   203           }, false);
   204           imp.removeAttribute("disabled");
   206           // Import queued profiles.
   207           for (let [name, data] of this.controller.profiles) {
   208             this.importProfile(name, data.data);
   209           }
   211           this.isReady = true;
   212           this.emit("ready");
   213           deferred.resolve(this);
   214         });
   216         this.controller.on("profileEnd", (_, data) => {
   217           this.importProfile(data.name, data.data);
   219           if (this.recordingProfile && !data.fromConsole)
   220             this.recordingProfile = null;
   222           this.emit("stopped");
   223         });
   225         return deferred.promise;
   226       })
   227       .then(null, (reason) =>
   228         Cu.reportError("ProfilePanel open failed: " + reason.message));
   229   },
   231   /**
   232    * Creates a new profile instance (see cleopatra.js) and
   233    * adds an appropriate item to the sidebar. Note that
   234    * this method doesn't automatically switch user to
   235    * the newly created profile, they have do to switch
   236    * explicitly.
   237    *
   238    * @param string name
   239    *        (optional) name of the new profile
   240    *
   241    * @return Profile
   242    */
   243   createProfile: function (name, opts={}) {
   244     if (name && this.getProfileByName(name)) {
   245       return this.getProfileByName(name);
   246     }
   248     let uid = ++this._uid;
   249     let name = name || this.controller.getProfileName();
   250     let profile = new Cleopatra(this, {
   251       uid: uid,
   252       name: name,
   253       showPlatformData: this.showPlatformData,
   254       external: opts.external
   255     });
   257     this.profiles.set(uid, profile);
   258     this.sidebar.addProfile(profile);
   259     this.emit("profileCreated", uid);
   261     return profile;
   262   },
   264   /**
   265    * Imports profile data
   266    *
   267    * @param string name, new profile name
   268    * @param object data, profile data to import
   269    * @param object opts, (optional) if property 'external' is found
   270    *                     Cleopatra will hide arrow buttons.
   271    *
   272    * @return Profile
   273    */
   274   importProfile: function (name, data, opts={}) {
   275     let profile = this.createProfile(name, { external: opts.external });
   276     profile.isStarted = false;
   277     profile.isFinished = true;
   278     profile.data = data;
   279     profile.parse(data, () => this.emit("parsed"));
   281     this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
   282     if (!this.sidebar.selectedItem)
   283       this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
   285     return profile;
   286   },
   288   /**
   289    * Starts or stops profile recording.
   290    */
   291   toggleRecording: function () {
   292     let profile = this.recordingProfile;
   294     if (!profile) {
   295       profile = this.createProfile();
   297       this.startProfiling(profile.name, () => {
   298         profile.isStarted = true;
   300         this.sidebar.setProfileState(profile, PROFILE_RUNNING);
   301         this.recordingProfile = profile;
   302         this.emit("started");
   303       });
   305       return;
   306     }
   308     this.stopProfiling(profile.name, (data) => {
   309       profile.isStarted = false;
   310       profile.isFinished = true;
   311       profile.data = data;
   312       profile.parse(data, () => this.emit("parsed"));
   314       this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
   315       this.activeProfile = profile;
   316       this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
   317       this.recordingProfile = null;
   318       this.emit("stopped");
   319     });
   320   },
   322   /**
   323    * Start collecting profile data.
   324    *
   325    * @param function onStart
   326    *   A function to call once we get the message
   327    *   that profiling had been successfuly started.
   328    */
   329   startProfiling: function (name, onStart) {
   330     this.controller.start(name, (err) => {
   331       if (err) {
   332         return void Cu.reportError("ProfilerController.start: " + err.message);
   333       }
   335       onStart();
   336       this.emit("started");
   337     });
   338   },
   340   /**
   341    * Stop collecting profile data.
   342    *
   343    * @param function onStop
   344    *   A function to call once we get the message
   345    *   that profiling had been successfuly stopped.
   346    */
   347   stopProfiling: function (name, onStop) {
   348     this.controller.isActive((err, isActive) => {
   349       if (err) {
   350         Cu.reportError("ProfilerController.isActive: " + err.message);
   351         return;
   352       }
   354       if (!isActive) {
   355         return;
   356       }
   358       this.controller.stop(name, (err, data) => {
   359         if (err) {
   360           Cu.reportError("ProfilerController.stop: " + err.message);
   361           return;
   362         }
   364         onStop(data);
   365         this.emit("stopped", data);
   366       });
   367     });
   368   },
   370   /**
   371    * Lookup an individual profile by its name.
   372    *
   373    * @param string name name of the profile
   374    * @return profile object or null
   375    */
   376   getProfileByName: function PP_getProfileByName(name) {
   377     if (!this.profiles) {
   378       return null;
   379     }
   381     for (let [ uid, profile ] of this.profiles) {
   382       if (profile.name === name) {
   383         return profile;
   384       }
   385     }
   387     return null;
   388   },
   390   /**
   391    * Lookup an individual profile by its UID.
   392    *
   393    * @param number uid UID of the profile
   394    * @return profile object or null
   395    */
   396   getProfileByUID: function PP_getProfileByUID(uid) {
   397     if (!this.profiles) {
   398       return null;
   399     }
   401     return this.profiles.get(uid) || null;
   402   },
   404   /**
   405    * Iterates over each available profile and calls
   406    * a callback with it as a parameter.
   407    *
   408    * @param function cb a callback to call
   409    */
   410   eachProfile: function PP_eachProfile(cb) {
   411     let uid = this._uid;
   413     if (!this.profiles) {
   414       return;
   415     }
   417     while (uid >= 0) {
   418       if (this.profiles.has(uid)) {
   419         cb(this.profiles.get(uid));
   420       }
   422       uid -= 1;
   423     }
   424   },
   426   /**
   427    * Broadcast messages to all Cleopatra instances.
   428    *
   429    * @param number target
   430    *   UID of the recepient profile. All profiles will receive the message
   431    *   but the profile specified by 'target' will have a special property,
   432    *   isCurrent, set to true.
   433    * @param object data
   434    *   An object with a property 'task' that will be sent over to Cleopatra.
   435    */
   436   broadcast: function PP_broadcast(target, data) {
   437     if (!this.profiles) {
   438       return;
   439     }
   441     this.eachProfile((profile) => {
   442       profile.message({
   443         uid: target,
   444         isCurrent: target === profile.uid,
   445         task: data.task
   446       });
   447     });
   448   },
   450   /**
   451    * Open file specified in data in either a debugger or view-source.
   452    *
   453    * @param object data
   454    *   An object describing the file. It must have three properties:
   455    *    - uri
   456    *    - line
   457    *    - isChrome (chrome files are opened via view-source)
   458    */
   459   displaySource: function PP_displaySource(data) {
   460     let { browserWindow: win, document: doc } = this;
   461     let { uri, line, isChrome } = data;
   462     let deferred = promise.defer();
   464     if (isChrome) {
   465       return void win.gViewSourceUtils.viewSource(uri, null, doc, line);
   466     }
   468     let showSource = ({ DebuggerView }) => {
   469       if (DebuggerView.Sources.containsValue(uri)) {
   470         DebuggerView.setEditorLocation(uri, line).then(deferred.resolve);
   471       }
   472       // XXX: What to do if the source isn't present in the Debugger?
   473       // Switch back to the Profiler panel and viewSource()?
   474     }
   476     // If the Debugger was already open, switch to it and try to show the
   477     // source immediately. Otherwise, initialize it and wait for the sources
   478     // to be added first.
   479     let toolbox = gDevTools.getToolbox(this.target);
   480     let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
   481     toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
   482       if (debuggerAlreadyOpen) {
   483         showSource(dbg);
   484       } else {
   485         dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
   486       }
   487     });
   489     return deferred.promise;
   490   },
   492   /**
   493    * Opens a normal file dialog.
   494    *
   495    * @params object opts, (optional) property 'mode' can be used to
   496    *                      specify which dialog to open. Can be either
   497    *                      'save' or 'open' (default is 'open').
   498    * @return promise
   499    */
   500   openFileDialog: function (opts={}) {
   501     let deferred = promise.defer();
   503     let picker = Ci.nsIFilePicker;
   504     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(picker);
   505     let { name, mode } = opts;
   506     let save = mode === "save";
   507     let title = L10N.getStr(save ? "profiler.saveFileAs" : "profiler.openFile");
   509     fp.init(this.window, title, save ? picker.modeSave : picker.modeOpen);
   510     fp.appendFilter("JSON", "*.json");
   511     fp.appendFilters(picker.filterText | picker.filterAll);
   513     if (save)
   514       fp.defaultString = (name || "profile") + ".json";
   516     fp.open((result) => {
   517       deferred.resolve(result === picker.returnCancel ? null : fp.file);
   518     });
   520     return deferred.promise;
   521   },
   523   /**
   524    * Saves profile data to disk
   525    *
   526    * @param File file
   527    * @param object data
   528    *
   529    * @return promise
   530    */
   531   saveProfile: function (file, data) {
   532     let encoder = new TextEncoder();
   533     let buffer = encoder.encode(JSON.stringify({ profile: data }, null, "  "));
   534     let opts = { tmpPath: file.path + ".tmp" };
   536     return OS.File.writeAtomic(file.path, buffer, opts);
   537   },
   539   /**
   540    * Reads profile data from disk
   541    *
   542    * @param File file
   543    * @return promise
   544    */
   545   loadProfile: function (file) {
   546     let deferred = promise.defer();
   547     let ch = NetUtil.newChannel(file);
   548     ch.contentType = "application/json";
   550     NetUtil.asyncFetch(ch, (input, status) => {
   551       if (!components.isSuccessCode(status)) throw new Error(status);
   553       let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
   554         .createInstance(Ci.nsIScriptableUnicodeConverter);
   555       conv.charset = "UTF-8";
   557       let data = NetUtil.readInputStreamToString(input, input.available());
   558       data = conv.ConvertToUnicode(data);
   559       this.importProfile(file.leafName, JSON.parse(data).profile, { external: true });
   561       deferred.resolve();
   562     });
   564     return deferred.promise;
   565   },
   567   /**
   568    * Cleanup.
   569    */
   570   destroy: function PP_destroy() {
   571     if (this.profiles) {
   572       let uid = this._uid;
   574       while (uid >= 0) {
   575         if (this.profiles.has(uid)) {
   576           this.profiles.get(uid).destroy();
   577           this.profiles.delete(uid);
   578         }
   579         uid -= 1;
   580       }
   581     }
   583     if (this.controller) {
   584       this.controller.destroy();
   585     }
   587     this.isReady = null;
   588     this.window = null;
   589     this.document = null;
   590     this.target = null;
   591     this.controller = null;
   592     this.profiles = null;
   593     this._uid = null;
   594     this._activeUid = null;
   596     this.emit("destroyed");
   597   }
   598 };
   600 module.exports = ProfilerPanel;

mercurial