diff options
Diffstat (limited to 'mobile/android/modules/DelayedInit.jsm')
-rw-r--r-- | mobile/android/modules/DelayedInit.jsm | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/mobile/android/modules/DelayedInit.jsm b/mobile/android/modules/DelayedInit.jsm new file mode 100644 index 000000000..7c33a3ede --- /dev/null +++ b/mobile/android/modules/DelayedInit.jsm @@ -0,0 +1,177 @@ +/* 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" + +/*globals MessageLoop */ + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +this.EXPORTED_SYMBOLS = ["DelayedInit"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "MessageLoop", + "@mozilla.org/message-loop;1", + "nsIMessageLoop"); + +/** + * Use DelayedInit to schedule initializers to run some time after startup. + * Initializers are added to a list of pending inits. Whenever the main thread + * message loop is idle, DelayedInit will start running initializers from the + * pending list. To prevent monopolizing the message loop, every idling period + * has a maximum duration. When that's reached, we give up the message loop and + * wait for the next idle. + * + * DelayedInit is compatible with lazy getters like those from XPCOMUtils. When + * the lazy getter is first accessed, its corresponding initializer is run + * automatically if it hasn't been run already. Each initializer also has a + * maximum wait parameter that specifies a mandatory timeout; when the timeout + * is reached, the initializer is forced to run. + * + * DelayedInit.schedule(() => Foo.init(), null, null, 5000); + * + * In the example above, Foo.init will run automatically when the message loop + * becomes idle, or when 5000ms has elapsed, whichever comes first. + * + * DelayedInit.schedule(() => Foo.init(), this, "Foo", 5000); + * + * In the example above, Foo.init will run automatically when the message loop + * becomes idle, when |this.Foo| is accessed, or when 5000ms has elapsed, + * whichever comes first. + * + * It may be simpler to have a wrapper for DelayedInit.schedule. For example, + * + * function InitLater(fn, obj, name) { + * return DelayedInit.schedule(fn, obj, name, 5000); // constant max wait + * } + * InitLater(() => Foo.init()); + * InitLater(() => Bar.init(), this, "Bar"); + */ +var DelayedInit = { + schedule: function (fn, object, name, maxWait) { + return Impl.scheduleInit(fn, object, name, maxWait); + }, +}; + +// Maximum duration for each idling period. Pending inits are run until this +// duration is exceeded; then we wait for next idling period. +const MAX_IDLE_RUN_MS = 50; + +var Impl = { + pendingInits: [], + + onIdle: function () { + let startTime = Cu.now(); + let time = startTime; + let nextDue; + + // Go through all the pending inits. Even if we don't run them, + // we still need to find out when the next timeout should be. + for (let init of this.pendingInits) { + if (init.complete) { + continue; + } + + if (time - startTime < MAX_IDLE_RUN_MS) { + init.maybeInit(); + time = Cu.now(); + } else { + // We ran out of time; find when the next closest due time is. + nextDue = nextDue ? Math.min(nextDue, init.due) : init.due; + } + } + + // Get rid of completed ones. + this.pendingInits = this.pendingInits.filter((init) => !init.complete); + + if (nextDue !== undefined) { + // Schedule the next idle, if we still have pending inits. + MessageLoop.postIdleTask(() => this.onIdle(), + Math.max(0, nextDue - time)); + } + }, + + addPendingInit: function (fn, wait) { + let init = { + fn: fn, + due: Cu.now() + wait, + complete: false, + maybeInit: function () { + if (this.complete) { + return false; + } + this.complete = true; + this.fn.call(); + this.fn = null; + return true; + }, + }; + + if (!this.pendingInits.length) { + // Schedule for the first idle. + MessageLoop.postIdleTask(() => this.onIdle(), wait); + } + this.pendingInits.push(init); + return init; + }, + + scheduleInit: function (fn, object, name, wait) { + let init = this.addPendingInit(fn, wait); + + if (!object || !name) { + // No lazy getter needed. + return; + } + + // Get any existing information about the property. + let prop = Object.getOwnPropertyDescriptor(object, name) || + { configurable: true, enumerable: true, writable: true }; + + if (!prop.configurable) { + // Object.defineProperty won't work, so just perform init here. + init.maybeInit(); + return; + } + + // Define proxy getter/setter that will call first initializer first, + // before delegating the get/set to the original target. + Object.defineProperty(object, name, { + get: function proxy_getter() { + init.maybeInit(); + + // If the initializer actually ran, it may have replaced our proxy + // property with a real one, so we need to reload he property. + let newProp = Object.getOwnPropertyDescriptor(object, name); + if (newProp.get !== proxy_getter) { + // Set prop if newProp doesn't refer to our proxy property. + prop = newProp; + } else { + // Otherwise, reset to the original property. + Object.defineProperty(object, name, prop); + } + + if (prop.get) { + return prop.get.call(object); + } + return prop.value; + }, + set: function (newVal) { + init.maybeInit(); + + // Since our initializer already ran, + // we can get rid of our proxy property. + if (prop.get || prop.set) { + Object.defineProperty(object, name, prop); + return prop.set.call(object); + } + + prop.value = newVal; + Object.defineProperty(object, name, prop); + return newVal; + }, + configurable: true, + enumerable: true, + }); + } +}; |