diff options
Diffstat (limited to 'devtools/client/performance/legacy')
-rw-r--r-- | devtools/client/performance/legacy/actors.js | 263 | ||||
-rw-r--r-- | devtools/client/performance/legacy/compatibility.js | 66 | ||||
-rw-r--r-- | devtools/client/performance/legacy/front.js | 484 | ||||
-rw-r--r-- | devtools/client/performance/legacy/moz.build | 12 | ||||
-rw-r--r-- | devtools/client/performance/legacy/recording.js | 174 |
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; |