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