diff options
Diffstat (limited to 'devtools/server/performance/profiler.js')
-rw-r--r-- | devtools/server/performance/profiler.js | 546 |
1 files changed, 546 insertions, 0 deletions
diff --git a/devtools/server/performance/profiler.js b/devtools/server/performance/profiler.js new file mode 100644 index 000000000..700d48147 --- /dev/null +++ b/devtools/server/performance/profiler.js @@ -0,0 +1,546 @@ +/* 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", "devtools/shared/task", 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); +} |