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.js546
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);
+}