Bug 1166452 - Add DelayedInit module for Fennec startup; r=mfinkle
authorJim Chen <nchen@mozilla.com>
Wed, 10 Jun 2015 00:25:02 -0400
changeset 247894 f2535394c20508982fc7e39f0e3ff9bfb0705b0a
parent 247893 795f7d597eadef6e38793edaca83819bab32cf23
child 247895 f84de1ca1d119d7a31aa0b3887f7d6b6a7dbb875
push id28885
push usercbook@mozilla.com
push dateWed, 10 Jun 2015 13:18:59 +0000
treeherderautoland@e101c589c242 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs1166452
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1166452 - Add DelayedInit module for Fennec startup; r=mfinkle
mobile/android/modules/DelayedInit.jsm
mobile/android/modules/moz.build
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/DelayedInit.jsm
@@ -0,0 +1,175 @@
+/* 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"
+
+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");
+ */
+let 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;
+
+let 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,
+    });
+  }
+};
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -4,16 +4,17 @@
 # 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/.
 
 EXTRA_JS_MODULES += [
     'Accounts.jsm',
     'AndroidLog.jsm',
     'ContactService.jsm',
     'dbg-browser-actors.js',
+    'DelayedInit.jsm',
     'DownloadNotifications.jsm',
     'HelperApps.jsm',
     'Home.jsm',
     'HomeProvider.jsm',
     'JNI.jsm',
     'LightweightThemeConsumer.jsm',
     'MatchstickApp.jsm',
     'MediaPlayerApp.jsm',