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