summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/StartupPerformance.sys.mjs
blob: a13333d9d1ee77146c2e97cdc52c9cda2a9a299e (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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/* 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/. */

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

const COLLECT_RESULTS_AFTER_MS = 10000;

const OBSERVED_TOPICS = [
  "sessionstore-restoring-on-startup",
  "sessionstore-initiating-manual-restore",
];

export var StartupPerformance = {
  /**
   * Once we have finished restoring initial tabs, we broadcast on this topic.
   */
  RESTORED_TOPIC: "sessionstore-finished-restoring-initial-tabs",

  // Instant at which we have started restoration (notification "sessionstore-restoring-on-startup")
  _startTimeStamp: null,

  // Latest instant at which we have finished restoring a tab (DOM event "SSTabRestored")
  _latestRestoredTimeStamp: null,

  // A promise resolved once we have finished restoring all the startup tabs.
  _promiseFinished: null,

  // Function `resolve()` for `_promiseFinished`.
  _resolveFinished: null,

  // A timer
  _deadlineTimer: null,

  // `true` once the timer has fired
  _hasFired: false,

  // `true` once we are restored
  _isRestored: false,

  // Statistics on the session we need to restore.
  _totalNumberOfEagerTabs: 0,
  _totalNumberOfTabs: 0,
  _totalNumberOfWindows: 0,

  init() {
    for (let topic of OBSERVED_TOPICS) {
      Services.obs.addObserver(this, topic);
    }
  },

  /**
   * Return the timestamp at which we finished restoring the latest tab.
   *
   * This information is not really interesting until we have finished restoring
   * tabs.
   */
  get latestRestoredTimeStamp() {
    return this._latestRestoredTimeStamp;
  },

  /**
   * `true` once we have finished restoring startup tabs.
   */
  get isRestored() {
    return this._isRestored;
  },

  // Called when restoration starts.
  // Record the start timestamp, setup the timer and `this._promiseFinished`.
  // Behavior is unspecified if there was already an ongoing measure.
  _onRestorationStarts(isAutoRestore) {
    ChromeUtils.addProfilerMarker("_onRestorationStarts");
    this._latestRestoredTimeStamp = this._startTimeStamp = Date.now();
    this._totalNumberOfEagerTabs = 0;
    this._totalNumberOfTabs = 0;
    this._totalNumberOfWindows = 0;

    // While we may restore several sessions in a single run of the browser,
    // that's a very unusual case, and not really worth measuring, so let's
    // stop listening for further restorations.

    for (let topic of OBSERVED_TOPICS) {
      Services.obs.removeObserver(this, topic);
    }

    Services.obs.addObserver(this, "sessionstore-single-window-restored");
    this._promiseFinished = new Promise(resolve => {
      this._resolveFinished = resolve;
    });
    this._promiseFinished.then(() => {
      try {
        this._isRestored = true;
        Services.obs.notifyObservers(null, this.RESTORED_TOPIC);

        if (this._latestRestoredTimeStamp == this._startTimeStamp) {
          // Apparently, we haven't restored any tab.
          return;
        }

        // Once we are done restoring tabs, update Telemetry.
        let histogramName = isAutoRestore
          ? "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS"
          : "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS";
        let histogram = Services.telemetry.getHistogramById(histogramName);
        let delta = this._latestRestoredTimeStamp - this._startTimeStamp;
        histogram.add(delta);

        Services.telemetry
          .getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED")
          .add(this._totalNumberOfEagerTabs);
        Services.telemetry
          .getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED")
          .add(this._totalNumberOfTabs);
        Services.telemetry
          .getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED")
          .add(this._totalNumberOfWindows);

        // Reset
        this._startTimeStamp = null;
      } catch (ex) {
        console.error("StartupPerformance: error after resolving promise", ex);
      }
    });
  },

  _startTimer() {
    if (this._hasFired) {
      return;
    }
    if (this._deadlineTimer) {
      lazy.clearTimeout(this._deadlineTimer);
    }
    this._deadlineTimer = lazy.setTimeout(() => {
      try {
        this._resolveFinished();
      } catch (ex) {
        console.error("StartupPerformance: Error in timeout handler", ex);
      } finally {
        // Clean up.
        this._deadlineTimer = null;
        this._hasFired = true;
        this._resolveFinished = null;
        Services.obs.removeObserver(
          this,
          "sessionstore-single-window-restored"
        );
      }
    }, COLLECT_RESULTS_AFTER_MS);
  },

  observe(subject, topic, details) {
    try {
      switch (topic) {
        case "sessionstore-restoring-on-startup":
          this._onRestorationStarts(true);
          break;
        case "sessionstore-initiating-manual-restore":
          this._onRestorationStarts(false);
          break;
        case "sessionstore-single-window-restored":
          {
            // Session Restore has just opened a window with (initially empty) tabs.
            // Some of these tabs will be restored eagerly, while others will be
            // restored on demand. The process becomes usable only when all windows
            // have finished restored their eager tabs.
            //
            // While it would be possible to track the restoration of each tab
            // from within SessionRestore to determine exactly when the process
            // becomes usable, experience shows that this is too invasive. Rather,
            // we employ the following heuristic:
            // - we maintain a timer of `COLLECT_RESULTS_AFTER_MS` that we expect
            //   will be triggered only once all tabs have been restored;
            // - whenever we restore a new window (hence a bunch of eager tabs),
            //   we postpone the timer to ensure that the new eager tabs have
            //   `COLLECT_RESULTS_AFTER_MS` to be restored;
            // - whenever a tab is restored, we update
            //   `this._latestRestoredTimeStamp`;
            // - after `COLLECT_RESULTS_AFTER_MS`, we collect the final version
            //   of `this._latestRestoredTimeStamp`, and use it to determine the
            //   entire duration of the collection.
            //
            // Note that this heuristic may be inaccurate if a user clicks
            // immediately on a restore-on-demand tab before the end of
            // `COLLECT_RESULTS_AFTER_MS`. We assume that this will not
            // affect too much the results.
            //
            // Reset the delay, to give the tabs a little (more) time to restore.
            this._startTimer();

            this._totalNumberOfWindows += 1;

            // Observe the restoration of all tabs. We assume that all tabs of this
            // window will have been restored before `COLLECT_RESULTS_AFTER_MS`.
            // The last call to `observer` will let us determine how long it took
            // to reach that point.
            let win = subject;

            let observer = event => {
              // We don't care about tab restorations that are due to
              // a browser flipping from out-of-main-process to in-main-process
              // or vice-versa. We only care about restorations that are due
              // to the user switching to a lazily restored tab, or for tabs
              // that are restoring eagerly.
              if (!event.detail.isRemotenessUpdate) {
                ChromeUtils.addProfilerMarker("SSTabRestored");
                this._latestRestoredTimeStamp = Date.now();
                this._totalNumberOfEagerTabs += 1;
              }
            };
            win.gBrowser.tabContainer.addEventListener(
              "SSTabRestored",
              observer
            );
            this._totalNumberOfTabs += win.gBrowser.tabContainer.itemCount;

            // Once we have finished collecting the results, clean up the observers.
            this._promiseFinished.then(() => {
              if (!win.gBrowser.tabContainer) {
                // May be undefined during shutdown and/or some tests.
                return;
              }
              win.gBrowser.tabContainer.removeEventListener(
                "SSTabRestored",
                observer
              );
            });
          }
          break;
        default:
          throw new Error(`Unexpected topic ${topic}`);
      }
    } catch (ex) {
      console.error("StartupPerformance error", ex, ex.stack);
      throw ex;
    }
  },
};