summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/legacy
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/legacy')
-rw-r--r--devtools/client/performance/legacy/actors.js263
-rw-r--r--devtools/client/performance/legacy/compatibility.js66
-rw-r--r--devtools/client/performance/legacy/front.js484
-rw-r--r--devtools/client/performance/legacy/moz.build12
-rw-r--r--devtools/client/performance/legacy/recording.js174
5 files changed, 999 insertions, 0 deletions
diff --git a/devtools/client/performance/legacy/actors.js b/devtools/client/performance/legacy/actors.js
new file mode 100644
index 000000000..22b4f85b1
--- /dev/null
+++ b/devtools/client/performance/legacy/actors.js
@@ -0,0 +1,263 @@
+/* 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 { Task } = require("devtools/shared/task");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Poller } = require("devtools/client/shared/poller");
+
+const CompatUtils = require("devtools/client/performance/legacy/compatibility");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const { TimelineFront } = require("devtools/shared/fronts/timeline");
+const { ProfilerFront } = require("devtools/shared/fronts/profiler");
+
+// How often do we check the status of the profiler's circular buffer in milliseconds.
+const PROFILER_CHECK_TIMER = 5000;
+
+const TIMELINE_ACTOR_METHODS = [
+ "start", "stop",
+];
+
+const PROFILER_ACTOR_METHODS = [
+ "startProfiler", "getStartOptions", "stopProfiler",
+ "registerEventNotifications", "unregisterEventNotifications"
+];
+
+/**
+ * Constructor for a facade around an underlying ProfilerFront.
+ */
+function LegacyProfilerFront(target) {
+ this._target = target;
+ this._onProfilerEvent = this._onProfilerEvent.bind(this);
+ this._checkProfilerStatus = this._checkProfilerStatus.bind(this);
+ this._PROFILER_CHECK_TIMER = this._target.TEST_MOCK_PROFILER_CHECK_TIMER ||
+ PROFILER_CHECK_TIMER;
+
+ EventEmitter.decorate(this);
+}
+
+LegacyProfilerFront.prototype = {
+ EVENTS: ["console-api-profiler", "profiler-stopped"],
+
+ // Connects to the targets underlying real ProfilerFront.
+ connect: Task.async(function* () {
+ let target = this._target;
+ this._front = new ProfilerFront(target.client, target.form);
+
+ // Fetch and store information about the SPS profiler and
+ // server profiler.
+ this.traits = {};
+ this.traits.filterable = target.getTrait("profilerDataFilterable");
+
+ // Directly register to event notifications when connected
+ // to hook into `console.profile|profileEnd` calls.
+ yield this.registerEventNotifications({ events: this.EVENTS });
+ target.client.addListener("eventNotification", this._onProfilerEvent);
+ }),
+
+ /**
+ * Unregisters events for the underlying profiler actor.
+ */
+ destroy: Task.async(function* () {
+ if (this._poller) {
+ yield this._poller.destroy();
+ }
+ yield this.unregisterEventNotifications({ events: this.EVENTS });
+ this._target.client.removeListener("eventNotification", this._onProfilerEvent);
+ yield this._front.destroy();
+ }),
+
+ /**
+ * Starts the profiler actor, if necessary.
+ *
+ * @option {number?} bufferSize
+ * @option {number?} sampleFrequency
+ */
+ start: Task.async(function* (options = {}) {
+ // Check for poller status even if the profiler is already active --
+ // profiler can be activated via `console.profile` or another source, like
+ // the Gecko Profiler.
+ if (!this._poller) {
+ this._poller = new Poller(this._checkProfilerStatus, this._PROFILER_CHECK_TIMER,
+ false);
+ }
+ if (!this._poller.isPolling()) {
+ this._poller.on();
+ }
+
+ // Start the profiler only if it wasn't already active. The built-in
+ // nsIPerformance module will be kept recording, because it's the same instance
+ // for all targets and interacts with the whole platform, so we don't want
+ // to affect other clients by stopping (or restarting) it.
+ let {
+ isActive,
+ currentTime,
+ position,
+ generation,
+ totalSize
+ } = yield this.getStatus();
+
+ if (isActive) {
+ return { startTime: currentTime, position, generation, totalSize };
+ }
+
+ // Translate options from the recording model into profiler-specific
+ // options for the nsIProfiler
+ let profilerOptions = {
+ entries: options.bufferSize,
+ interval: options.sampleFrequency
+ ? (1000 / (options.sampleFrequency * 1000))
+ : void 0
+ };
+
+ let startInfo = yield this.startProfiler(profilerOptions);
+ let startTime = 0;
+ if ("currentTime" in startInfo) {
+ startTime = startInfo.currentTime;
+ }
+
+ return { startTime, position, generation, totalSize };
+ }),
+
+ /**
+ * Indicates the end of a recording -- does not actually stop the profiler
+ * (stopProfiler does that), but notes that we no longer need to poll
+ * for buffer status.
+ */
+ stop: Task.async(function* () {
+ yield this._poller.off();
+ }),
+
+ /**
+ * Wrapper around `profiler.isActive()` to take profiler status data and emit.
+ */
+ getStatus: Task.async(function* () {
+ let data = yield (CompatUtils.callFrontMethod("isActive").call(this));
+ // If no data, the last poll for `isActive()` was wrapping up, and the target.client
+ // is now null, so we no longer have data, so just abort here.
+ if (!data) {
+ return undefined;
+ }
+
+ // If TEST_PROFILER_FILTER_STATUS defined (via array of fields), filter
+ // out any field from isActive, used only in tests. Used to filter out
+ // buffer status fields to simulate older geckos.
+ if (this._target.TEST_PROFILER_FILTER_STATUS) {
+ data = Object.keys(data).reduce((acc, prop) => {
+ if (this._target.TEST_PROFILER_FILTER_STATUS.indexOf(prop) === -1) {
+ acc[prop] = data[prop];
+ }
+ return acc;
+ }, {});
+ }
+
+ this.emit("profiler-status", data);
+ return data;
+ }),
+
+ /**
+ * Returns profile data from now since `startTime`.
+ */
+ getProfile: Task.async(function* (options) {
+ let profilerData = yield (CompatUtils.callFrontMethod("getProfile")
+ .call(this, options));
+ // If the backend is not deduped, dedupe it ourselves, as rest of the code
+ // expects a deduped profile.
+ if (profilerData.profile.meta.version === 2) {
+ RecordingUtils.deflateProfile(profilerData.profile);
+ }
+
+ // If the backend does not support filtering by start and endtime on
+ // platform (< Fx40), do it on the client (much slower).
+ if (!this.traits.filterable) {
+ RecordingUtils.filterSamples(profilerData.profile, options.startTime || 0);
+ }
+
+ return profilerData;
+ }),
+
+ /**
+ * Invoked whenever a registered event was emitted by the profiler actor.
+ *
+ * @param object response
+ * The data received from the backend.
+ */
+ _onProfilerEvent: function (_, { topic, subject, details }) {
+ if (topic === "console-api-profiler") {
+ if (subject.action === "profile") {
+ this.emit("console-profile-start", details);
+ } else if (subject.action === "profileEnd") {
+ this.emit("console-profile-stop", details);
+ }
+ } else if (topic === "profiler-stopped") {
+ this.emit("profiler-stopped");
+ }
+ },
+
+ _checkProfilerStatus: Task.async(function* () {
+ // Calling `getStatus()` will emit the "profiler-status" on its own
+ yield this.getStatus();
+ }),
+
+ toString: () => "[object LegacyProfilerFront]"
+};
+
+/**
+ * Constructor for a facade around an underlying TimelineFront.
+ */
+function LegacyTimelineFront(target) {
+ this._target = target;
+ EventEmitter.decorate(this);
+}
+
+LegacyTimelineFront.prototype = {
+ EVENTS: ["markers", "frames", "ticks"],
+
+ connect: Task.async(function* () {
+ let supported = yield CompatUtils.timelineActorSupported(this._target);
+ this._front = supported ?
+ new TimelineFront(this._target.client, this._target.form) :
+ new CompatUtils.MockTimelineFront();
+
+ this.IS_MOCK = !supported;
+
+ // Binds underlying actor events and consolidates them to a `timeline-data`
+ // exposed event.
+ this.EVENTS.forEach(type => {
+ let handler = this[`_on${type}`] = this._onTimelineData.bind(this, type);
+ this._front.on(type, handler);
+ });
+ }),
+
+ /**
+ * Override actor's destroy, so we can unregister listeners before
+ * destroying the underlying actor.
+ */
+ destroy: Task.async(function* () {
+ this.EVENTS.forEach(type => this._front.off(type, this[`_on${type}`]));
+ yield this._front.destroy();
+ }),
+
+ /**
+ * An aggregate of all events (markers, frames, ticks) and exposes
+ * to PerformanceActorsConnection as a single event.
+ */
+ _onTimelineData: function (type, ...data) {
+ this.emit("timeline-data", type, ...data);
+ },
+
+ toString: () => "[object LegacyTimelineFront]"
+};
+
+// Bind all the methods that directly proxy to the actor
+PROFILER_ACTOR_METHODS.forEach(m => {
+ LegacyProfilerFront.prototype[m] = CompatUtils.callFrontMethod(m);
+});
+TIMELINE_ACTOR_METHODS.forEach(m => {
+ LegacyTimelineFront.prototype[m] = CompatUtils.callFrontMethod(m);
+});
+
+exports.LegacyProfilerFront = LegacyProfilerFront;
+exports.LegacyTimelineFront = LegacyTimelineFront;
diff --git a/devtools/client/performance/legacy/compatibility.js b/devtools/client/performance/legacy/compatibility.js
new file mode 100644
index 000000000..0c67800d0
--- /dev/null
+++ b/devtools/client/performance/legacy/compatibility.js
@@ -0,0 +1,66 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * A dummy front decorated with the provided methods.
+ *
+ * @param array blueprint
+ * A list of [funcName, retVal] describing the class.
+ */
+function MockFront(blueprint) {
+ EventEmitter.decorate(this);
+
+ for (let [funcName, retVal] of blueprint) {
+ this[funcName] = (x => typeof x === "function" ? x() : x).bind(this, retVal);
+ }
+}
+
+function MockTimelineFront() {
+ MockFront.call(this, [
+ ["destroy"],
+ ["start", 0],
+ ["stop", 0],
+ ]);
+}
+
+/**
+ * Takes a TabTarget, and checks existence of a TimelineActor on
+ * the server, or if TEST_MOCK_TIMELINE_ACTOR is to be used.
+ *
+ * @param {TabTarget} target
+ * @return {Boolean}
+ */
+function timelineActorSupported(target) {
+ // This `target` property is used only in tests to test
+ // instances where the timeline actor is not available.
+ if (target.TEST_MOCK_TIMELINE_ACTOR) {
+ return false;
+ }
+
+ return target.hasActor("timeline");
+}
+
+/**
+ * Returns a function to be used as a method on an "Front" in ./actors.
+ * Calls the underlying actor's method.
+ */
+function callFrontMethod(method) {
+ return function () {
+ // If there's no target or client on this actor facade,
+ // abort silently -- this occurs in tests when polling occurs
+ // after the test ends, when tests do not wait for toolbox destruction
+ // (which will destroy the actor facade, turning off the polling).
+ if (!this._target || !this._target.client) {
+ return undefined;
+ }
+ return this._front[method].apply(this._front, arguments);
+ };
+}
+
+exports.MockTimelineFront = MockTimelineFront;
+exports.timelineActorSupported = timelineActorSupported;
+exports.callFrontMethod = callFrontMethod;
diff --git a/devtools/client/performance/legacy/front.js b/devtools/client/performance/legacy/front.js
new file mode 100644
index 000000000..34fb16665
--- /dev/null
+++ b/devtools/client/performance/legacy/front.js
@@ -0,0 +1,484 @@
+/* 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 { Task } = require("devtools/shared/task");
+
+const Services = require("Services");
+const promise = require("promise");
+const { extend } = require("sdk/util/object");
+
+const Actors = require("devtools/client/performance/legacy/actors");
+const { LegacyPerformanceRecording } = require("devtools/client/performance/legacy/recording");
+const { importRecording } = require("devtools/client/performance/legacy/recording");
+const { normalizePerformanceFeatures } = require("devtools/shared/performance/recording-utils");
+const flags = require("devtools/shared/flags");
+const { getDeviceFront } = require("devtools/shared/device/device");
+const { getSystemInfo } = require("devtools/shared/system");
+const events = require("sdk/event/core");
+const { EventTarget } = require("sdk/event/target");
+const { Class } = require("sdk/core/heritage");
+
+/**
+ * A connection to underlying actors (profiler, framerate, etc.)
+ * shared by all tools in a target.
+ */
+const LegacyPerformanceFront = Class({
+ extends: EventTarget,
+
+ LEGACY_FRONT: true,
+
+ traits: {
+ features: {
+ withMarkers: true,
+ withTicks: true,
+ withMemory: false,
+ withFrames: false,
+ withGCEvents: false,
+ withDocLoadingEvents: false,
+ withAllocations: false,
+ },
+ },
+
+ initialize: function (target) {
+ let { form, client } = target;
+ this._target = target;
+ this._form = form;
+ this._client = client;
+ this._pendingConsoleRecordings = [];
+ this._sitesPullTimeout = 0;
+ this._recordings = [];
+
+ this._pipeToFront = this._pipeToFront.bind(this);
+ this._onTimelineData = this._onTimelineData.bind(this);
+ this._onConsoleProfileStart = this._onConsoleProfileStart.bind(this);
+ this._onConsoleProfileStop = this._onConsoleProfileStop.bind(this);
+ this._onProfilerStatus = this._onProfilerStatus.bind(this);
+ this._onProfilerUnexpectedlyStopped = this._onProfilerUnexpectedlyStopped.bind(this);
+ },
+
+ /**
+ * Initializes a connection to the profiler and other miscellaneous actors.
+ * If in the process of opening, or already open, nothing happens.
+ *
+ * @return object
+ * A promise that is resolved once the connection is established.
+ */
+ connect: Task.async(function* () {
+ if (this._connecting) {
+ return this._connecting.promise;
+ }
+
+ // Create a promise that gets resolved upon connecting, so that
+ // other attempts to open the connection use the same resolution promise
+ this._connecting = promise.defer();
+
+ // Sets `this._profiler`, `this._timeline`.
+ // Only initialize the timeline 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.
+ yield this._connectActors();
+ yield this._registerListeners();
+
+ this._connecting.resolve();
+ return this._connecting.promise;
+ }),
+
+ /**
+ * Destroys this connection.
+ */
+ destroy: Task.async(function* () {
+ if (this._connecting) {
+ yield this._connecting.promise;
+ } else {
+ return;
+ }
+
+ yield this._unregisterListeners();
+ yield this._disconnectActors();
+
+ this._connecting = null;
+ this._profiler = null;
+ this._timeline = null;
+ this._client = null;
+ this._form = null;
+ this._target = this._target;
+ }),
+
+ /**
+ * Initializes fronts and connects to the underlying actors using the facades
+ * found in ./actors.js.
+ */
+ _connectActors: Task.async(function* () {
+ this._profiler = new Actors.LegacyProfilerFront(this._target);
+ this._timeline = new Actors.LegacyTimelineFront(this._target);
+
+ yield promise.all([
+ this._profiler.connect(),
+ this._timeline.connect()
+ ]);
+
+ // If mocked timeline, update the traits
+ this.traits.features.withMarkers = !this._timeline.IS_MOCK;
+ this.traits.features.withTicks = !this._timeline.IS_MOCK;
+ }),
+
+ /**
+ * Registers listeners on events from the underlying
+ * actors, so the connection can handle them.
+ */
+ _registerListeners: function () {
+ this._timeline.on("timeline-data", this._onTimelineData);
+ this._profiler.on("console-profile-start", this._onConsoleProfileStart);
+ this._profiler.on("console-profile-stop", this._onConsoleProfileStop);
+ this._profiler.on("profiler-stopped", this._onProfilerUnexpectedlyStopped);
+ this._profiler.on("profiler-status", this._onProfilerStatus);
+ },
+
+ /**
+ * Unregisters listeners on events on the underlying actors.
+ */
+ _unregisterListeners: function () {
+ this._timeline.off("timeline-data", this._onTimelineData);
+ this._profiler.off("console-profile-start", this._onConsoleProfileStart);
+ this._profiler.off("console-profile-stop", this._onConsoleProfileStop);
+ this._profiler.off("profiler-stopped", this._onProfilerUnexpectedlyStopped);
+ this._profiler.off("profiler-status", this._onProfilerStatus);
+ },
+
+ /**
+ * Closes the connections to non-profiler actors.
+ */
+ _disconnectActors: Task.async(function* () {
+ yield promise.all([
+ this._profiler.destroy(),
+ this._timeline.destroy(),
+ ]);
+ }),
+
+ /**
+ * 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;
+ }
+
+ events.emit(this, "console-profile-start");
+
+ yield this.startRecording(extend({}, getLegacyPerformanceRecordingPrefs(), {
+ 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.
+ */
+ _onConsoleProfileStop: 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 } = 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);
+ } 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) {
+ console.error(
+ "console.profileEnd() called with label that does not match a recording.");
+ return;
+ }
+
+ yield this.stopRecording(model);
+ }),
+
+ /**
+ * TODO handle bug 1144438
+ */
+ _onProfilerUnexpectedlyStopped: function () {
+ console.error("Profiler unexpectedly stopped.", arguments);
+ },
+
+ /**
+ * Called whenever there is timeline data of any of the following types:
+ * - markers
+ * - frames
+ * - ticks
+ *
+ * Populate our internal store of recordings for all currently recording sessions.
+ */
+ _onTimelineData: function (_, ...data) {
+ this._recordings.forEach(e => e._addTimelineData.apply(e, data));
+ events.emit(this, "timeline-data", ...data);
+ },
+
+ /**
+ * Called whenever the underlying profiler polls its current status.
+ */
+ _onProfilerStatus: function (_, data) {
+ // If no data emitted (whether from an older actor being destroyed
+ // from a previous test, or the server does not support it), just ignore.
+ if (!data || data.position === void 0) {
+ return;
+ }
+
+ this._currentBufferStatus = data;
+ events.emit(this, "profiler-status", data);
+ },
+
+ /**
+ * Begins a recording session
+ *
+ * @param object options
+ * An options object to pass to the actors. Supported properties are
+ * `withTicks`, `withMemory` and `withAllocations`, `probability`, and
+ * `maxLogLength`.
+ * @return object
+ * A promise that is resolved once recording has started.
+ */
+ startRecording: Task.async(function* (options = {}) {
+ let model = new LegacyPerformanceRecording(
+ normalizePerformanceFeatures(options, this.traits.features));
+
+ // All actors are started asynchronously over the remote debugging protocol.
+ // Get the corresponding start times from each one of them.
+ // The timeline actors are target-dependent, so start those as well,
+ // even though these are mocked in older Geckos (FF < 35)
+ let profilerStart = this._profiler.start(options);
+ let timelineStart = this._timeline.start(options);
+
+ let { startTime, position, generation, totalSize } = yield profilerStart;
+ let timelineStartTime = yield timelineStart;
+
+ let data = {
+ profilerStartTime: startTime, timelineStartTime,
+ generation, position, totalSize
+ };
+
+ // Signify to the model that the recording has started,
+ // populate with data and store the recording model here.
+ model._populate(data);
+ this._recordings.push(model);
+
+ events.emit(this, "recording-started", model);
+ return model;
+ }),
+
+ /**
+ * Manually ends the recording session for the corresponding LegacyPerformanceRecording.
+ *
+ * @param LegacyPerformanceRecording model
+ * The corresponding LegacyPerformanceRecording that belongs to the recording
+ * session wished to stop.
+ * @return LegacyPerformanceRecording
+ * Returns the same model, populated with the profiling data.
+ */
+ stopRecording: Task.async(function* (model) {
+ // If model isn't in the LegacyPerformanceFront internal store,
+ // then do nothing.
+ if (this._recordings.indexOf(model) === -1) {
+ return undefined;
+ }
+
+ // 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();
+ model._onStoppingRecording(endTime);
+ 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 LegacyPerformanceFront 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 config = model.getConfiguration();
+ let startTime = model._getProfilerStartTime();
+ let profilerData = yield this._profiler.getProfile({ startTime });
+ let timelineEndTime = Date.now();
+
+ // Only if there are no more sessions recording do we stop
+ // the underlying timeline actors. If we're still recording,
+ // juse use Date.now() for the timeline end times, as those
+ // are only used in tests.
+ if (!this.isRecording()) {
+ // This doesn't stop the profiler, just turns off polling for
+ // events, and also turns off events on timeline actors.
+ yield this._profiler.stop();
+ timelineEndTime = yield this._timeline.stop(config);
+ }
+
+ let form = yield this._client.listTabs();
+ let systemHost = yield getDeviceFront(this._client, form).getDescription();
+ let systemClient = yield getSystemInfo();
+
+ // Set the results on the LegacyPerformanceRecording itself.
+ model._onStopRecording({
+ // Data available only at the end of a recording.
+ profile: profilerData.profile,
+
+ // End times for all the actors.
+ profilerEndTime: profilerData.currentTime,
+ timelineEndTime: timelineEndTime,
+ systemHost,
+ systemClient,
+ });
+
+ events.emit(this, "recording-stopped", model);
+ return model;
+ }),
+
+ /**
+ * Creates a recording object when given a nsILocalFile.
+ *
+ * @param {nsILocalFile} file
+ * The file to import the data from.
+ * @return {Promise<LegacyPerformanceRecording>}
+ */
+ importRecording: function (file) {
+ return importRecording(file);
+ },
+
+ /**
+ * Checks all currently stored recording models and returns a boolean
+ * if there is a session currently being recorded.
+ *
+ * @return Boolean
+ */
+ isRecording: function () {
+ return this._recordings.some(recording => recording.isRecording());
+ },
+
+ /**
+ * Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much
+ * of this recording's lifetime remains without being overwritten.
+ *
+ * @param {PerformanceRecording} recording
+ * @return {number?}
+ */
+ getBufferUsageForRecording: function (recording) {
+ if (!recording.isRecording() || !this._currentBufferStatus) {
+ return null;
+ }
+ let {
+ position: currentPosition,
+ totalSize,
+ generation: currentGeneration
+ } = this._currentBufferStatus;
+ let {
+ position: origPosition,
+ generation: origGeneration
+ } = recording.getStartingBufferStatus();
+
+ let normalizedCurrent = (totalSize * (currentGeneration - origGeneration))
+ + currentPosition;
+ let percent = (normalizedCurrent - origPosition) / totalSize;
+ return percent > 1 ? 1 : percent;
+ },
+
+ /**
+ * Returns the configurations set on underlying components, used in tests.
+ * Returns an object with `probability`, `maxLogLength` for allocations, and
+ * `entries` and `interval` for profiler.
+ *
+ * @return {object}
+ */
+ getConfiguration: Task.async(function* () {
+ let profilerConfig = yield this._request("profiler", "getStartOptions");
+ return profilerConfig;
+ }),
+
+ /**
+ * An event from an underlying actor that we just want
+ * to pipe to the front itself.
+ */
+ _pipeToFront: function (eventName, ...args) {
+ events.emit(this, eventName, ...args);
+ },
+
+ /**
+ * Helper method to interface with the underlying actors directly.
+ * Used only in tests.
+ */
+ _request: function (actorName, method, ...args) {
+ if (!flags.testing) {
+ throw new Error("LegacyPerformanceFront._request may only be used in tests.");
+ }
+ let actor = this[`_${actorName}`];
+ return actor[method].apply(actor, args);
+ },
+
+ /**
+ * Sets how often the "profiler-status" event should be emitted.
+ * Used in tests.
+ */
+ setProfilerStatusInterval: function (n) {
+ if (this._profiler._poller) {
+ this._profiler._poller._wait = n;
+ }
+ this._profiler._PROFILER_CHECK_TIMER = n;
+ },
+
+ toString: () => "[object LegacyPerformanceFront]"
+});
+
+/**
+ * Creates an object of configurations based off of preferences for a
+ * LegacyPerformanceRecording.
+ */
+function getLegacyPerformanceRecordingPrefs() {
+ 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.LegacyPerformanceFront = LegacyPerformanceFront;
diff --git a/devtools/client/performance/legacy/moz.build b/devtools/client/performance/legacy/moz.build
new file mode 100644
index 000000000..00eab217b
--- /dev/null
+++ b/devtools/client/performance/legacy/moz.build
@@ -0,0 +1,12 @@
+# -*- 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(
+ 'actors.js',
+ 'compatibility.js',
+ 'front.js',
+ 'recording.js',
+)
diff --git a/devtools/client/performance/legacy/recording.js b/devtools/client/performance/legacy/recording.js
new file mode 100644
index 000000000..2ba141471
--- /dev/null
+++ b/devtools/client/performance/legacy/recording.js
@@ -0,0 +1,174 @@
+/* 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 { Task } = require("devtools/shared/task");
+
+const PerformanceIO = require("devtools/client/performance/modules/io");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const { PerformanceRecordingCommon } = require("devtools/shared/performance/recording-common");
+const { merge } = require("sdk/util/object");
+
+/**
+ * Model for a wholistic profile, containing the duration, profiling data,
+ * frames data, timeline (marker, tick, memory) data, and methods to mark
+ * a recording as 'in progress' or 'finished'.
+ */
+const LegacyPerformanceRecording = function (options = {}) {
+ this._label = options.label || "";
+ this._console = options.console || false;
+
+ this._configuration = {
+ withMarkers: options.withMarkers || false,
+ withTicks: options.withTicks || false,
+ withMemory: options.withMemory || false,
+ withAllocations: options.withAllocations || false,
+ allocationsSampleProbability: options.allocationsSampleProbability || 0,
+ allocationsMaxLogLength: options.allocationsMaxLogLength || 0,
+ bufferSize: options.bufferSize || 0,
+ sampleFrequency: options.sampleFrequency || 1
+ };
+};
+
+LegacyPerformanceRecording.prototype = merge({
+ _profilerStartTime: 0,
+ _timelineStartTime: 0,
+ _memoryStartTime: 0,
+
+ /**
+ * Saves the current recording to a file.
+ *
+ * @param nsILocalFile file
+ * The file to stream the data into.
+ */
+ exportRecording: Task.async(function* (file) {
+ let recordingData = this.getAllData();
+ yield PerformanceIO.saveRecordingToFile(recordingData, file);
+ }),
+
+ /**
+ * Sets up the instance with data from the PerformanceFront when
+ * starting a recording. Should only be called by PerformanceFront.
+ */
+ _populate: function (info) {
+ // Times must come from the actor in order to be self-consistent.
+ // However, we also want to update the view with the elapsed time
+ // even when the actor is not generating data. To do this we get
+ // the local time and use it to compute a reasonable elapsed time.
+ this._localStartTime = Date.now();
+
+ this._profilerStartTime = info.profilerStartTime;
+ this._timelineStartTime = info.timelineStartTime;
+ this._memoryStartTime = info.memoryStartTime;
+ this._startingBufferStatus = {
+ position: info.position,
+ totalSize: info.totalSize,
+ generation: info.generation
+ };
+
+ this._recording = true;
+
+ this._systemHost = {};
+ this._systemClient = {};
+ this._markers = [];
+ this._frames = [];
+ this._memory = [];
+ this._ticks = [];
+ this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] };
+ },
+
+ /**
+ * Called when the signal was sent to the front to no longer record more
+ * data, and begin fetching the data. There's some delay during fetching,
+ * even though the recording is stopped, the model is not yet completed until
+ * all the data is fetched.
+ */
+ _onStoppingRecording: function (endTime) {
+ this._duration = endTime - this._localStartTime;
+ this._recording = false;
+ },
+
+ /**
+ * Sets results available from stopping a recording from PerformanceFront.
+ * Should only be called by PerformanceFront.
+ */
+ _onStopRecording: Task.async(function* ({ profilerEndTime, profile, systemClient,
+ systemHost }) {
+ // Update the duration with the accurate profilerEndTime, so we don't have
+ // samples outside of the approximate duration set in `_onStoppingRecording`.
+ this._duration = profilerEndTime - this._profilerStartTime;
+ this._profile = profile;
+ this._completed = true;
+
+ // We filter out all samples that fall out of current profile's range
+ // since the profiler is continuously running. Because of this, sample
+ // times are not guaranteed to have a zero epoch, so offset the
+ // timestamps.
+ RecordingUtils.offsetSampleTimes(this._profile, this._profilerStartTime);
+
+ // Markers need to be sorted ascending by time, to be properly displayed
+ // in a waterfall view.
+ this._markers = this._markers.sort((a, b) => (a.start > b.start));
+
+ this._systemHost = systemHost;
+ this._systemClient = systemClient;
+ }),
+
+ /**
+ * Gets the profile's start time.
+ * @return number
+ */
+ _getProfilerStartTime: function () {
+ return this._profilerStartTime;
+ },
+
+ /**
+ * Fired whenever the PerformanceFront emits markers, memory or ticks.
+ */
+ _addTimelineData: function (eventName, ...data) {
+ // If this model isn't currently recording,
+ // ignore the timeline data.
+ if (!this.isRecording()) {
+ return;
+ }
+
+ let config = this.getConfiguration();
+
+ switch (eventName) {
+ // Accumulate timeline markers into an array. Furthermore, the timestamps
+ // do not have a zero epoch, so offset all of them by the start time.
+ case "markers": {
+ if (!config.withMarkers) {
+ break;
+ }
+ let [markers] = data;
+ RecordingUtils.offsetMarkerTimes(markers, this._timelineStartTime);
+ RecordingUtils.pushAll(this._markers, markers);
+ break;
+ }
+ // Accumulate stack frames into an array.
+ case "frames": {
+ if (!config.withMarkers) {
+ break;
+ }
+ let [, frames] = data;
+ RecordingUtils.pushAll(this._frames, frames);
+ break;
+ }
+ // Save the accumulated refresh driver ticks.
+ case "ticks": {
+ if (!config.withTicks) {
+ break;
+ }
+ let [, timestamps] = data;
+ this._ticks = timestamps;
+ break;
+ }
+ }
+ },
+
+ toString: () => "[object LegacyPerformanceRecording]"
+}, PerformanceRecordingCommon);
+
+exports.LegacyPerformanceRecording = LegacyPerformanceRecording;