1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/profiler/panel.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,600 @@ 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 +const { Cu, Cc, Ci, components } = require("chrome"); 1.11 + 1.12 +const { 1.13 + PROFILE_IDLE, 1.14 + PROFILE_RUNNING, 1.15 + PROFILE_COMPLETED, 1.16 + SHOW_PLATFORM_DATA, 1.17 + L10N_BUNDLE 1.18 +} = require("devtools/profiler/consts"); 1.19 + 1.20 +const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}); 1.21 + 1.22 +var EventEmitter = require("devtools/toolkit/event-emitter"); 1.23 +var Cleopatra = require("devtools/profiler/cleopatra"); 1.24 +var Sidebar = require("devtools/profiler/sidebar"); 1.25 +var ProfilerController = require("devtools/profiler/controller"); 1.26 +var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.27 + 1.28 +Cu.import("resource:///modules/devtools/gDevTools.jsm"); 1.29 +Cu.import("resource://gre/modules/devtools/Console.jsm"); 1.30 +Cu.import("resource://gre/modules/Services.jsm"); 1.31 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); 1.32 +Cu.import("resource://gre/modules/osfile.jsm"); 1.33 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.34 + 1.35 +loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE)); 1.36 + 1.37 +/** 1.38 + * Profiler panel. It is responsible for creating and managing 1.39 + * different profile instances (see cleopatra.js). 1.40 + * 1.41 + * ProfilerPanel is an event emitter. It can emit the following 1.42 + * events: 1.43 + * 1.44 + * - ready: after the panel is done loading everything, 1.45 + * including the default profile instance. 1.46 + * - started: after the panel successfuly starts our SPS 1.47 + * profiler. 1.48 + * - stopped: after the panel successfuly stops our SPS 1.49 + * profiler and is ready to hand over profiling 1.50 + * data 1.51 + * - parsed: after Cleopatra finishes parsing profiling 1.52 + * data. 1.53 + * - destroyed: after the panel cleans up after itself and 1.54 + * is ready to be destroyed. 1.55 + * 1.56 + * The following events are used mainly by tests to prevent 1.57 + * accidential oranges: 1.58 + * 1.59 + * - profileCreated: after a new profile is created. 1.60 + * - profileSwitched: after user switches to a different 1.61 + * profile. 1.62 + */ 1.63 +function ProfilerPanel(frame, toolbox) { 1.64 + this.isReady = false; 1.65 + this.window = frame.window; 1.66 + this.document = frame.document; 1.67 + this.target = toolbox.target; 1.68 + 1.69 + this.profiles = new Map(); 1.70 + this._uid = 0; 1.71 + this._msgQueue = {}; 1.72 + 1.73 + EventEmitter.decorate(this); 1.74 +} 1.75 + 1.76 +ProfilerPanel.prototype = { 1.77 + isReady: null, 1.78 + window: null, 1.79 + document: null, 1.80 + target: null, 1.81 + controller: null, 1.82 + profiles: null, 1.83 + sidebar: null, 1.84 + 1.85 + _uid: null, 1.86 + _activeUid: null, 1.87 + _runningUid: null, 1.88 + _browserWin: null, 1.89 + _msgQueue: null, 1.90 + 1.91 + get controls() { 1.92 + let doc = this.document; 1.93 + 1.94 + return { 1.95 + get record() doc.querySelector("#profiler-start"), 1.96 + get import() doc.querySelector("#profiler-import"), 1.97 + }; 1.98 + }, 1.99 + 1.100 + get activeProfile() { 1.101 + return this.profiles.get(this._activeUid); 1.102 + }, 1.103 + 1.104 + set activeProfile(profile) { 1.105 + if (this._activeUid === profile.uid) 1.106 + return; 1.107 + 1.108 + if (this.activeProfile) 1.109 + this.activeProfile.hide(); 1.110 + 1.111 + this._activeUid = profile.uid; 1.112 + profile.show(); 1.113 + }, 1.114 + 1.115 + set recordingProfile(profile) { 1.116 + let btn = this.controls.record; 1.117 + this._runningUid = profile ? profile.uid : null; 1.118 + 1.119 + if (this._runningUid) 1.120 + btn.setAttribute("checked", true); 1.121 + else 1.122 + btn.removeAttribute("checked"); 1.123 + }, 1.124 + 1.125 + get recordingProfile() { 1.126 + return this.profiles.get(this._runningUid); 1.127 + }, 1.128 + 1.129 + get browserWindow() { 1.130 + if (this._browserWin) { 1.131 + return this._browserWin; 1.132 + } 1.133 + 1.134 + let win = this.window.top; 1.135 + let type = win.document.documentElement.getAttribute("windowtype"); 1.136 + 1.137 + if (type !== "navigator:browser") { 1.138 + win = Services.wm.getMostRecentWindow("navigator:browser"); 1.139 + } 1.140 + 1.141 + return this._browserWin = win; 1.142 + }, 1.143 + 1.144 + get showPlatformData() { 1.145 + return Services.prefs.getBoolPref(SHOW_PLATFORM_DATA); 1.146 + }, 1.147 + 1.148 + set showPlatformData(enabled) { 1.149 + Services.prefs.setBoolPref(SHOW_PLATFORM_DATA, enabled); 1.150 + }, 1.151 + 1.152 + /** 1.153 + * Open a debug connection and, on success, switch to the newly created 1.154 + * profile. 1.155 + * 1.156 + * @return Promise 1.157 + */ 1.158 + open: function PP_open() { 1.159 + // Local profiling needs to make the target remote. 1.160 + let target = this.target; 1.161 + let targetPromise = !target.isRemote ? target.makeRemote() : promise.resolve(target); 1.162 + 1.163 + return targetPromise 1.164 + .then((target) => { 1.165 + let deferred = promise.defer(); 1.166 + 1.167 + this.controller = new ProfilerController(this.target); 1.168 + this.sidebar = new Sidebar(this.document.querySelector("#profiles-list")); 1.169 + 1.170 + this.sidebar.on("save", (_, uid) => { 1.171 + let profile = this.profiles.get(uid); 1.172 + 1.173 + if (!profile.data) 1.174 + return void Cu.reportError("Can't save profile because there's no data."); 1.175 + 1.176 + this.openFileDialog({ mode: "save", name: profile.name }).then((file) => { 1.177 + if (file) 1.178 + this.saveProfile(file, profile.data); 1.179 + }); 1.180 + }); 1.181 + 1.182 + this.sidebar.on("select", (_, uid) => { 1.183 + let profile = this.profiles.get(uid); 1.184 + this.activeProfile = profile; 1.185 + 1.186 + if (profile.isReady) { 1.187 + return void this.emit("profileSwitched", profile.uid); 1.188 + } 1.189 + 1.190 + profile.once("ready", () => { 1.191 + this.emit("profileSwitched", profile.uid); 1.192 + }); 1.193 + }); 1.194 + 1.195 + this.controller.connect(() => { 1.196 + let btn = this.controls.record; 1.197 + btn.addEventListener("click", () => this.toggleRecording(), false); 1.198 + btn.removeAttribute("disabled"); 1.199 + 1.200 + let imp = this.controls.import; 1.201 + imp.addEventListener("click", () => { 1.202 + this.openFileDialog({ mode: "open" }).then((file) => { 1.203 + if (file) 1.204 + this.loadProfile(file); 1.205 + }); 1.206 + }, false); 1.207 + imp.removeAttribute("disabled"); 1.208 + 1.209 + // Import queued profiles. 1.210 + for (let [name, data] of this.controller.profiles) { 1.211 + this.importProfile(name, data.data); 1.212 + } 1.213 + 1.214 + this.isReady = true; 1.215 + this.emit("ready"); 1.216 + deferred.resolve(this); 1.217 + }); 1.218 + 1.219 + this.controller.on("profileEnd", (_, data) => { 1.220 + this.importProfile(data.name, data.data); 1.221 + 1.222 + if (this.recordingProfile && !data.fromConsole) 1.223 + this.recordingProfile = null; 1.224 + 1.225 + this.emit("stopped"); 1.226 + }); 1.227 + 1.228 + return deferred.promise; 1.229 + }) 1.230 + .then(null, (reason) => 1.231 + Cu.reportError("ProfilePanel open failed: " + reason.message)); 1.232 + }, 1.233 + 1.234 + /** 1.235 + * Creates a new profile instance (see cleopatra.js) and 1.236 + * adds an appropriate item to the sidebar. Note that 1.237 + * this method doesn't automatically switch user to 1.238 + * the newly created profile, they have do to switch 1.239 + * explicitly. 1.240 + * 1.241 + * @param string name 1.242 + * (optional) name of the new profile 1.243 + * 1.244 + * @return Profile 1.245 + */ 1.246 + createProfile: function (name, opts={}) { 1.247 + if (name && this.getProfileByName(name)) { 1.248 + return this.getProfileByName(name); 1.249 + } 1.250 + 1.251 + let uid = ++this._uid; 1.252 + let name = name || this.controller.getProfileName(); 1.253 + let profile = new Cleopatra(this, { 1.254 + uid: uid, 1.255 + name: name, 1.256 + showPlatformData: this.showPlatformData, 1.257 + external: opts.external 1.258 + }); 1.259 + 1.260 + this.profiles.set(uid, profile); 1.261 + this.sidebar.addProfile(profile); 1.262 + this.emit("profileCreated", uid); 1.263 + 1.264 + return profile; 1.265 + }, 1.266 + 1.267 + /** 1.268 + * Imports profile data 1.269 + * 1.270 + * @param string name, new profile name 1.271 + * @param object data, profile data to import 1.272 + * @param object opts, (optional) if property 'external' is found 1.273 + * Cleopatra will hide arrow buttons. 1.274 + * 1.275 + * @return Profile 1.276 + */ 1.277 + importProfile: function (name, data, opts={}) { 1.278 + let profile = this.createProfile(name, { external: opts.external }); 1.279 + profile.isStarted = false; 1.280 + profile.isFinished = true; 1.281 + profile.data = data; 1.282 + profile.parse(data, () => this.emit("parsed")); 1.283 + 1.284 + this.sidebar.setProfileState(profile, PROFILE_COMPLETED); 1.285 + if (!this.sidebar.selectedItem) 1.286 + this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile); 1.287 + 1.288 + return profile; 1.289 + }, 1.290 + 1.291 + /** 1.292 + * Starts or stops profile recording. 1.293 + */ 1.294 + toggleRecording: function () { 1.295 + let profile = this.recordingProfile; 1.296 + 1.297 + if (!profile) { 1.298 + profile = this.createProfile(); 1.299 + 1.300 + this.startProfiling(profile.name, () => { 1.301 + profile.isStarted = true; 1.302 + 1.303 + this.sidebar.setProfileState(profile, PROFILE_RUNNING); 1.304 + this.recordingProfile = profile; 1.305 + this.emit("started"); 1.306 + }); 1.307 + 1.308 + return; 1.309 + } 1.310 + 1.311 + this.stopProfiling(profile.name, (data) => { 1.312 + profile.isStarted = false; 1.313 + profile.isFinished = true; 1.314 + profile.data = data; 1.315 + profile.parse(data, () => this.emit("parsed")); 1.316 + 1.317 + this.sidebar.setProfileState(profile, PROFILE_COMPLETED); 1.318 + this.activeProfile = profile; 1.319 + this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile); 1.320 + this.recordingProfile = null; 1.321 + this.emit("stopped"); 1.322 + }); 1.323 + }, 1.324 + 1.325 + /** 1.326 + * Start collecting profile data. 1.327 + * 1.328 + * @param function onStart 1.329 + * A function to call once we get the message 1.330 + * that profiling had been successfuly started. 1.331 + */ 1.332 + startProfiling: function (name, onStart) { 1.333 + this.controller.start(name, (err) => { 1.334 + if (err) { 1.335 + return void Cu.reportError("ProfilerController.start: " + err.message); 1.336 + } 1.337 + 1.338 + onStart(); 1.339 + this.emit("started"); 1.340 + }); 1.341 + }, 1.342 + 1.343 + /** 1.344 + * Stop collecting profile data. 1.345 + * 1.346 + * @param function onStop 1.347 + * A function to call once we get the message 1.348 + * that profiling had been successfuly stopped. 1.349 + */ 1.350 + stopProfiling: function (name, onStop) { 1.351 + this.controller.isActive((err, isActive) => { 1.352 + if (err) { 1.353 + Cu.reportError("ProfilerController.isActive: " + err.message); 1.354 + return; 1.355 + } 1.356 + 1.357 + if (!isActive) { 1.358 + return; 1.359 + } 1.360 + 1.361 + this.controller.stop(name, (err, data) => { 1.362 + if (err) { 1.363 + Cu.reportError("ProfilerController.stop: " + err.message); 1.364 + return; 1.365 + } 1.366 + 1.367 + onStop(data); 1.368 + this.emit("stopped", data); 1.369 + }); 1.370 + }); 1.371 + }, 1.372 + 1.373 + /** 1.374 + * Lookup an individual profile by its name. 1.375 + * 1.376 + * @param string name name of the profile 1.377 + * @return profile object or null 1.378 + */ 1.379 + getProfileByName: function PP_getProfileByName(name) { 1.380 + if (!this.profiles) { 1.381 + return null; 1.382 + } 1.383 + 1.384 + for (let [ uid, profile ] of this.profiles) { 1.385 + if (profile.name === name) { 1.386 + return profile; 1.387 + } 1.388 + } 1.389 + 1.390 + return null; 1.391 + }, 1.392 + 1.393 + /** 1.394 + * Lookup an individual profile by its UID. 1.395 + * 1.396 + * @param number uid UID of the profile 1.397 + * @return profile object or null 1.398 + */ 1.399 + getProfileByUID: function PP_getProfileByUID(uid) { 1.400 + if (!this.profiles) { 1.401 + return null; 1.402 + } 1.403 + 1.404 + return this.profiles.get(uid) || null; 1.405 + }, 1.406 + 1.407 + /** 1.408 + * Iterates over each available profile and calls 1.409 + * a callback with it as a parameter. 1.410 + * 1.411 + * @param function cb a callback to call 1.412 + */ 1.413 + eachProfile: function PP_eachProfile(cb) { 1.414 + let uid = this._uid; 1.415 + 1.416 + if (!this.profiles) { 1.417 + return; 1.418 + } 1.419 + 1.420 + while (uid >= 0) { 1.421 + if (this.profiles.has(uid)) { 1.422 + cb(this.profiles.get(uid)); 1.423 + } 1.424 + 1.425 + uid -= 1; 1.426 + } 1.427 + }, 1.428 + 1.429 + /** 1.430 + * Broadcast messages to all Cleopatra instances. 1.431 + * 1.432 + * @param number target 1.433 + * UID of the recepient profile. All profiles will receive the message 1.434 + * but the profile specified by 'target' will have a special property, 1.435 + * isCurrent, set to true. 1.436 + * @param object data 1.437 + * An object with a property 'task' that will be sent over to Cleopatra. 1.438 + */ 1.439 + broadcast: function PP_broadcast(target, data) { 1.440 + if (!this.profiles) { 1.441 + return; 1.442 + } 1.443 + 1.444 + this.eachProfile((profile) => { 1.445 + profile.message({ 1.446 + uid: target, 1.447 + isCurrent: target === profile.uid, 1.448 + task: data.task 1.449 + }); 1.450 + }); 1.451 + }, 1.452 + 1.453 + /** 1.454 + * Open file specified in data in either a debugger or view-source. 1.455 + * 1.456 + * @param object data 1.457 + * An object describing the file. It must have three properties: 1.458 + * - uri 1.459 + * - line 1.460 + * - isChrome (chrome files are opened via view-source) 1.461 + */ 1.462 + displaySource: function PP_displaySource(data) { 1.463 + let { browserWindow: win, document: doc } = this; 1.464 + let { uri, line, isChrome } = data; 1.465 + let deferred = promise.defer(); 1.466 + 1.467 + if (isChrome) { 1.468 + return void win.gViewSourceUtils.viewSource(uri, null, doc, line); 1.469 + } 1.470 + 1.471 + let showSource = ({ DebuggerView }) => { 1.472 + if (DebuggerView.Sources.containsValue(uri)) { 1.473 + DebuggerView.setEditorLocation(uri, line).then(deferred.resolve); 1.474 + } 1.475 + // XXX: What to do if the source isn't present in the Debugger? 1.476 + // Switch back to the Profiler panel and viewSource()? 1.477 + } 1.478 + 1.479 + // If the Debugger was already open, switch to it and try to show the 1.480 + // source immediately. Otherwise, initialize it and wait for the sources 1.481 + // to be added first. 1.482 + let toolbox = gDevTools.getToolbox(this.target); 1.483 + let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger"); 1.484 + toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => { 1.485 + if (debuggerAlreadyOpen) { 1.486 + showSource(dbg); 1.487 + } else { 1.488 + dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg)); 1.489 + } 1.490 + }); 1.491 + 1.492 + return deferred.promise; 1.493 + }, 1.494 + 1.495 + /** 1.496 + * Opens a normal file dialog. 1.497 + * 1.498 + * @params object opts, (optional) property 'mode' can be used to 1.499 + * specify which dialog to open. Can be either 1.500 + * 'save' or 'open' (default is 'open'). 1.501 + * @return promise 1.502 + */ 1.503 + openFileDialog: function (opts={}) { 1.504 + let deferred = promise.defer(); 1.505 + 1.506 + let picker = Ci.nsIFilePicker; 1.507 + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(picker); 1.508 + let { name, mode } = opts; 1.509 + let save = mode === "save"; 1.510 + let title = L10N.getStr(save ? "profiler.saveFileAs" : "profiler.openFile"); 1.511 + 1.512 + fp.init(this.window, title, save ? picker.modeSave : picker.modeOpen); 1.513 + fp.appendFilter("JSON", "*.json"); 1.514 + fp.appendFilters(picker.filterText | picker.filterAll); 1.515 + 1.516 + if (save) 1.517 + fp.defaultString = (name || "profile") + ".json"; 1.518 + 1.519 + fp.open((result) => { 1.520 + deferred.resolve(result === picker.returnCancel ? null : fp.file); 1.521 + }); 1.522 + 1.523 + return deferred.promise; 1.524 + }, 1.525 + 1.526 + /** 1.527 + * Saves profile data to disk 1.528 + * 1.529 + * @param File file 1.530 + * @param object data 1.531 + * 1.532 + * @return promise 1.533 + */ 1.534 + saveProfile: function (file, data) { 1.535 + let encoder = new TextEncoder(); 1.536 + let buffer = encoder.encode(JSON.stringify({ profile: data }, null, " ")); 1.537 + let opts = { tmpPath: file.path + ".tmp" }; 1.538 + 1.539 + return OS.File.writeAtomic(file.path, buffer, opts); 1.540 + }, 1.541 + 1.542 + /** 1.543 + * Reads profile data from disk 1.544 + * 1.545 + * @param File file 1.546 + * @return promise 1.547 + */ 1.548 + loadProfile: function (file) { 1.549 + let deferred = promise.defer(); 1.550 + let ch = NetUtil.newChannel(file); 1.551 + ch.contentType = "application/json"; 1.552 + 1.553 + NetUtil.asyncFetch(ch, (input, status) => { 1.554 + if (!components.isSuccessCode(status)) throw new Error(status); 1.555 + 1.556 + let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.557 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.558 + conv.charset = "UTF-8"; 1.559 + 1.560 + let data = NetUtil.readInputStreamToString(input, input.available()); 1.561 + data = conv.ConvertToUnicode(data); 1.562 + this.importProfile(file.leafName, JSON.parse(data).profile, { external: true }); 1.563 + 1.564 + deferred.resolve(); 1.565 + }); 1.566 + 1.567 + return deferred.promise; 1.568 + }, 1.569 + 1.570 + /** 1.571 + * Cleanup. 1.572 + */ 1.573 + destroy: function PP_destroy() { 1.574 + if (this.profiles) { 1.575 + let uid = this._uid; 1.576 + 1.577 + while (uid >= 0) { 1.578 + if (this.profiles.has(uid)) { 1.579 + this.profiles.get(uid).destroy(); 1.580 + this.profiles.delete(uid); 1.581 + } 1.582 + uid -= 1; 1.583 + } 1.584 + } 1.585 + 1.586 + if (this.controller) { 1.587 + this.controller.destroy(); 1.588 + } 1.589 + 1.590 + this.isReady = null; 1.591 + this.window = null; 1.592 + this.document = null; 1.593 + this.target = null; 1.594 + this.controller = null; 1.595 + this.profiles = null; 1.596 + this._uid = null; 1.597 + this._activeUid = null; 1.598 + 1.599 + this.emit("destroyed"); 1.600 + } 1.601 +}; 1.602 + 1.603 +module.exports = ProfilerPanel;