diff options
Diffstat (limited to 'devtools/client/performance/legacy/front.js')
-rw-r--r-- | devtools/client/performance/legacy/front.js | 484 |
1 files changed, 484 insertions, 0 deletions
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; |