Bug 1900094 - Add telemetry for impressions missing due to domain-to-categories map...
[gecko.git] / toolkit / components / asyncshutdown / AsyncShutdown.sys.mjs
blob4577fa49a49cdc13926f87f707e644723d0f44c0
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * Managing safe shutdown of asynchronous services.
7  *
8  * Firefox shutdown is composed of phases that take place
9  * sequentially. Typically, each shutdown phase removes some
10  * capabilities from the application. For instance, at the end of
11  * phase profileBeforeChange, no service is permitted to write to the
12  * profile directory (with the exception of Telemetry). Consequently,
13  * if any service has requested I/O to the profile directory before or
14  * during phase profileBeforeChange, the system must be informed that
15  * these requests need to be completed before the end of phase
16  * profileBeforeChange. Failing to inform the system of this
17  * requirement can (and has been known to) cause data loss.
18  *
19  * Example: At some point during shutdown, the Add-On Manager needs to
20  * ensure that all add-ons have safely written their data to disk,
21  * before writing its own data. Since the data is saved to the
22  * profile, this must be completed during phase profileBeforeChange.
23  *
24  * AsyncShutdown.profileBeforeChange.addBlocker(
25  *   "Add-on manager: shutting down",
26  *   function condition() {
27  *     // Do things.
28  *     // Perform I/O that must take place during phase profile-before-change
29  *     return promise;
30  *   }
31  * });
32  *
33  * In this example, function |condition| will be called at some point
34  * during phase profileBeforeChange and phase profileBeforeChange
35  * itself is guaranteed to not terminate until |promise| is either
36  * resolved or rejected.
37  */
39 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
41 const lazy = {};
43 XPCOMUtils.defineLazyServiceGetter(
44   lazy,
45   "gDebug",
46   "@mozilla.org/xpcom/debug;1",
47   "nsIDebug2"
50 // `true` if this is a content process, `false` otherwise.
51 // It would be nicer to go through `Services.appinfo`, but some tests need to be
52 // able to replace that field with a custom implementation before it is first
53 // called.
54 const isContent =
55   // eslint-disable-next-line mozilla/use-services
56   Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType ==
57   Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
59 // Display timeout warnings after 10 seconds
60 const DELAY_WARNING_MS = 10 * 1000;
62 // Crash the process if shutdown is really too long
63 // (allowing for sleep).
64 const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
65 var DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS, 60 * 1000); // One minute
66 Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function () {
67   DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
68 });
70 /**
71  * Any addBlocker calls that failed. We add this into barrier wait
72  * crash annotations to help with debugging. When we fail to add
73  * shutdown blockers that can break shutdown. We track these globally
74  * rather than per barrier because one failure mode is when the
75  * barrier has already finished by the time addBlocker is invoked -
76  * but the failure to add the blocker may result in later barriers
77  * waiting indefinitely, so the debug information is still useful
78  * for those later barriers. See bug 1801674 for more context.
79  */
80 let gBrokenAddBlockers = [];
82 /**
83  * A set of Promise that supports waiting.
84  *
85  * Promise items may be added or removed during the wait. The wait will
86  * resolve once all Promise items have been resolved or removed.
87  */
88 function PromiseSet() {
89   /**
90    * key: the Promise passed pass the client of the `PromiseSet`.
91    * value: an indirection on top of `key`, as an object with
92    *   the following fields:
93    *   - indirection: a Promise resolved if `key` is resolved or
94    *     if `resolve` is called
95    *   - resolve: a function used to resolve the indirection.
96    */
97   this._indirections = new Map();
98   // Once all the tracked promises have been resolved we are done. Once Wait()
99   // resolves, it should not be possible anymore to add further promises.
100   // This covers for a possibly rare case, where something may try to add a
101   // blocker after wait() is done, that would never be awaited for.
102   this._done = false;
104 PromiseSet.prototype = {
105   /**
106    * Wait until all Promise have been resolved or removed.
107    *
108    * Note that calling `wait()` causes Promise to be removed from the
109    * Set once they are resolved.
110    * @param {function} onDoneCb invoked synchronously once all the entries
111    * have been handled and no new entries will be accepted.
112    * @return {Promise} Resolved once all Promise have been resolved or removed,
113    * or rejected after at least one Promise has rejected.
114    */
115   wait(onDoneCb) {
116     // Pick an arbitrary element in the map, if any exists.
117     let entry = this._indirections.entries().next();
118     if (entry.done) {
119       // No indirections left, we are done.
120       this._done = true;
121       onDoneCb();
122       return Promise.resolve();
123     }
125     let [, indirection] = entry.value;
126     let promise = indirection.promise;
127     promise = promise.then(() =>
128       // At this stage, the entry has been cleaned up.
129       this.wait(onDoneCb)
130     );
131     return promise;
132   },
134   /**
135    * Add a new Promise to the set.
136    *
137    * Calls to wait (including ongoing calls) will only return once
138    * `key` has either resolved or been removed.
139    */
140   add(key) {
141     if (this._done) {
142       throw new Error("Wait is complete, cannot add further promises.");
143     }
144     this._ensurePromise(key);
145     let indirection = Promise.withResolvers();
146     key
147       .then(
148         x => {
149           // Clean up immediately.
150           // This needs to be done before the call to `resolve`, otherwise
151           // `wait()` may loop forever.
152           this._indirections.delete(key);
153           indirection.resolve(x);
154         },
155         err => {
156           this._indirections.delete(key);
157           indirection.reject(err);
158         }
159       )
160       .finally(() => {
161         this._indirections.delete(key);
162         // Normally the promise is resolved or rejected, but if its global
163         // goes away, only finally may be invoked. In all the other cases this
164         // is a no-op since the promise has been fulfilled already.
165         indirection.reject(
166           new Error("Promise not fulfilled, did it lost its global?")
167         );
168       });
169     this._indirections.set(key, indirection);
170   },
172   /**
173    * Remove a Promise from the set.
174    *
175    * Calls to wait (including ongoing calls) will ignore this promise,
176    * unless it is added again.
177    */
178   delete(key) {
179     this._ensurePromise(key);
180     let value = this._indirections.get(key);
181     if (!value) {
182       return false;
183     }
184     this._indirections.delete(key);
185     value.resolve();
186     return true;
187   },
189   _ensurePromise(key) {
190     if (!key || typeof key != "object") {
191       throw new Error("Expected an object");
192     }
193     if (!("then" in key) || typeof key.then != "function") {
194       throw new Error("Expected a Promise");
195     }
196   },
200  * Display a warning.
202  * As this code is generally used during shutdown, there are chances
203  * that the UX will not be available to display warnings on the
204  * console. We therefore use dump() rather than Cu.reportError().
205  */
206 function log(msg, prefix = "", error = null) {
207   try {
208     dump(prefix + msg + "\n");
209     if (error) {
210       dump(prefix + error + "\n");
211       if (typeof error == "object" && "stack" in error) {
212         dump(prefix + error.stack + "\n");
213       }
214     }
215   } catch (ex) {
216     dump("INTERNAL ERROR in AsyncShutdown: cannot log message.\n");
217   }
219 const PREF_DEBUG_LOG = "toolkit.asyncshutdown.log";
220 var DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG, false);
221 Services.prefs.addObserver(PREF_DEBUG_LOG, function () {
222   DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
225 function debug(msg, error = null) {
226   if (DEBUG_LOG) {
227     log(msg, "DEBUG: ", error);
228   }
230 function warn(msg, error = null) {
231   log(msg, "WARNING: ", error);
233 function fatalerr(msg, error = null) {
234   log(msg, "FATAL ERROR: ", error);
237 // Utility function designed to get the current state of execution
238 // of a blocker.
239 // We are a little paranoid here to ensure that in case of evaluation
240 // error we do not block the AsyncShutdown.
241 function safeGetState(fetchState) {
242   if (!fetchState) {
243     return "(none)";
244   }
245   let data, string;
246   try {
247     // Evaluate fetchState(), normalize the result into something that we can
248     // safely stringify or upload.
249     let state = fetchState();
250     if (!state) {
251       return "(none)";
252     }
253     string = JSON.stringify(state);
254     data = JSON.parse(string);
255     // Simplify the rest of the code by ensuring that we can simply
256     // concatenate the result to a message.
257     if (data && typeof data == "object") {
258       data.toString = function () {
259         return string;
260       };
261     }
262     return data;
263   } catch (ex) {
264     // Make sure that this causes test failures
265     Promise.reject(ex);
267     if (string) {
268       return string;
269     }
270     try {
271       return "Error getting state: " + ex + " at " + ex.stack;
272     } catch (ex2) {
273       return "Error getting state but could not display error";
274     }
275   }
279  * Countdown for a given duration, skipping beats if the computer is too busy,
280  * sleeping or otherwise unavailable.
282  * @param {number} delay An approximate delay to wait in milliseconds (rounded
283  * up to the closest second).
285  * @return Deferred
286  */
287 function looseTimer(delay) {
288   let DELAY_BEAT = 1000;
289   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
290   let beats = Math.ceil(delay / DELAY_BEAT);
291   let deferred = Promise.withResolvers();
292   timer.initWithCallback(
293     function () {
294       if (beats <= 0) {
295         deferred.resolve();
296       }
297       --beats;
298     },
299     DELAY_BEAT,
300     Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP
301   );
302   // Ensure that the timer is both canceled once we are done with it
303   // and not garbage-collected until then.
304   deferred.promise.then(
305     () => timer.cancel(),
306     () => timer.cancel()
307   );
308   return deferred;
312  * Given an nsIStackFrame object, find the caller filename, line number,
313  * and stack if necessary, and return them as an object.
315  * @param {nsIStackFrame} topFrame Top frame of the call stack.
316  * @param {string} filename Pre-supplied filename or null if unknown.
317  * @param {number} lineNumber Pre-supplied line number or null if unknown.
318  * @param {string} stack Pre-supplied stack or null if unknown.
320  * @return object
321  */
322 function getOrigin(topFrame, filename = null, lineNumber = null, stack = null) {
323   try {
324     // Determine the filename and line number of the caller.
325     let frame = topFrame;
327     for (; frame && frame.filename == topFrame.filename; frame = frame.caller) {
328       // Climb up the stack
329     }
331     if (filename == null) {
332       filename = frame ? frame.filename : "?";
333     }
334     if (lineNumber == null) {
335       lineNumber = frame ? frame.lineNumber : 0;
336     }
337     if (stack == null) {
338       // Now build the rest of the stack as a string.
339       stack = [];
340       while (frame != null) {
341         stack.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber);
342         frame = frame.caller;
343       }
344     }
346     return {
347       filename,
348       lineNumber,
349       stack,
350     };
351   } catch (ex) {
352     return {
353       filename: "<internal error: could not get origin>",
354       lineNumber: -1,
355       stack: "<internal error: could not get origin>",
356     };
357   }
361  * {string} topic -> phase
362  */
363 var gPhases = new Map();
365 export var AsyncShutdown = {
366   /**
367    * Access function getPhase. For testing purposes only.
368    */
369   get _getPhase() {
370     let accepted = Services.prefs.getBoolPref(
371       "toolkit.asyncshutdown.testing",
372       false
373     );
374     if (accepted) {
375       return getPhase;
376     }
377     return undefined;
378   },
380   /**
381    * This constant is used as the amount of milliseconds to allow shutdown to be
382    * blocked until we crash the process forcibly and is read from the
383    * 'toolkit.asyncshutdown.crash_timeout' pref.
384    */
385   get DELAY_CRASH_MS() {
386     return DELAY_CRASH_MS;
387   },
391  * Register a new phase.
393  * @param {string} topic The notification topic for this Phase.
394  * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications}
395  */
396 function getPhase(topic) {
397   let phase = gPhases.get(topic);
398   if (phase) {
399     return phase;
400   }
401   let spinner = new Spinner(topic);
402   phase = Object.freeze({
403     /**
404      * Register a blocker for the completion of a phase.
405      *
406      * @param {string} name The human-readable name of the blocker. Used
407      * for debugging/error reporting. Please make sure that the name
408      * respects the following model: "Some Service: some action in progress" -
409      * for instance "OS.File: flushing all pending I/O";
410      * @param {function|promise|*} condition A condition blocking the
411      * completion of the phase. Generally, this is a function
412      * returning a promise. This function is evaluated during the
413      * phase and the phase is guaranteed to not terminate until the
414      * resulting promise is either resolved or rejected. If
415      * |condition| is not a function but another value |v|, it behaves
416      * as if it were a function returning |v|.
417      * @param {object*} details Optionally, an object with details
418      * that may be useful for error reporting, as a subset of of the following
419      * fields:
420      * - fetchState (strongly recommended) A function returning
421      *    information about the current state of the blocker as an
422      *    object. Used for providing more details when logging errors or
423      *    crashing.
424      * - stack. A string containing stack information. This module can
425      *    generally infer stack information if it is not provided.
426      * - lineNumber A number containing the line number for the caller.
427      *    This module can generally infer this information if it is not
428      *    provided.
429      * - filename A string containing the filename for the caller. This
430      *    module can generally infer  the information if it is not provided.
431      *
432      * Examples:
433      * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
434      *      promise); // profileBeforeChange will not complete until
435      *                // promise is resolved or rejected
436      *
437      * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback",
438      *     function callback() {
439      *       // ...
440      *       // Execute this code during profileBeforeChange
441      *       return promise;
442      *       // profileBeforeChange will not complete until promise
443      *       // is resolved or rejected
444      * });
445      *
446      * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback",
447      *     function callback() {
448      *       // ...
449      *       // Execute this code during profileBeforeChange
450      *       // No specific guarantee about completion of profileBeforeChange
451      * });
452      */
453     addBlocker(name, condition, details = null) {
454       spinner.addBlocker(name, condition, details);
455     },
456     /**
457      * Remove the blocker for a condition.
458      *
459      * If several blockers have been registered for the same
460      * condition, remove all these blockers. If no blocker has been
461      * registered for this condition, this is a noop.
462      *
463      * @return {boolean} true if a blocker has been removed, false
464      * otherwise. Note that a result of false may mean either that
465      * the blocker has never been installed or that the phase has
466      * completed and the blocker has already been resolved.
467      */
468     removeBlocker(condition) {
469       return spinner.removeBlocker(condition);
470     },
472     get name() {
473       return spinner.name;
474     },
476     get isClosed() {
477       return spinner.isClosed;
478     },
480     /**
481      * Trigger the phase without having to broadcast a
482      * notification. For testing purposes only.
483      */
484     get _trigger() {
485       let accepted = Services.prefs.getBoolPref(
486         "toolkit.asyncshutdown.testing",
487         false
488       );
489       if (accepted) {
490         return () => spinner.observe();
491       }
492       return undefined;
493     },
494   });
495   gPhases.set(topic, phase);
496   return phase;
500  * Utility class used to spin the event loop until all blockers for a
501  * Phase are satisfied.
503  * @param {string} topic The xpcom notification for that phase.
504  */
505 function Spinner(topic) {
506   this._barrier = new Barrier(topic);
507   this._topic = topic;
508   Services.obs.addObserver(this, topic);
511 Spinner.prototype = {
512   /**
513    * Register a new condition for this phase.
514    *
515    * See the documentation of `addBlocker` in property `client`
516    * of instances of `Barrier`.
517    */
518   addBlocker(name, condition, details) {
519     this._barrier.client.addBlocker(name, condition, details);
520   },
521   /**
522    * Remove the blocker for a condition.
523    *
524    * See the documentation of `removeBlocker` in rpoperty `client`
525    * of instances of `Barrier`
526    *
527    * @return {boolean} true if a blocker has been removed, false
528    * otherwise. Note that a result of false may mean either that
529    * the blocker has never been installed or that the phase has
530    * completed and the blocker has already been resolved.
531    */
532   removeBlocker(condition) {
533     return this._barrier.client.removeBlocker(condition);
534   },
536   get name() {
537     return this._barrier.client.name;
538   },
540   get isClosed() {
541     return this._barrier.client.isClosed;
542   },
544   // nsIObserver.observe
545   observe() {
546     let topic = this._topic;
547     debug(`Starting phase ${topic}`);
548     Services.obs.removeObserver(this, topic);
550     // Setup the promise that will signal our phase's end.
551     let isPhaseEnd = false;
552     try {
553       this._barrier
554         .wait({
555           warnAfterMS: DELAY_WARNING_MS,
556           crashAfterMS: DELAY_CRASH_MS,
557         })
558         .finally(() => {
559           isPhaseEnd = true;
560         });
561     } catch (ex) {
562       debug("Error waiting for notification");
563       throw ex;
564     }
566     // Now, spin the event loop. In case of a hang we will just crash without
567     // ever leaving this loop.
568     debug("Spinning the event loop");
569     Services.tm.spinEventLoopUntil(
570       `AsyncShutdown Spinner for ${topic}`,
571       () => isPhaseEnd
572     );
574     debug(`Finished phase ${topic}`);
575   },
579  * A mechanism used to register blockers that prevent some action from
580  * happening.
582  * An instance of |Barrier| provides a capability |client| that
583  * clients can use to register blockers. The barrier is resolved once
584  * all registered blockers have been resolved. The owner of the
585  * |Barrier| may wait for the resolution of the barrier and obtain
586  * information on which blockers have not been resolved yet.
588  * @param {string} name The name of the blocker. Used mainly for error-
589  *     reporting.
590  */
591 function Barrier(name) {
592   if (!name) {
593     throw new TypeError("Instances of Barrier need a (non-empty) name");
594   }
596   /**
597    * The set of all Promise for which we need to wait before the barrier
598    * is lifted. Note that this set may be changed while we are waiting.
599    *
600    * Set to `null` once the wait is complete.
601    */
602   this._waitForMe = new PromiseSet();
604   /**
605    * A map from conditions, as passed by users during the call to `addBlocker`,
606    * to `promise`, as present in `this._waitForMe`.
607    *
608    * Used to let users perform cleanup through `removeBlocker`.
609    * Set to `null` once the wait is complete.
610    *
611    * Key: condition (any, as passed by user)
612    * Value: promise used as a key in `this._waitForMe`. Note that there is
613    * no guarantee that the key is still present in `this._waitForMe`.
614    */
615   this._conditionToPromise = new Map();
617   /**
618    * A map from Promise, as present in `this._waitForMe` or
619    * `this._conditionToPromise`, to information on blockers.
620    *
621    * Key: Promise (as present in this._waitForMe or this._conditionToPromise).
622    * Value:  {
623    *  trigger: function,
624    *  promise,
625    *  name,
626    *  fetchState: function,
627    *  stack,
628    *  filename,
629    *  lineNumber
630    * };
631    */
632   this._promiseToBlocker = new Map();
634   /**
635    * The name of the barrier.
636    */
637   if (typeof name != "string") {
638     throw new TypeError("The name of the barrier must be a string");
639   }
640   this._name = name;
642   /**
643    * A cache for the promise returned by wait().
644    */
645   this._promise = null;
647   /**
648    * `true` once we have started waiting.
649    */
650   this._isStarted = false;
652   /**
653    * `true` once we're done and won't accept any new blockers.
654    */
655   this._isClosed = false;
657   /**
658    * The capability of adding blockers. This object may safely be returned
659    * or passed to clients.
660    */
661   this.client = {
662     /**
663      * The name of the barrier owning this client.
664      */
665     get name() {
666       return name;
667     },
669     /**
670      * Register a blocker for the completion of this barrier.
671      *
672      * @param {string} name The human-readable name of the blocker. Used
673      * for debugging/error reporting. Please make sure that the name
674      * respects the following model: "Some Service: some action in progress" -
675      * for instance "OS.File: flushing all pending I/O";
676      * @param {function|promise|*} condition A condition blocking the
677      * completion of the phase. Generally, this is a function
678      * returning a promise. This function is evaluated during the
679      * phase and the phase is guaranteed to not terminate until the
680      * resulting promise is either resolved or rejected. If
681      * |condition| is not a function but another value |v|, it behaves
682      * as if it were a function returning |v|.
683      * @param {object*} details Optionally, an object with details
684      * that may be useful for error reporting, as a subset of of the following
685      * fields:
686      * - fetchState (strongly recommended) A function returning
687      *    information about the current state of the blocker as an
688      *    object. Used for providing more details when logging errors or
689      *    crashing.
690      * - stack. A string containing stack information. This module can
691      *    generally infer stack information if it is not provided.
692      * - lineNumber A number containing the line number for the caller.
693      *    This module can generally infer this information if it is not
694      *    provided.
695      * - filename A string containing the filename for the caller. This
696      *    module can generally infer  the information if it is not provided.
697      */
698     addBlocker: (name, condition, details) => {
699       if (typeof name != "string") {
700         gBrokenAddBlockers.push("No-name blocker");
701         throw new TypeError("Expected a human-readable name as first argument");
702       }
703       if (details && typeof details == "function") {
704         details = {
705           fetchState: details,
706         };
707       } else if (!details) {
708         details = {};
709       }
710       if (typeof details != "object") {
711         gBrokenAddBlockers.push(`${name} - invalid details`);
712         throw new TypeError(
713           "Expected an object as third argument to `addBlocker`, got " + details
714         );
715       }
716       if (!this._waitForMe) {
717         gBrokenAddBlockers.push(`${name} - ${this._name} finished`);
718         throw new Error(
719           `Phase "${this._name}" is finished, it is too late to register completion condition "${name}"`
720         );
721       }
722       debug(`Adding blocker ${name} for phase ${this._name}`);
724       try {
725         this.client._internalAddBlocker(name, condition, details);
726       } catch (ex) {
727         gBrokenAddBlockers.push(`${name} - ${ex.message}`);
728         throw ex;
729       }
730     },
732     _internalAddBlocker: (
733       name,
734       condition,
735       { fetchState = null, filename = null, lineNumber = null, stack = null }
736     ) => {
737       if (fetchState != null && typeof fetchState != "function") {
738         throw new TypeError("Expected a function for option `fetchState`");
739       }
741       // Split the condition between a trigger function and a promise.
743       // The function to call to notify the blocker that we have started waiting.
744       // This function returns a promise resolved/rejected once the
745       // condition is complete, and never throws.
746       let trigger;
748       // A promise resolved once the condition is complete.
749       let promise;
750       if (typeof condition == "function") {
751         promise = new Promise((resolve, reject) => {
752           trigger = () => {
753             try {
754               resolve(condition());
755             } catch (ex) {
756               reject(ex);
757             }
758           };
759         });
760       } else {
761         // If `condition` is not a function, `trigger` is not particularly
762         // interesting, and `condition` needs to be normalized to a promise.
763         trigger = () => {};
764         promise = Promise.resolve(condition);
765       }
767       // Make sure that `promise` never rejects.
768       promise = promise
769         .catch(error => {
770           let msg = `A blocker encountered an error while we were waiting.
771           Blocker:  ${name}
772           Phase: ${this._name}
773           State: ${safeGetState(fetchState)}`;
774           warn(msg, error);
776           // The error should remain uncaught, to ensure that it
777           // still causes tests to fail.
778           Promise.reject(error);
779         })
780         // Added as a last line of defense, in case `warn`, `this._name` or
781         // `safeGetState` somehow throws an error.
782         .catch(() => {});
784       let topFrame = null;
785       if (filename == null || lineNumber == null || stack == null) {
786         topFrame = Components.stack;
787       }
789       let blocker = {
790         trigger,
791         promise,
792         name,
793         fetchState,
794         getOrigin: () => getOrigin(topFrame, filename, lineNumber, stack),
795       };
797       this._waitForMe.add(promise);
798       this._promiseToBlocker.set(promise, blocker);
799       this._conditionToPromise.set(condition, promise);
801       // As conditions may hold lots of memory, we attempt to cleanup
802       // as soon as we are done (which might be in the next tick, if
803       // we have been passed a resolved promise).
804       promise = promise.then(() => {
805         debug(`Completed blocker ${name} for phase ${this._name}`);
806         this._removeBlocker(condition);
807       });
809       if (this._isStarted) {
810         // The wait has already started. The blocker should be
811         // notified asap. We do it out of band as clients probably
812         // expect `addBlocker` to return immediately.
813         Promise.resolve().then(trigger);
814       }
815     },
817     /**
818      * Remove the blocker for a condition.
819      *
820      * If several blockers have been registered for the same
821      * condition, remove all these blockers. If no blocker has been
822      * registered for this condition, this is a noop.
823      *
824      * @return {boolean} true if at least one blocker has been
825      * removed, false otherwise.
826      */
827     removeBlocker: condition => {
828       return this._removeBlocker(condition);
829     },
830   };
832   /**
833    * Whether this client still accepts new blockers.
834    */
835   Object.defineProperty(this.client, "isClosed", {
836     get: () => {
837       return this._isClosed;
838     },
839   });
841 Barrier.prototype = Object.freeze({
842   /**
843    * The current state of the barrier, as a JSON-serializable object
844    * designed for error-reporting.
845    */
846   get state() {
847     if (!this._isStarted) {
848       return "Not started";
849     }
850     if (!this._waitForMe) {
851       return "Complete";
852     }
853     let frozen = [];
854     for (let blocker of this._promiseToBlocker.values()) {
855       let { name, fetchState } = blocker;
856       let { stack, filename, lineNumber } = blocker.getOrigin();
857       frozen.push({
858         name,
859         state: safeGetState(fetchState),
860         filename,
861         lineNumber,
862         stack,
863       });
864     }
865     return frozen;
866   },
868   /**
869    * Wait until all currently registered blockers are complete.
870    *
871    * Once this method has been called, any attempt to register a new blocker
872    * for this barrier will cause an error.
873    *
874    * Successive calls to this method always return the same value.
875    *
876    * @param {object=}  options Optionally, an object  that may contain
877    * the following fields:
878    *  {number} warnAfterMS If provided and > 0, print a warning if the barrier
879    *   has not been resolved after the given number of milliseconds.
880    *  {number} crashAfterMS If provided and > 0, crash the process if the barrier
881    *   has not been resolved after the give number of milliseconds (rounded up
882    *   to the next second). To avoid crashing simply because the computer is busy
883    *   or going to sleep, we actually wait for ceil(crashAfterMS/1000) successive
884    *   periods of at least one second. Upon crashing, if a crash reporter is present,
885    *   prepare a crash report with the state of this barrier.
886    *
887    *
888    * @return {Promise} A promise satisfied once all blockers are complete.
889    */
890   wait(options = {}) {
891     // This method only implements caching on top of _wait()
892     if (this._promise) {
893       return this._promise;
894     }
895     return (this._promise = this._wait(options));
896   },
897   _wait(options) {
898     // Sanity checks
899     if (this._isStarted) {
900       throw new TypeError("Internal error: already started " + this._name);
901     }
902     if (
903       !this._waitForMe ||
904       !this._conditionToPromise ||
905       !this._promiseToBlocker
906     ) {
907       throw new TypeError("Internal error: already finished " + this._name);
908     }
910     let topic = this._name;
912     // Notify blockers
913     for (let blocker of this._promiseToBlocker.values()) {
914       blocker.trigger(); // We have guarantees that this method will never throw
915     }
917     this._isStarted = true;
919     // Now, wait
920     let promise = this._waitForMe.wait(() => {
921       this._isClosed = true;
922     });
924     promise = promise.catch(function onError(error) {
925       // I don't think that this can happen.
926       // However, let's be overcautious with async/shutdown error reporting.
927       let msg =
928         "An uncaught error appeared while completing the phase." +
929         " Phase: " +
930         topic;
931       warn(msg, error);
932     });
934     promise = promise.then(() => {
935       // Cleanup memory
936       this._waitForMe = null;
937       this._promiseToBlocker = null;
938       this._conditionToPromise = null;
939     });
941     // Now handle warnings and crashes
942     let warnAfterMS = DELAY_WARNING_MS;
943     if (options && "warnAfterMS" in options) {
944       if (
945         typeof options.warnAfterMS == "number" ||
946         options.warnAfterMS == null
947       ) {
948         // Change the delay or deactivate warnAfterMS
949         warnAfterMS = options.warnAfterMS;
950       } else {
951         throw new TypeError("Wrong option value for warnAfterMS");
952       }
953     }
955     if (warnAfterMS && warnAfterMS > 0) {
956       // If the promise takes too long to be resolved/rejected,
957       // we need to notify the user.
958       let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
959       timer.initWithCallback(
960         () => {
961           let msg =
962             "At least one completion condition is taking too long to complete." +
963             " Conditions: " +
964             JSON.stringify(this.state) +
965             " Barrier: " +
966             topic;
967           warn(msg);
968         },
969         warnAfterMS,
970         Ci.nsITimer.TYPE_ONE_SHOT
971       );
973       promise = promise.then(function onSuccess() {
974         timer.cancel();
975         // As a side-effect, this prevents |timer| from
976         // being garbage-collected too early.
977       });
978     }
980     let crashAfterMS = DELAY_CRASH_MS;
981     if (options && "crashAfterMS" in options) {
982       if (
983         typeof options.crashAfterMS == "number" ||
984         options.crashAfterMS == null
985       ) {
986         // Change the delay or deactivate crashAfterMS
987         crashAfterMS = options.crashAfterMS;
988       } else {
989         throw new TypeError("Wrong option value for crashAfterMS");
990       }
991     }
993     if (crashAfterMS > 0) {
994       let timeToCrash = null;
996       // If after |crashAfterMS| milliseconds (adjusted to take into
997       // account sleep and otherwise busy computer) we have not finished
998       // this shutdown phase, we assume that the shutdown is somehow
999       // frozen, presumably deadlocked. At this stage, the only thing we
1000       // can do to avoid leaving the user's computer in an unstable (and
1001       // battery-sucking) situation is report the issue and crash.
1002       timeToCrash = looseTimer(crashAfterMS);
1003       timeToCrash.promise.then(
1004         () => {
1005           // Report the problem as best as we can, then crash.
1006           let state = this.state;
1008           // If you change the following message, please make sure
1009           // that any information on the topic and state appears
1010           // within the first 200 characters of the message. This
1011           // helps automatically sort oranges.
1012           let msg =
1013             "AsyncShutdown timeout in " +
1014             topic +
1015             " Conditions: " +
1016             JSON.stringify(state) +
1017             " At least one completion condition failed to complete" +
1018             " within a reasonable amount of time. Causing a crash to" +
1019             " ensure that we do not leave the user with an unresponsive" +
1020             " process draining resources.";
1021           fatalerr(msg);
1022           if (gBrokenAddBlockers.length) {
1023             fatalerr(
1024               "Broken addBlocker calls: " + JSON.stringify(gBrokenAddBlockers)
1025             );
1026           }
1027           if (Services.appinfo.crashReporterEnabled) {
1028             Services.appinfo.annotateCrashReport(
1029               "AsyncShutdownTimeout",
1030               JSON.stringify(this._gatherCrashReportTimeoutData(topic, state))
1031             );
1032           } else {
1033             warn("No crash reporter available");
1034           }
1036           // To help sorting out bugs, we want to make sure that the
1037           // call to nsIDebug2.abort points to a guilty client, rather
1038           // than to AsyncShutdown itself. We pick a client that is
1039           // still blocking and use its filename/lineNumber,
1040           // which have been determined during the call to `addBlocker`.
1041           let filename = "?";
1042           let lineNumber = -1;
1043           for (let blocker of this._promiseToBlocker.values()) {
1044             ({ filename, lineNumber } = blocker.getOrigin());
1045             break;
1046           }
1047           lazy.gDebug.abort(filename, lineNumber);
1048         },
1049         function onSatisfied() {
1050           // The promise has been rejected, which means that we have satisfied
1051           // all completion conditions.
1052         }
1053       );
1055       promise = promise.then(
1056         function () {
1057           timeToCrash.reject();
1058         } /* No error is possible here*/
1059       );
1060     }
1062     return promise;
1063   },
1065   _gatherCrashReportTimeoutData(phase, conditions) {
1066     let data = { phase, conditions };
1067     if (gBrokenAddBlockers.length) {
1068       data.brokenAddBlockers = gBrokenAddBlockers;
1069     }
1070     return data;
1071   },
1073   _removeBlocker(condition) {
1074     if (
1075       !this._waitForMe ||
1076       !this._promiseToBlocker ||
1077       !this._conditionToPromise
1078     ) {
1079       // We have already cleaned up everything.
1080       return false;
1081     }
1083     let promise = this._conditionToPromise.get(condition);
1084     if (!promise) {
1085       // The blocker has already been removed
1086       return false;
1087     }
1088     this._conditionToPromise.delete(condition);
1089     this._promiseToBlocker.delete(promise);
1090     return this._waitForMe.delete(promise);
1091   },
1094 // List of well-known phases
1095 // Ideally, phases should be registered from the component that decides
1096 // when they start/stop. For compatibility with existing startup/shutdown
1097 // mechanisms, we register a few phases here.
1099 // Parent process
1100 if (!isContent) {
1101   AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
1102   AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
1103   AsyncShutdown.sendTelemetry = getPhase("profile-before-change-telemetry");
1106 // Notifications that fire in the parent and content process, but should
1107 // only have phases in the parent process.
1108 if (!isContent) {
1109   AsyncShutdown.quitApplicationGranted = getPhase("quit-application-granted");
1112 // Don't add a barrier for content-child-shutdown because this
1113 // makes it easier to cause shutdown hangs.
1115 // All processes
1116 AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
1117 AsyncShutdown.xpcomWillShutdown = getPhase("xpcom-will-shutdown");
1119 AsyncShutdown.Barrier = Barrier;
1121 Object.freeze(AsyncShutdown);