tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

perf.js (8860B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /**
      8 * @typedef {import("perf").BulkReceiving} BulkSending
      9 */
     10 
     11 const { Actor } = require("resource://devtools/shared/protocol.js");
     12 const { perfSpec } = require("resource://devtools/shared/specs/perf.js");
     13 
     14 ChromeUtils.defineESModuleGetters(
     15  this,
     16  {
     17    RecordingUtils:
     18      "resource://devtools/shared/performance-new/recording-utils.sys.mjs",
     19    Symbolication:
     20      "resource://devtools/shared/performance-new/symbolication.sys.mjs",
     21  },
     22  { global: "contextual" }
     23 );
     24 
     25 // Some platforms are built without the Gecko Profiler.
     26 const IS_SUPPORTED_PLATFORM = "nsIProfiler" in Ci;
     27 
     28 /**
     29 * The PerfActor wraps the Gecko Profiler interface (aka Services.profiler).
     30 */
     31 exports.PerfActor = class PerfActor extends Actor {
     32  /**
     33   * This counter is incremented at each new capture. This makes sure that the
     34   * profile data and the additionalInformation are in sync.
     35   *
     36   * @type {number}
     37   */
     38  #captureHandleCounter = 0;
     39 
     40  /**
     41   * This stores the profile data retrieved from the last call to
     42   * startCaptureAndStopProfiler.
     43   *
     44   * @type {Promise<ArrayBuffer> |null}
     45   */
     46  #previouslyRetrievedProfileDataPromise = null;
     47 
     48  /**
     49   * This stores the additionalInformation returned by
     50   * getProfileDataAsGzippedArrayBufferThenStop so that it can be sent to the
     51   * front using getPreviouslyRetrievedAdditionalInformation.
     52   *
     53   * @type {Promise<MockedExports.ProfileGenerationAdditionalInformation>| null}
     54   */
     55  #previouslyRetrievedAdditionalInformationPromise = null;
     56 
     57  constructor(conn) {
     58    super(conn, perfSpec);
     59 
     60    // Only setup the observers on a supported platform.
     61    if (IS_SUPPORTED_PLATFORM) {
     62      this._observer = {
     63        observe: this._observe.bind(this),
     64      };
     65      Services.obs.addObserver(this._observer, "profiler-started");
     66      Services.obs.addObserver(this._observer, "profiler-stopped");
     67    }
     68  }
     69 
     70  destroy() {
     71    super.destroy();
     72 
     73    if (!IS_SUPPORTED_PLATFORM) {
     74      return;
     75    }
     76    Services.obs.removeObserver(this._observer, "profiler-started");
     77    Services.obs.removeObserver(this._observer, "profiler-stopped");
     78  }
     79 
     80  startProfiler(options) {
     81    if (!IS_SUPPORTED_PLATFORM) {
     82      return false;
     83    }
     84 
     85    // For a quick implementation, decide on some default values. These may need
     86    // to be tweaked or made configurable as needed.
     87    const settings = {
     88      entries: options.entries || 1000000,
     89      duration: options.duration || 0,
     90      interval: options.interval || 1,
     91      features: options.features || ["js", "stackwalk", "cpu", "memory"],
     92      threads: options.threads || ["GeckoMain", "Compositor"],
     93      activeTabID: RecordingUtils.getActiveBrowserID(),
     94    };
     95 
     96    try {
     97      // This can throw an error if the profiler is in the wrong state.
     98      Services.profiler.StartProfiler(
     99        settings.entries,
    100        settings.interval,
    101        settings.features,
    102        settings.threads,
    103        settings.activeTabID,
    104        settings.duration
    105      );
    106    } catch (e) {
    107      // In case any errors get triggered, bailout with a false.
    108      return false;
    109    }
    110 
    111    return true;
    112  }
    113 
    114  stopProfilerAndDiscardProfile() {
    115    if (!IS_SUPPORTED_PLATFORM) {
    116      return null;
    117    }
    118    return Services.profiler.StopProfiler();
    119  }
    120 
    121  /**
    122   * @type {string} debugPath
    123   * @type {string} breakpadId
    124   * @returns {Promise<[number[], number[], number[]]>}
    125   */
    126  async getSymbolTable(debugPath, breakpadId) {
    127    const libraries = Services.profiler.sharedLibraries;
    128    const symbolicationService = Symbolication.createLocalSymbolicationService(
    129      libraries,
    130      []
    131    );
    132    const debugName = libraries.find(
    133      lib => lib.path === debugPath && lib.breakpadId === breakpadId
    134    )?.debugName;
    135 
    136    if (debugName === undefined) {
    137      throw new Error(
    138        `Couldn't find the library with path ${debugPath} and breakpadId ${breakpadId}`
    139      );
    140    }
    141 
    142    const [addr, index, buffer] = await symbolicationService.getSymbolTable(
    143      debugName,
    144      breakpadId
    145    );
    146    // The protocol does not support the transfer of typed arrays, so we convert
    147    // these typed arrays to plain JS arrays of numbers now.
    148    // Our return value type is declared as "array:array:number".
    149    return [Array.from(addr), Array.from(index), Array.from(buffer)];
    150  }
    151 
    152  async startCaptureAndStopProfiler() {
    153    if (!IS_SUPPORTED_PLATFORM) {
    154      throw new Error("Profiling is not supported on this platform.");
    155    }
    156 
    157    const capturePromise =
    158      RecordingUtils.getProfileDataAsGzippedArrayBufferThenStop();
    159 
    160    this.#previouslyRetrievedProfileDataPromise = capturePromise.then(
    161      ({ profileCaptureResult }) => {
    162        if (profileCaptureResult.type === "ERROR") {
    163          throw profileCaptureResult.error;
    164        }
    165 
    166        return profileCaptureResult.profile;
    167      }
    168    );
    169 
    170    this.#previouslyRetrievedAdditionalInformationPromise = capturePromise.then(
    171      ({ additionalInformation }) => additionalInformation
    172    );
    173 
    174    return ++this.#captureHandleCounter;
    175  }
    176 
    177  /**
    178   * This actor function returns the profile data using the bulk protocol.
    179   *
    180   * @param {number} handle returned by startCaptureAndStopProfiler
    181   * @returns {Promise<void>}
    182   */
    183  async getPreviouslyCapturedProfileDataBulk(handle, startBulkSend) {
    184    if (handle < this.#captureHandleCounter) {
    185      // This handle is outdated, write a message to the console and throw an error
    186      console.error(
    187        `[devtools perf actor] In getPreviouslyCapturedProfileDataBulk, the requested handle ${handle} is smaller than the current counter ${this.#captureHandleCounter}.`
    188      );
    189      throw new Error(`The requested data was not found.`);
    190    }
    191 
    192    if (this.#previouslyRetrievedProfileDataPromise === null) {
    193      // No capture operation has been started, write a message and throw an error.
    194      console.error(
    195        `[devtools perf actor] In getPreviouslyCapturedProfileDataBulk, there's no data to be returned.`
    196      );
    197      throw new Error(`The requested data was not found.`);
    198    }
    199 
    200    // Note that this promise might be rejected if there was an error. That's OK
    201    // and part of the design.
    202    const profile = await this.#previouslyRetrievedProfileDataPromise;
    203    this.#previouslyRetrievedProfileDataPromise = null;
    204 
    205    const bulk = await startBulkSend(profile.byteLength);
    206    await bulk.copyFromBuffer(profile);
    207  }
    208 
    209  /**
    210   * @param {number} handle returned by startCaptureAndStopProfiler
    211   * @returns {Promise<MockedExports.ProfileGenerationAdditionalInformation>}
    212   */
    213  async getPreviouslyRetrievedAdditionalInformation(handle) {
    214    if (handle < this.#captureHandleCounter) {
    215      // This handle is outdated, write a message to the console and throw an error
    216      console.error(
    217        `[devtools perf actor] In getPreviouslyRetrievedAdditionalInformation, the requested handle ${handle} is smaller than the current counter ${this.#captureHandleCounter}.`
    218      );
    219      throw new Error(`The requested data was not found.`);
    220    }
    221 
    222    if (this.#previouslyRetrievedAdditionalInformationPromise === null) {
    223      // No capture operation has been started, write a message and throw an error.
    224      console.error(
    225        `[devtools perf actor] In getPreviouslyRetrievedAdditionalInformation, there's no data to be returned.`
    226      );
    227      throw new Error(`The requested data was not found.`);
    228    }
    229 
    230    try {
    231      return this.#previouslyRetrievedAdditionalInformationPromise;
    232    } finally {
    233      this.#previouslyRetrievedAdditionalInformationPromise = null;
    234    }
    235  }
    236 
    237  isActive() {
    238    if (!IS_SUPPORTED_PLATFORM) {
    239      return false;
    240    }
    241    return Services.profiler.IsActive();
    242  }
    243 
    244  isSupportedPlatform() {
    245    return IS_SUPPORTED_PLATFORM;
    246  }
    247 
    248  /**
    249   * Watch for events that happen within the browser. These can affect the
    250   * current availability and state of the Gecko Profiler.
    251   */
    252  _observe(subject, topic, _data) {
    253    // Note! If emitting new events make sure and update the list of bridged
    254    // events in the perf actor.
    255    switch (topic) {
    256      case "profiler-started": {
    257        const param = subject.QueryInterface(Ci.nsIProfilerStartParams);
    258        this.emit(
    259          topic,
    260          param.entries,
    261          param.interval,
    262          param.features,
    263          param.duration,
    264          param.activeTabID
    265        );
    266        break;
    267      }
    268      case "profiler-stopped":
    269        this.emit(topic);
    270        break;
    271    }
    272  }
    273 
    274  /**
    275   * Lists the supported features of the profiler for the current browser.
    276   *
    277   * @returns {string[]}
    278   */
    279  getSupportedFeatures() {
    280    if (!IS_SUPPORTED_PLATFORM) {
    281      return [];
    282    }
    283    return Services.profiler.GetFeatures();
    284  }
    285 };