2012-04-06 10:26:06 +04:00
|
|
|
/* 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/. */
|
2011-05-31 05:52:23 +04:00
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
var EXPORTED_SYMBOLS = ["Async"];
|
2011-05-31 05:52:23 +04:00
|
|
|
|
2019-01-17 21:18:31 +03:00
|
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
2011-05-31 05:52:23 +04:00
|
|
|
|
2019-08-13 21:48:25 +03:00
|
|
|
const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer");
|
|
|
|
|
2011-05-31 05:52:23 +04:00
|
|
|
/*
|
|
|
|
* Helpers for various async operations.
|
|
|
|
*/
|
2012-10-31 20:13:28 +04:00
|
|
|
var Async = {
|
2011-08-26 21:25:42 +04:00
|
|
|
/**
|
|
|
|
* Execute an arbitrary number of asynchronous functions one after the
|
|
|
|
* other, passing the callback arguments on to the next one. All functions
|
|
|
|
* must take a callback function as their last argument. The 'this' object
|
|
|
|
* will be whatever chain()'s is.
|
2012-04-06 10:26:06 +04:00
|
|
|
*
|
2011-08-26 21:25:42 +04:00
|
|
|
* @usage this._chain = Async.chain;
|
|
|
|
* this._chain(this.foo, this.bar, this.baz)(args, for, foo)
|
2012-04-06 10:26:06 +04:00
|
|
|
*
|
2011-08-26 21:25:42 +04:00
|
|
|
* This is equivalent to:
|
|
|
|
*
|
|
|
|
* let self = this;
|
|
|
|
* self.foo(args, for, foo, function (bars, args) {
|
|
|
|
* self.bar(bars, args, function (baz, params) {
|
|
|
|
* self.baz(baz, params);
|
|
|
|
* });
|
|
|
|
* });
|
|
|
|
*/
|
2019-04-17 22:03:17 +03:00
|
|
|
chain: function chain(...funcs) {
|
2011-08-26 21:25:42 +04:00
|
|
|
let thisObj = this;
|
|
|
|
return function callback() {
|
|
|
|
if (funcs.length) {
|
2019-04-17 22:03:17 +03:00
|
|
|
let args = [...arguments, callback];
|
2011-08-26 21:25:42 +04:00
|
|
|
let f = funcs.shift();
|
|
|
|
f.apply(thisObj, args);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2011-08-26 21:25:29 +04:00
|
|
|
/**
|
2016-11-15 07:28:52 +03:00
|
|
|
* Check if the app is still ready (not quitting). Returns true, or throws an
|
|
|
|
* exception if not ready.
|
2011-08-26 21:25:29 +04:00
|
|
|
*/
|
|
|
|
checkAppReady: function checkAppReady() {
|
|
|
|
// Watch for app-quit notification to stop any sync calls
|
|
|
|
Services.obs.addObserver(function onQuitApplication() {
|
|
|
|
Services.obs.removeObserver(onQuitApplication, "quit-application");
|
2017-06-06 01:50:07 +03:00
|
|
|
Async.checkAppReady = Async.promiseYield = function() {
|
2015-08-31 04:57:25 +03:00
|
|
|
let exception = Components.Exception(
|
|
|
|
"App. Quitting",
|
|
|
|
Cr.NS_ERROR_ABORT
|
|
|
|
);
|
|
|
|
exception.appIsShuttingDown = true;
|
|
|
|
throw exception;
|
2011-08-26 21:25:29 +04:00
|
|
|
};
|
2017-04-15 00:39:22 +03:00
|
|
|
}, "quit-application");
|
2011-08-26 21:25:29 +04:00
|
|
|
// In the common case, checkAppReady just returns true
|
|
|
|
return (Async.checkAppReady = function() {
|
|
|
|
return true;
|
|
|
|
})();
|
|
|
|
},
|
|
|
|
|
2016-11-15 07:28:52 +03:00
|
|
|
/**
|
|
|
|
* Check if the app is still ready (not quitting). Returns true if the app
|
|
|
|
* is ready, or false if it is being shut down.
|
|
|
|
*/
|
|
|
|
isAppReady() {
|
|
|
|
try {
|
2017-10-15 21:50:30 +03:00
|
|
|
return Async.checkAppReady();
|
2016-11-15 07:28:52 +03:00
|
|
|
} catch (ex) {
|
|
|
|
if (!Async.isShutdownException(ex)) {
|
|
|
|
throw ex;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2015-08-31 04:57:25 +03:00
|
|
|
/**
|
|
|
|
* Check if the passed exception is one raised by checkAppReady. Typically
|
|
|
|
* this will be used in exception handlers to allow such exceptions to
|
|
|
|
* make their way to the top frame and allow the app to actually terminate.
|
|
|
|
*/
|
|
|
|
isShutdownException(exception) {
|
|
|
|
return exception && exception.appIsShuttingDown === true;
|
|
|
|
},
|
|
|
|
|
2017-06-06 01:50:07 +03:00
|
|
|
/**
|
|
|
|
* A "tight loop" of promises can still lock up the browser for some time.
|
|
|
|
* Periodically waiting for a promise returned by this function will solve
|
|
|
|
* that.
|
|
|
|
* You should probably not use this method directly and instead use jankYielder
|
|
|
|
* below.
|
|
|
|
* Some reference here:
|
|
|
|
* - https://gist.github.com/jesstelford/bbb30b983bddaa6e5fef2eb867d37678
|
|
|
|
* - https://bugzilla.mozilla.org/show_bug.cgi?id=1094248
|
|
|
|
*/
|
|
|
|
promiseYield() {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
Bug 1536170 - Replace Async.jankYielder r=tcsc,markh,eoger
`Async.jankYielder` is known to, unfortunately, cause jank by creating a lot of
immediately resolved promises that must be then GCed. For a collection of 50
items, it will create 50 promises and 49 of them will immediately resolve.
Instead of `Async.jankYielder`, we now have `Async.yieldState`, which simply
keeps track of whether or not the caller should yield to the event loop. Two
higher level looping constructs are built on top of it:
* `Async.yieldingIterator`, which has been rewritten to not create extraneous
promises; and
* `Async.yieldingForEach`, which is a replacement for awaiting
`Async.jankYielder` in a loop. Instead, it accepts the loop body as a
function.
Each of these can share an instance of an `Async.yieldState`, which allows an
object with multiple loops to yield every N iterations overall, instead of
every N iterations of each loop, which keeps the behaviour of using one
`Async.jankYielders` in multiple places.
Differential Revision: https://phabricator.services.mozilla.com/D26229
--HG--
extra : moz-landing-system : lando
2019-04-17 06:00:35 +03:00
|
|
|
/**
|
|
|
|
* Shared state for yielding every N calls.
|
|
|
|
*
|
|
|
|
* Can be passed to multiple Async.yieldingForEach to have them overall yield
|
|
|
|
* every N iterations.
|
|
|
|
*/
|
|
|
|
yieldState(yieldEvery = 50) {
|
2017-06-06 01:50:07 +03:00
|
|
|
let iterations = 0;
|
Bug 1536170 - Replace Async.jankYielder r=tcsc,markh,eoger
`Async.jankYielder` is known to, unfortunately, cause jank by creating a lot of
immediately resolved promises that must be then GCed. For a collection of 50
items, it will create 50 promises and 49 of them will immediately resolve.
Instead of `Async.jankYielder`, we now have `Async.yieldState`, which simply
keeps track of whether or not the caller should yield to the event loop. Two
higher level looping constructs are built on top of it:
* `Async.yieldingIterator`, which has been rewritten to not create extraneous
promises; and
* `Async.yieldingForEach`, which is a replacement for awaiting
`Async.jankYielder` in a loop. Instead, it accepts the loop body as a
function.
Each of these can share an instance of an `Async.yieldState`, which allows an
object with multiple loops to yield every N iterations overall, instead of
every N iterations of each loop, which keeps the behaviour of using one
`Async.jankYielders` in multiple places.
Differential Revision: https://phabricator.services.mozilla.com/D26229
--HG--
extra : moz-landing-system : lando
2019-04-17 06:00:35 +03:00
|
|
|
|
|
|
|
return {
|
|
|
|
shouldYield() {
|
|
|
|
++iterations;
|
|
|
|
return iterations % yieldEvery === 0;
|
|
|
|
},
|
2017-10-15 21:50:30 +03:00
|
|
|
};
|
2018-01-05 02:06:57 +03:00
|
|
|
},
|
|
|
|
|
2018-02-22 21:19:16 +03:00
|
|
|
/**
|
Bug 1536170 - Replace Async.jankYielder r=tcsc,markh,eoger
`Async.jankYielder` is known to, unfortunately, cause jank by creating a lot of
immediately resolved promises that must be then GCed. For a collection of 50
items, it will create 50 promises and 49 of them will immediately resolve.
Instead of `Async.jankYielder`, we now have `Async.yieldState`, which simply
keeps track of whether or not the caller should yield to the event loop. Two
higher level looping constructs are built on top of it:
* `Async.yieldingIterator`, which has been rewritten to not create extraneous
promises; and
* `Async.yieldingForEach`, which is a replacement for awaiting
`Async.jankYielder` in a loop. Instead, it accepts the loop body as a
function.
Each of these can share an instance of an `Async.yieldState`, which allows an
object with multiple loops to yield every N iterations overall, instead of
every N iterations of each loop, which keeps the behaviour of using one
`Async.jankYielders` in multiple places.
Differential Revision: https://phabricator.services.mozilla.com/D26229
--HG--
extra : moz-landing-system : lando
2019-04-17 06:00:35 +03:00
|
|
|
* Apply the given function to each element of the iterable, yielding the
|
|
|
|
* event loop every yieldEvery iterations.
|
2018-02-22 21:19:16 +03:00
|
|
|
*
|
|
|
|
* @param iterable {Iterable}
|
Bug 1536170 - Replace Async.jankYielder r=tcsc,markh,eoger
`Async.jankYielder` is known to, unfortunately, cause jank by creating a lot of
immediately resolved promises that must be then GCed. For a collection of 50
items, it will create 50 promises and 49 of them will immediately resolve.
Instead of `Async.jankYielder`, we now have `Async.yieldState`, which simply
keeps track of whether or not the caller should yield to the event loop. Two
higher level looping constructs are built on top of it:
* `Async.yieldingIterator`, which has been rewritten to not create extraneous
promises; and
* `Async.yieldingForEach`, which is a replacement for awaiting
`Async.jankYielder` in a loop. Instead, it accepts the loop body as a
function.
Each of these can share an instance of an `Async.yieldState`, which allows an
object with multiple loops to yield every N iterations overall, instead of
every N iterations of each loop, which keeps the behaviour of using one
`Async.jankYielders` in multiple places.
Differential Revision: https://phabricator.services.mozilla.com/D26229
--HG--
extra : moz-landing-system : lando
2019-04-17 06:00:35 +03:00
|
|
|
* The iterable or iterator to iterate through.
|
|
|
|
*
|
|
|
|
* @param fn {(*) -> void|boolean}
|
|
|
|
* The function to be called on each element of the iterable.
|
|
|
|
*
|
|
|
|
* Returning true from the function will stop the iteration.
|
Bug 1536170 - Replace Async.jankYielder r=tcsc,markh,eoger
`Async.jankYielder` is known to, unfortunately, cause jank by creating a lot of
immediately resolved promises that must be then GCed. For a collection of 50
items, it will create 50 promises and 49 of them will immediately resolve.
Instead of `Async.jankYielder`, we now have `Async.yieldState`, which simply
keeps track of whether or not the caller should yield to the event loop. Two
higher level looping constructs are built on top of it:
* `Async.yieldingIterator`, which has been rewritten to not create extraneous
promises; and
* `Async.yieldingForEach`, which is a replacement for awaiting
`Async.jankYielder` in a loop. Instead, it accepts the loop body as a
function.
Each of these can share an instance of an `Async.yieldState`, which allows an
object with multiple loops to yield every N iterations overall, instead of
every N iterations of each loop, which keeps the behaviour of using one
`Async.jankYielders` in multiple places.
Differential Revision: https://phabricator.services.mozilla.com/D26229
--HG--
extra : moz-landing-system : lando
2019-04-12 22:07:00 +03:00
|
|
|
*
|
Bug 1536170 - Replace Async.jankYielder r=tcsc,markh,eoger
`Async.jankYielder` is known to, unfortunately, cause jank by creating a lot of
immediately resolved promises that must be then GCed. For a collection of 50
items, it will create 50 promises and 49 of them will immediately resolve.
Instead of `Async.jankYielder`, we now have `Async.yieldState`, which simply
keeps track of whether or not the caller should yield to the event loop. Two
higher level looping constructs are built on top of it:
* `Async.yieldingIterator`, which has been rewritten to not create extraneous
promises; and
* `Async.yieldingForEach`, which is a replacement for awaiting
`Async.jankYielder` in a loop. Instead, it accepts the loop body as a
function.
Each of these can share an instance of an `Async.yieldState`, which allows an
object with multiple loops to yield every N iterations overall, instead of
every N iterations of each loop, which keeps the behaviour of using one
`Async.jankYielders` in multiple places.
Differential Revision: https://phabricator.services.mozilla.com/D26229
--HG--
extra : moz-landing-system : lando
2019-04-17 06:00:35 +03:00
|
|
|
* @param [yieldEvery = 50] {number|object}
|
|
|
|
* The number of iterations to complete before yielding back to the event
|
|
|
|
* loop.
|
|
|
|
*
|
|
|
|
* @return {boolean}
|
|
|
|
* Whether or not the function returned early.
|
2018-02-22 21:19:16 +03:00
|
|
|
*/
|
Bug 1536170 - Replace Async.jankYielder r=tcsc,markh,eoger
`Async.jankYielder` is known to, unfortunately, cause jank by creating a lot of
immediately resolved promises that must be then GCed. For a collection of 50
items, it will create 50 promises and 49 of them will immediately resolve.
Instead of `Async.jankYielder`, we now have `Async.yieldState`, which simply
keeps track of whether or not the caller should yield to the event loop. Two
higher level looping constructs are built on top of it:
* `Async.yieldingIterator`, which has been rewritten to not create extraneous
promises; and
* `Async.yieldingForEach`, which is a replacement for awaiting
`Async.jankYielder` in a loop. Instead, it accepts the loop body as a
function.
Each of these can share an instance of an `Async.yieldState`, which allows an
object with multiple loops to yield every N iterations overall, instead of
every N iterations of each loop, which keeps the behaviour of using one
`Async.jankYielders` in multiple places.
Differential Revision: https://phabricator.services.mozilla.com/D26229
--HG--
extra : moz-landing-system : lando
2019-04-17 06:00:35 +03:00
|
|
|
async yieldingForEach(iterable, fn, yieldEvery = 50) {
|
|
|
|
const yieldState =
|
|
|
|
typeof yieldEvery === "number"
|
|
|
|
? Async.yieldState(yieldEvery)
|
|
|
|
: yieldEvery;
|
|
|
|
let iteration = 0;
|
|
|
|
|
|
|
|
for (const item of iterable) {
|
|
|
|
let result = fn(item, iteration++);
|
|
|
|
if (typeof result !== "undefined" && typeof result.then !== "undefined") {
|
|
|
|
// If we await result when it is not a Promise, we create an
|
|
|
|
// automatically resolved promise, which is exactly the case that we
|
|
|
|
// are trying to avoid.
|
|
|
|
result = await result;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result === true) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (yieldState.shouldYield()) {
|
|
|
|
await Async.promiseYield();
|
|
|
|
Async.checkAppReady();
|
|
|
|
}
|
2019-04-13 00:39:14 +03:00
|
|
|
}
|
Bug 1536170 - Replace Async.jankYielder r=tcsc,markh,eoger
`Async.jankYielder` is known to, unfortunately, cause jank by creating a lot of
immediately resolved promises that must be then GCed. For a collection of 50
items, it will create 50 promises and 49 of them will immediately resolve.
Instead of `Async.jankYielder`, we now have `Async.yieldState`, which simply
keeps track of whether or not the caller should yield to the event loop. Two
higher level looping constructs are built on top of it:
* `Async.yieldingIterator`, which has been rewritten to not create extraneous
promises; and
* `Async.yieldingForEach`, which is a replacement for awaiting
`Async.jankYielder` in a loop. Instead, it accepts the loop body as a
function.
Each of these can share an instance of an `Async.yieldState`, which allows an
object with multiple loops to yield every N iterations overall, instead of
every N iterations of each loop, which keeps the behaviour of using one
`Async.jankYielders` in multiple places.
Differential Revision: https://phabricator.services.mozilla.com/D26229
--HG--
extra : moz-landing-system : lando
2019-04-17 06:00:35 +03:00
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2018-01-05 02:06:57 +03:00
|
|
|
asyncQueueCaller(log) {
|
|
|
|
return new AsyncQueueCaller(log);
|
|
|
|
},
|
|
|
|
|
|
|
|
asyncObserver(log, obj) {
|
|
|
|
return new AsyncObserver(log, obj);
|
2017-06-06 01:50:07 +03:00
|
|
|
},
|
2019-08-13 21:48:25 +03:00
|
|
|
|
|
|
|
watchdog() {
|
|
|
|
return new Watchdog();
|
|
|
|
},
|
2011-05-31 05:52:23 +04:00
|
|
|
};
|
2018-01-05 02:06:57 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Allows consumers to enqueue asynchronous callbacks to be called in order.
|
|
|
|
* Typically this is used when providing a callback to a caller that doesn't
|
|
|
|
* await on promises.
|
|
|
|
*/
|
|
|
|
class AsyncQueueCaller {
|
|
|
|
constructor(log) {
|
|
|
|
this._log = log;
|
|
|
|
this._queue = Promise.resolve();
|
|
|
|
this.QueryInterface = ChromeUtils.generateQI([
|
|
|
|
Ci.nsIObserver,
|
|
|
|
Ci.nsISupportsWeakReference,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* /!\ Never await on another function that calls enqueueCall /!\
|
|
|
|
* on the same queue or we will deadlock.
|
|
|
|
*/
|
|
|
|
enqueueCall(func) {
|
|
|
|
this._queue = (async () => {
|
|
|
|
await this._queue;
|
|
|
|
try {
|
2018-05-29 21:30:22 +03:00
|
|
|
return await func();
|
2018-01-05 02:06:57 +03:00
|
|
|
} catch (e) {
|
|
|
|
this._log.error(e);
|
2018-05-29 21:30:22 +03:00
|
|
|
return false;
|
2018-01-05 02:06:57 +03:00
|
|
|
}
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
|
|
|
|
promiseCallsComplete() {
|
|
|
|
return this._queue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Subclass of AsyncQueueCaller that can be used with Services.obs directly.
|
|
|
|
* When this observe() is called, it will enqueue a call to the consumers's
|
|
|
|
* observe().
|
|
|
|
*/
|
|
|
|
class AsyncObserver extends AsyncQueueCaller {
|
|
|
|
constructor(obj, log) {
|
|
|
|
super(log);
|
|
|
|
this.obj = obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
observe(subject, topic, data) {
|
|
|
|
this.enqueueCall(() => this.obj.observe(subject, topic, data));
|
|
|
|
}
|
|
|
|
|
|
|
|
promiseObserversComplete() {
|
|
|
|
return this.promiseCallsComplete();
|
|
|
|
}
|
|
|
|
}
|
2019-08-13 21:48:25 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Woof! Signals an operation to abort, either at shutdown or after a timeout.
|
|
|
|
* The buffered engine uses this to abort long-running merges, so that they
|
|
|
|
* don't prevent Firefox from quitting, or block future syncs.
|
|
|
|
*/
|
|
|
|
class Watchdog {
|
|
|
|
constructor() {
|
|
|
|
this.controller = new AbortController();
|
|
|
|
this.timer = new Timer();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The reason for signaling an abort. `null` if not signaled,
|
|
|
|
* `"timeout"` if the watchdog timer fired, or `"shutdown"` if the app is
|
|
|
|
* is quitting.
|
|
|
|
*
|
|
|
|
* @type {String?}
|
|
|
|
*/
|
|
|
|
this.abortReason = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the abort signal for this watchdog. This can be passed to APIs
|
|
|
|
* that take a signal for cancellation, like `SyncedBookmarksMirror::apply`
|
|
|
|
* or `fetch`.
|
|
|
|
*
|
|
|
|
* @type {AbortSignal}
|
|
|
|
*/
|
|
|
|
get signal() {
|
|
|
|
return this.controller.signal;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts the watchdog timer, and listens for the app quitting.
|
|
|
|
*
|
|
|
|
* @param {Number} delay
|
|
|
|
* The time to wait before signaling the operation to abort.
|
|
|
|
*/
|
|
|
|
start(delay) {
|
|
|
|
if (!this.signal.aborted) {
|
|
|
|
Services.obs.addObserver(this, "quit-application");
|
|
|
|
this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the watchdog timer and removes any listeners. This should be called
|
|
|
|
* after the operation finishes.
|
|
|
|
*/
|
|
|
|
stop() {
|
|
|
|
if (!this.signal.aborted) {
|
|
|
|
Services.obs.removeObserver(this, "quit-application");
|
|
|
|
this.timer.cancel();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
observe(subject, topic, data) {
|
|
|
|
if (topic == "timer-callback") {
|
|
|
|
this.abortReason = "timeout";
|
|
|
|
} else if (topic == "quit-application") {
|
|
|
|
this.abortReason = "shutdown";
|
|
|
|
}
|
|
|
|
this.stop();
|
|
|
|
this.controller.abort();
|
|
|
|
}
|
|
|
|
}
|