/* 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"; /** * @typedef {import("perf").BulkReceiving} BulkSending */ const { Actor } = require("resource://devtools/shared/protocol.js"); const { perfSpec } = require("resource://devtools/shared/specs/perf.js"); ChromeUtils.defineESModuleGetters( this, { RecordingUtils: "resource://devtools/shared/performance-new/recording-utils.sys.mjs", }, { global: "contextual" } ); // Some platforms are built without the Gecko Profiler. const IS_SUPPORTED_PLATFORM = "nsIProfiler" in Ci; /** * The PerfActor wraps the Gecko Profiler interface (aka Services.profiler). */ exports.PerfActor = class PerfActor extends Actor { /** * This counter is incremented at each new capture. This makes sure that the * profile data and the additionalInformation are in sync. * @type {number} */ #captureHandleCounter = 0; /** * This stores the profile data retrieved from the last call to * startCaptureAndStopProfiler. * @type {Promise |null} */ #previouslyRetrievedProfileDataPromise = null; /** * This stores the additionalInformation returned by * getProfileDataAsGzippedArrayBufferThenStop so that it can be sent to the * front using getPreviouslyRetrievedAdditionalInformation. * @type {Promise| null} */ #previouslyRetrievedAdditionalInformationPromise = null; constructor(conn) { super(conn, perfSpec); // Only setup the observers on a supported platform. if (IS_SUPPORTED_PLATFORM) { this._observer = { observe: this._observe.bind(this), }; Services.obs.addObserver(this._observer, "profiler-started"); Services.obs.addObserver(this._observer, "profiler-stopped"); } } destroy() { super.destroy(); if (!IS_SUPPORTED_PLATFORM) { return; } Services.obs.removeObserver(this._observer, "profiler-started"); Services.obs.removeObserver(this._observer, "profiler-stopped"); } startProfiler(options) { if (!IS_SUPPORTED_PLATFORM) { return false; } // For a quick implementation, decide on some default values. These may need // to be tweaked or made configurable as needed. const settings = { entries: options.entries || 1000000, duration: options.duration || 0, interval: options.interval || 1, features: options.features || ["js", "stackwalk", "cpu", "memory"], threads: options.threads || ["GeckoMain", "Compositor"], activeTabID: RecordingUtils.getActiveBrowserID(), }; try { // This can throw an error if the profiler is in the wrong state. Services.profiler.StartProfiler( settings.entries, settings.interval, settings.features, settings.threads, settings.activeTabID, settings.duration ); } catch (e) { // In case any errors get triggered, bailout with a false. return false; } return true; } stopProfilerAndDiscardProfile() { if (!IS_SUPPORTED_PLATFORM) { return null; } return Services.profiler.StopProfiler(); } /** * @type {string} debugPath * @type {string} breakpadId * @returns {Promise<[number[], number[], number[]]>} */ async getSymbolTable(debugPath, breakpadId) { const [addr, index, buffer] = await Services.profiler.getSymbolTable( debugPath, breakpadId ); // The protocol does not support the transfer of typed arrays, so we convert // these typed arrays to plain JS arrays of numbers now. // Our return value type is declared as "array:array:number". return [Array.from(addr), Array.from(index), Array.from(buffer)]; } /* @backward-compat { version 140 } * Version 140 introduced getProfileAndStopProfilerBulk below, a more * efficient version of getProfileAndStopProfiler. getProfileAndStopProfiler * needs to stay in the spec to support older versions of Firefox, so it's * also present here. */ async getProfileAndStopProfiler() { throw new Error( "Unexpected getProfileAndStopProfiler function called in Firefox v140+. Most likely you're using an older version of Firefox to debug this application. Please use at least Firefox v140." ); } async startCaptureAndStopProfiler() { if (!IS_SUPPORTED_PLATFORM) { throw new Error("Profiling is not supported on this platform."); } const capturePromise = RecordingUtils.getProfileDataAsGzippedArrayBufferThenStop(); this.#previouslyRetrievedProfileDataPromise = capturePromise.then( ({ profileCaptureResult }) => { if (profileCaptureResult.type === "ERROR") { throw profileCaptureResult.error; } return profileCaptureResult.profile; } ); this.#previouslyRetrievedAdditionalInformationPromise = capturePromise.then( ({ additionalInformation }) => additionalInformation ); return ++this.#captureHandleCounter; } /** * This actor function returns the profile data using the bulk protocol. * @param {number} handle returned by startCaptureAndStopProfiler * @returns {Promise} */ async getPreviouslyCapturedProfileDataBulk(handle, startBulkSend) { if (handle < this.#captureHandleCounter) { // This handle is outdated, write a message to the console and throw an error console.error( `[devtools perf actor] In getPreviouslyCapturedProfileDataBulk, the requested handle ${handle} is smaller than the current counter ${this.#captureHandleCounter}.` ); throw new Error(`The requested data was not found.`); } if (this.#previouslyRetrievedProfileDataPromise === null) { // No capture operation has been started, write a message and throw an error. console.error( `[devtools perf actor] In getPreviouslyCapturedProfileDataBulk, there's no data to be returned.` ); throw new Error(`The requested data was not found.`); } // Note that this promise might be rejected if there was an error. That's OK // and part of the design. const profile = await this.#previouslyRetrievedProfileDataPromise; this.#previouslyRetrievedProfileDataPromise = null; const bulk = await startBulkSend(profile.byteLength); await bulk.copyFromBuffer(profile); } /** * @param {number} handle returned by startCaptureAndStopProfiler * @returns {Promise} */ async getPreviouslyRetrievedAdditionalInformation(handle) { if (handle < this.#captureHandleCounter) { // This handle is outdated, write a message to the console and throw an error console.error( `[devtools perf actor] In getPreviouslyRetrievedAdditionalInformation, the requested handle ${handle} is smaller than the current counter ${this.#captureHandleCounter}.` ); throw new Error(`The requested data was not found.`); } if (this.#previouslyRetrievedAdditionalInformationPromise === null) { // No capture operation has been started, write a message and throw an error. console.error( `[devtools perf actor] In getPreviouslyRetrievedAdditionalInformation, there's no data to be returned.` ); throw new Error(`The requested data was not found.`); } try { return this.#previouslyRetrievedAdditionalInformationPromise; } finally { this.#previouslyRetrievedAdditionalInformationPromise = null; } } isActive() { if (!IS_SUPPORTED_PLATFORM) { return false; } return Services.profiler.IsActive(); } isSupportedPlatform() { return IS_SUPPORTED_PLATFORM; } /** * Watch for events that happen within the browser. These can affect the * current availability and state of the Gecko Profiler. */ _observe(subject, topic, _data) { // Note! If emitting new events make sure and update the list of bridged // events in the perf actor. switch (topic) { case "profiler-started": { const param = subject.QueryInterface(Ci.nsIProfilerStartParams); this.emit( topic, param.entries, param.interval, param.features, param.duration, param.activeTabID ); break; } case "profiler-stopped": this.emit(topic); break; } } /** * Lists the supported features of the profiler for the current browser. * @returns {string[]} */ getSupportedFeatures() { if (!IS_SUPPORTED_PLATFORM) { return []; } return Services.profiler.GetFeatures(); } };