mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-29 21:25:35 +00:00
601 lines
16 KiB
JavaScript
601 lines
16 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const { Cu, Cc, Ci, components } = require("chrome");
|
|
|
|
const {
|
|
PROFILE_IDLE,
|
|
PROFILE_RUNNING,
|
|
PROFILE_COMPLETED,
|
|
SHOW_PLATFORM_DATA,
|
|
L10N_BUNDLE
|
|
} = require("devtools/profiler/consts");
|
|
|
|
const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
|
|
|
var EventEmitter = require("devtools/shared/event-emitter");
|
|
var Cleopatra = require("devtools/profiler/cleopatra");
|
|
var Sidebar = require("devtools/profiler/sidebar");
|
|
var ProfilerController = require("devtools/profiler/controller");
|
|
var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
|
|
|
|
Cu.import("resource:///modules/devtools/gDevTools.jsm");
|
|
Cu.import("resource://gre/modules/devtools/Console.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
|
Cu.import("resource://gre/modules/osfile.jsm");
|
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
|
|
|
loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
|
|
|
|
/**
|
|
* Profiler panel. It is responsible for creating and managing
|
|
* different profile instances (see cleopatra.js).
|
|
*
|
|
* ProfilerPanel is an event emitter. It can emit the following
|
|
* events:
|
|
*
|
|
* - ready: after the panel is done loading everything,
|
|
* including the default profile instance.
|
|
* - started: after the panel successfuly starts our SPS
|
|
* profiler.
|
|
* - stopped: after the panel successfuly stops our SPS
|
|
* profiler and is ready to hand over profiling
|
|
* data
|
|
* - parsed: after Cleopatra finishes parsing profiling
|
|
* data.
|
|
* - destroyed: after the panel cleans up after itself and
|
|
* is ready to be destroyed.
|
|
*
|
|
* The following events are used mainly by tests to prevent
|
|
* accidential oranges:
|
|
*
|
|
* - profileCreated: after a new profile is created.
|
|
* - profileSwitched: after user switches to a different
|
|
* profile.
|
|
*/
|
|
function ProfilerPanel(frame, toolbox) {
|
|
this.isReady = false;
|
|
this.window = frame.window;
|
|
this.document = frame.document;
|
|
this.target = toolbox.target;
|
|
|
|
this.profiles = new Map();
|
|
this._uid = 0;
|
|
this._msgQueue = {};
|
|
|
|
EventEmitter.decorate(this);
|
|
}
|
|
|
|
ProfilerPanel.prototype = {
|
|
isReady: null,
|
|
window: null,
|
|
document: null,
|
|
target: null,
|
|
controller: null,
|
|
profiles: null,
|
|
sidebar: null,
|
|
|
|
_uid: null,
|
|
_activeUid: null,
|
|
_runningUid: null,
|
|
_browserWin: null,
|
|
_msgQueue: null,
|
|
|
|
get controls() {
|
|
let doc = this.document;
|
|
|
|
return {
|
|
get record() doc.querySelector("#profiler-start"),
|
|
get import() doc.querySelector("#profiler-import"),
|
|
};
|
|
},
|
|
|
|
get activeProfile() {
|
|
return this.profiles.get(this._activeUid);
|
|
},
|
|
|
|
set activeProfile(profile) {
|
|
if (this._activeUid === profile.uid)
|
|
return;
|
|
|
|
if (this.activeProfile)
|
|
this.activeProfile.hide();
|
|
|
|
this._activeUid = profile.uid;
|
|
profile.show();
|
|
},
|
|
|
|
set recordingProfile(profile) {
|
|
let btn = this.controls.record;
|
|
this._runningUid = profile ? profile.uid : null;
|
|
|
|
if (this._runningUid)
|
|
btn.setAttribute("checked", true);
|
|
else
|
|
btn.removeAttribute("checked");
|
|
},
|
|
|
|
get recordingProfile() {
|
|
return this.profiles.get(this._runningUid);
|
|
},
|
|
|
|
get browserWindow() {
|
|
if (this._browserWin) {
|
|
return this._browserWin;
|
|
}
|
|
|
|
let win = this.window.top;
|
|
let type = win.document.documentElement.getAttribute("windowtype");
|
|
|
|
if (type !== "navigator:browser") {
|
|
win = Services.wm.getMostRecentWindow("navigator:browser");
|
|
}
|
|
|
|
return this._browserWin = win;
|
|
},
|
|
|
|
get showPlatformData() {
|
|
return Services.prefs.getBoolPref(SHOW_PLATFORM_DATA);
|
|
},
|
|
|
|
set showPlatformData(enabled) {
|
|
Services.prefs.setBoolPref(SHOW_PLATFORM_DATA, enabled);
|
|
},
|
|
|
|
/**
|
|
* Open a debug connection and, on success, switch to the newly created
|
|
* profile.
|
|
*
|
|
* @return Promise
|
|
*/
|
|
open: function PP_open() {
|
|
// Local profiling needs to make the target remote.
|
|
let target = this.target;
|
|
let targetPromise = !target.isRemote ? target.makeRemote() : promise.resolve(target);
|
|
|
|
return targetPromise
|
|
.then((target) => {
|
|
let deferred = promise.defer();
|
|
|
|
this.controller = new ProfilerController(this.target);
|
|
this.sidebar = new Sidebar(this.document.querySelector("#profiles-list"));
|
|
|
|
this.sidebar.on("save", (_, uid) => {
|
|
let profile = this.profiles.get(uid);
|
|
|
|
if (!profile.data)
|
|
return void Cu.reportError("Can't save profile because there's no data.");
|
|
|
|
this.openFileDialog({ mode: "save", name: profile.name }).then((file) => {
|
|
if (file)
|
|
this.saveProfile(file, profile.data);
|
|
});
|
|
});
|
|
|
|
this.sidebar.on("select", (_, uid) => {
|
|
let profile = this.profiles.get(uid);
|
|
this.activeProfile = profile;
|
|
|
|
if (profile.isReady) {
|
|
return void this.emit("profileSwitched", profile.uid);
|
|
}
|
|
|
|
profile.once("ready", () => {
|
|
this.emit("profileSwitched", profile.uid);
|
|
});
|
|
});
|
|
|
|
this.controller.connect(() => {
|
|
let btn = this.controls.record;
|
|
btn.addEventListener("click", () => this.toggleRecording(), false);
|
|
btn.removeAttribute("disabled");
|
|
|
|
let imp = this.controls.import;
|
|
imp.addEventListener("click", () => {
|
|
this.openFileDialog({ mode: "open" }).then((file) => {
|
|
if (file)
|
|
this.loadProfile(file);
|
|
});
|
|
}, false);
|
|
imp.removeAttribute("disabled");
|
|
|
|
// Import queued profiles.
|
|
for (let [name, data] of this.controller.profiles) {
|
|
this.importProfile(name, data.data);
|
|
}
|
|
|
|
this.isReady = true;
|
|
this.emit("ready");
|
|
deferred.resolve(this);
|
|
});
|
|
|
|
this.controller.on("profileEnd", (_, data) => {
|
|
this.importProfile(data.name, data.data);
|
|
|
|
if (this.recordingProfile && !data.fromConsole)
|
|
this.recordingProfile = null;
|
|
|
|
this.emit("stopped");
|
|
});
|
|
|
|
return deferred.promise;
|
|
})
|
|
.then(null, (reason) =>
|
|
Cu.reportError("ProfilePanel open failed: " + reason.message));
|
|
},
|
|
|
|
/**
|
|
* Creates a new profile instance (see cleopatra.js) and
|
|
* adds an appropriate item to the sidebar. Note that
|
|
* this method doesn't automatically switch user to
|
|
* the newly created profile, they have do to switch
|
|
* explicitly.
|
|
*
|
|
* @param string name
|
|
* (optional) name of the new profile
|
|
*
|
|
* @return Profile
|
|
*/
|
|
createProfile: function (name, opts={}) {
|
|
if (name && this.getProfileByName(name)) {
|
|
return this.getProfileByName(name);
|
|
}
|
|
|
|
let uid = ++this._uid;
|
|
let name = name || this.controller.getProfileName();
|
|
let profile = new Cleopatra(this, {
|
|
uid: uid,
|
|
name: name,
|
|
showPlatformData: this.showPlatformData,
|
|
external: opts.external
|
|
});
|
|
|
|
this.profiles.set(uid, profile);
|
|
this.sidebar.addProfile(profile);
|
|
this.emit("profileCreated", uid);
|
|
|
|
return profile;
|
|
},
|
|
|
|
/**
|
|
* Imports profile data
|
|
*
|
|
* @param string name, new profile name
|
|
* @param object data, profile data to import
|
|
* @param object opts, (optional) if property 'external' is found
|
|
* Cleopatra will hide arrow buttons.
|
|
*
|
|
* @return Profile
|
|
*/
|
|
importProfile: function (name, data, opts={}) {
|
|
let profile = this.createProfile(name, { external: opts.external });
|
|
profile.isStarted = false;
|
|
profile.isFinished = true;
|
|
profile.data = data;
|
|
profile.parse(data, () => this.emit("parsed"));
|
|
|
|
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
|
|
if (!this.sidebar.selectedItem)
|
|
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
|
|
|
|
return profile;
|
|
},
|
|
|
|
/**
|
|
* Starts or stops profile recording.
|
|
*/
|
|
toggleRecording: function () {
|
|
let profile = this.recordingProfile;
|
|
|
|
if (!profile) {
|
|
profile = this.createProfile();
|
|
|
|
this.startProfiling(profile.name, () => {
|
|
profile.isStarted = true;
|
|
|
|
this.sidebar.setProfileState(profile, PROFILE_RUNNING);
|
|
this.recordingProfile = profile;
|
|
this.emit("started");
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
this.stopProfiling(profile.name, (data) => {
|
|
profile.isStarted = false;
|
|
profile.isFinished = true;
|
|
profile.data = data;
|
|
profile.parse(data, () => this.emit("parsed"));
|
|
|
|
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
|
|
this.activeProfile = profile;
|
|
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
|
|
this.recordingProfile = null;
|
|
this.emit("stopped");
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Start collecting profile data.
|
|
*
|
|
* @param function onStart
|
|
* A function to call once we get the message
|
|
* that profiling had been successfuly started.
|
|
*/
|
|
startProfiling: function (name, onStart) {
|
|
this.controller.start(name, (err) => {
|
|
if (err) {
|
|
return void Cu.reportError("ProfilerController.start: " + err.message);
|
|
}
|
|
|
|
onStart();
|
|
this.emit("started");
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Stop collecting profile data.
|
|
*
|
|
* @param function onStop
|
|
* A function to call once we get the message
|
|
* that profiling had been successfuly stopped.
|
|
*/
|
|
stopProfiling: function (name, onStop) {
|
|
this.controller.isActive((err, isActive) => {
|
|
if (err) {
|
|
Cu.reportError("ProfilerController.isActive: " + err.message);
|
|
return;
|
|
}
|
|
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
this.controller.stop(name, (err, data) => {
|
|
if (err) {
|
|
Cu.reportError("ProfilerController.stop: " + err.message);
|
|
return;
|
|
}
|
|
|
|
onStop(data);
|
|
this.emit("stopped", data);
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Lookup an individual profile by its name.
|
|
*
|
|
* @param string name name of the profile
|
|
* @return profile object or null
|
|
*/
|
|
getProfileByName: function PP_getProfileByName(name) {
|
|
if (!this.profiles) {
|
|
return null;
|
|
}
|
|
|
|
for (let [ uid, profile ] of this.profiles) {
|
|
if (profile.name === name) {
|
|
return profile;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Lookup an individual profile by its UID.
|
|
*
|
|
* @param number uid UID of the profile
|
|
* @return profile object or null
|
|
*/
|
|
getProfileByUID: function PP_getProfileByUID(uid) {
|
|
if (!this.profiles) {
|
|
return null;
|
|
}
|
|
|
|
return this.profiles.get(uid) || null;
|
|
},
|
|
|
|
/**
|
|
* Iterates over each available profile and calls
|
|
* a callback with it as a parameter.
|
|
*
|
|
* @param function cb a callback to call
|
|
*/
|
|
eachProfile: function PP_eachProfile(cb) {
|
|
let uid = this._uid;
|
|
|
|
if (!this.profiles) {
|
|
return;
|
|
}
|
|
|
|
while (uid >= 0) {
|
|
if (this.profiles.has(uid)) {
|
|
cb(this.profiles.get(uid));
|
|
}
|
|
|
|
uid -= 1;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Broadcast messages to all Cleopatra instances.
|
|
*
|
|
* @param number target
|
|
* UID of the recepient profile. All profiles will receive the message
|
|
* but the profile specified by 'target' will have a special property,
|
|
* isCurrent, set to true.
|
|
* @param object data
|
|
* An object with a property 'task' that will be sent over to Cleopatra.
|
|
*/
|
|
broadcast: function PP_broadcast(target, data) {
|
|
if (!this.profiles) {
|
|
return;
|
|
}
|
|
|
|
this.eachProfile((profile) => {
|
|
profile.message({
|
|
uid: target,
|
|
isCurrent: target === profile.uid,
|
|
task: data.task
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Open file specified in data in either a debugger or view-source.
|
|
*
|
|
* @param object data
|
|
* An object describing the file. It must have three properties:
|
|
* - uri
|
|
* - line
|
|
* - isChrome (chrome files are opened via view-source)
|
|
*/
|
|
displaySource: function PP_displaySource(data) {
|
|
let { browserWindow: win, document: doc } = this;
|
|
let { uri, line, isChrome } = data;
|
|
let deferred = promise.defer();
|
|
|
|
if (isChrome) {
|
|
return void win.gViewSourceUtils.viewSource(uri, null, doc, line);
|
|
}
|
|
|
|
let showSource = ({ DebuggerView }) => {
|
|
if (DebuggerView.Sources.containsValue(uri)) {
|
|
DebuggerView.setEditorLocation(uri, line).then(deferred.resolve);
|
|
}
|
|
// XXX: What to do if the source isn't present in the Debugger?
|
|
// Switch back to the Profiler panel and viewSource()?
|
|
}
|
|
|
|
// If the Debugger was already open, switch to it and try to show the
|
|
// source immediately. Otherwise, initialize it and wait for the sources
|
|
// to be added first.
|
|
let toolbox = gDevTools.getToolbox(this.target);
|
|
let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
|
|
toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
|
|
if (debuggerAlreadyOpen) {
|
|
showSource(dbg);
|
|
} else {
|
|
dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
|
|
}
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
/**
|
|
* Opens a normal file dialog.
|
|
*
|
|
* @params object opts, (optional) property 'mode' can be used to
|
|
* specify which dialog to open. Can be either
|
|
* 'save' or 'open' (default is 'open').
|
|
* @return promise
|
|
*/
|
|
openFileDialog: function (opts={}) {
|
|
let deferred = promise.defer();
|
|
|
|
let picker = Ci.nsIFilePicker;
|
|
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(picker);
|
|
let { name, mode } = opts;
|
|
let save = mode === "save";
|
|
let title = L10N.getStr(save ? "profiler.saveFileAs" : "profiler.openFile");
|
|
|
|
fp.init(this.window, title, save ? picker.modeSave : picker.modeOpen);
|
|
fp.appendFilter("JSON", "*.json");
|
|
fp.appendFilters(picker.filterText | picker.filterAll);
|
|
|
|
if (save)
|
|
fp.defaultString = (name || "profile") + ".json";
|
|
|
|
fp.open((result) => {
|
|
deferred.resolve(result === picker.returnCancel ? null : fp.file);
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
/**
|
|
* Saves profile data to disk
|
|
*
|
|
* @param File file
|
|
* @param object data
|
|
*
|
|
* @return promise
|
|
*/
|
|
saveProfile: function (file, data) {
|
|
let encoder = new TextEncoder();
|
|
let buffer = encoder.encode(JSON.stringify({ profile: data }, null, " "));
|
|
let opts = { tmpPath: file.path + ".tmp" };
|
|
|
|
return OS.File.writeAtomic(file.path, buffer, opts);
|
|
},
|
|
|
|
/**
|
|
* Reads profile data from disk
|
|
*
|
|
* @param File file
|
|
* @return promise
|
|
*/
|
|
loadProfile: function (file) {
|
|
let deferred = promise.defer();
|
|
let ch = NetUtil.newChannel(file);
|
|
ch.contentType = "application/json";
|
|
|
|
NetUtil.asyncFetch(ch, (input, status) => {
|
|
if (!components.isSuccessCode(status)) throw new Error(status);
|
|
|
|
let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
|
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
|
conv.charset = "UTF-8";
|
|
|
|
let data = NetUtil.readInputStreamToString(input, input.available());
|
|
data = conv.ConvertToUnicode(data);
|
|
this.importProfile(file.leafName, JSON.parse(data).profile, { external: true });
|
|
|
|
deferred.resolve();
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
/**
|
|
* Cleanup.
|
|
*/
|
|
destroy: function PP_destroy() {
|
|
if (this.profiles) {
|
|
let uid = this._uid;
|
|
|
|
while (uid >= 0) {
|
|
if (this.profiles.has(uid)) {
|
|
this.profiles.get(uid).destroy();
|
|
this.profiles.delete(uid);
|
|
}
|
|
uid -= 1;
|
|
}
|
|
}
|
|
|
|
if (this.controller) {
|
|
this.controller.destroy();
|
|
}
|
|
|
|
this.isReady = null;
|
|
this.window = null;
|
|
this.document = null;
|
|
this.target = null;
|
|
this.controller = null;
|
|
this.profiles = null;
|
|
this._uid = null;
|
|
this._activeUid = null;
|
|
|
|
this.emit("destroyed");
|
|
}
|
|
};
|
|
|
|
module.exports = ProfilerPanel;
|