tor-browser

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

reflow.js (14252B)


      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 * About the types of objects in this file:
      9 *
     10 * - ReflowActor: the actor class used for protocol purposes.
     11 *   Mostly empty, just gets an instance of LayoutChangesObserver and forwards
     12 *   its "reflows" events to clients.
     13 *
     14 * - LayoutChangesObserver: extends Observable and uses the ReflowObserver, to
     15 *   track reflows on the page.
     16 *   Used by the LayoutActor, but is also exported on the module, so can be used
     17 *   by any other actor that needs it.
     18 *
     19 * - Observable: A utility parent class, meant at being extended by classes that
     20 *   need a to observe something on the targetActor's windows.
     21 *
     22 * - Dedicated observers: There's only one of them for now: ReflowObserver which
     23 *   listens to reflow events via the docshell,
     24 *   These dedicated classes are used by the LayoutChangesObserver.
     25 */
     26 
     27 const { Actor } = require("resource://devtools/shared/protocol.js");
     28 const { reflowSpec } = require("resource://devtools/shared/specs/reflow.js");
     29 
     30 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     31 
     32 /**
     33 * The reflow actor tracks reflows and emits events about them.
     34 */
     35 exports.ReflowActor = class ReflowActor extends Actor {
     36  constructor(conn, targetActor) {
     37    super(conn, reflowSpec);
     38 
     39    this.targetActor = targetActor;
     40    this._onReflow = this._onReflow.bind(this);
     41    this.observer = getLayoutChangesObserver(targetActor);
     42    this._isStarted = false;
     43  }
     44 
     45  destroy() {
     46    this.stop();
     47    releaseLayoutChangesObserver(this.targetActor);
     48    this.observer = null;
     49    this.targetActor = null;
     50 
     51    super.destroy();
     52  }
     53 
     54  /**
     55   * Start tracking reflows and sending events to clients about them.
     56   * This is a oneway method, do not expect a response and it won't return a
     57   * promise.
     58   */
     59  start() {
     60    if (!this._isStarted) {
     61      this.observer.on("reflows", this._onReflow);
     62      this._isStarted = true;
     63    }
     64  }
     65 
     66  /**
     67   * Stop tracking reflows and sending events to clients about them.
     68   * This is a oneway method, do not expect a response and it won't return a
     69   * promise.
     70   */
     71  stop() {
     72    if (this._isStarted) {
     73      this.observer.off("reflows", this._onReflow);
     74      this._isStarted = false;
     75    }
     76  }
     77 
     78  _onReflow(reflows) {
     79    if (this._isStarted) {
     80      this.emit("reflows", reflows);
     81    }
     82  }
     83 };
     84 
     85 /**
     86 * Base class for all sorts of observers that need to listen to events on the
     87 * targetActor's windows.
     88 *
     89 * @param {WindowGlobalTargetActor} targetActor
     90 * @param {Function} callback Executed everytime the observer observes something
     91 */
     92 class Observable {
     93  constructor(targetActor, callback) {
     94    this.targetActor = targetActor;
     95    this.callback = callback;
     96 
     97    this._onWindowReady = this._onWindowReady.bind(this);
     98    this._onWindowDestroyed = this._onWindowDestroyed.bind(this);
     99 
    100    this.targetActor.on("window-ready", this._onWindowReady);
    101    this.targetActor.on("window-destroyed", this._onWindowDestroyed);
    102  }
    103 
    104  /**
    105   * Is the observer currently observing
    106   */
    107  isObserving = false;
    108 
    109  /**
    110   * Stop observing and detroy this observer instance
    111   */
    112  destroy() {
    113    if (this.isDestroyed) {
    114      return;
    115    }
    116    this.isDestroyed = true;
    117 
    118    this.stop();
    119 
    120    this.targetActor.off("window-ready", this._onWindowReady);
    121    this.targetActor.off("window-destroyed", this._onWindowDestroyed);
    122 
    123    this.callback = null;
    124    this.targetActor = null;
    125  }
    126 
    127  /**
    128   * Start observing whatever it is this observer is supposed to observe
    129   */
    130  start() {
    131    if (this.isObserving) {
    132      return;
    133    }
    134    this.isObserving = true;
    135 
    136    this._startListeners(this.targetActor.windows);
    137  }
    138 
    139  /**
    140   * Stop observing
    141   */
    142  stop() {
    143    if (!this.isObserving) {
    144      return;
    145    }
    146    this.isObserving = false;
    147 
    148    if (!this.targetActor.isDestroyed() && this.targetActor.docShell) {
    149      // It's only worth stopping if the targetActor is still active
    150      this._stopListeners(this.targetActor.windows);
    151    }
    152  }
    153 
    154  _onWindowReady({ window }) {
    155    if (this.isObserving) {
    156      this._startListeners([window]);
    157    }
    158  }
    159 
    160  _onWindowDestroyed({ window }) {
    161    if (this.isObserving) {
    162      this._stopListeners([window]);
    163    }
    164  }
    165 
    166  _startListeners() {
    167    // To be implemented by sub-classes.
    168  }
    169 
    170  _stopListeners() {
    171    // To be implemented by sub-classes.
    172  }
    173 
    174  /**
    175   * To be called by sub-classes when something has been observed
    176   */
    177  notifyCallback(...args) {
    178    this.isObserving && this.callback && this.callback.apply(null, args);
    179  }
    180 }
    181 
    182 /**
    183 * The LayouChangesObserver will observe reflows as soon as it is started.
    184 * Some devtools actors may cause reflows and it may be wanted to "hide" these
    185 * reflows from the LayouChangesObserver consumers.
    186 * If this is the case, such actors should require this module and use this
    187 * global function to turn the ignore mode on and off temporarily.
    188 *
    189 * Note that if a node is provided, it will be used to force a sync reflow to
    190 * make sure all reflows which occurred before switching the mode on or off are
    191 * either observed or ignored depending on the current mode.
    192 *
    193 * @param {boolean} ignore
    194 * @param {DOMNode} syncReflowNode The node to use to force a sync reflow
    195 */
    196 var gIgnoreLayoutChanges = false;
    197 exports.setIgnoreLayoutChanges = function (ignore, syncReflowNode) {
    198  if (syncReflowNode) {
    199    let forceSyncReflow = syncReflowNode.offsetWidth; // eslint-disable-line
    200  }
    201  gIgnoreLayoutChanges = ignore;
    202 };
    203 
    204 class LayoutChangesObserver extends EventEmitter {
    205  /**
    206   * The LayoutChangesObserver class is instantiated only once per given tab
    207   * and is used to track reflows and dom and style changes in that tab.
    208   * The LayoutActor uses this class to send reflow events to its clients.
    209   *
    210   * This class isn't exported on the module because it shouldn't be instantiated
    211   * to avoid creating several instances per tabs.
    212   * Use `getLayoutChangesObserver(targetActor)`
    213   * and `releaseLayoutChangesObserver(targetActor)`
    214   * which are exported to get and release instances.
    215   *
    216   * The observer loops every EVENT_BATCHING_DELAY ms and checks if layout changes
    217   * have happened since the last loop iteration. If there are, it sends the
    218   * corresponding events:
    219   *
    220   * - "reflows", with an array of all the reflows that occured,
    221   * - "resizes", with an array of all the resizes that occured,
    222   *
    223   * @param {WindowGlobalTargetActor} targetActor
    224   */
    225  constructor(targetActor) {
    226    super();
    227 
    228    this.targetActor = targetActor;
    229 
    230    this._startEventLoop = this._startEventLoop.bind(this);
    231    this._onReflow = this._onReflow.bind(this);
    232    this._onResize = this._onResize.bind(this);
    233 
    234    // Creating the various observers we're going to need
    235    // For now, just the reflow observer, but later we can add markupMutation,
    236    // styleSheetChanges and styleRuleChanges
    237    this.reflowObserver = new ReflowObserver(this.targetActor, this._onReflow);
    238    this.resizeObserver = new WindowResizeObserver(
    239      this.targetActor,
    240      this._onResize
    241    );
    242  }
    243 
    244  /**
    245   * How long does this observer waits before emitting batched events.
    246   * The lower the value, the more event packets will be sent to clients,
    247   * potentially impacting performance.
    248   * The higher the value, the more time we'll wait, this is better for
    249   * performance but has an effect on how soon changes are shown in the toolbox.
    250   */
    251  EVENT_BATCHING_DELAY = 300;
    252 
    253  /**
    254   * Destroying this instance of LayoutChangesObserver will stop the batched
    255   * events from being sent.
    256   */
    257  destroy() {
    258    this.isObserving = false;
    259 
    260    this.reflowObserver.destroy();
    261    this.reflows = null;
    262 
    263    this.resizeObserver.destroy();
    264    this.hasResized = false;
    265 
    266    this.targetActor = null;
    267  }
    268 
    269  start() {
    270    if (this.isObserving) {
    271      return;
    272    }
    273    this.isObserving = true;
    274 
    275    this.reflows = [];
    276    this.hasResized = false;
    277 
    278    this._startEventLoop();
    279 
    280    this.reflowObserver.start();
    281    this.resizeObserver.start();
    282  }
    283 
    284  stop() {
    285    if (!this.isObserving) {
    286      return;
    287    }
    288    this.isObserving = false;
    289 
    290    this._stopEventLoop();
    291 
    292    this.reflows = [];
    293    this.hasResized = false;
    294 
    295    this.reflowObserver.stop();
    296    this.resizeObserver.stop();
    297  }
    298 
    299  /**
    300   * Start the event loop, which regularly checks if there are any observer
    301   * events to be sent as batched events
    302   * Calls itself in a loop.
    303   */
    304  _startEventLoop() {
    305    // Avoid emitting events if the targetActor has been detached (may happen
    306    // during shutdown)
    307    if (!this.targetActor || this.targetActor.isDestroyed()) {
    308      return;
    309    }
    310 
    311    // Send any reflows we have
    312    if (this.reflows && this.reflows.length) {
    313      this.emit("reflows", this.reflows);
    314      this.reflows = [];
    315    }
    316 
    317    // Send any resizes we have
    318    if (this.hasResized) {
    319      this.emit("resize");
    320      this.hasResized = false;
    321    }
    322 
    323    this.eventLoopTimer = this._setTimeout(
    324      this._startEventLoop,
    325      this.EVENT_BATCHING_DELAY
    326    );
    327  }
    328 
    329  _stopEventLoop() {
    330    this._clearTimeout(this.eventLoopTimer);
    331  }
    332 
    333  // Exposing set/clearTimeout here to let tests override them if needed
    334  _setTimeout(cb, ms) {
    335    return setTimeout(cb, ms);
    336  }
    337  _clearTimeout(t) {
    338    return clearTimeout(t);
    339  }
    340 
    341  /**
    342   * Executed whenever a reflow is observed. Only stacks the reflow in the
    343   * reflows array.
    344   * The EVENT_BATCHING_DELAY loop will take care of it later.
    345   *
    346   * @param {number} start When the reflow started
    347   * @param {number} end When the reflow ended
    348   * @param {boolean} isInterruptible
    349   */
    350  _onReflow(start, end, isInterruptible) {
    351    if (gIgnoreLayoutChanges) {
    352      return;
    353    }
    354 
    355    // XXX: when/if bug 997092 gets fixed, we will be able to know which
    356    // elements have been reflowed, which would be a nice thing to add here.
    357    this.reflows.push({
    358      start,
    359      end,
    360      isInterruptible,
    361    });
    362  }
    363 
    364  /**
    365   * Executed whenever a resize is observed. Only store a flag saying that a
    366   * resize occured.
    367   * The EVENT_BATCHING_DELAY loop will take care of it later.
    368   */
    369  _onResize() {
    370    if (gIgnoreLayoutChanges) {
    371      return;
    372    }
    373 
    374    this.hasResized = true;
    375  }
    376 }
    377 exports.LayoutChangesObserver = LayoutChangesObserver;
    378 
    379 /**
    380 * Get a LayoutChangesObserver instance for a given window. This function makes
    381 * sure there is only one instance per window.
    382 *
    383 * @param {WindowGlobalTargetActor} targetActor
    384 * @return {LayoutChangesObserver}
    385 */
    386 var observedWindows = new Map();
    387 function getLayoutChangesObserver(targetActor) {
    388  const observerData = observedWindows.get(targetActor);
    389  if (observerData) {
    390    observerData.refCounting++;
    391    return observerData.observer;
    392  }
    393 
    394  const obs = new LayoutChangesObserver(targetActor);
    395  observedWindows.set(targetActor, {
    396    observer: obs,
    397    // counting references allows to stop the observer when no targetActor owns an
    398    // instance.
    399    refCounting: 1,
    400  });
    401  obs.start();
    402  return obs;
    403 }
    404 exports.getLayoutChangesObserver = getLayoutChangesObserver;
    405 
    406 /**
    407 * Release a LayoutChangesObserver instance that was retrieved by
    408 * getLayoutChangesObserver. This is required to ensure the targetActor reference
    409 * is removed and the observer is eventually stopped and destroyed.
    410 *
    411 * @param {WindowGlobalTargetActor} targetActor
    412 */
    413 function releaseLayoutChangesObserver(targetActor) {
    414  const observerData = observedWindows.get(targetActor);
    415  if (!observerData) {
    416    return;
    417  }
    418 
    419  observerData.refCounting--;
    420  if (!observerData.refCounting) {
    421    observerData.observer.destroy();
    422    observedWindows.delete(targetActor);
    423  }
    424 }
    425 exports.releaseLayoutChangesObserver = releaseLayoutChangesObserver;
    426 
    427 /**
    428 * Reports any reflow that occurs in the targetActor's docshells.
    429 *
    430 * @augments Observable
    431 * @param {WindowGlobalTargetActor} targetActor
    432 * @param {Function} callback Executed everytime a reflow occurs
    433 */
    434 class ReflowObserver extends Observable {
    435  constructor(targetActor, callback) {
    436    super(targetActor, callback);
    437  }
    438 
    439  _startListeners(windows) {
    440    for (const window of windows) {
    441      window.docShell.addWeakReflowObserver(this);
    442    }
    443  }
    444 
    445  _stopListeners(windows) {
    446    for (const window of windows) {
    447      try {
    448        window.docShell.removeWeakReflowObserver(this);
    449      } catch (e) {
    450        // Corner cases where a global has already been freed may happen, in
    451        // which case, no need to remove the observer.
    452      }
    453    }
    454  }
    455 
    456  reflow(start, end) {
    457    this.notifyCallback(start, end, false);
    458  }
    459 
    460  reflowInterruptible(start, end) {
    461    this.notifyCallback(start, end, true);
    462  }
    463 }
    464 
    465 ReflowObserver.prototype.QueryInterface = ChromeUtils.generateQI([
    466  "nsIReflowObserver",
    467  "nsISupportsWeakReference",
    468 ]);
    469 
    470 /**
    471 * Reports window resize events on the targetActor's windows.
    472 *
    473 * @augments Observable
    474 * @param {WindowGlobalTargetActor} targetActor
    475 * @param {Function} callback Executed everytime a resize occurs
    476 */
    477 class WindowResizeObserver extends Observable {
    478  constructor(targetActor, callback) {
    479    super(targetActor, callback);
    480 
    481    this.onNavigate = this.onNavigate.bind(this);
    482    this.onResize = this.onResize.bind(this);
    483 
    484    this.targetActor.on("navigate", this.onNavigate);
    485  }
    486 
    487  _startListeners() {
    488    this._abortController = new AbortController();
    489    this.listenerTarget.addEventListener("resize", this.onResize, {
    490      signal: this._abortController.signal,
    491    });
    492  }
    493 
    494  _stopListeners() {
    495    if (this._abortController) {
    496      this._abortController.abort();
    497      this._abortController = null;
    498    }
    499  }
    500 
    501  onNavigate() {
    502    if (this.isObserving) {
    503      this._stopListeners();
    504      this._startListeners();
    505    }
    506  }
    507 
    508  onResize() {
    509    this.notifyCallback();
    510  }
    511 
    512  destroy() {
    513    if (this.targetActor) {
    514      this.targetActor.off("navigate", this.onNavigate);
    515    }
    516    this._stopListeners();
    517    super.destroy();
    518  }
    519 
    520  get listenerTarget() {
    521    // For the rootActor, return its window.
    522    if (this.targetActor.isRootActor) {
    523      return this.targetActor.window;
    524    }
    525 
    526    // Otherwise, get the targetActor's chromeEventHandler.
    527    return this.targetActor.chromeEventHandler;
    528  }
    529 }