gecko-dev/devtools/server/performance/profiler.js

547 строки
17 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";
const { Cc, Ci, Cu } = require("chrome");
const Services = require("Services");
const { Class } = require("sdk/core/heritage");
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
loader.lazyRequireGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm", true);
loader.lazyRequireGetter(this, "Task", "resource://gre/modules/Task.jsm", true);
// Events piped from system observers to Profiler instances.
const PROFILER_SYSTEM_EVENTS = [
"console-api-profiler",
"profiler-started",
"profiler-stopped"
];
// How often the "profiler-status" is emitted by default
const BUFFER_STATUS_INTERVAL_DEFAULT = 5000; // ms
loader.lazyGetter(this, "nsIProfilerModule", () => {
return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
});
var DEFAULT_PROFILER_OPTIONS = {
// When using the DevTools Performance Tools, this will be overridden
// by the pref `devtools.performance.profiler.buffer-size`.
entries: Math.pow(10, 7),
// When using the DevTools Performance Tools, this will be overridden
// by the pref `devtools.performance.profiler.sample-rate-khz`.
interval: 1,
features: ["js"],
threadFilters: ["GeckoMain"]
};
/**
* Main interface for interacting with nsIProfiler
*/
const ProfilerManager = (function () {
let consumers = new Set();
return {
// How often the "profiler-status" is emitted
_profilerStatusInterval: BUFFER_STATUS_INTERVAL_DEFAULT,
// How many subscribers there
_profilerStatusSubscribers: 0,
/**
* The nsIProfiler is target agnostic and interacts with the whole platform.
* Therefore, special care needs to be given to make sure different profiler
* consumers (i.e. "toolboxes") don't interfere with each other. Register
* the profiler actor instances here.
*
* @param Profiler instance
* A profiler actor class.
*/
addInstance: function (instance) {
consumers.add(instance);
// Lazily register events
this.registerEventListeners();
},
/**
* Remove the profiler actor instances here.
*
* @param Profiler instance
* A profiler actor class.
*/
removeInstance: function (instance) {
consumers.delete(instance);
if (this.length < 0) {
let msg = "Somehow the number of started profilers is now negative.";
DevToolsUtils.reportException("Profiler", msg);
}
if (this.length === 0) {
this.unregisterEventListeners();
this.stop();
}
},
/**
* Starts the nsIProfiler module. Doing so will discard any samples
* that might have been accumulated so far.
*
* @param {number} entries [optional]
* @param {number} interval [optional]
* @param {Array<string>} features [optional]
* @param {Array<string>} threadFilters [description]
*
* @return {object}
*/
start: function (options = {}) {
let config = this._profilerStartOptions = {
entries: options.entries || DEFAULT_PROFILER_OPTIONS.entries,
interval: options.interval || DEFAULT_PROFILER_OPTIONS.interval,
features: options.features || DEFAULT_PROFILER_OPTIONS.features,
threadFilters: options.threadFilters || DEFAULT_PROFILER_OPTIONS.threadFilters,
};
// The start time should be before any samples we might be
// interested in.
let currentTime = nsIProfilerModule.getElapsedTime();
try {
nsIProfilerModule.StartProfiler(
config.entries,
config.interval,
config.features,
config.features.length,
config.threadFilters,
config.threadFilters.length
);
} catch (e) {
// For some reason, the profiler couldn't be started. This could happen,
// for example, when in private browsing mode.
Cu.reportError(`Could not start the profiler module: ${e.message}`);
return { started: false, reason: e, currentTime };
}
this._updateProfilerStatusPolling();
let { position, totalSize, generation } = this.getBufferInfo();
return { started: true, position, totalSize, generation, currentTime };
},
/**
* Attempts to stop the nsIProfiler module.
*/
stop: function () {
// Actually stop the profiler only if the last client has stopped profiling.
// Since this is used as a root actor, and the profiler module interacts
// with the whole platform, we need to avoid a case in which the profiler
// is stopped when there might be other clients still profiling.
if (this.length <= 1) {
nsIProfilerModule.StopProfiler();
}
this._updateProfilerStatusPolling();
return { started: false };
},
/**
* Returns all the samples accumulated since the profiler was started,
* along with the current time. The data has the following format:
* {
* libs: string,
* meta: {
* interval: number,
* platform: string,
* ...
* },
* threads: [{
* samples: [{
* frames: [{
* line: number,
* location: string,
* category: number
* } ... ],
* name: string
* responsiveness: number
* time: number
* } ... ]
* } ... ]
* }
*
*
* @param number startTime
* Since the circular buffer will only grow as long as the profiler lives,
* the buffer can contain unwanted samples. Pass in a `startTime` to only retrieve
* samples that took place after the `startTime`, with 0 being when the profiler
* just started.
* @param boolean stringify
* Whether or not the returned profile object should be a string or not to save
* JSON parse/stringify cycle if emitting over RDP.
*/
getProfile: function (options) {
let startTime = options.startTime || 0;
let profile = options.stringify ?
nsIProfilerModule.GetProfile(startTime) :
nsIProfilerModule.getProfileData(startTime);
return { profile: profile, currentTime: nsIProfilerModule.getElapsedTime() };
},
/**
* Returns an array of feature strings, describing the profiler features
* that are available on this platform. Can be called while the profiler
* is stopped.
*
* @return {object}
*/
getFeatures: function () {
return { features: nsIProfilerModule.GetFeatures([]) };
},
/**
* Returns an object with the values of the current status of the
* circular buffer in the profiler, returning `position`, `totalSize`,
* and the current `generation` of the buffer.
*
* @return {object}
*/
getBufferInfo: function() {
let position = {}, totalSize = {}, generation = {};
nsIProfilerModule.GetBufferInfo(position, totalSize, generation);
return {
position: position.value,
totalSize: totalSize.value,
generation: generation.value
}
},
/**
* Returns the configuration used that was originally passed in to start up the
* profiler. Used for tests, and does not account for others using nsIProfiler.
*
* @param {object}
*/
getStartOptions: function() {
return this._profilerStartOptions || {};
},
/**
* Verifies whether or not the nsIProfiler module has started.
* If already active, the current time is also returned.
*
* @return {object}
*/
isActive: function() {
let isActive = nsIProfilerModule.IsActive();
let elapsedTime = isActive ? nsIProfilerModule.getElapsedTime() : undefined;
let { position, totalSize, generation } = this.getBufferInfo();
return { isActive: isActive, currentTime: elapsedTime, position, totalSize, generation };
},
/**
* Returns a stringified JSON object that describes the shared libraries
* which are currently loaded into our process. Can be called while the
* profiler is stopped.
*/
getSharedLibraryInformation: function() {
return { sharedLibraryInformation: nsIProfilerModule.getSharedLibraryInformation() };
},
/**
* Number of profiler instances.
*
* @return {number}
*/
get length() {
return consumers.size;
},
/**
* Callback for all observed notifications.
* @param object subject
* @param string topic
* @param object data
*/
observe: sanitizeHandler(function (subject, topic, data) {
let details;
// An optional label may be specified when calling `console.profile`.
// If that's the case, stringify it and send it over with the response.
let { action, arguments: args } = subject || {};
let profileLabel = args && args.length > 0 ? `${args[0]}` : void 0;
// If the event was generated from `console.profile` or `console.profileEnd`
// we need to start the profiler right away and then just notify the client.
// Otherwise, we'll lose precious samples.
if (topic === "console-api-profiler" && (action === "profile" || action === "profileEnd")) {
let { isActive, currentTime } = this.isActive();
// Start the profiler only if it wasn't already active. Otherwise, any
// samples that might have been accumulated so far will be discarded.
if (!isActive && action === "profile") {
this.start();
details = { profileLabel, currentTime: 0 };
}
// Otherwise, if inactive and a call to profile end, do nothing
// and don't emit event.
else if (!isActive) {
return;
}
// Otherwise, the profiler is already active, so just send
// to the front the current time, label, and the notification
// adds the action as well.
details = { profileLabel, currentTime };
}
// Propagate the event to the profiler instances that
// are subscribed to this event.
this.emitEvent(topic, { subject, topic, data, details });
}, "ProfilerManager.observe"),
/**
* Registers handlers for the following events to be emitted
* on active Profiler instances:
* - "console-api-profiler"
* - "profiler-started"
* - "profiler-stopped"
* - "profiler-status"
*
* The ProfilerManager listens to all events, and individual
* consumers filter which events they are interested in.
*/
registerEventListeners: function () {
if (!this._eventsRegistered) {
PROFILER_SYSTEM_EVENTS.forEach(eventName =>
Services.obs.addObserver(this, eventName, false));
this._eventsRegistered = true;
}
},
/**
* Unregisters handlers for all system events.
*/
unregisterEventListeners: function () {
if (this._eventsRegistered) {
PROFILER_SYSTEM_EVENTS.forEach(eventName =>
Services.obs.removeObserver(this, eventName));
this._eventsRegistered = false;
}
},
/**
* Takes an event name and additional data and emits them
* through each profiler instance that is subscribed to the event.
*
* @param {string} eventName
* @param {object} data
*/
emitEvent: function (eventName, data) {
let subscribers = Array.from(consumers).filter(c => c.subscribedEvents.has(eventName));
for (let subscriber of subscribers) {
events.emit(subscriber, eventName, data);
}
},
/**
* Updates the frequency that the "profiler-status" event is emitted
* during recording.
*
* @param {number} interval
*/
setProfilerStatusInterval: function (interval) {
this._profilerStatusInterval = interval;
if (this._poller) {
this._poller._delayMs = interval;
}
},
subscribeToProfilerStatusEvents: function () {
this._profilerStatusSubscribers++;
this._updateProfilerStatusPolling();
},
unsubscribeToProfilerStatusEvents: function () {
this._profilerStatusSubscribers--;
this._updateProfilerStatusPolling();
},
/**
* Will enable or disable "profiler-status" events depending on
* if there are subscribers and if the profiler is current recording.
*/
_updateProfilerStatusPolling: function () {
if (this._profilerStatusSubscribers > 0 && nsIProfilerModule.IsActive()) {
if (!this._poller) {
this._poller = new DeferredTask(this._emitProfilerStatus.bind(this), this._profilerStatusInterval);
}
this._poller.arm();
}
// No subscribers; turn off if it exists.
else if (this._poller) {
this._poller.disarm();
}
},
_emitProfilerStatus: function () {
this.emitEvent("profiler-status", this.isActive());
this._poller.arm();
}
};
})();
/**
* The profiler actor provides remote access to the built-in nsIProfiler module.
*/
var Profiler = exports.Profiler = Class({
extends: EventTarget,
initialize: function () {
this.subscribedEvents = new Set();
ProfilerManager.addInstance(this);
},
destroy: function() {
this.unregisterEventNotifications({ events: Array.from(this.subscribedEvents) });
this.subscribedEvents = null;
ProfilerManager.removeInstance(this);
},
/**
* @see ProfilerManager.start
*/
start: function (options) { return ProfilerManager.start(options); },
/**
* @see ProfilerManager.stop
*/
stop: function () { return ProfilerManager.stop(); },
/**
* @see ProfilerManager.getProfile
*/
getProfile: function (request={}) { return ProfilerManager.getProfile(request); },
/**
* @see ProfilerManager.getFeatures
*/
getFeatures: function() { return ProfilerManager.getFeatures(); },
/**
* @see ProfilerManager.getBufferInfo
*/
getBufferInfo: function() { return ProfilerManager.getBufferInfo(); },
/**
* @see ProfilerManager.getStartOptions
*/
getStartOptions: function() { return ProfilerManager.getStartOptions(); },
/**
* @see ProfilerManager.isActive
*/
isActive: function() { return ProfilerManager.isActive(); },
/**
* @see ProfilerManager.isActive
*/
getSharedLibraryInformation: function() { return ProfilerManager.getSharedLibraryInformation(); },
/**
* @see ProfilerManager.setProfilerStatusInterval
*/
setProfilerStatusInterval: function(interval) { return ProfilerManager.setProfilerStatusInterval(interval); },
/**
* Subscribes this instance to one of several events defined in
* an events array.
* - "console-api-profiler",
* - "profiler-started",
* - "profiler-stopped"
* - "profiler-status"
*
* @param {Array<string>} data.event
* @return {object}
*/
registerEventNotifications: function(data={}) {
let response = [];
(data.events || []).forEach(e => {
if (!this.subscribedEvents.has(e)) {
if (e === "profiler-status") {
ProfilerManager.subscribeToProfilerStatusEvents();
}
this.subscribedEvents.add(e);
response.push(e);
}
});
return { registered: response };
},
/**
* Unsubscribes this instance to one of several events defined in
* an events array.
*
* @param {Array<string>} data.event
* @return {object}
*/
unregisterEventNotifications: function(data={}) {
let response = [];
(data.events || []).forEach(e => {
if (this.subscribedEvents.has(e)) {
if (e === "profiler-status") {
ProfilerManager.unsubscribeToProfilerStatusEvents();
}
this.subscribedEvents.delete(e);
response.push(e);
}
});
return { registered: response };
},
});
/**
* Checks whether or not the profiler module can currently run.
* @return boolean
*/
Profiler.canProfile = function() {
return nsIProfilerModule.CanProfile();
};
/**
* JSON.stringify callback used in Profiler.prototype.observe.
*/
function cycleBreaker(key, value) {
if (key == "wrappedJSObject") {
return undefined;
}
return value;
}
/**
* Create JSON objects suitable for transportation across the RDP,
* by breaking cycles and making a copy of the `subject` and `data` via
* JSON.stringifying those values with a replacer that omits properties
* known to introduce cycles, and then JSON.parsing the result.
* This spends some CPU cycles, but it's simple.
*
* @TODO Also wraps it in a `makeInfallible` -- is this still necessary?
*
* @param {function} handler
* @return {function}
*/
function sanitizeHandler (handler, identifier) {
return DevToolsUtils.makeInfallible(function (subject, topic, data) {
subject = (subject && !Cu.isXrayWrapper(subject) && subject.wrappedJSObject) || subject;
subject = JSON.parse(JSON.stringify(subject, cycleBreaker));
data = (data && !Cu.isXrayWrapper(data) && data.wrappedJSObject) || data;
data = JSON.parse(JSON.stringify(data, cycleBreaker));
// Pass in clean data to the underlying handler
return handler.call(this, subject, topic, data);
}, identifier);
}