/* 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} */ 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;