/* 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 { Cc, Ci, Cu, Cr } = require("chrome"); const { Task } = require("resource://gre/modules/Task.jsm"); const { extend } = require("sdk/util/object"); loader.lazyRequireGetter(this, "Services"); loader.lazyRequireGetter(this, "promise"); loader.lazyRequireGetter(this, "EventEmitter", "devtools/toolkit/event-emitter"); loader.lazyRequireGetter(this, "TimelineFront", "devtools/server/actors/timeline", true); loader.lazyRequireGetter(this, "MemoryFront", "devtools/server/actors/memory", true); loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/toolkit/DevToolsUtils"); loader.lazyRequireGetter(this, "compatibility", "devtools/performance/compatibility"); loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); loader.lazyImporter(this, "setTimeout", "resource://gre/modules/Timer.jsm"); loader.lazyImporter(this, "clearTimeout", "resource://gre/modules/Timer.jsm"); // How often do we pull allocation sites from the memory actor. const DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT = 200; // ms /** * A cache of all PerformanceActorsConnection instances. * The keys are Target objects. */ let SharedPerformanceActors = new WeakMap(); /** * Instantiates a shared PerformanceActorsConnection for the specified target. * Consumers must yield on `open` to make sure the connection is established. * * @param Target target * The target owning this connection. * @return PerformanceActorsConnection * The shared connection for the specified target. */ SharedPerformanceActors.forTarget = function(target) { if (this.has(target)) { return this.get(target); } let instance = new PerformanceActorsConnection(target); this.set(target, instance); return instance; }; /** * A connection to underlying actors (profiler, memory, framerate, etc.) * shared by all tools in a target. * * Use `SharedPerformanceActors.forTarget` to make sure you get the same * instance every time, and the `PerformanceFront` to start/stop recordings. * * @param Target target * The target owning this connection. */ function PerformanceActorsConnection(target) { EventEmitter.decorate(this); this._target = target; this._client = this._target.client; this._request = this._request.bind(this); Services.obs.notifyObservers(null, "performance-actors-connection-created", null); } PerformanceActorsConnection.prototype = { // Properties set when mocks are being used _usingMockMemory: false, _usingMockTimeline: false, /** * 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. */ open: Task.async(function*() { if (this._connected) { return; } // Local debugging needs to make the target remote. yield this._target.makeRemote(); // Sets `this._profiler`, `this._timeline` and `this._memory`. // Only initialize the timeline and memory 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._connectProfilerActor(); yield this._connectTimelineActor(); yield this._connectMemoryActor(); this._connected = true; Services.obs.notifyObservers(null, "performance-actors-connection-opened", null); }), /** * Destroys this connection. */ destroy: Task.async(function*() { yield this._disconnectActors(); this._connected = false; }), /** * Initializes a connection to the profiler actor. */ _connectProfilerActor: Task.async(function*() { // Chrome and content process targets already have obtained a reference // to the profiler tab actor. Use it immediately. if (this._target.form && this._target.form.profilerActor) { this._profiler = this._target.form.profilerActor; } // Check if we already have a grip to the `listTabs` response object // and, if we do, use it to get to the profiler actor. else if (this._target.root && this._target.root.profilerActor) { this._profiler = this._target.root.profilerActor; } // Otherwise, call `listTabs`. else { this._profiler = (yield listTabs(this._client)).profilerActor; } }), /** * Initializes a connection to a timeline actor. */ _connectTimelineActor: function() { let supported = yield compatibility.timelineActorSupported(this._target); if (supported) { this._timeline = new TimelineFront(this._target.client, this._target.form); } else { this._usingMockTimeline = true; this._timeline = new compatibility.MockTimelineFront(); } }, /** * Initializes a connection to a memory actor. */ _connectMemoryActor: Task.async(function* () { let supported = yield compatibility.memoryActorSupported(this._target); if (supported) { this._memory = new MemoryFront(this._target.client, this._target.form); } else { this._usingMockMemory = true; this._memory = new compatibility.MockMemoryFront(); } }), /** * Closes the connections to non-profiler actors. */ _disconnectActors: Task.async(function* () { yield this._timeline.destroy(); yield this._memory.destroy(); }), /** * Sends the request over the remote debugging protocol to the * specified actor. * * @param string actor * Currently supported: "profiler", "timeline", "memory". * @param string method * Method to call on the backend. * @param any args [optional] * Additional data or arguments to send with the request. * @return object * A promise resolved with the response once the request finishes. */ _request: function(actor, method, ...args) { // Handle requests to the profiler actor. if (actor == "profiler") { let deferred = promise.defer(); let data = args[0] || {}; data.to = this._profiler; data.type = method; this._client.request(data, deferred.resolve); return deferred.promise; } // Handle requests to the timeline actor. if (actor == "timeline") { return this._timeline[method].apply(this._timeline, args); } // Handle requests to the memory actor. if (actor == "memory") { return this._memory[method].apply(this._memory, args); } } }; /** * A thin wrapper around a shared PerformanceActorsConnection for the parent target. * Handles manually starting and stopping a recording. * * @param PerformanceActorsConnection connection * The shared instance for the parent target. */ function PerformanceFront(connection) { EventEmitter.decorate(this); this._request = connection._request; // Pipe events from TimelineActor to the PerformanceFront connection._timeline.on("markers", markers => this.emit("markers", markers)); connection._timeline.on("frames", (delta, frames) => this.emit("frames", delta, frames)); connection._timeline.on("memory", (delta, measurement) => this.emit("memory", delta, measurement)); connection._timeline.on("ticks", (delta, timestamps) => this.emit("ticks", delta, timestamps)); // Set when mocks are being used this._usingMockMemory = connection._usingMockMemory; this._usingMockTimeline = connection._usingMockTimeline; this._pullAllocationSites = this._pullAllocationSites.bind(this); this._sitesPullTimeout = 0; } PerformanceFront.prototype = { /** * Manually begins a recording session. * * @param object options * An options object to pass to the actors. Supported properties are * `withTicks`, `withMemory` and `withAllocations`. * @return object * A promise that is resolved once recording has started. */ startRecording: Task.async(function*(options = {}) { // All actors are started asynchronously over the remote debugging protocol. // Get the corresponding start times from each one of them. let profilerStartTime = yield this._startProfiler(); let timelineStartTime = yield this._startTimeline(options); let memoryStartTime = yield this._startMemory(options); return { profilerStartTime, timelineStartTime, memoryStartTime }; }), /** * Manually ends the current recording session. * * @param object options * @see PerformanceFront.prototype.startRecording * @return object * A promise that is resolved once recording has stopped, * with the profiler and memory data, along with all the end times. */ stopRecording: Task.async(function*(options = {}) { let memoryEndTime = yield this._stopMemory(options); let timelineEndTime = yield this._stopTimeline(options); let profilerData = yield this._request("profiler", "getProfile"); return { // Data available only at the end of a recording. profile: profilerData.profile, // End times for all the actors. profilerEndTime: profilerData.currentTime, timelineEndTime: timelineEndTime, memoryEndTime: memoryEndTime }; }), /** * Starts the profiler actor, if necessary. */ _startProfiler: Task.async(function *() { // 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 profilerStatus = yield this._request("profiler", "isActive"); if (profilerStatus.isActive) { this.emit("profiler-already-active"); return profilerStatus.currentTime; } // Extend the profiler options so that protocol.js doesn't modify the original. let profilerOptions = extend({}, this._customProfilerOptions); yield this._request("profiler", "startProfiler", profilerOptions); this.emit("profiler-activated"); return 0; }), /** * Starts the timeline actor. */ _startTimeline: Task.async(function *(options) { // The timeline actor is target-dependent, so just make sure it's recording. // It won't, however, be available in older Geckos (FF < 35). return (yield this._request("timeline", "start", options)); }), /** * Stops the timeline actor. */ _stopTimeline: Task.async(function *(options) { return (yield this._request("timeline", "stop")); }), /** * Starts polling for allocations from the memory actor, if necessary. */ _startMemory: Task.async(function *(options) { if (!options.withAllocations) { return 0; } let memoryStartTime = yield this._startRecordingAllocations(options); yield this._pullAllocationSites(); return memoryStartTime; }), /** * Stops polling for allocations from the memory actor, if necessary. */ _stopMemory: Task.async(function *(options) { if (!options.withAllocations) { return 0; } // Since `_pullAllocationSites` is usually running inside a timeout, and // it's performing asynchronous requests to the server, a recording may // be stopped before that method finishes executing. Therefore, we need to // wait for the last request to `getAllocations` to finish before actually // stopping recording allocations. yield this._lastPullAllocationSitesFinished; clearTimeout(this._sitesPullTimeout); return yield this._stopRecordingAllocations(); }), /** * Starts recording allocations in the memory actor. */ _startRecordingAllocations: Task.async(function*(options) { yield this._request("memory", "attach"); let memoryStartTime = yield this._request("memory", "startRecordingAllocations", { probability: options.allocationsSampleProbability, maxLogLength: options.allocationsMaxLogLength }); return memoryStartTime; }), /** * Stops recording allocations in the memory actor. */ _stopRecordingAllocations: Task.async(function*() { let memoryEndTime = yield this._request("memory", "stopRecordingAllocations"); yield this._request("memory", "detach"); return memoryEndTime; }), /** * At regular intervals, pull allocations from the memory actor, and forward * them to consumers. */ _pullAllocationSites: Task.async(function *() { let deferred = promise.defer(); this._lastPullAllocationSitesFinished = deferred.promise; let isDetached = (yield this._request("memory", "getState")) !== "attached"; if (isDetached) { deferred.resolve(); return; } let memoryData = yield this._request("memory", "getAllocations"); this.emit("allocations", { sites: memoryData.allocations, timestamps: memoryData.allocationsTimestamps, frames: memoryData.frames, counts: memoryData.counts }); let delay = DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT; this._sitesPullTimeout = setTimeout(this._pullAllocationSites, delay); deferred.resolve(); }), /** * Overrides the options sent to the built-in profiler module when activating, * such as the maximum entries count, the sampling interval etc. * * Used in tests and for older backend implementations. */ _customProfilerOptions: { entries: 1000000, interval: 1, features: ["js"], threadFilters: ["GeckoMain"] }, /** * Returns an object indicating if mock actors are being used or not. */ getMocksInUse: function () { return { memory: this._usingMockMemory, timeline: this._usingMockTimeline }; } }; /** * Returns a promise resolved with a listing of all the tabs in the * provided thread client. */ function listTabs(client) { let deferred = promise.defer(); client.listTabs(deferred.resolve); return deferred.promise; } exports.getPerformanceActorsConnection = target => SharedPerformanceActors.forTarget(target); exports.PerformanceFront = PerformanceFront;