gecko-dev/browser/devtools/profiler/controller.js

411 lines
11 KiB
JavaScript

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