summaryrefslogtreecommitdiffstats
path: root/devtools/server/performance
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/performance')
-rw-r--r--devtools/server/performance/framerate.js99
-rw-r--r--devtools/server/performance/memory.js425
-rw-r--r--devtools/server/performance/moz.build13
-rw-r--r--devtools/server/performance/profiler.js546
-rw-r--r--devtools/server/performance/recorder.js494
-rw-r--r--devtools/server/performance/timeline.js356
6 files changed, 1933 insertions, 0 deletions
diff --git a/devtools/server/performance/framerate.js b/devtools/server/performance/framerate.js
new file mode 100644
index 000000000..24f8a7a6b
--- /dev/null
+++ b/devtools/server/performance/framerate.js
@@ -0,0 +1,99 @@
+/* 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 { on, once, off, emit } = require("sdk/event/core");
+const { Class } = require("sdk/core/heritage");
+
+/**
+ * A very simple utility for monitoring framerate. Takes a `tabActor`
+ * and monitors framerate over time. The actor wrapper around this
+ * can be found at devtools/server/actors/framerate.js
+ */
+var Framerate = exports.Framerate = Class({
+ initialize: function (tabActor) {
+ this.tabActor = tabActor;
+ this._contentWin = tabActor.window;
+ this._onRefreshDriverTick = this._onRefreshDriverTick.bind(this);
+ this._onGlobalCreated = this._onGlobalCreated.bind(this);
+ on(this.tabActor, "window-ready", this._onGlobalCreated);
+ },
+ destroy: function (conn) {
+ off(this.tabActor, "window-ready", this._onGlobalCreated);
+ this.stopRecording();
+ },
+
+ /**
+ * Starts monitoring framerate, storing the frames per second.
+ */
+ startRecording: function () {
+ if (this._recording) {
+ return;
+ }
+ this._recording = true;
+ this._ticks = [];
+ this._startTime = this.tabActor.docShell.now();
+ this._rafID = this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
+ },
+
+ /**
+ * Stops monitoring framerate, returning the recorded values.
+ */
+ stopRecording: function (beginAt = 0, endAt = Number.MAX_SAFE_INTEGER) {
+ if (!this._recording) {
+ return [];
+ }
+ let ticks = this.getPendingTicks(beginAt, endAt);
+ this.cancelRecording();
+ return ticks;
+ },
+
+ /**
+ * Stops monitoring framerate, without returning the recorded values.
+ */
+ cancelRecording: function () {
+ this._contentWin.cancelAnimationFrame(this._rafID);
+ this._recording = false;
+ this._ticks = null;
+ this._rafID = -1;
+ },
+
+ /**
+ * Returns whether this instance is currently recording.
+ */
+ isRecording: function () {
+ return !!this._recording;
+ },
+
+ /**
+ * Gets the refresh driver ticks recorded so far.
+ */
+ getPendingTicks: function (beginAt = 0, endAt = Number.MAX_SAFE_INTEGER) {
+ if (!this._ticks) {
+ return [];
+ }
+ return this._ticks.filter(e => e >= beginAt && e <= endAt);
+ },
+
+ /**
+ * Function invoked along with the refresh driver.
+ */
+ _onRefreshDriverTick: function () {
+ if (!this._recording) {
+ return;
+ }
+ this._rafID = this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
+ this._ticks.push(this.tabActor.docShell.now() - this._startTime);
+ },
+
+ /**
+ * When the content window for the tab actor is created.
+ */
+ _onGlobalCreated: function (win) {
+ if (this._recording) {
+ this._contentWin.cancelAnimationFrame(this._rafID);
+ this._rafID = this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
+ }
+ }
+});
diff --git a/devtools/server/performance/memory.js b/devtools/server/performance/memory.js
new file mode 100644
index 000000000..77ce348cc
--- /dev/null
+++ b/devtools/server/performance/memory.js
@@ -0,0 +1,425 @@
+/* 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 { reportException } = require("devtools/shared/DevToolsUtils");
+const { Class } = require("sdk/core/heritage");
+const { expectState } = require("devtools/server/actors/common");
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
+loader.lazyRequireGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm", true);
+loader.lazyRequireGetter(this, "StackFrameCache",
+ "devtools/server/actors/utils/stack", true);
+loader.lazyRequireGetter(this, "ThreadSafeChromeUtils");
+loader.lazyRequireGetter(this, "HeapSnapshotFileUtils",
+ "devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
+loader.lazyRequireGetter(this, "ChromeActor", "devtools/server/actors/chrome",
+ true);
+loader.lazyRequireGetter(this, "ChildProcessActor",
+ "devtools/server/actors/child-process", true);
+
+/**
+ * A class that returns memory data for a parent actor's window.
+ * Using a tab-scoped actor with this instance will measure the memory footprint of its
+ * parent tab. Using a global-scoped actor instance however, will measure the memory
+ * footprint of the chrome window referenced by its root actor.
+ *
+ * To be consumed by actor's, like MemoryActor using this module to
+ * send information over RDP, and TimelineActor for using more light-weight
+ * utilities like GC events and measuring memory consumption.
+ */
+var Memory = exports.Memory = Class({
+ extends: EventTarget,
+
+ /**
+ * Requires a root actor and a StackFrameCache.
+ */
+ initialize: function (parent, frameCache = new StackFrameCache()) {
+ this.parent = parent;
+ this._mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(Ci.nsIMemoryReporterManager);
+ this.state = "detached";
+ this._dbg = null;
+ this._frameCache = frameCache;
+
+ this._onGarbageCollection = this._onGarbageCollection.bind(this);
+ this._emitAllocations = this._emitAllocations.bind(this);
+ this._onWindowReady = this._onWindowReady.bind(this);
+
+ events.on(this.parent, "window-ready", this._onWindowReady);
+ },
+
+ destroy: function () {
+ events.off(this.parent, "window-ready", this._onWindowReady);
+
+ this._mgr = null;
+ if (this.state === "attached") {
+ this.detach();
+ }
+ },
+
+ get dbg() {
+ if (!this._dbg) {
+ this._dbg = this.parent.makeDebugger();
+ }
+ return this._dbg;
+ },
+
+ /**
+ * Attach to this MemoryBridge.
+ *
+ * This attaches the MemoryBridge's Debugger instance so that you can start
+ * recording allocations or take a census of the heap. In addition, the
+ * MemoryBridge will start emitting GC events.
+ */
+ attach: expectState("detached", function () {
+ this.dbg.addDebuggees();
+ this.dbg.memory.onGarbageCollection = this._onGarbageCollection.bind(this);
+ this.state = "attached";
+ }, "attaching to the debugger"),
+
+ /**
+ * Detach from this MemoryBridge.
+ */
+ detach: expectState("attached", function () {
+ this._clearDebuggees();
+ this.dbg.enabled = false;
+ this._dbg = null;
+ this.state = "detached";
+ }, "detaching from the debugger"),
+
+ /**
+ * Gets the current MemoryBridge attach/detach state.
+ */
+ getState: function () {
+ return this.state;
+ },
+
+ _clearDebuggees: function () {
+ if (this._dbg) {
+ if (this.isRecordingAllocations()) {
+ this.dbg.memory.drainAllocationsLog();
+ }
+ this._clearFrames();
+ this.dbg.removeAllDebuggees();
+ }
+ },
+
+ _clearFrames: function () {
+ if (this.isRecordingAllocations()) {
+ this._frameCache.clearFrames();
+ }
+ },
+
+ /**
+ * Handler for the parent actor's "window-ready" event.
+ */
+ _onWindowReady: function ({ isTopLevel }) {
+ if (this.state == "attached") {
+ this._clearDebuggees();
+ if (isTopLevel && this.isRecordingAllocations()) {
+ this._frameCache.initFrames();
+ }
+ this.dbg.addDebuggees();
+ }
+ },
+
+ /**
+ * Returns a boolean indicating whether or not allocation
+ * sites are being tracked.
+ */
+ isRecordingAllocations: function () {
+ return this.dbg.memory.trackingAllocationSites;
+ },
+
+ /**
+ * Save a heap snapshot scoped to the current debuggees' portion of the heap
+ * graph.
+ *
+ * @param {Object|null} boundaries
+ *
+ * @returns {String} The snapshot id.
+ */
+ saveHeapSnapshot: expectState("attached", function (boundaries = null) {
+ // If we are observing the whole process, then scope the snapshot
+ // accordingly. Otherwise, use the debugger's debuggees.
+ if (!boundaries) {
+ boundaries = this.parent instanceof ChromeActor || this.parent instanceof ChildProcessActor
+ ? { runtime: true }
+ : { debugger: this.dbg };
+ }
+ const path = ThreadSafeChromeUtils.saveHeapSnapshot(boundaries);
+ return HeapSnapshotFileUtils.getSnapshotIdFromPath(path);
+ }, "saveHeapSnapshot"),
+
+ /**
+ * Take a census of the heap. See js/src/doc/Debugger/Debugger.Memory.md for
+ * more information.
+ */
+ takeCensus: expectState("attached", function () {
+ return this.dbg.memory.takeCensus();
+ }, "taking census"),
+
+ /**
+ * Start recording allocation sites.
+ *
+ * @param {number} options.probability
+ * The probability we sample any given allocation when recording allocations.
+ * Must be between 0 and 1 -- defaults to 1.
+ * @param {number} options.maxLogLength
+ * The maximum number of allocation events to keep in the
+ * log. If new allocs occur while at capacity, oldest
+ * allocations are lost. Must fit in a 32 bit signed integer.
+ * @param {number} options.drainAllocationsTimeout
+ * A number in milliseconds of how often, at least, an `allocation` event
+ * gets emitted (and drained), and also emits and drains on every GC event,
+ * resetting the timer.
+ */
+ startRecordingAllocations: expectState("attached", function (options = {}) {
+ if (this.isRecordingAllocations()) {
+ return this._getCurrentTime();
+ }
+
+ this._frameCache.initFrames();
+
+ this.dbg.memory.allocationSamplingProbability = options.probability != null
+ ? options.probability
+ : 1.0;
+
+ this.drainAllocationsTimeoutTimer = typeof options.drainAllocationsTimeout === "number" ? options.drainAllocationsTimeout : null;
+
+ if (this.drainAllocationsTimeoutTimer != null) {
+ if (this._poller) {
+ this._poller.disarm();
+ }
+ this._poller = new DeferredTask(this._emitAllocations, this.drainAllocationsTimeoutTimer);
+ this._poller.arm();
+ }
+
+ if (options.maxLogLength != null) {
+ this.dbg.memory.maxAllocationsLogLength = options.maxLogLength;
+ }
+ this.dbg.memory.trackingAllocationSites = true;
+
+ return this._getCurrentTime();
+ }, "starting recording allocations"),
+
+ /**
+ * Stop recording allocation sites.
+ */
+ stopRecordingAllocations: expectState("attached", function () {
+ if (!this.isRecordingAllocations()) {
+ return this._getCurrentTime();
+ }
+ this.dbg.memory.trackingAllocationSites = false;
+ this._clearFrames();
+
+ if (this._poller) {
+ this._poller.disarm();
+ this._poller = null;
+ }
+
+ return this._getCurrentTime();
+ }, "stopping recording allocations"),
+
+ /**
+ * Return settings used in `startRecordingAllocations` for `probability`
+ * and `maxLogLength`. Currently only uses in tests.
+ */
+ getAllocationsSettings: expectState("attached", function () {
+ return {
+ maxLogLength: this.dbg.memory.maxAllocationsLogLength,
+ probability: this.dbg.memory.allocationSamplingProbability
+ };
+ }, "getting allocations settings"),
+
+ /**
+ * Get a list of the most recent allocations since the last time we got
+ * allocations, as well as a summary of all allocations since we've been
+ * recording.
+ *
+ * @returns Object
+ * An object of the form:
+ *
+ * {
+ * allocations: [<index into "frames" below>, ...],
+ * allocationsTimestamps: [
+ * <timestamp for allocations[0]>,
+ * <timestamp for allocations[1]>,
+ * ...
+ * ],
+ * allocationSizes: [
+ * <bytesize for allocations[0]>,
+ * <bytesize for allocations[1]>,
+ * ...
+ * ],
+ * frames: [
+ * {
+ * line: <line number for this frame>,
+ * column: <column number for this frame>,
+ * source: <filename string for this frame>,
+ * functionDisplayName: <this frame's inferred function name function or null>,
+ * parent: <index into "frames">
+ * },
+ * ...
+ * ],
+ * }
+ *
+ * The timestamps' unit is microseconds since the epoch.
+ *
+ * Subsequent `getAllocations` request within the same recording and
+ * tab navigation will always place the same stack frames at the same
+ * indices as previous `getAllocations` requests in the same
+ * recording. In other words, it is safe to use the index as a
+ * unique, persistent id for its frame.
+ *
+ * Additionally, the root node (null) is always at index 0.
+ *
+ * We use the indices into the "frames" array to avoid repeating the
+ * description of duplicate stack frames both when listing
+ * allocations, and when many stacks share the same tail of older
+ * frames. There shouldn't be any duplicates in the "frames" array,
+ * as that would defeat the purpose of this compression trick.
+ *
+ * In the future, we might want to split out a frame's "source" and
+ * "functionDisplayName" properties out the same way we have split
+ * frames out with the "frames" array. While this would further
+ * compress the size of the response packet, it would increase CPU
+ * usage to build the packet, and it should, of course, be guided by
+ * profiling and done only when necessary.
+ */
+ getAllocations: expectState("attached", function () {
+ if (this.dbg.memory.allocationsLogOverflowed) {
+ // Since the last time we drained the allocations log, there have been
+ // more allocations than the log's capacity, and we lost some data. There
+ // isn't anything actionable we can do about this, but put a message in
+ // the browser console so we at least know that it occurred.
+ reportException("MemoryBridge.prototype.getAllocations",
+ "Warning: allocations log overflowed and lost some data.");
+ }
+
+ const allocations = this.dbg.memory.drainAllocationsLog();
+ const packet = {
+ allocations: [],
+ allocationsTimestamps: [],
+ allocationSizes: [],
+ };
+ for (let { frame: stack, timestamp, size } of allocations) {
+ if (stack && Cu.isDeadWrapper(stack)) {
+ continue;
+ }
+
+ // Safe because SavedFrames are frozen/immutable.
+ let waived = Cu.waiveXrays(stack);
+
+ // Ensure that we have a form, size, and index for new allocations
+ // because we potentially haven't seen some or all of them yet. After this
+ // loop, we can rely on the fact that every frame we deal with already has
+ // its metadata stored.
+ let index = this._frameCache.addFrame(waived);
+
+ packet.allocations.push(index);
+ packet.allocationsTimestamps.push(timestamp);
+ packet.allocationSizes.push(size);
+ }
+
+ return this._frameCache.updateFramePacket(packet);
+ }, "getting allocations"),
+
+ /*
+ * Force a browser-wide GC.
+ */
+ forceGarbageCollection: function () {
+ for (let i = 0; i < 3; i++) {
+ Cu.forceGC();
+ }
+ },
+
+ /**
+ * Force an XPCOM cycle collection. For more information on XPCOM cycle
+ * collection, see
+ * https://developer.mozilla.org/en-US/docs/Interfacing_with_the_XPCOM_cycle_collector#What_the_cycle_collector_does
+ */
+ forceCycleCollection: function () {
+ Cu.forceCC();
+ },
+
+ /**
+ * A method that returns a detailed breakdown of the memory consumption of the
+ * associated window.
+ *
+ * @returns object
+ */
+ measure: function () {
+ let result = {};
+
+ let jsObjectsSize = {};
+ let jsStringsSize = {};
+ let jsOtherSize = {};
+ let domSize = {};
+ let styleSize = {};
+ let otherSize = {};
+ let totalSize = {};
+ let jsMilliseconds = {};
+ let nonJSMilliseconds = {};
+
+ try {
+ this._mgr.sizeOfTab(this.parent.window, jsObjectsSize, jsStringsSize, jsOtherSize,
+ domSize, styleSize, otherSize, totalSize, jsMilliseconds, nonJSMilliseconds);
+ result.total = totalSize.value;
+ result.domSize = domSize.value;
+ result.styleSize = styleSize.value;
+ result.jsObjectsSize = jsObjectsSize.value;
+ result.jsStringsSize = jsStringsSize.value;
+ result.jsOtherSize = jsOtherSize.value;
+ result.otherSize = otherSize.value;
+ result.jsMilliseconds = jsMilliseconds.value.toFixed(1);
+ result.nonJSMilliseconds = nonJSMilliseconds.value.toFixed(1);
+ } catch (e) {
+ reportException("MemoryBridge.prototype.measure", e);
+ }
+
+ return result;
+ },
+
+ residentUnique: function () {
+ return this._mgr.residentUnique;
+ },
+
+ /**
+ * Handler for GC events on the Debugger.Memory instance.
+ */
+ _onGarbageCollection: function (data) {
+ events.emit(this, "garbage-collection", data);
+
+ // If `drainAllocationsTimeout` set, fire an allocations event with the drained log,
+ // which will restart the timer.
+ if (this._poller) {
+ this._poller.disarm();
+ this._emitAllocations();
+ }
+ },
+
+
+ /**
+ * Called on `drainAllocationsTimeoutTimer` interval if and only if set during `startRecordingAllocations`,
+ * or on a garbage collection event if drainAllocationsTimeout was set.
+ * Drains allocation log and emits as an event and restarts the timer.
+ */
+ _emitAllocations: function () {
+ events.emit(this, "allocations", this.getAllocations());
+ this._poller.arm();
+ },
+
+ /**
+ * Accesses the docshell to return the current process time.
+ */
+ _getCurrentTime: function () {
+ return (this.parent.isRootActor ? this.parent.docShell : this.parent.originalDocShell).now();
+ },
+
+});
diff --git a/devtools/server/performance/moz.build b/devtools/server/performance/moz.build
new file mode 100644
index 000000000..e7b1ed00c
--- /dev/null
+++ b/devtools/server/performance/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'framerate.js',
+ 'memory.js',
+ 'profiler.js',
+ 'recorder.js',
+ 'timeline.js',
+)
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);
+}
diff --git a/devtools/server/performance/recorder.js b/devtools/server/performance/recorder.js
new file mode 100644
index 000000000..fda61ca99
--- /dev/null
+++ b/devtools/server/performance/recorder.js
@@ -0,0 +1,494 @@
+/* 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, Cr } = require("chrome");
+const { Task } = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "Services");
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "extend",
+ "sdk/util/object", true);
+loader.lazyRequireGetter(this, "Class",
+ "sdk/core/heritage", true);
+loader.lazyRequireGetter(this, "EventTarget",
+ "sdk/event/target", true);
+loader.lazyRequireGetter(this, "events",
+ "sdk/event/core");
+
+loader.lazyRequireGetter(this, "Memory",
+ "devtools/server/performance/memory", true);
+loader.lazyRequireGetter(this, "Timeline",
+ "devtools/server/performance/timeline", true);
+loader.lazyRequireGetter(this, "Profiler",
+ "devtools/server/performance/profiler", true);
+loader.lazyRequireGetter(this, "PerformanceRecordingActor",
+ "devtools/server/actors/performance-recording", true);
+loader.lazyRequireGetter(this, "PerformanceRecordingFront",
+ "devtools/server/actors/performance-recording", true);
+loader.lazyRequireGetter(this, "mapRecordingOptions",
+ "devtools/shared/performance/recording-utils", true);
+loader.lazyRequireGetter(this, "DevToolsUtils",
+ "devtools/shared/DevToolsUtils");
+loader.lazyRequireGetter(this, "getSystemInfo",
+ "devtools/shared/system", true);
+
+const PROFILER_EVENTS = [
+ "console-api-profiler",
+ "profiler-started",
+ "profiler-stopped",
+ "profiler-status"
+];
+
+// Max time in milliseconds for the allocations event to occur, which will
+// occur on every GC, or at least as often as DRAIN_ALLOCATIONS_TIMEOUT.
+const DRAIN_ALLOCATIONS_TIMEOUT = 2000;
+
+/**
+ * A connection to underlying actors (profiler, memory, framerate, etc.)
+ * shared by all tools in a target.
+ *
+ * @param Target target
+ * The target owning this connection.
+ */
+const PerformanceRecorder = exports.PerformanceRecorder = Class({
+ extends: EventTarget,
+
+ initialize: function (conn, tabActor) {
+ this.conn = conn;
+ this.tabActor = tabActor;
+
+ this._pendingConsoleRecordings = [];
+ this._recordings = [];
+
+ this._onTimelineData = this._onTimelineData.bind(this);
+ this._onProfilerEvent = this._onProfilerEvent.bind(this);
+ },
+
+ /**
+ * Initializes a connection to the profiler and other miscellaneous actors.
+ * If in the process of opening, or already open, nothing happens.
+ *
+ * @param {Object} options.systemClient
+ * Metadata about the client's system to attach to the recording models.
+ *
+ * @return object
+ * A promise that is resolved once the connection is established.
+ */
+ connect: function (options) {
+ if (this._connected) {
+ return;
+ }
+
+ // Sets `this._profiler`, `this._timeline` and `this._memory`.
+ // Only initialize the timeline and memory fronts if the respective actors
+ // are available. Older Gecko versions don't have existing implementations,
+ // in which case all the methods we need can be easily mocked.
+ this._connectComponents();
+ this._registerListeners();
+
+ this._systemClient = options.systemClient;
+
+ this._connected = true;
+ },
+
+ /**
+ * Destroys this connection.
+ */
+ destroy: function () {
+ this._unregisterListeners();
+ this._disconnectComponents();
+
+ this._connected = null;
+ this._profiler = null;
+ this._timeline = null;
+ this._memory = null;
+ this._target = null;
+ this._client = null;
+ },
+
+ /**
+ * Initializes fronts and connects to the underlying actors using the facades
+ * found in ./actors.js.
+ */
+ _connectComponents: function () {
+ this._profiler = new Profiler(this.tabActor);
+ this._memory = new Memory(this.tabActor);
+ this._timeline = new Timeline(this.tabActor);
+ this._profiler.registerEventNotifications({ events: PROFILER_EVENTS });
+ },
+
+ /**
+ * Registers listeners on events from the underlying
+ * actors, so the connection can handle them.
+ */
+ _registerListeners: function () {
+ this._timeline.on("*", this._onTimelineData);
+ this._memory.on("*", this._onTimelineData);
+ this._profiler.on("*", this._onProfilerEvent);
+ },
+
+ /**
+ * Unregisters listeners on events on the underlying actors.
+ */
+ _unregisterListeners: function () {
+ this._timeline.off("*", this._onTimelineData);
+ this._memory.off("*", this._onTimelineData);
+ this._profiler.off("*", this._onProfilerEvent);
+ },
+
+ /**
+ * Closes the connections to non-profiler actors.
+ */
+ _disconnectComponents: function () {
+ this._profiler.unregisterEventNotifications({ events: PROFILER_EVENTS });
+ this._profiler.destroy();
+ this._timeline.destroy();
+ this._memory.destroy();
+ },
+
+ _onProfilerEvent: function (topic, data) {
+ if (topic === "console-api-profiler") {
+ if (data.subject.action === "profile") {
+ this._onConsoleProfileStart(data.details);
+ } else if (data.subject.action === "profileEnd") {
+ this._onConsoleProfileEnd(data.details);
+ }
+ } else if (topic === "profiler-stopped") {
+ this._onProfilerUnexpectedlyStopped();
+ } else if (topic === "profiler-status") {
+ events.emit(this, "profiler-status", data);
+ }
+ },
+
+ /**
+ * Invoked whenever `console.profile` is called.
+ *
+ * @param string profileLabel
+ * The provided string argument if available; undefined otherwise.
+ * @param number currentTime
+ * The time (in milliseconds) when the call was made, relative to when
+ * the nsIProfiler module was started.
+ */
+ _onConsoleProfileStart: Task.async(function* ({ profileLabel, currentTime: startTime }) {
+ let recordings = this._recordings;
+
+ // Abort if a profile with this label already exists.
+ if (recordings.find(e => e.getLabel() === profileLabel)) {
+ return;
+ }
+
+ // Immediately emit this so the client can start setting things up,
+ // expecting a recording very soon.
+ events.emit(this, "console-profile-start");
+
+ let model = yield this.startRecording(extend({}, getPerformanceRecordingPrefs(), {
+ console: true,
+ label: profileLabel
+ }));
+ }),
+
+ /**
+ * Invoked whenever `console.profileEnd` is called.
+ *
+ * @param string profileLabel
+ * The provided string argument if available; undefined otherwise.
+ * @param number currentTime
+ * The time (in milliseconds) when the call was made, relative to when
+ * the nsIProfiler module was started.
+ */
+ _onConsoleProfileEnd: Task.async(function* (data) {
+ // If no data, abort; can occur if profiler isn't running and we get a surprise
+ // call to console.profileEnd()
+ if (!data) {
+ return;
+ }
+ let { profileLabel, currentTime: endTime } = data;
+
+ let pending = this._recordings.filter(r => r.isConsole() && r.isRecording());
+ if (pending.length === 0) {
+ return;
+ }
+
+ let model;
+ // Try to find the corresponding `console.profile` call if
+ // a label was used in profileEnd(). If no matches, abort.
+ if (profileLabel) {
+ model = pending.find(e => e.getLabel() === profileLabel);
+ }
+ // If no label supplied, pop off the most recent pending console recording
+ else {
+ model = pending[pending.length - 1];
+ }
+
+ // If `profileEnd()` was called with a label, and there are no matching
+ // sessions, abort.
+ if (!model) {
+ Cu.reportError("console.profileEnd() called with label that does not match a recording.");
+ return;
+ }
+
+ yield this.stopRecording(model);
+ }),
+
+ /**
+ * TODO handle bug 1144438
+ */
+ _onProfilerUnexpectedlyStopped: function () {
+ Cu.reportError("Profiler unexpectedly stopped.", arguments);
+ },
+
+ /**
+ * Called whenever there is timeline data of any of the following types:
+ * - markers
+ * - frames
+ * - memory
+ * - ticks
+ * - allocations
+ */
+ _onTimelineData: function (eventName, ...data) {
+ let eventData = Object.create(null);
+
+ switch (eventName) {
+ case "markers": {
+ eventData = { markers: data[0], endTime: data[1] };
+ break;
+ }
+ case "ticks": {
+ eventData = { delta: data[0], timestamps: data[1] };
+ break;
+ }
+ case "memory": {
+ eventData = { delta: data[0], measurement: data[1] };
+ break;
+ }
+ case "frames": {
+ eventData = { delta: data[0], frames: data[1] };
+ break;
+ }
+ case "allocations": {
+ eventData = data[0];
+ break;
+ }
+ }
+
+ // Filter by only recordings that are currently recording;
+ // TODO should filter by recordings that have realtimeMarkers enabled.
+ let activeRecordings = this._recordings.filter(r => r.isRecording());
+
+ if (activeRecordings.length) {
+ events.emit(this, "timeline-data", eventName, eventData, activeRecordings);
+ }
+ },
+
+ /**
+ * Checks whether or not recording is currently supported. At the moment,
+ * this is only influenced by private browsing mode and the profiler.
+ */
+ canCurrentlyRecord: function () {
+ let success = true;
+ let reasons = [];
+
+ if (!Profiler.canProfile()) {
+ success = false,
+ reasons.push("profiler-unavailable");
+ }
+
+ // Check other factors that will affect the possibility of successfully
+ // starting a recording here.
+
+ return { success, reasons };
+ },
+
+ /**
+ * Begins a recording session
+ *
+ * @param boolean options.withMarkers
+ * @param boolean options.withTicks
+ * @param boolean options.withMemory
+ * @param boolean options.withAllocations
+ * @param boolean options.allocationsSampleProbability
+ * @param boolean options.allocationsMaxLogLength
+ * @param boolean options.bufferSize
+ * @param boolean options.sampleFrequency
+ * @param boolean options.console
+ * @param string options.label
+ * @param boolean options.realtimeMarkers
+ * @return object
+ * A promise that is resolved once recording has started.
+ */
+ startRecording: Task.async(function* (options) {
+ let profilerStart, timelineStart, memoryStart;
+
+ profilerStart = Task.spawn(function* () {
+ let data = yield this._profiler.isActive();
+ if (data.isActive) {
+ return data;
+ }
+ let startData = yield this._profiler.start(mapRecordingOptions("profiler", options));
+
+ // If no current time is exposed from starting, set it to 0 -- this is an
+ // older Gecko that does not return its starting time, and uses an epoch based
+ // on the profiler's start time.
+ if (startData.currentTime == null) {
+ startData.currentTime = 0;
+ }
+ return startData;
+ }.bind(this));
+
+ // Timeline will almost always be on if using the DevTools, but using component
+ // independently could result in no timeline.
+ if (options.withMarkers || options.withTicks || options.withMemory) {
+ timelineStart = this._timeline.start(mapRecordingOptions("timeline", options));
+ }
+
+ if (options.withAllocations) {
+ if (this._memory.getState() === "detached") {
+ this._memory.attach();
+ }
+ memoryStart = this._memory.startRecordingAllocations(extend(mapRecordingOptions("memory", options), {
+ drainAllocationsTimeout: DRAIN_ALLOCATIONS_TIMEOUT
+ }));
+ }
+
+ let [profilerStartData, timelineStartData, memoryStartData] = yield promise.all([
+ profilerStart, timelineStart, memoryStart
+ ]);
+
+ let data = Object.create(null);
+ // Filter out start times that are not actually used (0 or undefined), and
+ // find the earliest time since all sources use same epoch.
+ let startTimes = [profilerStartData.currentTime, memoryStartData, timelineStartData].filter(Boolean);
+ data.startTime = Math.min(...startTimes);
+ data.position = profilerStartData.position;
+ data.generation = profilerStartData.generation;
+ data.totalSize = profilerStartData.totalSize;
+
+ data.systemClient = this._systemClient;
+ data.systemHost = yield getSystemInfo();
+
+ let model = new PerformanceRecordingActor(this.conn, options, data);
+ this._recordings.push(model);
+
+ events.emit(this, "recording-started", model);
+ return model;
+ }),
+
+ /**
+ * Manually ends the recording session for the corresponding PerformanceRecording.
+ *
+ * @param PerformanceRecording model
+ * The corresponding PerformanceRecording that belongs to the recording session wished to stop.
+ * @return PerformanceRecording
+ * Returns the same model, populated with the profiling data.
+ */
+ stopRecording: Task.async(function* (model) {
+ // If model isn't in the Recorder's internal store,
+ // then do nothing, like if this was a console.profileEnd
+ // from a different target.
+ if (this._recordings.indexOf(model) === -1) {
+ return model;
+ }
+
+ // Flag the recording as no longer recording, so that `model.isRecording()`
+ // is false. Do this before we fetch all the data, and then subsequently
+ // the recording can be considered "completed".
+ let endTime = Date.now();
+ events.emit(this, "recording-stopping", model);
+
+ // Currently there are two ways profiles stop recording. Either manually in the
+ // performance tool, or via console.profileEnd. Once a recording is done,
+ // we want to deliver the model to the performance tool (either as a return
+ // from the PerformanceFront or via `console-profile-stop` event) and then
+ // remove it from the internal store.
+ //
+ // In the case where a console.profile is generated via the console (so the tools are
+ // open), we initialize the Performance tool so it can listen to those events.
+ this._recordings.splice(this._recordings.indexOf(model), 1);
+
+ let startTime = model._startTime;
+ let profilerData = this._profiler.getProfile({ startTime });
+
+ // Only if there are no more sessions recording do we stop
+ // the underlying memory and timeline actors. If we're still recording,
+ // juse use Date.now() for the memory and timeline end times, as those
+ // are only used in tests.
+ if (!this.isRecording()) {
+ // Check to see if memory is recording, so we only stop recording
+ // if necessary (otherwise if the memory component is not attached, this will fail)
+ if (this._memory.isRecordingAllocations()) {
+ this._memory.stopRecordingAllocations();
+ }
+ this._timeline.stop();
+ }
+
+ let recordingData = {
+ // Data available only at the end of a recording.
+ profile: profilerData.profile,
+ // End times for all the actors.
+ duration: profilerData.currentTime - startTime,
+ };
+
+ events.emit(this, "recording-stopped", model, recordingData);
+ return model;
+ }),
+
+ /**
+ * Checks all currently stored recording handles and returns a boolean
+ * if there is a session currently being recorded.
+ *
+ * @return Boolean
+ */
+ isRecording: function () {
+ return this._recordings.some(h => h.isRecording());
+ },
+
+ /**
+ * Returns all current recordings.
+ */
+ getRecordings: function () {
+ return this._recordings;
+ },
+
+ /**
+ * Sets how often the "profiler-status" event should be emitted.
+ * Used in tests.
+ */
+ setProfilerStatusInterval: function (n) {
+ this._profiler.setProfilerStatusInterval(n);
+ },
+
+ /**
+ * Returns the configurations set on underlying components, used in tests.
+ * Returns an object with `probability`, `maxLogLength` for allocations, and
+ * `features`, `threadFilters`, `entries` and `interval` for profiler.
+ *
+ * @return {object}
+ */
+ getConfiguration: function () {
+ let allocationSettings = Object.create(null);
+
+ if (this._memory.getState() === "attached") {
+ allocationSettings = this._memory.getAllocationsSettings();
+ }
+
+ return extend({}, allocationSettings, this._profiler.getStartOptions());
+ },
+
+ toString: () => "[object PerformanceRecorder]"
+});
+
+/**
+ * Creates an object of configurations based off of preferences for a PerformanceRecording.
+ */
+function getPerformanceRecordingPrefs() {
+ return {
+ withMarkers: true,
+ withMemory: Services.prefs.getBoolPref("devtools.performance.ui.enable-memory"),
+ withTicks: Services.prefs.getBoolPref("devtools.performance.ui.enable-framerate"),
+ withAllocations: Services.prefs.getBoolPref("devtools.performance.ui.enable-allocations"),
+ allocationsSampleProbability: +Services.prefs.getCharPref("devtools.performance.memory.sample-probability"),
+ allocationsMaxLogLength: Services.prefs.getIntPref("devtools.performance.memory.max-log-length")
+ };
+}
diff --git a/devtools/server/performance/timeline.js b/devtools/server/performance/timeline.js
new file mode 100644
index 000000000..3d7a90811
--- /dev/null
+++ b/devtools/server/performance/timeline.js
@@ -0,0 +1,356 @@
+/* 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";
+
+/**
+ * Many Gecko operations (painting, reflows, restyle, ...) can be tracked
+ * in real time. A marker is a representation of one operation. A marker
+ * has a name, start and end timestamps. Markers are stored in docShells.
+ *
+ * This module exposes this tracking mechanism. To use with devtools' RDP,
+ * use devtools/server/actors/timeline.js directly.
+ *
+ * To start/stop recording markers:
+ * timeline.start()
+ * timeline.stop()
+ * timeline.isRecording()
+ *
+ * When markers are available, an event is emitted:
+ * timeline.on("markers", function(markers) {...})
+ */
+
+const { Ci, Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+// Be aggressive about lazy loading, as this will run on every
+// toolbox startup
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+loader.lazyRequireGetter(this, "Task", "devtools/shared/task", true);
+loader.lazyRequireGetter(this, "Memory", "devtools/server/performance/memory", true);
+loader.lazyRequireGetter(this, "Framerate", "devtools/server/performance/framerate", true);
+loader.lazyRequireGetter(this, "StackFrameCache", "devtools/server/actors/utils/stack", true);
+loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
+
+// How often do we pull markers from the docShells, and therefore, how often do
+// we send events to the front (knowing that when there are no markers in the
+// docShell, no event is sent).
+const DEFAULT_TIMELINE_DATA_PULL_TIMEOUT = 200; // ms
+
+/**
+ * The timeline actor pops and forwards timeline markers registered in docshells.
+ */
+var Timeline = exports.Timeline = Class({
+ extends: EventTarget,
+
+ /**
+ * Initializes this actor with the provided connection and tab actor.
+ */
+ initialize: function (tabActor) {
+ this.tabActor = tabActor;
+
+ this._isRecording = false;
+ this._stackFrames = null;
+ this._memory = null;
+ this._framerate = null;
+
+ // Make sure to get markers from new windows as they become available
+ this._onWindowReady = this._onWindowReady.bind(this);
+ this._onGarbageCollection = this._onGarbageCollection.bind(this);
+ events.on(this.tabActor, "window-ready", this._onWindowReady);
+ },
+
+ /**
+ * Destroys this actor, stopping recording first.
+ */
+ destroy: function () {
+ this.stop();
+
+ events.off(this.tabActor, "window-ready", this._onWindowReady);
+ this.tabActor = null;
+ },
+
+ /**
+ * Get the list of docShells in the currently attached tabActor. Note that we
+ * always list the docShells included in the real root docShell, even if the
+ * tabActor was switched to a child frame. This is because for now, paint
+ * markers are only recorded at parent frame level so switching the timeline
+ * to a child frame would hide all paint markers.
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1050773#c14
+ * @return {Array}
+ */
+ get docShells() {
+ let originalDocShell;
+ let docShells = [];
+
+ if (this.tabActor.isRootActor) {
+ originalDocShell = this.tabActor.docShell;
+ } else {
+ originalDocShell = this.tabActor.originalDocShell;
+ }
+
+ if (!originalDocShell) {
+ return docShells;
+ }
+
+ let docShellsEnum = originalDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeAll,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS
+ );
+
+ while (docShellsEnum.hasMoreElements()) {
+ let docShell = docShellsEnum.getNext();
+ docShells.push(docShell.QueryInterface(Ci.nsIDocShell));
+ }
+
+ return docShells;
+ },
+
+ /**
+ * At regular intervals, pop the markers from the docshell, and forward
+ * markers, memory, tick and frames events, if any.
+ */
+ _pullTimelineData: function () {
+ let docShells = this.docShells;
+ if (!this._isRecording || !docShells.length) {
+ return;
+ }
+
+ let endTime = docShells[0].now();
+ let markers = [];
+
+ // Gather markers if requested.
+ if (this._withMarkers || this._withDocLoadingEvents) {
+ for (let docShell of docShells) {
+ for (let marker of docShell.popProfileTimelineMarkers()) {
+ markers.push(marker);
+
+ // The docshell may return markers with stack traces attached.
+ // Here we transform the stack traces via the stack frame cache,
+ // which lets us preserve tail sharing when transferring the
+ // frames to the client. We must waive xrays here because Firefox
+ // doesn't understand that the Debugger.Frame object is safe to
+ // use from chrome. See Tutorial-Alloc-Log-Tree.md.
+ if (this._withFrames) {
+ if (marker.stack) {
+ marker.stack = this._stackFrames.addFrame(Cu.waiveXrays(marker.stack));
+ }
+ if (marker.endStack) {
+ marker.endStack = this._stackFrames.addFrame(Cu.waiveXrays(marker.endStack));
+ }
+ }
+
+ // Emit some helper events for "DOMContentLoaded" and "Load" markers.
+ if (this._withDocLoadingEvents) {
+ if (marker.name == "document::DOMContentLoaded" ||
+ marker.name == "document::Load") {
+ events.emit(this, "doc-loading", marker, endTime);
+ }
+ }
+ }
+ }
+ }
+
+ // Emit markers if requested.
+ if (this._withMarkers && markers.length > 0) {
+ events.emit(this, "markers", markers, endTime);
+ }
+
+ // Emit framerate data if requested.
+ if (this._withTicks) {
+ events.emit(this, "ticks", endTime, this._framerate.getPendingTicks());
+ }
+
+ // Emit memory data if requested.
+ if (this._withMemory) {
+ events.emit(this, "memory", endTime, this._memory.measure());
+ }
+
+ // Emit stack frames data if requested.
+ if (this._withFrames && this._withMarkers) {
+ let frames = this._stackFrames.makeEvent();
+ if (frames) {
+ events.emit(this, "frames", endTime, frames);
+ }
+ }
+
+ this._dataPullTimeout = setTimeout(() => {
+ this._pullTimelineData();
+ }, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
+ },
+
+ /**
+ * Are we recording profile markers currently?
+ */
+ isRecording: function () {
+ return this._isRecording;
+ },
+
+ /**
+ * Start recording profile markers.
+ *
+ * @option {boolean} withMarkers
+ * Boolean indicating whether or not timeline markers are emitted
+ * once they're accumulated every `DEFAULT_TIMELINE_DATA_PULL_TIMEOUT`
+ * milliseconds.
+ * @option {boolean} withTicks
+ * Boolean indicating whether a `ticks` event is fired and a
+ * FramerateActor is created.
+ * @option {boolean} withMemory
+ * Boolean indiciating whether we want memory measurements sampled.
+ * @option {boolean} withFrames
+ * Boolean indicating whether or not stack frames should be handled
+ * from timeline markers.
+ * @option {boolean} withGCEvents
+ * Boolean indicating whether or not GC markers should be emitted.
+ * TODO: Remove these fake GC markers altogether in bug 1198127.
+ * @option {boolean} withDocLoadingEvents
+ * Boolean indicating whether or not DOMContentLoaded and Load
+ * marker events are emitted.
+ */
+ start: Task.async(function* ({
+ withMarkers,
+ withTicks,
+ withMemory,
+ withFrames,
+ withGCEvents,
+ withDocLoadingEvents,
+ }) {
+ let docShells = this.docShells;
+ if (!docShells.length) {
+ return -1;
+ }
+ let startTime = this._startTime = docShells[0].now();
+ if (this._isRecording) {
+ return startTime;
+ }
+
+ this._isRecording = true;
+ this._withMarkers = !!withMarkers;
+ this._withTicks = !!withTicks;
+ this._withMemory = !!withMemory;
+ this._withFrames = !!withFrames;
+ this._withGCEvents = !!withGCEvents;
+ this._withDocLoadingEvents = !!withDocLoadingEvents;
+
+ if (this._withMarkers || this._withDocLoadingEvents) {
+ for (let docShell of docShells) {
+ docShell.recordProfileTimelineMarkers = true;
+ }
+ }
+
+ if (this._withTicks) {
+ this._framerate = new Framerate(this.tabActor);
+ this._framerate.startRecording();
+ }
+
+ if (this._withMemory || this._withGCEvents) {
+ this._memory = new Memory(this.tabActor, this._stackFrames);
+ this._memory.attach();
+ }
+
+ if (this._withGCEvents) {
+ events.on(this._memory, "garbage-collection", this._onGarbageCollection);
+ }
+
+ if (this._withFrames && this._withMarkers) {
+ this._stackFrames = new StackFrameCache();
+ this._stackFrames.initFrames();
+ }
+
+ this._pullTimelineData();
+ return startTime;
+ }),
+
+ /**
+ * Stop recording profile markers.
+ */
+ stop: Task.async(function* () {
+ let docShells = this.docShells;
+ if (!docShells.length) {
+ return -1;
+ }
+ let endTime = this._startTime = docShells[0].now();
+ if (!this._isRecording) {
+ return endTime;
+ }
+
+ if (this._withMarkers || this._withDocLoadingEvents) {
+ for (let docShell of docShells) {
+ docShell.recordProfileTimelineMarkers = false;
+ }
+ }
+
+ if (this._withTicks) {
+ this._framerate.stopRecording();
+ this._framerate.destroy();
+ this._framerate = null;
+ }
+
+ if (this._withMemory || this._withGCEvents) {
+ this._memory.detach();
+ this._memory.destroy();
+ }
+
+ if (this._withGCEvents) {
+ events.off(this._memory, "garbage-collection", this._onGarbageCollection);
+ }
+
+ if (this._withFrames && this._withMarkers) {
+ this._stackFrames = null;
+ }
+
+ this._isRecording = false;
+ this._withMarkers = false;
+ this._withTicks = false;
+ this._withMemory = false;
+ this._withFrames = false;
+ this._withDocLoadingEvents = false;
+ this._withGCEvents = false;
+
+ clearTimeout(this._dataPullTimeout);
+
+ return endTime;
+ }),
+
+ /**
+ * When a new window becomes available in the tabActor, start recording its
+ * markers if we were recording.
+ */
+ _onWindowReady: function ({ window }) {
+ if (this._isRecording) {
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.recordProfileTimelineMarkers = true;
+ }
+ },
+
+ /**
+ * Fired when the Memory component emits a `garbage-collection` event. Used to
+ * take the data and make it look like the rest of our markers.
+ *
+ * A GC "marker" here represents a full GC cycle, which may contain several incremental
+ * events within its `collection` array. The marker contains a `reason` field, indicating
+ * why there was a GC, and may contain a `nonincrementalReason` when SpiderMonkey could
+ * not incrementally collect garbage.
+ */
+ _onGarbageCollection: function ({ collections, gcCycleNumber, reason, nonincrementalReason }) {
+ let docShells = this.docShells;
+ if (!this._isRecording || !docShells.length) {
+ return;
+ }
+
+ let endTime = docShells[0].now();
+
+ events.emit(this, "markers", collections.map(({ startTimestamp: start, endTimestamp: end }) => {
+ return {
+ name: "GarbageCollection",
+ causeName: reason,
+ nonincrementalReason: nonincrementalReason,
+ cycle: gcCycleNumber,
+ start,
+ end,
+ };
+ }), endTime);
+ },
+});