summaryrefslogtreecommitdiffstats
path: root/devtools/server/performance/profiler.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/performance/profiler.js')
-rw-r--r--devtools/server/performance/profiler.js604
1 files changed, 604 insertions, 0 deletions
diff --git a/devtools/server/performance/profiler.js b/devtools/server/performance/profiler.js
new file mode 100644
index 0000000000..3d4d02e5df
--- /dev/null
+++ b/devtools/server/performance/profiler.js
@@ -0,0 +1,604 @@
+/* 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 { Cu } = require("chrome");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(
+ this,
+ "DevToolsUtils",
+ "devtools/shared/DevToolsUtils"
+);
+loader.lazyRequireGetter(
+ this,
+ "DeferredTask",
+ "resource://gre/modules/DeferredTask.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 (in ms)
+const BUFFER_STATUS_INTERVAL_DEFAULT = 5000;
+
+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-frequency-hz`.
+ interval: 1,
+ features: ["js"],
+ threadFilters: ["GeckoMain"],
+};
+
+/**
+ * Main interface for interacting with nsIProfiler
+ */
+const ProfilerManager = (function() {
+ const consumers = new Set();
+
+ return {
+ // How often the "profiler-status" is emitted
+ _profilerStatusInterval: BUFFER_STATUS_INTERVAL_DEFAULT,
+
+ // How many subscribers there
+ _profilerStatusSubscribers: 0,
+
+ // Has the profiler ever been started by the actor?
+ started: false,
+
+ /**
+ * 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) {
+ const 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 = {}) {
+ const 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.
+ const currentTime = Services.profiler.getElapsedTime();
+
+ try {
+ Services.profiler.StartProfiler(
+ config.entries,
+ config.interval,
+ config.features,
+ config.threadFilters
+ );
+ } 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.started = true;
+
+ this._updateProfilerStatusPolling();
+
+ const { 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.
+ // Also check for `started` to only stop the profiler when the actor
+ // actually started it. This is to prevent stopping the profiler initiated
+ // by some other code, like Talos.
+ if (this.length <= 1 && this.started) {
+ Services.profiler.StopProfiler();
+ this.started = false;
+ }
+ 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) {
+ const startTime = options.startTime || 0;
+ const profile = options.stringify
+ ? Services.profiler.GetProfile(startTime)
+ : Services.profiler.getProfileData(startTime);
+
+ return {
+ profile: profile,
+ currentTime: Services.profiler.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: Services.profiler.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() {
+ const position = {},
+ totalSize = {},
+ generation = {};
+ Services.profiler.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() {
+ const isActive = Services.profiler.IsActive();
+ const elapsedTime = isActive
+ ? Services.profiler.getElapsedTime()
+ : undefined;
+ const { position, totalSize, generation } = this.getBufferInfo();
+ return {
+ isActive,
+ currentTime: elapsedTime,
+ position,
+ totalSize,
+ generation,
+ };
+ },
+
+ /**
+ * Returns an array of objects that describes the shared libraries
+ * which are currently loaded into our process. Can be called while the
+ * profiler is stopped.
+ */
+ get sharedLibraries() {
+ return {
+ sharedLibraries: Services.profiler.sharedLibraries,
+ };
+ },
+
+ /**
+ * 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.
+ const { action, arguments: args } = subject || {};
+ const 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")
+ ) {
+ const { 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 };
+ } else if (!isActive) {
+ // Otherwise, if inactive and a call to profile end, do nothing
+ // and don't emit event.
+ 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)
+ );
+ 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) {
+ const subscribers = Array.from(consumers).filter(c => {
+ return c.subscribedEvents.has(eventName);
+ });
+
+ for (const subscriber of subscribers) {
+ subscriber.emit(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 && Services.profiler.IsActive()) {
+ if (!this._poller) {
+ this._poller = new DeferredTask(
+ this._emitProfilerStatus.bind(this),
+ this._profilerStatusInterval,
+ 0
+ );
+ }
+ this._poller.arm();
+ } else if (this._poller) {
+ // No subscribers; turn off if it exists.
+ 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.
+ */
+class Profiler {
+ constructor() {
+ EventEmitter.decorate(this);
+
+ this.subscribedEvents = new Set();
+ ProfilerManager.addInstance(this);
+ }
+
+ destroy() {
+ this.unregisterEventNotifications({
+ events: Array.from(this.subscribedEvents),
+ });
+ this.subscribedEvents = null;
+
+ ProfilerManager.removeInstance(this);
+ }
+
+ /**
+ * @see ProfilerManager.start
+ */
+ start(options) {
+ return ProfilerManager.start(options);
+ }
+
+ /**
+ * @see ProfilerManager.stop
+ */
+ stop() {
+ return ProfilerManager.stop();
+ }
+
+ /**
+ * @see ProfilerManager.getProfile
+ */
+ getProfile(request = {}) {
+ return ProfilerManager.getProfile(request);
+ }
+
+ /**
+ * @see ProfilerManager.getFeatures
+ */
+ getFeatures() {
+ return ProfilerManager.getFeatures();
+ }
+
+ /**
+ * @see ProfilerManager.getBufferInfo
+ */
+ getBufferInfo() {
+ return ProfilerManager.getBufferInfo();
+ }
+
+ /**
+ * @see ProfilerManager.getStartOptions
+ */
+ getStartOptions() {
+ return ProfilerManager.getStartOptions();
+ }
+
+ /**
+ * @see ProfilerManager.isActive
+ */
+ isActive() {
+ return ProfilerManager.isActive();
+ }
+
+ /**
+ * @see ProfilerManager.sharedLibraries
+ */
+ sharedLibraries() {
+ return ProfilerManager.sharedLibraries;
+ }
+
+ /**
+ * @see ProfilerManager.setProfilerStatusInterval
+ */
+ setProfilerStatusInterval(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(data = {}) {
+ const 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(data = {}) {
+ const 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
+ */
+ static canProfile() {
+ return Services.profiler.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);
+}
+
+exports.Profiler = Profiler;