1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/profiler/controller.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,411 @@ 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 +var isJSM = typeof require !== "function"; 1.11 + 1.12 +// This code is needed because, for whatever reason, mochitest can't 1.13 +// find any requirejs module so we have to load it old school way. :( 1.14 + 1.15 +if (isJSM) { 1.16 + var Cu = this["Components"].utils; 1.17 + let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils; 1.18 + this["loader"] = { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils) }; 1.19 + this["require"] = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; 1.20 +} else { 1.21 + var { Cu } = require("chrome"); 1.22 +} 1.23 + 1.24 +const { L10N_BUNDLE } = require("devtools/profiler/consts"); 1.25 + 1.26 +var EventEmitter = require("devtools/toolkit/event-emitter"); 1.27 + 1.28 +Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); 1.29 +Cu.import("resource://gre/modules/devtools/Console.jsm"); 1.30 +Cu.import("resource://gre/modules/AddonManager.jsm"); 1.31 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); 1.32 + 1.33 +loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE)); 1.34 + 1.35 +loader.lazyGetter(this, "gDevTools", 1.36 + () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools); 1.37 + 1.38 +loader.lazyGetter(this, "DebuggerServer", 1.39 + () => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer); 1.40 + 1.41 +/** 1.42 + * Data structure that contains information that has 1.43 + * to be shared between separate ProfilerController 1.44 + * instances. 1.45 + */ 1.46 +const sharedData = { 1.47 + data: new WeakMap(), 1.48 + controllers: new WeakMap(), 1.49 +}; 1.50 + 1.51 +/** 1.52 + * Makes a structure representing an individual profile. 1.53 + */ 1.54 +function makeProfile(name, def={}) { 1.55 + if (def.timeStarted == null) 1.56 + def.timeStarted = null; 1.57 + 1.58 + if (def.timeEnded == null) 1.59 + def.timeEnded = null; 1.60 + 1.61 + return { 1.62 + name: name, 1.63 + timeStarted: def.timeStarted, 1.64 + timeEnded: def.timeEnded, 1.65 + fromConsole: def.fromConsole || false 1.66 + }; 1.67 +} 1.68 + 1.69 +// Three functions below all operate with sharedData 1.70 +// structure defined above. They should be self-explanatory. 1.71 + 1.72 +function addTarget(target) { 1.73 + sharedData.data.set(target, new Map()); 1.74 +} 1.75 + 1.76 +function getProfiles(target) { 1.77 + return sharedData.data.get(target); 1.78 +} 1.79 + 1.80 +/** 1.81 + * Object to control the JavaScript Profiler over the remote 1.82 + * debugging protocol. 1.83 + * 1.84 + * @param Target target 1.85 + * A target object as defined in Target.jsm 1.86 + */ 1.87 +function ProfilerController(target) { 1.88 + if (sharedData.controllers.has(target)) { 1.89 + return sharedData.controllers.get(target); 1.90 + } 1.91 + 1.92 + this.target = target; 1.93 + this.client = target.client; 1.94 + this.isConnected = false; 1.95 + this.consoleProfiles = []; 1.96 + this.reservedNames = {}; 1.97 + 1.98 + addTarget(target); 1.99 + 1.100 + // Chrome debugging targets have already obtained a reference 1.101 + // to the profiler actor. 1.102 + if (target.chrome) { 1.103 + this.isConnected = true; 1.104 + this.actor = target.form.profilerActor; 1.105 + } 1.106 + 1.107 + sharedData.controllers.set(target, this); 1.108 + EventEmitter.decorate(this); 1.109 +}; 1.110 + 1.111 +ProfilerController.prototype = { 1.112 + target: null, 1.113 + client: null, 1.114 + isConnected: null, 1.115 + consoleProfiles: null, 1.116 + reservedNames: null, 1.117 + 1.118 + /** 1.119 + * Return a map of profile results for the current target. 1.120 + * 1.121 + * @return Map 1.122 + */ 1.123 + get profiles() { 1.124 + return getProfiles(this.target); 1.125 + }, 1.126 + 1.127 + /** 1.128 + * Checks whether the profile is currently recording. 1.129 + * 1.130 + * @param object profile 1.131 + * An object made by calling makeProfile function. 1.132 + * @return boolean 1.133 + */ 1.134 + isProfileRecording: function PC_isProfileRecording(profile) { 1.135 + return profile.timeStarted !== null && profile.timeEnded === null; 1.136 + }, 1.137 + 1.138 + getProfileName: function PC_getProfileName() { 1.139 + let num = 1; 1.140 + let name = L10N.getFormatStr("profiler.profileName", [num]); 1.141 + 1.142 + while (this.reservedNames[name]) { 1.143 + num += 1; 1.144 + name = L10N.getFormatStr("profiler.profileName", [num]); 1.145 + } 1.146 + 1.147 + this.reservedNames[name] = true; 1.148 + return name; 1.149 + }, 1.150 + 1.151 + /** 1.152 + * A listener that fires whenever console.profile or console.profileEnd 1.153 + * is called. 1.154 + * 1.155 + * @param string type 1.156 + * Type of a call. Either 'profile' or 'profileEnd'. 1.157 + * @param object data 1.158 + * Event data. 1.159 + */ 1.160 + onConsoleEvent: function (type, data) { 1.161 + let name = data.extra.name; 1.162 + 1.163 + let profileStart = () => { 1.164 + if (name && this.profiles.has(name)) 1.165 + return; 1.166 + 1.167 + // Add profile structure to shared data. 1.168 + let profile = makeProfile(name || this.getProfileName(), { 1.169 + timeStarted: data.extra.currentTime, 1.170 + fromConsole: true 1.171 + }); 1.172 + 1.173 + this.profiles.set(profile.name, profile); 1.174 + this.consoleProfiles.push(profile.name); 1.175 + this.emit("profileStart", profile); 1.176 + }; 1.177 + 1.178 + let profileEnd = () => { 1.179 + if (!name && !this.consoleProfiles.length) 1.180 + return; 1.181 + 1.182 + if (!name) 1.183 + name = this.consoleProfiles.pop(); 1.184 + else 1.185 + this.consoleProfiles.filter((n) => n !== name); 1.186 + 1.187 + if (!this.profiles.has(name)) 1.188 + return; 1.189 + 1.190 + let profile = this.profiles.get(name); 1.191 + if (!this.isProfileRecording(profile)) 1.192 + return; 1.193 + 1.194 + let profileData = data.extra.profile; 1.195 + profileData.threads = profileData.threads.map((thread) => { 1.196 + let samples = thread.samples.filter((sample) => { 1.197 + return sample.time >= profile.timeStarted; 1.198 + }); 1.199 + 1.200 + return { samples: samples }; 1.201 + }); 1.202 + 1.203 + profile.timeEnded = data.extra.currentTime; 1.204 + profile.data = profileData; 1.205 + 1.206 + this.emit("profileEnd", profile); 1.207 + }; 1.208 + 1.209 + if (type === "profile") 1.210 + profileStart(); 1.211 + 1.212 + if (type === "profileEnd") 1.213 + profileEnd(); 1.214 + }, 1.215 + 1.216 + /** 1.217 + * Connects to the client unless we're already connected. 1.218 + * 1.219 + * @param function cb 1.220 + * Function to be called once we're connected. If 1.221 + * the controller is already connected, this function 1.222 + * will be called immediately (synchronously). 1.223 + */ 1.224 + connect: function (cb=function(){}) { 1.225 + if (this.isConnected) { 1.226 + return void cb(); 1.227 + } 1.228 + 1.229 + // Check if we already have a grip to the listTabs response object 1.230 + // and, if we do, use it to get to the profilerActor. Otherwise, 1.231 + // call listTabs. The problem is that if we call listTabs twice 1.232 + // webconsole tests fail (see bug 872826). 1.233 + 1.234 + let register = () => { 1.235 + let data = { events: ["console-api-profiler"] }; 1.236 + 1.237 + // Check if Gecko Profiler Addon [1] is installed and, if it is, 1.238 + // don't register our own console event listeners. Gecko Profiler 1.239 + // Addon takes care of console.profile and console.profileEnd methods 1.240 + // and we don't want to break it. 1.241 + // 1.242 + // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/ 1.243 + 1.244 + AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => { 1.245 + if (addon && !addon.userDisabled && !addon.softDisabled) 1.246 + return void cb(); 1.247 + 1.248 + this.request("registerEventNotifications", data, (resp) => { 1.249 + this.client.addListener("eventNotification", (type, resp) => { 1.250 + let toolbox = gDevTools.getToolbox(this.target); 1.251 + if (toolbox == null) 1.252 + return; 1.253 + 1.254 + this.onConsoleEvent(resp.subject.action, resp.data); 1.255 + }); 1.256 + }); 1.257 + 1.258 + cb(); 1.259 + }); 1.260 + }; 1.261 + 1.262 + if (this.target.root) { 1.263 + this.actor = this.target.root.profilerActor; 1.264 + this.isConnected = true; 1.265 + return void register(); 1.266 + } 1.267 + 1.268 + this.client.listTabs((resp) => { 1.269 + this.actor = resp.profilerActor; 1.270 + this.isConnected = true; 1.271 + register(); 1.272 + }); 1.273 + }, 1.274 + 1.275 + /** 1.276 + * Adds actor and type information to data and sends the request over 1.277 + * the remote debugging protocol. 1.278 + * 1.279 + * @param string type 1.280 + * Method to call on the other side 1.281 + * @param object data 1.282 + * Data to send with the request 1.283 + * @param function cb 1.284 + * A callback function 1.285 + */ 1.286 + request: function (type, data, cb) { 1.287 + data.to = this.actor; 1.288 + data.type = type; 1.289 + this.client.request(data, cb); 1.290 + }, 1.291 + 1.292 + /** 1.293 + * Checks whether the profiler is active. 1.294 + * 1.295 + * @param function cb 1.296 + * Function to be called with a response from the 1.297 + * client. It will be called with two arguments: 1.298 + * an error object (may be null) and a boolean 1.299 + * value indicating if the profiler is active or not. 1.300 + */ 1.301 + isActive: function (cb) { 1.302 + this.request("isActive", {}, (resp) => { 1.303 + cb(resp.error, resp.isActive, resp.currentTime); 1.304 + }); 1.305 + }, 1.306 + 1.307 + /** 1.308 + * Creates a new profile and starts the profiler, if needed. 1.309 + * 1.310 + * @param string name 1.311 + * Name of the profile. 1.312 + * @param function cb 1.313 + * Function to be called once the profiler is started 1.314 + * or we get an error. It will be called with a single 1.315 + * argument: an error object (may be null). 1.316 + */ 1.317 + start: function PC_start(name, cb) { 1.318 + if (this.profiles.has(name)) { 1.319 + return; 1.320 + } 1.321 + 1.322 + let profile = makeProfile(name); 1.323 + this.consoleProfiles.push(name); 1.324 + this.profiles.set(name, profile); 1.325 + 1.326 + // If profile is already running, no need to do anything. 1.327 + if (this.isProfileRecording(profile)) { 1.328 + return void cb(); 1.329 + } 1.330 + 1.331 + this.isActive((err, isActive, currentTime) => { 1.332 + if (isActive) { 1.333 + profile.timeStarted = currentTime; 1.334 + return void cb(); 1.335 + } 1.336 + 1.337 + let params = { 1.338 + entries: 1000000, 1.339 + interval: 1, 1.340 + features: ["js"], 1.341 + }; 1.342 + 1.343 + this.request("startProfiler", params, (resp) => { 1.344 + if (resp.error) { 1.345 + return void cb(resp.error); 1.346 + } 1.347 + 1.348 + profile.timeStarted = 0; 1.349 + cb(); 1.350 + }); 1.351 + }); 1.352 + }, 1.353 + 1.354 + /** 1.355 + * Stops the profiler. NOTE, that we don't stop the actual 1.356 + * SPS Profiler here. It will be stopped as soon as all 1.357 + * clients disconnect from the profiler actor. 1.358 + * 1.359 + * @param string name 1.360 + * Name of the profile that needs to be stopped. 1.361 + * @param function cb 1.362 + * Function to be called once the profiler is stopped 1.363 + * or we get an error. It will be called with a single 1.364 + * argument: an error object (may be null). 1.365 + */ 1.366 + stop: function PC_stop(name, cb) { 1.367 + if (!this.profiles.has(name)) { 1.368 + return; 1.369 + } 1.370 + 1.371 + let profile = this.profiles.get(name); 1.372 + if (!this.isProfileRecording(profile)) { 1.373 + return; 1.374 + } 1.375 + 1.376 + this.request("getProfile", {}, (resp) => { 1.377 + if (resp.error) { 1.378 + Cu.reportError("Failed to fetch profile data."); 1.379 + return void cb(resp.error, null); 1.380 + } 1.381 + 1.382 + let data = resp.profile; 1.383 + profile.timeEnded = resp.currentTime; 1.384 + 1.385 + // Filter out all samples that fall out of current 1.386 + // profile's range. 1.387 + 1.388 + data.threads = data.threads.map((thread) => { 1.389 + let samples = thread.samples.filter((sample) => { 1.390 + return sample.time >= profile.timeStarted; 1.391 + }); 1.392 + 1.393 + return { samples: samples }; 1.394 + }); 1.395 + 1.396 + cb(null, data); 1.397 + }); 1.398 + }, 1.399 + 1.400 + /** 1.401 + * Cleanup. 1.402 + */ 1.403 + destroy: function PC_destroy() { 1.404 + this.client = null; 1.405 + this.target = null; 1.406 + this.actor = null; 1.407 + } 1.408 +}; 1.409 + 1.410 +if (isJSM) { 1.411 + var EXPORTED_SYMBOLS = ["ProfilerController"]; 1.412 +} else { 1.413 + module.exports = ProfilerController; 1.414 +} 1.415 \ No newline at end of file