summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/geckoview/DelayedInit.jsm
blob: 6386fc6cdb301a18df03c46c13cd880045056a65 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/* 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 */

var EXPORTED_SYMBOLS = ["DelayedInit"];

const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "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(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.
      lazy.MessageLoop.postIdleTask(
        () => this.onIdle(),
        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.
      lazy.MessageLoop.postIdleTask(() => this.onIdle(), 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,
    });
  },
};