summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/reflow.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/reflow.js')
-rw-r--r--devtools/server/actors/reflow.js514
1 files changed, 514 insertions, 0 deletions
diff --git a/devtools/server/actors/reflow.js b/devtools/server/actors/reflow.js
new file mode 100644
index 000000000..0ebe00207
--- /dev/null
+++ b/devtools/server/actors/reflow.js
@@ -0,0 +1,514 @@
+/* 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;
+ }
+});