123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- /* 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";
- /**
- * About the types of objects in this file:
- *
- * - ReflowActor: the actor class used for protocol purposes.
- * Mostly empty, just gets an instance of LayoutChangesObserver and forwards
- * its "reflows" events to clients.
- *
- * - LayoutChangesObserver: extends Observable and uses the ReflowObserver, to
- * track reflows on the page.
- * Used by the LayoutActor, but is also exported on the module, so can be used
- * by any other actor that needs it.
- *
- * - Observable: A utility parent class, meant at being extended by classes that
- * need a to observe something on the tabActor's windows.
- *
- * - Dedicated observers: There's only one of them for now: ReflowObserver which
- * listens to reflow events via the docshell,
- * These dedicated classes are used by the LayoutChangesObserver.
- */
- const {Ci} = require("chrome");
- const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
- const protocol = require("devtools/shared/protocol");
- const {method, Arg} = protocol;
- const events = require("sdk/event/core");
- const Heritage = require("sdk/core/heritage");
- const EventEmitter = require("devtools/shared/event-emitter");
- const {reflowSpec} = require("devtools/shared/specs/reflow");
- /**
- * The reflow actor tracks reflows and emits events about them.
- */
- var ReflowActor = exports.ReflowActor = protocol.ActorClassWithSpec(reflowSpec, {
- initialize: function (conn, tabActor) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
- this._onReflow = this._onReflow.bind(this);
- this.observer = getLayoutChangesObserver(tabActor);
- this._isStarted = false;
- },
- /**
- * The reflow actor is the first (and last) in its hierarchy to use
- * protocol.js so it doesn't have a parent protocol actor that takes care of
- * its lifetime. So it needs a disconnect method to cleanup.
- */
- disconnect: function () {
- this.destroy();
- },
- destroy: function () {
- this.stop();
- releaseLayoutChangesObserver(this.tabActor);
- this.observer = null;
- this.tabActor = null;
- protocol.Actor.prototype.destroy.call(this);
- },
- /**
- * Start tracking reflows and sending events to clients about them.
- * This is a oneway method, do not expect a response and it won't return a
- * promise.
- */
- start: function () {
- if (!this._isStarted) {
- this.observer.on("reflows", this._onReflow);
- this._isStarted = true;
- }
- },
- /**
- * Stop tracking reflows and sending events to clients about them.
- * This is a oneway method, do not expect a response and it won't return a
- * promise.
- */
- stop: function () {
- if (this._isStarted) {
- this.observer.off("reflows", this._onReflow);
- this._isStarted = false;
- }
- },
- _onReflow: function (event, reflows) {
- if (this._isStarted) {
- events.emit(this, "reflows", reflows);
- }
- }
- });
- /**
- * Base class for all sorts of observers that need to listen to events on the
- * tabActor's windows.
- * @param {TabActor} tabActor
- * @param {Function} callback Executed everytime the observer observes something
- */
- function Observable(tabActor, callback) {
- this.tabActor = tabActor;
- this.callback = callback;
- this._onWindowReady = this._onWindowReady.bind(this);
- this._onWindowDestroyed = this._onWindowDestroyed.bind(this);
- events.on(this.tabActor, "window-ready", this._onWindowReady);
- events.on(this.tabActor, "window-destroyed", this._onWindowDestroyed);
- }
- Observable.prototype = {
- /**
- * Is the observer currently observing
- */
- isObserving: false,
- /**
- * Stop observing and detroy this observer instance
- */
- destroy: function () {
- if (this.isDestroyed) {
- return;
- }
- this.isDestroyed = true;
- this.stop();
- events.off(this.tabActor, "window-ready", this._onWindowReady);
- events.off(this.tabActor, "window-destroyed", this._onWindowDestroyed);
- this.callback = null;
- this.tabActor = null;
- },
- /**
- * Start observing whatever it is this observer is supposed to observe
- */
- start: function () {
- if (this.isObserving) {
- return;
- }
- this.isObserving = true;
- this._startListeners(this.tabActor.windows);
- },
- /**
- * Stop observing
- */
- stop: function () {
- if (!this.isObserving) {
- return;
- }
- this.isObserving = false;
- if (this.tabActor.attached && this.tabActor.docShell) {
- // It's only worth stopping if the tabActor is still attached
- this._stopListeners(this.tabActor.windows);
- }
- },
- _onWindowReady: function ({window}) {
- if (this.isObserving) {
- this._startListeners([window]);
- }
- },
- _onWindowDestroyed: function ({window}) {
- if (this.isObserving) {
- this._stopListeners([window]);
- }
- },
- _startListeners: function (windows) {
- // To be implemented by sub-classes.
- },
- _stopListeners: function (windows) {
- // To be implemented by sub-classes.
- },
- /**
- * To be called by sub-classes when something has been observed
- */
- notifyCallback: function (...args) {
- this.isObserving && this.callback && this.callback.apply(null, args);
- }
- };
- /**
- * The LayouChangesObserver will observe reflows as soon as it is started.
- * Some devtools actors may cause reflows and it may be wanted to "hide" these
- * reflows from the LayouChangesObserver consumers.
- * If this is the case, such actors should require this module and use this
- * global function to turn the ignore mode on and off temporarily.
- *
- * Note that if a node is provided, it will be used to force a sync reflow to
- * make sure all reflows which occurred before switching the mode on or off are
- * either observed or ignored depending on the current mode.
- *
- * @param {Boolean} ignore
- * @param {DOMNode} syncReflowNode The node to use to force a sync reflow
- */
- var gIgnoreLayoutChanges = false;
- exports.setIgnoreLayoutChanges = function (ignore, syncReflowNode) {
- if (syncReflowNode) {
- let forceSyncReflow = syncReflowNode.offsetWidth;
- }
- gIgnoreLayoutChanges = ignore;
- };
- /**
- * The LayoutChangesObserver class is instantiated only once per given tab
- * and is used to track reflows and dom and style changes in that tab.
- * The LayoutActor uses this class to send reflow events to its clients.
- *
- * This class isn't exported on the module because it shouldn't be instantiated
- * to avoid creating several instances per tabs.
- * Use `getLayoutChangesObserver(tabActor)`
- * and `releaseLayoutChangesObserver(tabActor)`
- * which are exported to get and release instances.
- *
- * The observer loops every EVENT_BATCHING_DELAY ms and checks if layout changes
- * have happened since the last loop iteration. If there are, it sends the
- * corresponding events:
- *
- * - "reflows", with an array of all the reflows that occured,
- * - "resizes", with an array of all the resizes that occured,
- *
- * @param {TabActor} tabActor
- */
- function LayoutChangesObserver(tabActor) {
- this.tabActor = tabActor;
- this._startEventLoop = this._startEventLoop.bind(this);
- this._onReflow = this._onReflow.bind(this);
- this._onResize = this._onResize.bind(this);
- // Creating the various observers we're going to need
- // For now, just the reflow observer, but later we can add markupMutation,
- // styleSheetChanges and styleRuleChanges
- this.reflowObserver = new ReflowObserver(this.tabActor, this._onReflow);
- this.resizeObserver = new WindowResizeObserver(this.tabActor, this._onResize);
- EventEmitter.decorate(this);
- }
- exports.LayoutChangesObserver = LayoutChangesObserver;
- LayoutChangesObserver.prototype = {
- /**
- * How long does this observer waits before emitting batched events.
- * The lower the value, the more event packets will be sent to clients,
- * potentially impacting performance.
- * The higher the value, the more time we'll wait, this is better for
- * performance but has an effect on how soon changes are shown in the toolbox.
- */
- EVENT_BATCHING_DELAY: 300,
- /**
- * Destroying this instance of LayoutChangesObserver will stop the batched
- * events from being sent.
- */
- destroy: function () {
- this.isObserving = false;
- this.reflowObserver.destroy();
- this.reflows = null;
- this.resizeObserver.destroy();
- this.hasResized = false;
- this.tabActor = null;
- },
- start: function () {
- if (this.isObserving) {
- return;
- }
- this.isObserving = true;
- this.reflows = [];
- this.hasResized = false;
- this._startEventLoop();
- this.reflowObserver.start();
- this.resizeObserver.start();
- },
- stop: function () {
- if (!this.isObserving) {
- return;
- }
- this.isObserving = false;
- this._stopEventLoop();
- this.reflows = [];
- this.hasResized = false;
- this.reflowObserver.stop();
- this.resizeObserver.stop();
- },
- /**
- * Start the event loop, which regularly checks if there are any observer
- * events to be sent as batched events
- * Calls itself in a loop.
- */
- _startEventLoop: function () {
- // Avoid emitting events if the tabActor has been detached (may happen
- // during shutdown)
- if (!this.tabActor || !this.tabActor.attached) {
- return;
- }
- // Send any reflows we have
- if (this.reflows && this.reflows.length) {
- this.emit("reflows", this.reflows);
- this.reflows = [];
- }
- // Send any resizes we have
- if (this.hasResized) {
- this.emit("resize");
- this.hasResized = false;
- }
- this.eventLoopTimer = this._setTimeout(this._startEventLoop,
- this.EVENT_BATCHING_DELAY);
- },
- _stopEventLoop: function () {
- this._clearTimeout(this.eventLoopTimer);
- },
- // Exposing set/clearTimeout here to let tests override them if needed
- _setTimeout: function (cb, ms) {
- return setTimeout(cb, ms);
- },
- _clearTimeout: function (t) {
- return clearTimeout(t);
- },
- /**
- * Executed whenever a reflow is observed. Only stacks the reflow in the
- * reflows array.
- * The EVENT_BATCHING_DELAY loop will take care of it later.
- * @param {Number} start When the reflow started
- * @param {Number} end When the reflow ended
- * @param {Boolean} isInterruptible
- */
- _onReflow: function (start, end, isInterruptible) {
- if (gIgnoreLayoutChanges) {
- return;
- }
- // XXX: when/if bug 997092 gets fixed, we will be able to know which
- // elements have been reflowed, which would be a nice thing to add here.
- this.reflows.push({
- start: start,
- end: end,
- isInterruptible: isInterruptible
- });
- },
- /**
- * Executed whenever a resize is observed. Only store a flag saying that a
- * resize occured.
- * The EVENT_BATCHING_DELAY loop will take care of it later.
- */
- _onResize: function () {
- if (gIgnoreLayoutChanges) {
- return;
- }
- this.hasResized = true;
- }
- };
- /**
- * Get a LayoutChangesObserver instance for a given window. This function makes
- * sure there is only one instance per window.
- * @param {TabActor} tabActor
- * @return {LayoutChangesObserver}
- */
- var observedWindows = new Map();
- function getLayoutChangesObserver(tabActor) {
- let observerData = observedWindows.get(tabActor);
- if (observerData) {
- observerData.refCounting ++;
- return observerData.observer;
- }
- let obs = new LayoutChangesObserver(tabActor);
- observedWindows.set(tabActor, {
- observer: obs,
- // counting references allows to stop the observer when no tabActor owns an
- // instance.
- refCounting: 1
- });
- obs.start();
- return obs;
- }
- exports.getLayoutChangesObserver = getLayoutChangesObserver;
- /**
- * Release a LayoutChangesObserver instance that was retrieved by
- * getLayoutChangesObserver. This is required to ensure the tabActor reference
- * is removed and the observer is eventually stopped and destroyed.
- * @param {TabActor} tabActor
- */
- function releaseLayoutChangesObserver(tabActor) {
- let observerData = observedWindows.get(tabActor);
- if (!observerData) {
- return;
- }
- observerData.refCounting --;
- if (!observerData.refCounting) {
- observerData.observer.destroy();
- observedWindows.delete(tabActor);
- }
- }
- exports.releaseLayoutChangesObserver = releaseLayoutChangesObserver;
- /**
- * Reports any reflow that occurs in the tabActor's docshells.
- * @extends Observable
- * @param {TabActor} tabActor
- * @param {Function} callback Executed everytime a reflow occurs
- */
- function ReflowObserver(tabActor, callback) {
- Observable.call(this, tabActor, callback);
- }
- ReflowObserver.prototype = Heritage.extend(Observable.prototype, {
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
- Ci.nsISupportsWeakReference]),
- _startListeners: function (windows) {
- for (let window of windows) {
- let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIDocShell);
- docshell.addWeakReflowObserver(this);
- }
- },
- _stopListeners: function (windows) {
- for (let window of windows) {
- try {
- let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIDocShell);
- docshell.removeWeakReflowObserver(this);
- } catch (e) {
- // Corner cases where a global has already been freed may happen, in
- // which case, no need to remove the observer.
- }
- }
- },
- reflow: function (start, end) {
- this.notifyCallback(start, end, false);
- },
- reflowInterruptible: function (start, end) {
- this.notifyCallback(start, end, true);
- }
- });
- /**
- * Reports window resize events on the tabActor's windows.
- * @extends Observable
- * @param {TabActor} tabActor
- * @param {Function} callback Executed everytime a resize occurs
- */
- function WindowResizeObserver(tabActor, callback) {
- Observable.call(this, tabActor, callback);
- this.onResize = this.onResize.bind(this);
- }
- WindowResizeObserver.prototype = Heritage.extend(Observable.prototype, {
- _startListeners: function () {
- this.listenerTarget.addEventListener("resize", this.onResize);
- },
- _stopListeners: function () {
- this.listenerTarget.removeEventListener("resize", this.onResize);
- },
- onResize: function () {
- this.notifyCallback();
- },
- get listenerTarget() {
- // For the rootActor, return its window.
- if (this.tabActor.isRootActor) {
- return this.tabActor.window;
- }
- // Otherwise, get the tabActor's chromeEventHandler.
- return this.tabActor.window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIDocShell)
- .chromeEventHandler;
- }
- });
|