--- a/toolkit/modules/BrowserUtils.jsm
+++ b/toolkit/modules/BrowserUtils.jsm
@@ -11,50 +11,155 @@ const {interfaces: Ci, utils: Cu, classe
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
Cu.importGlobalProperties(["URL"]);
-let reflowObservers = new WeakMap();
+let reflowSchedulers = new WeakMap();
-function ReflowObserver(doc) {
+function ReflowScheduler(doc) {
this._doc = doc;
+ this._win = doc.defaultView;
+
+ reflowSchedulers.set(this._doc, this);
- doc.docShell.addWeakReflowObserver(this);
- reflowObservers.set(this._doc, this);
+ this.frameCallbacks = [];
+ this.reflowCallbacks = [];
- this.callbacks = [];
+ this._onAnimationFrame = this._onAnimationFrame.bind(this);
+ this._onIdle = this._onIdle.bind(this);
+
+ this._frameRequest = null;
+ this._idleRequest = null;
+ this._reflowRequested = false;
}
-ReflowObserver.prototype = {
+ReflowScheduler.get = function(doc) {
+ let scheduler = reflowSchedulers.get(doc);
+ if (!scheduler) {
+ scheduler = new ReflowScheduler(doc);
+ reflowSchedulers.set(doc, scheduler);
+ }
+ return scheduler;
+}
+
+ReflowScheduler.prototype = {
QueryInterface: XPCOMUtils.generateQI(["nsIReflowObserver", "nsISupportsWeakReference"]),
_onReflow() {
- reflowObservers.delete(this._doc);
this._doc.docShell.removeWeakReflowObserver(this);
+ this._reflowRequested = false;
- for (let callback of this.callbacks) {
+ if (this._idleRequest) {
+ this._win.cancelIdleCallback(this._idleRequest);
+ this._idleRequest = null;
+ }
+
+ for (let callback of this.reflowCallbacks.slice()) {
try {
callback();
} catch (e) {
Cu.reportError(e);
}
}
},
+ _onIdle() {
+ this._idleRequest = null;
+ this.forceReflow();
+
+ // If we have no remaining animation frame callbacks, cancel the
+ // animation frame request.
+ if (this._frameRequest && !this.frameCallbacks.length) {
+ this._win.cancelAnimationFrame(this._frameRequest);
+ this._frameRequest = null;
+ }
+ },
+
+ _onAnimationFrame() {
+ // If we still have an idle callback registered at this point, that
+ // means we have reflow observers that need to run before the next
+ // paint. So force a reflow now, and run all pending callbacks in a
+ // group.
+ if (this._idleRequest) {
+ this._win.cancelIdleCallback(this._idleRequest);
+ this._idleRequest = null;
+
+ this.forceReflow();
+
+ // Flush the Promise microtask queue so waiting tasks have a
+ // chance to schedule animation frame callbacks.
+ let utils = this._doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ utils.performMicroTaskCheckpoint();
+ }
+
+ this._frameRequest = null;
+
+ for (let callback of this.frameCallbacks.slice()) {
+ try {
+ callback();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ forceReflow() {
+ // We should only ever call this when a layout flush is required, so
+ // just querying layout should be enough to force a flush.
+ void this._doc.documentElement.clientTop;
+
+ if (this._idleRequest) {
+ Cu.reportError(new Error("Unexpected idle callback registered"));
+ this._idleRequest = null;
+ }
+ },
+
reflow() {
this._onReflow();
},
reflowInterruptible() {
this._onReflow();
},
+
+ addReflowCallback(callback) {
+ this.reflowCallbacks.push(callback);
+ if (!this._reflowRequested) {
+ this._doc.docShell.addWeakReflowObserver(this);
+ this._reflowRequested = true;
+ }
+ },
+
+ addSlowReflowCallback(callback) {
+ this.addReflowCallback(callback);
+
+ // Add both idle and animation frame callbacks. If the idle callback
+ // fires first, we force a reflow immediately, and cancel our
+ // animation frame request. If the callback is still pending by the
+ // start of the next animation frame, we force a reflow then, and
+ // give pending promise tasks a chance to run.
+ if (!this._idleRequest) {
+ this._idleRequest = this._win.requestIdleCallback(this._onIdle);
+ }
+ if (!this._frameRequest) {
+ this._frameRequest = this._win.requestAnimationFrame(this._onAnimationFrame);
+ }
+ },
+
+ addFrameCallback(callback) {
+ this.frameCallbacks.push(callback);
+ if (!this._frameRequest) {
+ this._frameRequest = this._win.requestAnimationFrame(this._onAnimationFrame);
+ }
+ },
};
const FLUSH_TYPES = {
"style": Ci.nsIDOMWindowUtils.FLUSH_STYLE,
"layout": Ci.nsIDOMWindowUtils.FLUSH_LAYOUT,
"display": Ci.nsIDOMWindowUtils.FLUSH_DISPLAY,
};
@@ -675,24 +780,19 @@ this.BrowserUtils = {
* The function *must not trigger any reflows*, or make any changes
* which would require a layout flush.
*
* @param {Document} doc
* @param {function} callback
* @returns {Promise}
*/
promiseReflowed(doc, callback) {
- let observer = reflowObservers.get(doc);
- if (!observer) {
- observer = new ReflowObserver(doc);
- reflowObservers.set(doc, observer);
- }
-
+ let scheduler = ReflowScheduler.get(doc);
return new Promise((resolve, reject) => {
- observer.callbacks.push(() => {
+ scheduler.addReflowCallback(() => {
try {
resolve(callback());
} catch (e) {
reject(e);
}
});
});
},
@@ -722,30 +822,83 @@ this.BrowserUtils = {
if (!utils.needsFlush(FLUSH_TYPES[flushType])) {
return callback();
}
return this.promiseReflowed(doc, callback);
},
/**
+ * Like `promiseLayoutFlushed`, but guarantees that if a reflow is
+ * required, it will happen before the next paint, and that any
+ * pending promise handlers will likewise run before the paint. When
+ * combined with `promiseAnimationFrame, this allows multiple
+ * operations that depend on querying the layout state before paint to
+ * be grouped so that only a single extra layout flush is required.
+ *
+ * Please *USE THIS ONLY AS A LAST RESORT*. If you need to query the
+ * layout of something that hasn't painted yet, you almost certainly
+ * have better options. Forcing multiple reflows for a single
+ * animation frame will always be bad for responsiveness.
+ *
+ * @param {Document} doc
+ * @param {string} flushType
+ * As in `promiseLayoutFlushed`.
+ * @param {function} callback
+ * @returns {Promise}
+ */
+ async promiseSlowLayoutFlush(doc, flushType, callback) {
+ let utils = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ if (!utils.needsFlush(FLUSH_TYPES[flushType])) {
+ return callback();
+ }
+
+ let scheduler = ReflowScheduler.get(doc);
+ return new Promise((resolve, reject) => {
+ scheduler.addSlowReflowCallback(() => {
+ try {
+ resolve(callback());
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ },
+
+ /**
* Calls the given callback on the next animation frame for the given
* window, and resolves with its return value after it's been
* executed.
*
- * The callback function may alter the DOM freely, but *must not query the document's style or
- * layout state*.
+ * The callback function may alter the DOM freely, but *must not query
+ * the document's style or layout state*.
+ *
+ * This has an important difference from `window.requestAnimationFrame`
+ * when used alongside `promiseSlowLayoutFlush`:
+ *
+ * Sometimes `promiseSlowLayoutFlush` needs to force a layout flush
+ * from an animation frame callback before calling its callbacks. If
+ * those callbacks call `window.requestAnimationFrame` in that
+ * circumstance, the new animation frame callbacks will run in the
+ * *next* animation frame, after the current frame is painted.
+ * Animation frame callbacks added via `promiseAnimationFrame`,
+ * however, will run in the *same* animation frame, after all layout
+ * flush callbacks have been called and their pending promise
+ * microtasks processed.
*
* @param {Window} win
* @param {function} callback
* @returns {Promise}
*/
promiseAnimationFrame(win, callback) {
return new Promise((resolve, reject) => {
- win.requestAnimationFrame(timestamp => {
+ let scheduler = ReflowScheduler.get(win.document);
+ scheduler.addFrameCallback(timestamp => {
try {
resolve(callback(timestamp));
} catch (e) {
reject(e);
}
});
});
},