Source code

Revision control

Copy as Markdown

Other Tools

/* 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 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");
*/
export var DelayedInit = {
schedule(fn, object, name, maxWait) {
return Impl.scheduleInit(fn, object, name, maxWait);
},
scheduleList(fns, maxWait) {
for (const fn of fns) {
Impl.scheduleInit(fn, null, null, 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() {
const 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 (const 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.
ChromeUtils.idleDispatch(() => this.onIdle(), {
timeout: Math.max(0, nextDue - time),
});
}
},
addPendingInit(fn, wait) {
const init = {
fn,
due: Cu.now() + wait,
complete: false,
maybeInit() {
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.
ChromeUtils.idleDispatch(() => this.onIdle(), { timeout: wait });
}
this.pendingInits.push(init);
return init;
},
scheduleInit(fn, object, name, wait) {
const 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.
const 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(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);
prop.set.call(object);
return;
}
prop.value = newVal;
Object.defineProperty(object, name, prop);
},
configurable: true,
enumerable: true,
});
},
};