summaryrefslogtreecommitdiffstats
path: root/devtools/server/performance/recorder.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/performance/recorder.js')
-rw-r--r--devtools/server/performance/recorder.js529
1 files changed, 529 insertions, 0 deletions
diff --git a/devtools/server/performance/recorder.js b/devtools/server/performance/recorder.js
new file mode 100644
index 0000000000..7429491e08
--- /dev/null
+++ b/devtools/server/performance/recorder.js
@@ -0,0 +1,529 @@
+/* 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");
+
+loader.lazyRequireGetter(this, "Services");
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+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,
+ "mapRecordingOptions",
+ "devtools/shared/performance/recording-utils",
+ true
+);
+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.
+ */
+function PerformanceRecorder(conn, targetActor) {
+ EventEmitter.decorate(this);
+
+ this.conn = conn;
+ this.targetActor = targetActor;
+
+ this._pendingConsoleRecordings = [];
+ this._recordings = [];
+
+ this._onTimelineData = this._onTimelineData.bind(this);
+ this._onProfilerEvent = this._onProfilerEvent.bind(this);
+}
+
+PerformanceRecorder.prototype = {
+ /**
+ * 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.targetActor);
+ this._memory = new Memory(this.targetActor);
+ this._timeline = new Timeline(this.targetActor);
+ 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") {
+ // Some other API stopped the profiler. Ignore it.
+ } else if (topic === "profiler-status") {
+ this.emit("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.
+ */
+ async _onConsoleProfileStart({ profileLabel, currentTime }) {
+ const 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.
+ this.emit("console-profile-start");
+
+ await this.startRecording(
+ Object.assign({}, 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.
+ */
+ async _onConsoleProfileEnd(data) {
+ // If no data, abort; can occur if profiler isn't running and we get a surprise
+ // call to console.profileEnd()
+ if (!data) {
+ return;
+ }
+ const { profileLabel } = data;
+
+ const 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);
+ } else {
+ // If no label supplied, pop off the most recent pending console recording
+ 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;
+ }
+
+ await this.stopRecording(model);
+ },
+
+ /**
+ * 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.
+ const activeRecordings = this._recordings.filter(r => r.isRecording());
+
+ if (activeRecordings.length) {
+ this.emit("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;
+ const 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.
+ */
+ async startRecording(options) {
+ let timelineStart, memoryStart;
+
+ const profilerStart = async function() {
+ const data = await this._profiler.isActive();
+ if (data.isActive) {
+ return data;
+ }
+ const startData = await 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();
+ }
+ const recordingOptions = Object.assign(
+ mapRecordingOptions("memory", options),
+ {
+ drainAllocationsTimeout: DRAIN_ALLOCATIONS_TIMEOUT,
+ }
+ );
+ memoryStart = this._memory.startRecordingAllocations(recordingOptions);
+ }
+
+ const [
+ profilerStartData,
+ timelineStartData,
+ memoryStartData,
+ ] = await Promise.all([profilerStart, timelineStart, memoryStart]);
+
+ const 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.
+ const 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 = await getSystemInfo();
+
+ const model = new PerformanceRecordingActor(this.conn, options, data);
+ this._recordings.push(model);
+
+ this.emit("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.
+ */
+ async stopRecording(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.includes(model)) {
+ 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".
+ this.emit("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);
+
+ const startTime = model._startTime;
+ const 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();
+ }
+
+ const recordingData = {
+ // Data available only at the end of a recording.
+ profile: profilerData.profile,
+ // End times for all the actors.
+ duration: profilerData.currentTime - startTime,
+ };
+
+ this.emit("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 Object.assign(
+ {},
+ 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"
+ ),
+ };
+}
+
+exports.PerformanceRecorder = PerformanceRecorder;