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: var isJSM = typeof require !== "function"; michael@0: michael@0: // This code is needed because, for whatever reason, mochitest can't michael@0: // find any requirejs module so we have to load it old school way. :( michael@0: michael@0: if (isJSM) { michael@0: var Cu = this["Components"].utils; michael@0: let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils; michael@0: this["loader"] = { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils) }; michael@0: this["require"] = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; michael@0: } else { michael@0: var { Cu } = require("chrome"); michael@0: } michael@0: michael@0: const { L10N_BUNDLE } = require("devtools/profiler/consts"); michael@0: michael@0: var EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: michael@0: Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/Console.jsm"); michael@0: Cu.import("resource://gre/modules/AddonManager.jsm"); michael@0: Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); michael@0: michael@0: loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE)); michael@0: michael@0: loader.lazyGetter(this, "gDevTools", michael@0: () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools); michael@0: michael@0: loader.lazyGetter(this, "DebuggerServer", michael@0: () => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer); michael@0: michael@0: /** michael@0: * Data structure that contains information that has michael@0: * to be shared between separate ProfilerController michael@0: * instances. michael@0: */ michael@0: const sharedData = { michael@0: data: new WeakMap(), michael@0: controllers: new WeakMap(), michael@0: }; michael@0: michael@0: /** michael@0: * Makes a structure representing an individual profile. michael@0: */ michael@0: function makeProfile(name, def={}) { michael@0: if (def.timeStarted == null) michael@0: def.timeStarted = null; michael@0: michael@0: if (def.timeEnded == null) michael@0: def.timeEnded = null; michael@0: michael@0: return { michael@0: name: name, michael@0: timeStarted: def.timeStarted, michael@0: timeEnded: def.timeEnded, michael@0: fromConsole: def.fromConsole || false michael@0: }; michael@0: } michael@0: michael@0: // Three functions below all operate with sharedData michael@0: // structure defined above. They should be self-explanatory. michael@0: michael@0: function addTarget(target) { michael@0: sharedData.data.set(target, new Map()); michael@0: } michael@0: michael@0: function getProfiles(target) { michael@0: return sharedData.data.get(target); michael@0: } michael@0: michael@0: /** michael@0: * Object to control the JavaScript Profiler over the remote michael@0: * debugging protocol. michael@0: * michael@0: * @param Target target michael@0: * A target object as defined in Target.jsm michael@0: */ michael@0: function ProfilerController(target) { michael@0: if (sharedData.controllers.has(target)) { michael@0: return sharedData.controllers.get(target); michael@0: } michael@0: michael@0: this.target = target; michael@0: this.client = target.client; michael@0: this.isConnected = false; michael@0: this.consoleProfiles = []; michael@0: this.reservedNames = {}; michael@0: michael@0: addTarget(target); michael@0: michael@0: // Chrome debugging targets have already obtained a reference michael@0: // to the profiler actor. michael@0: if (target.chrome) { michael@0: this.isConnected = true; michael@0: this.actor = target.form.profilerActor; michael@0: } michael@0: michael@0: sharedData.controllers.set(target, this); michael@0: EventEmitter.decorate(this); michael@0: }; michael@0: michael@0: ProfilerController.prototype = { michael@0: target: null, michael@0: client: null, michael@0: isConnected: null, michael@0: consoleProfiles: null, michael@0: reservedNames: null, michael@0: michael@0: /** michael@0: * Return a map of profile results for the current target. michael@0: * michael@0: * @return Map michael@0: */ michael@0: get profiles() { michael@0: return getProfiles(this.target); michael@0: }, michael@0: michael@0: /** michael@0: * Checks whether the profile is currently recording. michael@0: * michael@0: * @param object profile michael@0: * An object made by calling makeProfile function. michael@0: * @return boolean michael@0: */ michael@0: isProfileRecording: function PC_isProfileRecording(profile) { michael@0: return profile.timeStarted !== null && profile.timeEnded === null; michael@0: }, michael@0: michael@0: getProfileName: function PC_getProfileName() { michael@0: let num = 1; michael@0: let name = L10N.getFormatStr("profiler.profileName", [num]); michael@0: michael@0: while (this.reservedNames[name]) { michael@0: num += 1; michael@0: name = L10N.getFormatStr("profiler.profileName", [num]); michael@0: } michael@0: michael@0: this.reservedNames[name] = true; michael@0: return name; michael@0: }, michael@0: michael@0: /** michael@0: * A listener that fires whenever console.profile or console.profileEnd michael@0: * is called. michael@0: * michael@0: * @param string type michael@0: * Type of a call. Either 'profile' or 'profileEnd'. michael@0: * @param object data michael@0: * Event data. michael@0: */ michael@0: onConsoleEvent: function (type, data) { michael@0: let name = data.extra.name; michael@0: michael@0: let profileStart = () => { michael@0: if (name && this.profiles.has(name)) michael@0: return; michael@0: michael@0: // Add profile structure to shared data. michael@0: let profile = makeProfile(name || this.getProfileName(), { michael@0: timeStarted: data.extra.currentTime, michael@0: fromConsole: true michael@0: }); michael@0: michael@0: this.profiles.set(profile.name, profile); michael@0: this.consoleProfiles.push(profile.name); michael@0: this.emit("profileStart", profile); michael@0: }; michael@0: michael@0: let profileEnd = () => { michael@0: if (!name && !this.consoleProfiles.length) michael@0: return; michael@0: michael@0: if (!name) michael@0: name = this.consoleProfiles.pop(); michael@0: else michael@0: this.consoleProfiles.filter((n) => n !== name); michael@0: michael@0: if (!this.profiles.has(name)) michael@0: return; michael@0: michael@0: let profile = this.profiles.get(name); michael@0: if (!this.isProfileRecording(profile)) michael@0: return; michael@0: michael@0: let profileData = data.extra.profile; michael@0: profileData.threads = profileData.threads.map((thread) => { michael@0: let samples = thread.samples.filter((sample) => { michael@0: return sample.time >= profile.timeStarted; michael@0: }); michael@0: michael@0: return { samples: samples }; michael@0: }); michael@0: michael@0: profile.timeEnded = data.extra.currentTime; michael@0: profile.data = profileData; michael@0: michael@0: this.emit("profileEnd", profile); michael@0: }; michael@0: michael@0: if (type === "profile") michael@0: profileStart(); michael@0: michael@0: if (type === "profileEnd") michael@0: profileEnd(); michael@0: }, michael@0: michael@0: /** michael@0: * Connects to the client unless we're already connected. michael@0: * michael@0: * @param function cb michael@0: * Function to be called once we're connected. If michael@0: * the controller is already connected, this function michael@0: * will be called immediately (synchronously). michael@0: */ michael@0: connect: function (cb=function(){}) { michael@0: if (this.isConnected) { michael@0: return void cb(); michael@0: } michael@0: michael@0: // Check if we already have a grip to the listTabs response object michael@0: // and, if we do, use it to get to the profilerActor. Otherwise, michael@0: // call listTabs. The problem is that if we call listTabs twice michael@0: // webconsole tests fail (see bug 872826). michael@0: michael@0: let register = () => { michael@0: let data = { events: ["console-api-profiler"] }; michael@0: michael@0: // Check if Gecko Profiler Addon [1] is installed and, if it is, michael@0: // don't register our own console event listeners. Gecko Profiler michael@0: // Addon takes care of console.profile and console.profileEnd methods michael@0: // and we don't want to break it. michael@0: // michael@0: // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/ michael@0: michael@0: AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => { michael@0: if (addon && !addon.userDisabled && !addon.softDisabled) michael@0: return void cb(); michael@0: michael@0: this.request("registerEventNotifications", data, (resp) => { michael@0: this.client.addListener("eventNotification", (type, resp) => { michael@0: let toolbox = gDevTools.getToolbox(this.target); michael@0: if (toolbox == null) michael@0: return; michael@0: michael@0: this.onConsoleEvent(resp.subject.action, resp.data); michael@0: }); michael@0: }); michael@0: michael@0: cb(); michael@0: }); michael@0: }; michael@0: michael@0: if (this.target.root) { michael@0: this.actor = this.target.root.profilerActor; michael@0: this.isConnected = true; michael@0: return void register(); michael@0: } michael@0: michael@0: this.client.listTabs((resp) => { michael@0: this.actor = resp.profilerActor; michael@0: this.isConnected = true; michael@0: register(); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Adds actor and type information to data and sends the request over michael@0: * the remote debugging protocol. michael@0: * michael@0: * @param string type michael@0: * Method to call on the other side michael@0: * @param object data michael@0: * Data to send with the request michael@0: * @param function cb michael@0: * A callback function michael@0: */ michael@0: request: function (type, data, cb) { michael@0: data.to = this.actor; michael@0: data.type = type; michael@0: this.client.request(data, cb); michael@0: }, michael@0: michael@0: /** michael@0: * Checks whether the profiler is active. michael@0: * michael@0: * @param function cb michael@0: * Function to be called with a response from the michael@0: * client. It will be called with two arguments: michael@0: * an error object (may be null) and a boolean michael@0: * value indicating if the profiler is active or not. michael@0: */ michael@0: isActive: function (cb) { michael@0: this.request("isActive", {}, (resp) => { michael@0: cb(resp.error, resp.isActive, resp.currentTime); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Creates a new profile and starts the profiler, if needed. michael@0: * michael@0: * @param string name michael@0: * Name of the profile. michael@0: * @param function cb michael@0: * Function to be called once the profiler is started michael@0: * or we get an error. It will be called with a single michael@0: * argument: an error object (may be null). michael@0: */ michael@0: start: function PC_start(name, cb) { michael@0: if (this.profiles.has(name)) { michael@0: return; michael@0: } michael@0: michael@0: let profile = makeProfile(name); michael@0: this.consoleProfiles.push(name); michael@0: this.profiles.set(name, profile); michael@0: michael@0: // If profile is already running, no need to do anything. michael@0: if (this.isProfileRecording(profile)) { michael@0: return void cb(); michael@0: } michael@0: michael@0: this.isActive((err, isActive, currentTime) => { michael@0: if (isActive) { michael@0: profile.timeStarted = currentTime; michael@0: return void cb(); michael@0: } michael@0: michael@0: let params = { michael@0: entries: 1000000, michael@0: interval: 1, michael@0: features: ["js"], michael@0: }; michael@0: michael@0: this.request("startProfiler", params, (resp) => { michael@0: if (resp.error) { michael@0: return void cb(resp.error); michael@0: } michael@0: michael@0: profile.timeStarted = 0; michael@0: cb(); michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Stops the profiler. NOTE, that we don't stop the actual michael@0: * SPS Profiler here. It will be stopped as soon as all michael@0: * clients disconnect from the profiler actor. michael@0: * michael@0: * @param string name michael@0: * Name of the profile that needs to be stopped. michael@0: * @param function cb michael@0: * Function to be called once the profiler is stopped michael@0: * or we get an error. It will be called with a single michael@0: * argument: an error object (may be null). michael@0: */ michael@0: stop: function PC_stop(name, cb) { michael@0: if (!this.profiles.has(name)) { michael@0: return; michael@0: } michael@0: michael@0: let profile = this.profiles.get(name); michael@0: if (!this.isProfileRecording(profile)) { michael@0: return; michael@0: } michael@0: michael@0: this.request("getProfile", {}, (resp) => { michael@0: if (resp.error) { michael@0: Cu.reportError("Failed to fetch profile data."); michael@0: return void cb(resp.error, null); michael@0: } michael@0: michael@0: let data = resp.profile; michael@0: profile.timeEnded = resp.currentTime; michael@0: michael@0: // Filter out all samples that fall out of current michael@0: // profile's range. michael@0: michael@0: data.threads = data.threads.map((thread) => { michael@0: let samples = thread.samples.filter((sample) => { michael@0: return sample.time >= profile.timeStarted; michael@0: }); michael@0: michael@0: return { samples: samples }; michael@0: }); michael@0: michael@0: cb(null, data); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Cleanup. michael@0: */ michael@0: destroy: function PC_destroy() { michael@0: this.client = null; michael@0: this.target = null; michael@0: this.actor = null; michael@0: } michael@0: }; michael@0: michael@0: if (isJSM) { michael@0: var EXPORTED_SYMBOLS = ["ProfilerController"]; michael@0: } else { michael@0: module.exports = ProfilerController; michael@0: }