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.js494
1 files changed, 494 insertions, 0 deletions
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")
+ };
+}