Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / sessionstore / StartupPerformance.sys.mjs
blobf0c1ca0da993caae79ac568821d4d7a9f457db10
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 const lazy = {};
6 ChromeUtils.defineESModuleGetters(lazy, {
7   clearTimeout: "resource://gre/modules/Timer.sys.mjs",
8   setTimeout: "resource://gre/modules/Timer.sys.mjs",
9 });
11 const COLLECT_RESULTS_AFTER_MS = 10000;
13 const OBSERVED_TOPICS = [
14   "sessionstore-restoring-on-startup",
15   "sessionstore-initiating-manual-restore",
18 export var StartupPerformance = {
19   /**
20    * Once we have finished restoring initial tabs, we broadcast on this topic.
21    */
22   RESTORED_TOPIC: "sessionstore-finished-restoring-initial-tabs",
24   // Instant at which we have started restoration (notification "sessionstore-restoring-on-startup")
25   _startTimeStamp: null,
27   // Latest instant at which we have finished restoring a tab (DOM event "SSTabRestored")
28   _latestRestoredTimeStamp: null,
30   // A promise resolved once we have finished restoring all the startup tabs.
31   _promiseFinished: null,
33   // Function `resolve()` for `_promiseFinished`.
34   _resolveFinished: null,
36   // A timer
37   _deadlineTimer: null,
39   // `true` once the timer has fired
40   _hasFired: false,
42   // `true` once we are restored
43   _isRestored: false,
45   // Statistics on the session we need to restore.
46   _totalNumberOfEagerTabs: 0,
47   _totalNumberOfTabs: 0,
48   _totalNumberOfWindows: 0,
50   init() {
51     for (let topic of OBSERVED_TOPICS) {
52       Services.obs.addObserver(this, topic);
53     }
54   },
56   /**
57    * Return the timestamp at which we finished restoring the latest tab.
58    *
59    * This information is not really interesting until we have finished restoring
60    * tabs.
61    */
62   get latestRestoredTimeStamp() {
63     return this._latestRestoredTimeStamp;
64   },
66   /**
67    * `true` once we have finished restoring startup tabs.
68    */
69   get isRestored() {
70     return this._isRestored;
71   },
73   // Called when restoration starts.
74   // Record the start timestamp, setup the timer and `this._promiseFinished`.
75   // Behavior is unspecified if there was already an ongoing measure.
76   _onRestorationStarts(isAutoRestore) {
77     ChromeUtils.addProfilerMarker("_onRestorationStarts");
78     this._latestRestoredTimeStamp = this._startTimeStamp = Date.now();
79     this._totalNumberOfEagerTabs = 0;
80     this._totalNumberOfTabs = 0;
81     this._totalNumberOfWindows = 0;
83     // While we may restore several sessions in a single run of the browser,
84     // that's a very unusual case, and not really worth measuring, so let's
85     // stop listening for further restorations.
87     for (let topic of OBSERVED_TOPICS) {
88       Services.obs.removeObserver(this, topic);
89     }
91     Services.obs.addObserver(this, "sessionstore-single-window-restored");
92     this._promiseFinished = new Promise(resolve => {
93       this._resolveFinished = resolve;
94     });
95     this._promiseFinished.then(() => {
96       try {
97         this._isRestored = true;
98         Services.obs.notifyObservers(null, this.RESTORED_TOPIC);
100         if (this._latestRestoredTimeStamp == this._startTimeStamp) {
101           // Apparently, we haven't restored any tab.
102           return;
103         }
105         // Once we are done restoring tabs, update Telemetry.
106         let delta = this._latestRestoredTimeStamp - this._startTimeStamp;
107         if (isAutoRestore) {
108           Services.telemetry
109             .getHistogramById(
110               "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS"
111             )
112             .add(delta);
113         } else {
114           Services.telemetry
115             .getHistogramById(
116               "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS"
117             )
118             .add(delta);
119         }
120         Services.telemetry
121           .getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED")
122           .add(this._totalNumberOfEagerTabs);
123         Services.telemetry
124           .getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED")
125           .add(this._totalNumberOfTabs);
126         Services.telemetry
127           .getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED")
128           .add(this._totalNumberOfWindows);
130         // Reset
131         this._startTimeStamp = null;
132       } catch (ex) {
133         console.error("StartupPerformance: error after resolving promise", ex);
134       }
135     });
136   },
138   _startTimer() {
139     if (this._hasFired) {
140       return;
141     }
142     if (this._deadlineTimer) {
143       lazy.clearTimeout(this._deadlineTimer);
144     }
145     this._deadlineTimer = lazy.setTimeout(() => {
146       try {
147         this._resolveFinished();
148       } catch (ex) {
149         console.error("StartupPerformance: Error in timeout handler", ex);
150       } finally {
151         // Clean up.
152         this._deadlineTimer = null;
153         this._hasFired = true;
154         this._resolveFinished = null;
155         Services.obs.removeObserver(
156           this,
157           "sessionstore-single-window-restored"
158         );
159       }
160     }, COLLECT_RESULTS_AFTER_MS);
161   },
163   observe(subject, topic) {
164     try {
165       switch (topic) {
166         case "sessionstore-restoring-on-startup":
167           this._onRestorationStarts(true);
168           break;
169         case "sessionstore-initiating-manual-restore":
170           this._onRestorationStarts(false);
171           break;
172         case "sessionstore-single-window-restored":
173           {
174             // Session Restore has just opened a window with (initially empty) tabs.
175             // Some of these tabs will be restored eagerly, while others will be
176             // restored on demand. The process becomes usable only when all windows
177             // have finished restored their eager tabs.
178             //
179             // While it would be possible to track the restoration of each tab
180             // from within SessionRestore to determine exactly when the process
181             // becomes usable, experience shows that this is too invasive. Rather,
182             // we employ the following heuristic:
183             // - we maintain a timer of `COLLECT_RESULTS_AFTER_MS` that we expect
184             //   will be triggered only once all tabs have been restored;
185             // - whenever we restore a new window (hence a bunch of eager tabs),
186             //   we postpone the timer to ensure that the new eager tabs have
187             //   `COLLECT_RESULTS_AFTER_MS` to be restored;
188             // - whenever a tab is restored, we update
189             //   `this._latestRestoredTimeStamp`;
190             // - after `COLLECT_RESULTS_AFTER_MS`, we collect the final version
191             //   of `this._latestRestoredTimeStamp`, and use it to determine the
192             //   entire duration of the collection.
193             //
194             // Note that this heuristic may be inaccurate if a user clicks
195             // immediately on a restore-on-demand tab before the end of
196             // `COLLECT_RESULTS_AFTER_MS`. We assume that this will not
197             // affect too much the results.
198             //
199             // Reset the delay, to give the tabs a little (more) time to restore.
200             this._startTimer();
202             this._totalNumberOfWindows += 1;
204             // Observe the restoration of all tabs. We assume that all tabs of this
205             // window will have been restored before `COLLECT_RESULTS_AFTER_MS`.
206             // The last call to `observer` will let us determine how long it took
207             // to reach that point.
208             let win = subject;
210             let observer = event => {
211               // We don't care about tab restorations that are due to
212               // a browser flipping from out-of-main-process to in-main-process
213               // or vice-versa. We only care about restorations that are due
214               // to the user switching to a lazily restored tab, or for tabs
215               // that are restoring eagerly.
216               if (!event.detail.isRemotenessUpdate) {
217                 ChromeUtils.addProfilerMarker("SSTabRestored");
218                 this._latestRestoredTimeStamp = Date.now();
219                 this._totalNumberOfEagerTabs += 1;
220               }
221             };
222             win.gBrowser.tabContainer.addEventListener(
223               "SSTabRestored",
224               observer
225             );
226             this._totalNumberOfTabs += win.gBrowser.tabContainer.itemCount;
228             // Once we have finished collecting the results, clean up the observers.
229             this._promiseFinished.then(() => {
230               if (!win.gBrowser.tabContainer) {
231                 // May be undefined during shutdown and/or some tests.
232                 return;
233               }
234               win.gBrowser.tabContainer.removeEventListener(
235                 "SSTabRestored",
236                 observer
237               );
238             });
239           }
240           break;
241         default:
242           throw new Error(`Unexpected topic ${topic}`);
243       }
244     } catch (ex) {
245       console.error("StartupPerformance error", ex, ex.stack);
246       throw ex;
247     }
248   },