browser/devtools/profiler/controller.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.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 var isJSM = typeof require !== "function";
michael@0 8
michael@0 9 // This code is needed because, for whatever reason, mochitest can't
michael@0 10 // find any requirejs module so we have to load it old school way. :(
michael@0 11
michael@0 12 if (isJSM) {
michael@0 13 var Cu = this["Components"].utils;
michael@0 14 let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils;
michael@0 15 this["loader"] = { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils) };
michael@0 16 this["require"] = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
michael@0 17 } else {
michael@0 18 var { Cu } = require("chrome");
michael@0 19 }
michael@0 20
michael@0 21 const { L10N_BUNDLE } = require("devtools/profiler/consts");
michael@0 22
michael@0 23 var EventEmitter = require("devtools/toolkit/event-emitter");
michael@0 24
michael@0 25 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
michael@0 26 Cu.import("resource://gre/modules/devtools/Console.jsm");
michael@0 27 Cu.import("resource://gre/modules/AddonManager.jsm");
michael@0 28 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
michael@0 29
michael@0 30 loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
michael@0 31
michael@0 32 loader.lazyGetter(this, "gDevTools",
michael@0 33 () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
michael@0 34
michael@0 35 loader.lazyGetter(this, "DebuggerServer",
michael@0 36 () => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer);
michael@0 37
michael@0 38 /**
michael@0 39 * Data structure that contains information that has
michael@0 40 * to be shared between separate ProfilerController
michael@0 41 * instances.
michael@0 42 */
michael@0 43 const sharedData = {
michael@0 44 data: new WeakMap(),
michael@0 45 controllers: new WeakMap(),
michael@0 46 };
michael@0 47
michael@0 48 /**
michael@0 49 * Makes a structure representing an individual profile.
michael@0 50 */
michael@0 51 function makeProfile(name, def={}) {
michael@0 52 if (def.timeStarted == null)
michael@0 53 def.timeStarted = null;
michael@0 54
michael@0 55 if (def.timeEnded == null)
michael@0 56 def.timeEnded = null;
michael@0 57
michael@0 58 return {
michael@0 59 name: name,
michael@0 60 timeStarted: def.timeStarted,
michael@0 61 timeEnded: def.timeEnded,
michael@0 62 fromConsole: def.fromConsole || false
michael@0 63 };
michael@0 64 }
michael@0 65
michael@0 66 // Three functions below all operate with sharedData
michael@0 67 // structure defined above. They should be self-explanatory.
michael@0 68
michael@0 69 function addTarget(target) {
michael@0 70 sharedData.data.set(target, new Map());
michael@0 71 }
michael@0 72
michael@0 73 function getProfiles(target) {
michael@0 74 return sharedData.data.get(target);
michael@0 75 }
michael@0 76
michael@0 77 /**
michael@0 78 * Object to control the JavaScript Profiler over the remote
michael@0 79 * debugging protocol.
michael@0 80 *
michael@0 81 * @param Target target
michael@0 82 * A target object as defined in Target.jsm
michael@0 83 */
michael@0 84 function ProfilerController(target) {
michael@0 85 if (sharedData.controllers.has(target)) {
michael@0 86 return sharedData.controllers.get(target);
michael@0 87 }
michael@0 88
michael@0 89 this.target = target;
michael@0 90 this.client = target.client;
michael@0 91 this.isConnected = false;
michael@0 92 this.consoleProfiles = [];
michael@0 93 this.reservedNames = {};
michael@0 94
michael@0 95 addTarget(target);
michael@0 96
michael@0 97 // Chrome debugging targets have already obtained a reference
michael@0 98 // to the profiler actor.
michael@0 99 if (target.chrome) {
michael@0 100 this.isConnected = true;
michael@0 101 this.actor = target.form.profilerActor;
michael@0 102 }
michael@0 103
michael@0 104 sharedData.controllers.set(target, this);
michael@0 105 EventEmitter.decorate(this);
michael@0 106 };
michael@0 107
michael@0 108 ProfilerController.prototype = {
michael@0 109 target: null,
michael@0 110 client: null,
michael@0 111 isConnected: null,
michael@0 112 consoleProfiles: null,
michael@0 113 reservedNames: null,
michael@0 114
michael@0 115 /**
michael@0 116 * Return a map of profile results for the current target.
michael@0 117 *
michael@0 118 * @return Map
michael@0 119 */
michael@0 120 get profiles() {
michael@0 121 return getProfiles(this.target);
michael@0 122 },
michael@0 123
michael@0 124 /**
michael@0 125 * Checks whether the profile is currently recording.
michael@0 126 *
michael@0 127 * @param object profile
michael@0 128 * An object made by calling makeProfile function.
michael@0 129 * @return boolean
michael@0 130 */
michael@0 131 isProfileRecording: function PC_isProfileRecording(profile) {
michael@0 132 return profile.timeStarted !== null && profile.timeEnded === null;
michael@0 133 },
michael@0 134
michael@0 135 getProfileName: function PC_getProfileName() {
michael@0 136 let num = 1;
michael@0 137 let name = L10N.getFormatStr("profiler.profileName", [num]);
michael@0 138
michael@0 139 while (this.reservedNames[name]) {
michael@0 140 num += 1;
michael@0 141 name = L10N.getFormatStr("profiler.profileName", [num]);
michael@0 142 }
michael@0 143
michael@0 144 this.reservedNames[name] = true;
michael@0 145 return name;
michael@0 146 },
michael@0 147
michael@0 148 /**
michael@0 149 * A listener that fires whenever console.profile or console.profileEnd
michael@0 150 * is called.
michael@0 151 *
michael@0 152 * @param string type
michael@0 153 * Type of a call. Either 'profile' or 'profileEnd'.
michael@0 154 * @param object data
michael@0 155 * Event data.
michael@0 156 */
michael@0 157 onConsoleEvent: function (type, data) {
michael@0 158 let name = data.extra.name;
michael@0 159
michael@0 160 let profileStart = () => {
michael@0 161 if (name && this.profiles.has(name))
michael@0 162 return;
michael@0 163
michael@0 164 // Add profile structure to shared data.
michael@0 165 let profile = makeProfile(name || this.getProfileName(), {
michael@0 166 timeStarted: data.extra.currentTime,
michael@0 167 fromConsole: true
michael@0 168 });
michael@0 169
michael@0 170 this.profiles.set(profile.name, profile);
michael@0 171 this.consoleProfiles.push(profile.name);
michael@0 172 this.emit("profileStart", profile);
michael@0 173 };
michael@0 174
michael@0 175 let profileEnd = () => {
michael@0 176 if (!name && !this.consoleProfiles.length)
michael@0 177 return;
michael@0 178
michael@0 179 if (!name)
michael@0 180 name = this.consoleProfiles.pop();
michael@0 181 else
michael@0 182 this.consoleProfiles.filter((n) => n !== name);
michael@0 183
michael@0 184 if (!this.profiles.has(name))
michael@0 185 return;
michael@0 186
michael@0 187 let profile = this.profiles.get(name);
michael@0 188 if (!this.isProfileRecording(profile))
michael@0 189 return;
michael@0 190
michael@0 191 let profileData = data.extra.profile;
michael@0 192 profileData.threads = profileData.threads.map((thread) => {
michael@0 193 let samples = thread.samples.filter((sample) => {
michael@0 194 return sample.time >= profile.timeStarted;
michael@0 195 });
michael@0 196
michael@0 197 return { samples: samples };
michael@0 198 });
michael@0 199
michael@0 200 profile.timeEnded = data.extra.currentTime;
michael@0 201 profile.data = profileData;
michael@0 202
michael@0 203 this.emit("profileEnd", profile);
michael@0 204 };
michael@0 205
michael@0 206 if (type === "profile")
michael@0 207 profileStart();
michael@0 208
michael@0 209 if (type === "profileEnd")
michael@0 210 profileEnd();
michael@0 211 },
michael@0 212
michael@0 213 /**
michael@0 214 * Connects to the client unless we're already connected.
michael@0 215 *
michael@0 216 * @param function cb
michael@0 217 * Function to be called once we're connected. If
michael@0 218 * the controller is already connected, this function
michael@0 219 * will be called immediately (synchronously).
michael@0 220 */
michael@0 221 connect: function (cb=function(){}) {
michael@0 222 if (this.isConnected) {
michael@0 223 return void cb();
michael@0 224 }
michael@0 225
michael@0 226 // Check if we already have a grip to the listTabs response object
michael@0 227 // and, if we do, use it to get to the profilerActor. Otherwise,
michael@0 228 // call listTabs. The problem is that if we call listTabs twice
michael@0 229 // webconsole tests fail (see bug 872826).
michael@0 230
michael@0 231 let register = () => {
michael@0 232 let data = { events: ["console-api-profiler"] };
michael@0 233
michael@0 234 // Check if Gecko Profiler Addon [1] is installed and, if it is,
michael@0 235 // don't register our own console event listeners. Gecko Profiler
michael@0 236 // Addon takes care of console.profile and console.profileEnd methods
michael@0 237 // and we don't want to break it.
michael@0 238 //
michael@0 239 // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/
michael@0 240
michael@0 241 AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => {
michael@0 242 if (addon && !addon.userDisabled && !addon.softDisabled)
michael@0 243 return void cb();
michael@0 244
michael@0 245 this.request("registerEventNotifications", data, (resp) => {
michael@0 246 this.client.addListener("eventNotification", (type, resp) => {
michael@0 247 let toolbox = gDevTools.getToolbox(this.target);
michael@0 248 if (toolbox == null)
michael@0 249 return;
michael@0 250
michael@0 251 this.onConsoleEvent(resp.subject.action, resp.data);
michael@0 252 });
michael@0 253 });
michael@0 254
michael@0 255 cb();
michael@0 256 });
michael@0 257 };
michael@0 258
michael@0 259 if (this.target.root) {
michael@0 260 this.actor = this.target.root.profilerActor;
michael@0 261 this.isConnected = true;
michael@0 262 return void register();
michael@0 263 }
michael@0 264
michael@0 265 this.client.listTabs((resp) => {
michael@0 266 this.actor = resp.profilerActor;
michael@0 267 this.isConnected = true;
michael@0 268 register();
michael@0 269 });
michael@0 270 },
michael@0 271
michael@0 272 /**
michael@0 273 * Adds actor and type information to data and sends the request over
michael@0 274 * the remote debugging protocol.
michael@0 275 *
michael@0 276 * @param string type
michael@0 277 * Method to call on the other side
michael@0 278 * @param object data
michael@0 279 * Data to send with the request
michael@0 280 * @param function cb
michael@0 281 * A callback function
michael@0 282 */
michael@0 283 request: function (type, data, cb) {
michael@0 284 data.to = this.actor;
michael@0 285 data.type = type;
michael@0 286 this.client.request(data, cb);
michael@0 287 },
michael@0 288
michael@0 289 /**
michael@0 290 * Checks whether the profiler is active.
michael@0 291 *
michael@0 292 * @param function cb
michael@0 293 * Function to be called with a response from the
michael@0 294 * client. It will be called with two arguments:
michael@0 295 * an error object (may be null) and a boolean
michael@0 296 * value indicating if the profiler is active or not.
michael@0 297 */
michael@0 298 isActive: function (cb) {
michael@0 299 this.request("isActive", {}, (resp) => {
michael@0 300 cb(resp.error, resp.isActive, resp.currentTime);
michael@0 301 });
michael@0 302 },
michael@0 303
michael@0 304 /**
michael@0 305 * Creates a new profile and starts the profiler, if needed.
michael@0 306 *
michael@0 307 * @param string name
michael@0 308 * Name of the profile.
michael@0 309 * @param function cb
michael@0 310 * Function to be called once the profiler is started
michael@0 311 * or we get an error. It will be called with a single
michael@0 312 * argument: an error object (may be null).
michael@0 313 */
michael@0 314 start: function PC_start(name, cb) {
michael@0 315 if (this.profiles.has(name)) {
michael@0 316 return;
michael@0 317 }
michael@0 318
michael@0 319 let profile = makeProfile(name);
michael@0 320 this.consoleProfiles.push(name);
michael@0 321 this.profiles.set(name, profile);
michael@0 322
michael@0 323 // If profile is already running, no need to do anything.
michael@0 324 if (this.isProfileRecording(profile)) {
michael@0 325 return void cb();
michael@0 326 }
michael@0 327
michael@0 328 this.isActive((err, isActive, currentTime) => {
michael@0 329 if (isActive) {
michael@0 330 profile.timeStarted = currentTime;
michael@0 331 return void cb();
michael@0 332 }
michael@0 333
michael@0 334 let params = {
michael@0 335 entries: 1000000,
michael@0 336 interval: 1,
michael@0 337 features: ["js"],
michael@0 338 };
michael@0 339
michael@0 340 this.request("startProfiler", params, (resp) => {
michael@0 341 if (resp.error) {
michael@0 342 return void cb(resp.error);
michael@0 343 }
michael@0 344
michael@0 345 profile.timeStarted = 0;
michael@0 346 cb();
michael@0 347 });
michael@0 348 });
michael@0 349 },
michael@0 350
michael@0 351 /**
michael@0 352 * Stops the profiler. NOTE, that we don't stop the actual
michael@0 353 * SPS Profiler here. It will be stopped as soon as all
michael@0 354 * clients disconnect from the profiler actor.
michael@0 355 *
michael@0 356 * @param string name
michael@0 357 * Name of the profile that needs to be stopped.
michael@0 358 * @param function cb
michael@0 359 * Function to be called once the profiler is stopped
michael@0 360 * or we get an error. It will be called with a single
michael@0 361 * argument: an error object (may be null).
michael@0 362 */
michael@0 363 stop: function PC_stop(name, cb) {
michael@0 364 if (!this.profiles.has(name)) {
michael@0 365 return;
michael@0 366 }
michael@0 367
michael@0 368 let profile = this.profiles.get(name);
michael@0 369 if (!this.isProfileRecording(profile)) {
michael@0 370 return;
michael@0 371 }
michael@0 372
michael@0 373 this.request("getProfile", {}, (resp) => {
michael@0 374 if (resp.error) {
michael@0 375 Cu.reportError("Failed to fetch profile data.");
michael@0 376 return void cb(resp.error, null);
michael@0 377 }
michael@0 378
michael@0 379 let data = resp.profile;
michael@0 380 profile.timeEnded = resp.currentTime;
michael@0 381
michael@0 382 // Filter out all samples that fall out of current
michael@0 383 // profile's range.
michael@0 384
michael@0 385 data.threads = data.threads.map((thread) => {
michael@0 386 let samples = thread.samples.filter((sample) => {
michael@0 387 return sample.time >= profile.timeStarted;
michael@0 388 });
michael@0 389
michael@0 390 return { samples: samples };
michael@0 391 });
michael@0 392
michael@0 393 cb(null, data);
michael@0 394 });
michael@0 395 },
michael@0 396
michael@0 397 /**
michael@0 398 * Cleanup.
michael@0 399 */
michael@0 400 destroy: function PC_destroy() {
michael@0 401 this.client = null;
michael@0 402 this.target = null;
michael@0 403 this.actor = null;
michael@0 404 }
michael@0 405 };
michael@0 406
michael@0 407 if (isJSM) {
michael@0 408 var EXPORTED_SYMBOLS = ["ProfilerController"];
michael@0 409 } else {
michael@0 410 module.exports = ProfilerController;
michael@0 411 }

mercurial