tor-browser

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

resources.js (14400B)


      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 const { throttle } = require("resource://devtools/shared/throttle.js");
      8 
      9 const { makeDebuggeeValue } = require("devtools/server/actors/object/utils");
     10 
     11 const {
     12  TYPES,
     13  getResourceWatcher,
     14 } = require("resource://devtools/server/actors/resources/index.js");
     15 const { JSTRACER_TRACE } = TYPES;
     16 
     17 const lazy = {};
     18 ChromeUtils.defineESModuleGetters(
     19  lazy,
     20  {
     21    JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs",
     22  },
     23  { global: "contextual" }
     24 );
     25 
     26 const {
     27  getActorIdForInternalSourceId,
     28 } = require("resource://devtools/server/actors/utils/dbg-source.js");
     29 
     30 const THROTTLING_DELAY = 250;
     31 
     32 class ResourcesTracingListener {
     33  constructor({ targetActor, traceValues, traceActor }) {
     34    this.targetActor = targetActor;
     35    this.traceValues = traceValues;
     36    this.sourcesManager = targetActor.sourcesManager;
     37    this.traceActor = traceActor;
     38 
     39    // On workers, we don't have access to setTimeout and can't have throttling
     40    this.throttleEmitTraces = isWorker
     41      ? this.flushTraces.bind(this)
     42      : throttle(this.flushTraces.bind(this), THROTTLING_DELAY);
     43  }
     44 
     45  // Collect pending data to be sent to the client in various arrays,
     46  // each focusing on particular data type.
     47  // All these arrays contains arrays as elements.
     48  #throttledTraces = [];
     49 
     50  // Index of the next collected frame
     51  #frameIndex = 0;
     52  // Three level of Maps, ultimately storing frame indexes.
     53  // The first level of Map is keyed by source ID,
     54  // the second by line number,
     55  // the last by column number.
     56  // Frame objects are sent to the client and not being held in memory,
     57  // we only store their related indexes which are put in trace arrays.
     58  #frameMap = new Map();
     59 
     60  /**
     61   * Called when the tracer stops recording JS executions.
     62   */
     63  stop() {
     64    this.#frameIndex = 0;
     65    this.#frameMap.clear();
     66  }
     67 
     68  /**
     69   * This method is throttled and will notify all pending traces to be logged in the console
     70   * via the console message watcher.
     71   */
     72  flushTraces() {
     73    const traceWatcher = getResourceWatcher(this.targetActor, JSTRACER_TRACE);
     74    // Ignore the request if the frontend isn't listening to traces for that target.
     75    if (!traceWatcher) {
     76      return;
     77    }
     78    const traces = this.#throttledTraces;
     79    this.#throttledTraces = [];
     80 
     81    traceWatcher.emitTraces(traces);
     82  }
     83 
     84  /**
     85   * Be notified by the underlying JavaScriptTracer class
     86   * in case it stops by itself, instead of being stopped when the Actor's stopTracing
     87   * method is called by the user.
     88   *
     89   * @param {boolean} enabled
     90   *        True if the tracer starts tracing, false it it stops.
     91   * @return {boolean}
     92   *         Return true, if the JavaScriptTracer should log a message to stdout.
     93   */
     94  onTracingToggled(enabled) {
     95    if (!enabled) {
     96      this.traceActor.stopTracing();
     97    }
     98    return false;
     99  }
    100 
    101  /**
    102   * Called when "trace on next user interaction" is enabled, to notify the user
    103   * that the tracer is initialized but waiting for the user first input.
    104   */
    105  onTracingPending() {
    106    const consoleMessageWatcher = getResourceWatcher(
    107      this.targetActor,
    108      TYPES.CONSOLE_MESSAGE
    109    );
    110    if (consoleMessageWatcher) {
    111      consoleMessageWatcher.emitMessages([
    112        {
    113          arguments: [lazy.JSTracer.NEXT_INTERACTION_MESSAGE],
    114          styles: [],
    115          level: "jstracer",
    116          chromeContext: false,
    117          timeStamp: ChromeUtils.dateNow(),
    118        },
    119      ]);
    120    }
    121    return false;
    122  }
    123 
    124  /**
    125   * Called by JavaScriptTracer class when a new mutation happened on any DOM Element.
    126   *
    127   * @param {object} options
    128   * @param {number} options.depth
    129   *        Represents the depth of the frame in the call stack.
    130   * @param {string} options.prefix
    131   *        A string to be displayed as a prefix of any logged frame.
    132   * @param {nsIStackFrame} options.caller
    133   *        The JS Callsite which caused this mutation.
    134   * @param {string} options.type
    135   *        Type of DOM Mutation:
    136   *        - "add": Node being added,
    137   *        - "attributes": Node whose attributes changed,
    138   *        - "remove": Node being removed,
    139   * @param {DOMNode} options.element
    140   *        The DOM Node related to the current mutation.
    141   * @return {boolean}
    142   *         Return true, if the JavaScriptTracer should log a message to stdout.
    143   */
    144  onTracingDOMMutation({ depth, prefix, type, caller, element }) {
    145    const dbgObj = makeDebuggeeValue(this.targetActor, element);
    146    const frameIndex = this.#getFrameIndex(
    147      null,
    148      null,
    149      caller
    150        ? getActorIdForInternalSourceId(this.targetActor, caller.sourceId)
    151        : null,
    152      caller?.lineNumber,
    153      caller?.columnNumber,
    154      caller?.filename
    155    );
    156    this.#throttledTraces.push([
    157      "dom-mutation",
    158      prefix,
    159      frameIndex,
    160      ChromeUtils.dateNow(),
    161      depth,
    162      type,
    163      this.traceActor.createValueGrip(dbgObj),
    164    ]);
    165    this.throttleEmitTraces();
    166    return false;
    167  }
    168 
    169  /**
    170   * Called by JavaScriptTracer class on each step of a function call.
    171   *
    172   * @param {object} options
    173   * @param {Debugger.Frame} options.frame
    174   *        A descriptor object for the JavaScript frame.
    175   * @param {number} options.depth
    176   *        Represents the depth of the frame in the call stack.
    177   * @param {string} options.prefix
    178   *        A string to be displayed as a prefix of any logged frame.
    179   * @return {boolean}
    180   *         Return true, if the JavaScriptTracer should log the step to stdout.
    181   */
    182  onTracingFrameStep({ frame, depth, prefix }) {
    183    const { script } = frame;
    184    const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
    185    const url = script.source.url;
    186 
    187    // NOTE: Debugger.Script.prototype.getOffsetMetadata returns
    188    //       columnNumber in 1-based.
    189    //       Convert to 0-based, while keeping the wasm's column (1) as is.
    190    //       (bug 1863878)
    191    const columnBase = script.format === "wasm" ? 0 : 1;
    192    const column = columnNumber - columnBase;
    193 
    194    // Ignore blackboxed sources
    195    if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) {
    196      return false;
    197    }
    198 
    199    const frameIndex = this.#getFrameIndex(
    200      frame.implementation,
    201      null,
    202      getActorIdForInternalSourceId(this.targetActor, script.source.id),
    203      lineNumber,
    204      column,
    205      url
    206    );
    207    this.#throttledTraces.push([
    208      "step",
    209      prefix,
    210      frameIndex,
    211      ChromeUtils.dateNow(),
    212      depth,
    213      null,
    214    ]);
    215    this.throttleEmitTraces();
    216 
    217    return false;
    218  }
    219 
    220  #getFrameIndex(implementation, name, sourceId, line, column, url) {
    221    let perSourceMap = this.#frameMap.get(sourceId);
    222    if (!perSourceMap) {
    223      perSourceMap = new Map();
    224      this.#frameMap.set(sourceId, perSourceMap);
    225    }
    226    let perLineMap = perSourceMap.get(line);
    227    if (!perLineMap) {
    228      perLineMap = new Map();
    229      perSourceMap.set(line, perLineMap);
    230    }
    231    let frameIndex = perLineMap.get(column);
    232 
    233    if (frameIndex == undefined) {
    234      frameIndex = this.#frameIndex++;
    235 
    236      // Remember updating TRACER_FIELDS_INDEXES when modifying the following array:
    237      const frameArray = [
    238        "frame",
    239        implementation,
    240        name,
    241        sourceId,
    242        line,
    243        column,
    244        url,
    245      ];
    246 
    247      perLineMap.set(column, frameIndex);
    248      this.#throttledTraces.push(frameArray);
    249    }
    250    return frameIndex;
    251  }
    252 
    253  /**
    254   * Called by JavaScriptTracer class when a new JavaScript frame is executed.
    255   *
    256   * @param {Debugger.Frame} frame
    257   *        A descriptor object for the JavaScript frame.
    258   * @param {number} depth
    259   *        Represents the depth of the frame in the call stack.
    260   * @param {string} formatedDisplayName
    261   *        A human readable name for the current frame.
    262   * @param {string} prefix
    263   *        A string to be displayed as a prefix of any logged frame.
    264   * @param {string} currentDOMEvent
    265   *        If this is a top level frame (depth==0), and we are currently processing
    266   *        a DOM Event, this will refer to the name of that DOM Event.
    267   *        Note that it may also refer to setTimeout and setTimeout callback calls.
    268   * @return {boolean}
    269   *         Return true, if the JavaScriptTracer should log the frame to stdout.
    270   */
    271  onTracingFrame({
    272    frame,
    273    depth,
    274    formatedDisplayName,
    275    prefix,
    276    currentDOMEvent,
    277  }) {
    278    const { script } = frame;
    279    const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
    280    const url = script.source.url;
    281 
    282    // NOTE: Debugger.Script.prototype.getOffsetMetadata returns
    283    //       columnNumber in 1-based.
    284    //       Convert to 0-based, while keeping the wasm's column (1) as is.
    285    //       (bug 1863878)
    286    const columnBase = script.format === "wasm" ? 0 : 1;
    287    const column = columnNumber - columnBase;
    288 
    289    // Ignore blackboxed sources
    290    if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) {
    291      return false;
    292    }
    293 
    294    // We may receive the currently processed DOM event (if this relates to one).
    295    // In this case, log a preliminary message, which looks different to highlight it.
    296    if (currentDOMEvent && depth == 0) {
    297      // Create a JSTRACER_TRACE resource with a slightly different shape
    298      this.#throttledTraces.push([
    299        "event",
    300        prefix,
    301        null,
    302        ChromeUtils.dateNow(),
    303        // Events are parent of any subsequent JS call, which has a 0 depth.
    304        -1,
    305        currentDOMEvent,
    306      ]);
    307    }
    308 
    309    let args = undefined,
    310      argNames = undefined;
    311    // Log arguments, but only when this feature is enabled as it introduce
    312    // some significant overhead in perf as well as memory as it may hold the objects in memory.
    313    // Also prevent trying to log function call arguments if we aren't logging a frame
    314    // with arguments (e.g. Debugger evaluation frames, when executing from the console)
    315    if (this.traceValues && frame.arguments) {
    316      args = [];
    317      for (let arg of frame.arguments) {
    318        // Debugger.Frame.arguments contains either a Debugger.Object or primitive object
    319        if (arg?.unsafeDereference) {
    320          arg = arg.unsafeDereference();
    321        }
    322        // Instantiate a object actor so that the tools can easily inspect these objects
    323        const dbgObj = makeDebuggeeValue(this.targetActor, arg);
    324        args.push(this.traceActor.createValueGrip(dbgObj));
    325      }
    326      argNames = frame.callee.script.parameterNames;
    327    }
    328 
    329    // In order for getActorIdForInternalSourceId to work reliably,
    330    // we have to ensure creating a source actor for that source.
    331    // It happens on Google Docs that some evaled sources aren't registered?
    332    this.sourcesManager.getOrCreateSourceActor(script.source);
    333 
    334    const frameIndex = this.#getFrameIndex(
    335      frame.implementation,
    336      formatedDisplayName,
    337      getActorIdForInternalSourceId(this.targetActor, script.source.id),
    338      lineNumber,
    339      column,
    340      url
    341    );
    342    this.#throttledTraces.push([
    343      "enter",
    344      prefix,
    345      frameIndex,
    346      ChromeUtils.dateNow(),
    347      depth,
    348      args,
    349      argNames,
    350    ]);
    351    this.throttleEmitTraces();
    352 
    353    return false;
    354  }
    355 
    356  /**
    357   * Called by JavaScriptTracer class when a JavaScript frame exits (i.e. a function returns or throw).
    358   *
    359   * @param {object} options
    360   * @param {number} options.frameId
    361   *        Unique identifier for the current frame.
    362   *        This should match a frame notified via onTracingFrame.
    363   * @param {Debugger.Frame} options.frame
    364   *        A descriptor object for the JavaScript frame.
    365   * @param {number} options.depth
    366   *        Represents the depth of the frame in the call stack.
    367   * @param {string} options.formatedDisplayName
    368   *        A human readable name for the current frame.
    369   * @param {string} options.prefix
    370   *        A string to be displayed as a prefix of any logged frame.
    371   * @param {string} options.why
    372   *        A string to explain why the function stopped.
    373   *        See tracer.sys.mjs's FRAME_EXIT_REASONS.
    374   * @param {Debugger.Object|primitive} options.rv
    375   *        The returned value. It can be the returned value, or the thrown exception.
    376   *        It is either a primitive object, otherwise it is a Debugger.Object for any other JS Object type.
    377   * @return {boolean}
    378   *         Return true, if the JavaScriptTracer should log the frame to stdout.
    379   */
    380  onTracingFrameExit({
    381    frameId,
    382    frame,
    383    depth,
    384    formatedDisplayName,
    385    prefix,
    386    why,
    387    rv,
    388  }) {
    389    const { script } = frame;
    390    const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
    391    const url = script.source.url;
    392 
    393    // NOTE: Debugger.Script.prototype.getOffsetMetadata returns
    394    //       columnNumber in 1-based.
    395    //       Convert to 0-based, while keeping the wasm's column (1) as is.
    396    //       (bug 1863878)
    397    const columnBase = script.format === "wasm" ? 0 : 1;
    398    const column = columnNumber - columnBase;
    399 
    400    // Ignore blackboxed sources
    401    if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) {
    402      return false;
    403    }
    404 
    405    let returnedValue = undefined;
    406    // Log arguments, but only when this feature is enabled as it introduce
    407    // some significant overhead in perf as well as memory as it may hold the objects in memory.
    408    if (this.traceValues) {
    409      // Debugger.Frame.arguments contains either a Debugger.Object or primitive object
    410      if (rv?.unsafeDereference) {
    411        rv = rv.unsafeDereference();
    412      }
    413      // Instantiate a object actor so that the tools can easily inspect these objects
    414      const dbgObj = makeDebuggeeValue(this.targetActor, rv);
    415      returnedValue = this.traceActor.createValueGrip(dbgObj);
    416    }
    417 
    418    const frameIndex = this.#getFrameIndex(
    419      frame.implementation,
    420      formatedDisplayName,
    421      getActorIdForInternalSourceId(this.targetActor, script.source.id),
    422      lineNumber,
    423      column,
    424      url
    425    );
    426    this.#throttledTraces.push([
    427      "exit",
    428      prefix,
    429      frameIndex,
    430      ChromeUtils.dateNow(),
    431      depth,
    432      frameId,
    433      returnedValue,
    434      why,
    435    ]);
    436    this.throttleEmitTraces();
    437 
    438    return false;
    439  }
    440 }
    441 
    442 exports.ResourcesTracingListener = ResourcesTracingListener;