Bug 1875768 - Call the appropriate postfork handler on MacOS r=glandium
[gecko.git] / toolkit / modules / DeferredTask.sys.mjs
blobd515c16e2b28769ace48cb14f038634a0fbd25cf
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /**
8  * Sets up a function or an asynchronous task whose execution can be triggered
9  * after a defined delay.  Multiple attempts to run the task before the delay
10  * has passed are coalesced.  The task cannot be re-entered while running, but
11  * can be executed again after a previous run finished.
12  *
13  * A common use case occurs when a data structure should be saved into a file
14  * every time the data changes, using asynchronous calls, and multiple changes
15  * to the data may happen within a short time:
16  *
17  *   let saveDeferredTask = new DeferredTask(async function() {
18  *     await OS.File.writeAtomic(...);
19  *     // Any uncaught exception will be reported.
20  *   }, 2000);
21  *
22  *   // The task is ready, but will not be executed until requested.
23  *
24  * The "arm" method can be used to start the internal timer that will result in
25  * the eventual execution of the task.  Multiple attempts to arm the timer don't
26  * introduce further delays:
27  *
28  *   saveDeferredTask.arm();
29  *
30  *   // The task will be executed in 2 seconds from now.
31  *
32  *   await waitOneSecond();
33  *   saveDeferredTask.arm();
34  *
35  *   // The task will be executed in 1 second from now.
36  *
37  * The timer can be disarmed to reset the delay, or just to cancel execution:
38  *
39  *   saveDeferredTask.disarm();
40  *   saveDeferredTask.arm();
41  *
42  *   // The task will be executed in 2 seconds from now.
43  *
44  * When the internal timer fires and the execution of the task starts, the task
45  * cannot be canceled anymore.  It is however possible to arm the timer again
46  * during the execution of the task, in which case the task will need to finish
47  * before the timer is started again, thus guaranteeing a time of inactivity
48  * between executions that is at least equal to the provided delay.
49  *
50  * The "finalize" method can be used to ensure that the task terminates
51  * properly.  The promise it returns is resolved only after the last execution
52  * of the task is finished.  To guarantee that the task is executed for the
53  * last time, the method prevents any attempt to arm the timer again.
54  *
55  * If the timer is already armed when the "finalize" method is called, then the
56  * task is executed immediately.  If the task was already running at this point,
57  * then one last execution from start to finish will happen again, immediately
58  * after the current execution terminates.  If the timer is not armed, the
59  * "finalize" method only ensures that any running task terminates.
60  *
61  * For example, during shutdown, you may want to ensure that any pending write
62  * is processed, using the latest version of the data if the timer is armed:
63  *
64  *   AsyncShutdown.profileBeforeChange.addBlocker(
65  *     "Example service: shutting down",
66  *     () => saveDeferredTask.finalize()
67  *   );
68  *
69  * Instead, if you are going to delete the saved data from disk anyways, you
70  * might as well prevent any pending write from starting, while still ensuring
71  * that any write that is currently in progress terminates, so that the file is
72  * not in use anymore:
73  *
74  *   saveDeferredTask.disarm();
75  *   saveDeferredTask.finalize().then(() => OS.File.remove(...))
76  *                              .then(null, Components.utils.reportError);
77  */
79 // Globals
81 const Timer = Components.Constructor(
82   "@mozilla.org/timer;1",
83   "nsITimer",
84   "initWithCallback"
87 // DeferredTask
89 /**
90  * Sets up a task whose execution can be triggered after a delay.
91  *
92  * @param aTaskFn
93  *        Function to execute.  If the function returns a promise, the task is
94  *        not considered complete until that promise resolves.  This
95  *        task is never re-entered while running.
96  * @param aDelayMs
97  *        Time between executions, in milliseconds.  Multiple attempts to run
98  *        the task before the delay has passed are coalesced.  This time of
99  *        inactivity is guaranteed to pass between multiple executions of the
100  *        task, except on finalization, when the task may restart immediately
101  *        after the previous execution finished.
102  * @param aIdleTimeoutMs
103  *        The maximum time to wait for an idle slot on the main thread after
104  *        aDelayMs have elapsed. If omitted, waits indefinitely for an idle
105  *        callback.
106  */
107 export var DeferredTask = function (aTaskFn, aDelayMs, aIdleTimeoutMs) {
108   this._taskFn = aTaskFn;
109   this._delayMs = aDelayMs;
110   this._timeoutMs = aIdleTimeoutMs;
111   this._caller = new Error().stack.split("\n", 2)[1];
112   let markerString = `delay: ${aDelayMs}ms`;
113   if (aIdleTimeoutMs) {
114     markerString += `, idle timeout: ${aIdleTimeoutMs}`;
115   }
116   ChromeUtils.addProfilerMarker(
117     "DeferredTask",
118     { captureStack: true },
119     markerString
120   );
123 DeferredTask.prototype = {
124   /**
125    * Function to execute.
126    */
127   _taskFn: null,
129   /**
130    * Time between executions, in milliseconds.
131    */
132   _delayMs: null,
134   /**
135    * Indicates whether the task is currently requested to start again later,
136    * regardless of whether it is currently running.
137    */
138   get isArmed() {
139     return this._armed;
140   },
141   _armed: false,
143   /**
144    * Indicates whether the task is currently running.  This is always true when
145    * read from code inside the task function, but can also be true when read
146    * from external code, in case the task is an asynchronous function.
147    */
148   get isRunning() {
149     return !!this._runningPromise;
150   },
152   /**
153    * Promise resolved when the current execution of the task terminates, or null
154    * if the task is not currently running.
155    */
156   _runningPromise: null,
158   /**
159    * nsITimer used for triggering the task after a delay, or null in case the
160    * task is running or there is no task scheduled for execution.
161    */
162   _timer: null,
164   /**
165    * Actually starts the timer with the delay specified on construction.
166    */
167   _startTimer() {
168     let callback, timer;
169     if (this._timeoutMs === 0) {
170       callback = () => this._timerCallback();
171     } else {
172       callback = () => {
173         this._startIdleDispatch(() => {
174           // _timer could have changed by now:
175           // - to null if disarm() or finalize() has been called.
176           // - to a new nsITimer if disarm() was called, followed by arm().
177           // In either case, don't invoke _timerCallback any more.
178           if (this._timer === timer) {
179             this._timerCallback();
180           }
181         }, this._timeoutMs);
182       };
183     }
184     timer = new Timer(callback, this._delayMs, Ci.nsITimer.TYPE_ONE_SHOT);
185     this._timer = timer;
186   },
188   /**
189    * Dispatches idle task. Can be overridden for testing by test_DeferredTask.
190    */
191   _startIdleDispatch(callback, timeout) {
192     ChromeUtils.idleDispatch(callback, { timeout });
193   },
195   /**
196    * Requests the execution of the task after the delay specified on
197    * construction.  Multiple calls don't introduce further delays.  If the task
198    * is running, the delay will start when the current execution finishes.
199    *
200    * The task will always be executed on a different tick of the event loop,
201    * even if the delay specified on construction is zero.  Multiple "arm" calls
202    * within the same tick of the event loop are guaranteed to result in a single
203    * execution of the task.
204    *
205    * @note By design, this method doesn't provide a way for the caller to detect
206    *       when the next execution terminates, or collect a result.  In fact,
207    *       doing that would often result in duplicate processing or logging.  If
208    *       a special operation or error logging is needed on completion, it can
209    *       be better handled from within the task itself, for example using a
210    *       try/catch/finally clause in the task.  The "finalize" method can be
211    *       used in the common case of waiting for completion on shutdown.
212    */
213   arm() {
214     if (this._finalized) {
215       throw new Error("Unable to arm timer, the object has been finalized.");
216     }
218     this._armed = true;
220     // In case the timer callback is running, do not create the timer now,
221     // because this will be handled by the timer callback itself.  Also, the
222     // timer is not restarted in case it is already running.
223     if (!this._runningPromise && !this._timer) {
224       this._startTimer();
225     }
226   },
228   /**
229    * Cancels any request for a delayed the execution of the task, though the
230    * task itself cannot be canceled in case it is already running.
231    *
232    * This method stops any currently running timer, thus the delay will restart
233    * from its original value in case the "arm" method is called again.
234    */
235   disarm() {
236     this._armed = false;
237     if (this._timer) {
238       // Calling the "cancel" method and discarding the timer reference makes
239       // sure that the timer callback will not be called later, even if the
240       // timer thread has already posted the timer event on the main thread.
241       this._timer.cancel();
242       this._timer = null;
243     }
244   },
246   /**
247    * Ensures that any pending task is executed from start to finish, while
248    * preventing any attempt to arm the timer again.
249    *
250    * - If the task is running and the timer is armed, then one last execution
251    *   from start to finish will happen again, immediately after the current
252    *   execution terminates, then the returned promise will be resolved.
253    * - If the task is running and the timer is not armed, the returned promise
254    *   will be resolved when the current execution terminates.
255    * - If the task is not running and the timer is armed, then the task is
256    *   started immediately, and the returned promise resolves when the new
257    *   execution terminates.
258    * - If the task is not running and the timer is not armed, the method returns
259    *   a resolved promise.
260    *
261    * @return {Promise}
262    * @resolves After the last execution of the task is finished.
263    * @rejects Never.
264    */
265   finalize() {
266     if (this._finalized) {
267       throw new Error("The object has been already finalized.");
268     }
269     this._finalized = true;
271     // If the timer is armed, it means that the task is not running but it is
272     // scheduled for execution.  Cancel the timer and run the task immediately,
273     // so we don't risk blocking async shutdown longer than necessary.
274     if (this._timer) {
275       this.disarm();
276       this._timerCallback();
277     }
279     // Wait for the operation to be completed, or resolve immediately.
280     if (this._runningPromise) {
281       return this._runningPromise;
282     }
283     return Promise.resolve();
284   },
285   _finalized: false,
287   /**
288    * Whether the DeferredTask has been finalized, and it cannot be armed anymore.
289    */
290   get isFinalized() {
291     return this._finalized;
292   },
294   /**
295    * Timer callback used to run the delayed task.
296    */
297   _timerCallback() {
298     let runningDeferred = Promise.withResolvers();
300     // All these state changes must occur at the same time directly inside the
301     // timer callback, to prevent race conditions and to ensure that all the
302     // methods behave consistently even if called from inside the task.  This
303     // means that the assignment of "this._runningPromise" must complete before
304     // the task gets a chance to start.
305     this._timer = null;
306     this._armed = false;
307     this._runningPromise = runningDeferred.promise;
309     runningDeferred.resolve(
310       (async () => {
311         // Execute the provided function asynchronously.
312         await this._runTask();
314         // Now that the task has finished, we check the state of the object to
315         // determine if we should restart the task again.
316         if (this._armed) {
317           if (!this._finalized) {
318             this._startTimer();
319           } else {
320             // Execute the task again immediately, for the last time.  The isArmed
321             // property should return false while the task is running, and should
322             // remain false after the last execution terminates.
323             this._armed = false;
324             await this._runTask();
325           }
326         }
328         // Indicate that the execution of the task has finished.  This happens
329         // synchronously with the previous state changes in the function.
330         this._runningPromise = null;
331       })().catch(console.error)
332     );
333   },
335   /**
336    * Executes the associated task and catches exceptions.
337    */
338   async _runTask() {
339     let startTime = Cu.now();
340     try {
341       await this._taskFn();
342     } catch (ex) {
343       console.error(ex);
344     } finally {
345       ChromeUtils.addProfilerMarker(
346         "DeferredTask",
347         { startTime },
348         this._caller
349       );
350     }
351   },