tor-browser

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

memory.js (15106B)


      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 {
      8  reportException,
      9 } = require("resource://devtools/shared/DevToolsUtils.js");
     10 const { expectState } = require("resource://devtools/server/actors/common.js");
     11 
     12 loader.lazyRequireGetter(
     13  this,
     14  "EventEmitter",
     15  "resource://devtools/shared/event-emitter.js"
     16 );
     17 const lazy = {};
     18 ChromeUtils.defineESModuleGetters(
     19  lazy,
     20  {
     21    DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
     22  },
     23  { global: "contextual" }
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "StackFrameCache",
     28  "resource://devtools/server/actors/utils/stack.js",
     29  true
     30 );
     31 loader.lazyRequireGetter(
     32  this,
     33  "ParentProcessTargetActor",
     34  "resource://devtools/server/actors/targets/parent-process.js",
     35  true
     36 );
     37 loader.lazyRequireGetter(
     38  this,
     39  "ContentProcessTargetActor",
     40  "resource://devtools/server/actors/targets/content-process.js",
     41  true
     42 );
     43 
     44 /**
     45 * A class that returns memory data for a parent actor's window.
     46 * Using a target-scoped actor with this instance will measure the memory footprint of its
     47 * parent tab. Using a global-scoped actor instance however, will measure the memory
     48 * footprint of the chrome window referenced by its root actor.
     49 *
     50 * To be consumed by actor's, like MemoryActor using this module to
     51 * send information over RDP, and TimelineActor for using more light-weight
     52 * utilities like GC events and measuring memory consumption.
     53 */
     54 class Memory extends EventEmitter {
     55  constructor(parent, frameCache = new StackFrameCache()) {
     56    super();
     57 
     58    this.parent = parent;
     59    this._mgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
     60      Ci.nsIMemoryReporterManager
     61    );
     62    this.state = "detached";
     63    this._dbg = null;
     64    this._frameCache = frameCache;
     65 
     66    this._onGarbageCollection = this._onGarbageCollection.bind(this);
     67    this._emitAllocations = this._emitAllocations.bind(this);
     68    this._onWindowReady = this._onWindowReady.bind(this);
     69 
     70    this.parent.on("window-ready", this._onWindowReady);
     71  }
     72 
     73  destroy() {
     74    this.parent.off("window-ready", this._onWindowReady);
     75 
     76    this._mgr = null;
     77    if (this.state === "attached") {
     78      this.detach();
     79    }
     80  }
     81 
     82  get dbg() {
     83    if (!this._dbg) {
     84      this._dbg = this.parent.makeDebugger();
     85    }
     86    return this._dbg;
     87  }
     88 
     89  /**
     90   * Attach to this MemoryBridge.
     91   *
     92   * This attaches the MemoryBridge's Debugger instance so that you can start
     93   * recording allocations or take a census of the heap. In addition, the
     94   * MemoryBridge will start emitting GC events.
     95   */
     96  attach() {
     97    // The actor may be attached by the Target via recordAllocation configuration
     98    // or manually by the frontend.
     99    if (this.state == "attached") {
    100      return this.state;
    101    }
    102    this.dbg.addDebuggees();
    103    this.dbg.memory.onGarbageCollection = this._onGarbageCollection.bind(this);
    104    this.state = "attached";
    105    return this.state;
    106  }
    107 
    108  /**
    109   * Detach from this MemoryBridge.
    110   */
    111  detach = expectState(
    112    "attached",
    113    function () {
    114      this._clearDebuggees();
    115      this.dbg.disable();
    116      this._dbg = null;
    117      this.state = "detached";
    118      return this.state;
    119    },
    120    "detaching from the debugger"
    121  );
    122 
    123  /**
    124   * Gets the current MemoryBridge attach/detach state.
    125   */
    126  getState() {
    127    return this.state;
    128  }
    129 
    130  _clearDebuggees() {
    131    if (this._dbg) {
    132      if (this.isRecordingAllocations()) {
    133        this.dbg.memory.drainAllocationsLog();
    134      }
    135      this._clearFrames();
    136      this.dbg.removeAllDebuggees();
    137    }
    138  }
    139 
    140  _clearFrames() {
    141    if (this.isRecordingAllocations()) {
    142      this._frameCache.clearFrames();
    143    }
    144  }
    145 
    146  /**
    147   * Handler for the parent actor's "window-ready" event.
    148   */
    149  _onWindowReady({ isTopLevel }) {
    150    if (this.state == "attached") {
    151      this._clearDebuggees();
    152      if (isTopLevel && this.isRecordingAllocations()) {
    153        this._frameCache.initFrames();
    154      }
    155      this.dbg.addDebuggees();
    156    }
    157  }
    158 
    159  /**
    160   * Returns a boolean indicating whether or not allocation
    161   * sites are being tracked.
    162   */
    163  isRecordingAllocations() {
    164    return this.dbg.memory.trackingAllocationSites;
    165  }
    166 
    167  /**
    168   * Save a heap snapshot scoped to the current debuggees' portion of the heap
    169   * graph.
    170   *
    171   * @param {object | null} boundaries
    172   *
    173   * @returns {string} The snapshot id.
    174   */
    175  saveHeapSnapshot = expectState(
    176    "attached",
    177    function (boundaries = null) {
    178      // If we are observing the whole process, then scope the snapshot
    179      // accordingly. Otherwise, use the debugger's debuggees.
    180      if (!boundaries) {
    181        if (
    182          this.parent instanceof ParentProcessTargetActor ||
    183          this.parent instanceof ContentProcessTargetActor
    184        ) {
    185          boundaries = { runtime: true };
    186        } else {
    187          boundaries = { debugger: this.dbg };
    188        }
    189      }
    190      return ChromeUtils.saveHeapSnapshotGetId(boundaries);
    191    },
    192    "saveHeapSnapshot"
    193  );
    194 
    195  /**
    196   * Take a census of the heap. See js/src/doc/Debugger/Debugger.Memory.md for
    197   * more information.
    198   */
    199  takeCensus = expectState(
    200    "attached",
    201    function () {
    202      return this.dbg.memory.takeCensus();
    203    },
    204    "taking census"
    205  );
    206 
    207  /**
    208   * Start recording allocation sites.
    209   *
    210   * @param {number} options.probability
    211   *                 The probability we sample any given allocation when recording
    212   *                 allocations. Must be between 0 and 1 -- defaults to 1.
    213   * @param {number} options.maxLogLength
    214   *                 The maximum number of allocation events to keep in the
    215   *                 log. If new allocs occur while at capacity, oldest
    216   *                 allocations are lost. Must fit in a 32 bit signed integer.
    217   * @param {number} options.drainAllocationsTimeout
    218   *                 A number in milliseconds of how often, at least, an `allocation`
    219   *                 event gets emitted (and drained), and also emits and drains on every
    220   *                 GC event, resetting the timer.
    221   */
    222  startRecordingAllocations = expectState(
    223    "attached",
    224    function (options = {}) {
    225      if (this.isRecordingAllocations()) {
    226        return this._getCurrentTime();
    227      }
    228 
    229      this._frameCache.initFrames();
    230 
    231      this.dbg.memory.allocationSamplingProbability =
    232        options.probability != null ? options.probability : 1.0;
    233 
    234      this.drainAllocationsTimeoutTimer = options.drainAllocationsTimeout;
    235 
    236      if (this.drainAllocationsTimeoutTimer != null) {
    237        if (this._poller) {
    238          this._poller.disarm();
    239        }
    240        this._poller = new lazy.DeferredTask(
    241          this._emitAllocations,
    242          this.drainAllocationsTimeoutTimer,
    243          0
    244        );
    245        this._poller.arm();
    246      }
    247 
    248      if (options.maxLogLength != null) {
    249        this.dbg.memory.maxAllocationsLogLength = options.maxLogLength;
    250      }
    251      this.dbg.memory.trackingAllocationSites = true;
    252 
    253      return this._getCurrentTime();
    254    },
    255    "starting recording allocations"
    256  );
    257 
    258  /**
    259   * Stop recording allocation sites.
    260   */
    261  stopRecordingAllocations = expectState(
    262    "attached",
    263    function () {
    264      if (!this.isRecordingAllocations()) {
    265        return this._getCurrentTime();
    266      }
    267      this.dbg.memory.trackingAllocationSites = false;
    268      this._clearFrames();
    269 
    270      if (this._poller) {
    271        this._poller.disarm();
    272        this._poller = null;
    273      }
    274 
    275      return this._getCurrentTime();
    276    },
    277    "stopping recording allocations"
    278  );
    279 
    280  /**
    281   * Return settings used in `startRecordingAllocations` for `probability`
    282   * and `maxLogLength`. Currently only uses in tests.
    283   */
    284  getAllocationsSettings = expectState(
    285    "attached",
    286    function () {
    287      return {
    288        maxLogLength: this.dbg.memory.maxAllocationsLogLength,
    289        probability: this.dbg.memory.allocationSamplingProbability,
    290      };
    291    },
    292    "getting allocations settings"
    293  );
    294 
    295  /**
    296   * Get a list of the most recent allocations since the last time we got
    297   * allocations, as well as a summary of all allocations since we've been
    298   * recording.
    299   *
    300   * @returns Object
    301   *          An object of the form:
    302   *
    303   *            {
    304   *              allocations: [<index into "frames" below>, ...],
    305   *              allocationsTimestamps: [
    306   *                <timestamp for allocations[0]>,
    307   *                <timestamp for allocations[1]>,
    308   *                ...
    309   *              ],
    310   *              allocationSizes: [
    311   *                <bytesize for allocations[0]>,
    312   *                <bytesize for allocations[1]>,
    313   *                ...
    314   *              ],
    315   *              frames: [
    316   *                {
    317   *                  line: <line number for this frame>,
    318   *                  column: <column number for this frame>,
    319   *                  source: <filename string for this frame>,
    320   *                  functionDisplayName:
    321   *                    <this frame's inferred function name function or null>,
    322   *                  parent: <index into "frames">
    323   *                },
    324   *                ...
    325   *              ],
    326   *            }
    327   *
    328   *          The timestamps' unit is microseconds since the epoch.
    329   *
    330   *          Subsequent `getAllocations` request within the same recording and
    331   *          tab navigation will always place the same stack frames at the same
    332   *          indices as previous `getAllocations` requests in the same
    333   *          recording. In other words, it is safe to use the index as a
    334   *          unique, persistent id for its frame.
    335   *
    336   *          Additionally, the root node (null) is always at index 0.
    337   *
    338   *          We use the indices into the "frames" array to avoid repeating the
    339   *          description of duplicate stack frames both when listing
    340   *          allocations, and when many stacks share the same tail of older
    341   *          frames. There shouldn't be any duplicates in the "frames" array,
    342   *          as that would defeat the purpose of this compression trick.
    343   *
    344   *          In the future, we might want to split out a frame's "source" and
    345   *          "functionDisplayName" properties out the same way we have split
    346   *          frames out with the "frames" array. While this would further
    347   *          compress the size of the response packet, it would increase CPU
    348   *          usage to build the packet, and it should, of course, be guided by
    349   *          profiling and done only when necessary.
    350   */
    351  getAllocations = expectState(
    352    "attached",
    353    function () {
    354      if (this.dbg.memory.allocationsLogOverflowed) {
    355        // Since the last time we drained the allocations log, there have been
    356        // more allocations than the log's capacity, and we lost some data. There
    357        // isn't anything actionable we can do about this, but put a message in
    358        // the browser console so we at least know that it occurred.
    359        reportException(
    360          "MemoryBridge.prototype.getAllocations",
    361          "Warning: allocations log overflowed and lost some data."
    362        );
    363      }
    364 
    365      const allocations = this.dbg.memory.drainAllocationsLog();
    366      const packet = {
    367        allocations: [],
    368        allocationsTimestamps: [],
    369        allocationSizes: [],
    370      };
    371      for (const { frame: stack, timestamp, size } of allocations) {
    372        if (stack && Cu.isDeadWrapper(stack)) {
    373          continue;
    374        }
    375 
    376        // Safe because SavedFrames are frozen/immutable.
    377        const waived = Cu.waiveXrays(stack);
    378 
    379        // Ensure that we have a form, size, and index for new allocations
    380        // because we potentially haven't seen some or all of them yet. After this
    381        // loop, we can rely on the fact that every frame we deal with already has
    382        // its metadata stored.
    383        const index = this._frameCache.addFrame(waived);
    384 
    385        packet.allocations.push(index);
    386        packet.allocationsTimestamps.push(timestamp);
    387        packet.allocationSizes.push(size);
    388      }
    389 
    390      return this._frameCache.updateFramePacket(packet);
    391    },
    392    "getting allocations"
    393  );
    394 
    395  /*
    396   * Force a browser-wide GC.
    397   */
    398  forceGarbageCollection() {
    399    for (let i = 0; i < 3; i++) {
    400      Cu.forceGC();
    401    }
    402  }
    403 
    404  /**
    405   * Force an XPCOM cycle collection. For more information on XPCOM cycle
    406   * collection, see
    407   * https://developer.mozilla.org/en-US/docs/Interfacing_with_the_XPCOM_cycle_collector#What_the_cycle_collector_does
    408   */
    409  forceCycleCollection() {
    410    Cu.forceCC();
    411  }
    412 
    413  /**
    414   * A method that returns a detailed breakdown of the memory consumption of the
    415   * associated window.
    416   *
    417   * @returns object
    418   */
    419  measure() {
    420    const result = {};
    421 
    422    const jsObjectsSize = {};
    423    const jsStringsSize = {};
    424    const jsOtherSize = {};
    425    const domSize = {};
    426    const styleSize = {};
    427    const otherSize = {};
    428    const totalSize = {};
    429    const jsMilliseconds = {};
    430    const nonJSMilliseconds = {};
    431 
    432    try {
    433      this._mgr.sizeOfTab(
    434        this.parent.window,
    435        jsObjectsSize,
    436        jsStringsSize,
    437        jsOtherSize,
    438        domSize,
    439        styleSize,
    440        otherSize,
    441        totalSize,
    442        jsMilliseconds,
    443        nonJSMilliseconds
    444      );
    445      result.total = totalSize.value;
    446      result.domSize = domSize.value;
    447      result.styleSize = styleSize.value;
    448      result.jsObjectsSize = jsObjectsSize.value;
    449      result.jsStringsSize = jsStringsSize.value;
    450      result.jsOtherSize = jsOtherSize.value;
    451      result.otherSize = otherSize.value;
    452      result.jsMilliseconds = jsMilliseconds.value.toFixed(1);
    453      result.nonJSMilliseconds = nonJSMilliseconds.value.toFixed(1);
    454    } catch (e) {
    455      reportException("MemoryBridge.prototype.measure", e);
    456    }
    457 
    458    return result;
    459  }
    460 
    461  residentUnique() {
    462    return this._mgr.residentUnique;
    463  }
    464 
    465  /**
    466   * Handler for GC events on the Debugger.Memory instance.
    467   */
    468  _onGarbageCollection(data) {
    469    this.emit("garbage-collection", data);
    470 
    471    // If `drainAllocationsTimeout` set, fire an allocations event with the drained log,
    472    // which will restart the timer.
    473    if (this._poller) {
    474      this._poller.disarm();
    475      this._emitAllocations();
    476    }
    477  }
    478 
    479  /**
    480   * Called on `drainAllocationsTimeoutTimer` interval if and only if set
    481   * during `startRecordingAllocations`, or on a garbage collection event if
    482   * drainAllocationsTimeout was set.
    483   * Drains allocation log and emits as an event and restarts the timer.
    484   */
    485  _emitAllocations() {
    486    this.emit("allocations", this.getAllocations());
    487    this._poller.arm();
    488  }
    489 
    490  /**
    491   * Accesses the docshell to return the current process time.
    492   */
    493  _getCurrentTime() {
    494    const docShell = this.parent.isRootActor
    495      ? this.parent.docShell
    496      : this.parent.originalDocShell;
    497    if (docShell) {
    498      return docShell.now();
    499    }
    500    // When used from the ContentProcessTargetActor, parent has no docShell,
    501    // so fallback to ChromeUtils.now
    502    return ChromeUtils.now();
    503  }
    504 }
    505 
    506 exports.Memory = Memory;