SessionSaver.jsm 12.5 KB
Newer Older
1
2
3
4
5
6
/* 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";

7
var EXPORTED_SYMBOLS = ["SessionSaver"];
8

9
10
11
ChromeUtils.import("resource://gre/modules/Timer.jsm", this);
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
12

13
14
XPCOMUtils.defineLazyModuleGetters(this, {
  AppConstants: "resource://gre/modules/AppConstants.jsm",
15
  PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
16
17
18
19
20
  RunState: "resource:///modules/sessionstore/RunState.jsm",
  SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
  SessionFile: "resource:///modules/sessionstore/SessionFile.jsm",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
});
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
 * Minimal interval between two save operations (in milliseconds).
 *
 * To save system resources, we generally do not save changes immediately when
 * a change is detected. Rather, we wait a little to see if this change is
 * followed by other changes, in which case only the last write is necessary.
 * This delay is defined by "browser.sessionstore.interval".
 *
 * Furthermore, when the user is not actively using the computer, webpages
 * may still perform changes that require (re)writing to sessionstore, e.g.
 * updating Session Cookies or DOM Session Storage, or refreshing, etc. We
 * expect that these changes are much less critical to the user and do not
 * need to be saved as often. In such cases, we increase the delay to
 *  "browser.sessionstore.interval.idle".
 *
 * When the user returns to the computer, if a save is pending, we reschedule
 * it to happen soon, with "browser.sessionstore.interval".
 */
const PREF_INTERVAL_ACTIVE = "browser.sessionstore.interval";
const PREF_INTERVAL_IDLE = "browser.sessionstore.interval.idle";
const PREF_IDLE_DELAY = "browser.sessionstore.idleDelay";
43
44
45

// Notify observers about a given topic with a given subject.
function notify(subject, topic) {
46
  Services.obs.notifyObservers(subject, topic);
47
48
49
50
}

// TelemetryStopwatch helper functions.
function stopWatch(method) {
51
  return function(...histograms) {
52
53
54
55
56
57
    for (let hist of histograms) {
      TelemetryStopwatch[method]("FX_SESSION_RESTORE_" + hist);
    }
  };
}

58
59
var stopWatchStart = stopWatch("start");
var stopWatchFinish = stopWatch("finish");
60
61
62
63

/**
 * The external API implemented by the SessionSaver module.
 */
64
var SessionSaver = Object.freeze({
65
66
67
  /**
   * Immediately saves the current session to disk.
   */
68
  run() {
69
    return SessionSaverInternal.run();
70
71
72
73
74
75
76
  },

  /**
   * Saves the current session to disk delayed by a given amount of time. Should
   * another delayed run be scheduled already, we will ignore the given delay
   * and state saving may occur a little earlier.
   */
77
  runDelayed() {
78
79
80
81
82
83
84
    SessionSaverInternal.runDelayed();
  },

  /**
   * Sets the last save time to the current time. This will cause us to wait for
   * at least the configured interval when runDelayed() is called next.
   */
85
  updateLastSaveTime() {
86
87
88
89
90
91
    SessionSaverInternal.updateLastSaveTime();
  },

  /**
   * Cancels all pending session saves.
   */
92
  cancel() {
93
    SessionSaverInternal.cancel();
94
  },
95
96
97
98
99
});

/**
 * The internal API.
 */
100
var SessionSaverInternal = {
101
102
103
104
105
106
  /**
   * The timeout ID referencing an active timer for a delayed save. When no
   * save is pending, this is null.
   */
  _timeoutID: null,

107
108
109
110
111
112
  /**
   * The idle callback ID referencing an active idle callback. When no idle
   * callback is pending, this is null.
   * */
  _idleCallbackID: null,

113
114
115
116
117
118
119
  /**
   * A timestamp that keeps track of when we saved the session last. We will
   * this to determine the correct interval between delayed saves to not deceed
   * the configured session write interval.
   */
  _lastSaveTime: 0,

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
  /**
   * `true` if the user has been idle for at least
   * `SessionSaverInternal._intervalWhileIdle` ms. Idleness is computed
   * with `nsIIdleService`.
   */
  _isIdle: false,

  /**
   * `true` if the user was idle when we last scheduled a delayed save.
   * See `_isIdle` for details on idleness.
   */
  _wasIdle: false,

  /**
   * Minimal interval between two save operations (in ms), while the user
   * is active.
   */
  _intervalWhileActive: null,

  /**
   * Minimal interval between two save operations (in ms), while the user
   * is idle.
   */
  _intervalWhileIdle: null,

  /**
   * How long before we assume that the user is idle (ms).
   */
  _idleDelay: null,

150
151
152
  /**
   * Immediately saves the current session to disk.
   */
153
  run() {
154
    return this._saveState(true /* force-update all windows */);
155
156
157
158
159
160
161
162
163
164
165
  },

  /**
   * Saves the current session to disk delayed by a given amount of time. Should
   * another delayed run be scheduled already, we will ignore the given delay
   * and state saving may occur a little earlier.
   *
   * @param delay (optional)
   *        The minimum delay in milliseconds to wait for until we collect and
   *        save the current session.
   */
166
  runDelayed(delay = 2000) {
167
168
169
170
171
172
    // Bail out if there's a pending run.
    if (this._timeoutID) {
      return;
    }

    // Interval until the next disk operation is allowed.
173
174
    let interval = this._isIdle ? this._intervalWhileIdle : this._intervalWhileActive;
    delay = Math.max(this._lastSaveTime + interval - Date.now(), delay, 0);
175
176

    // Schedule a state save.
177
    this._wasIdle = this._isIdle;
178
179
180
181
182
183
184
185
    this._timeoutID = setTimeout(() => {
      // Execute _saveStateAsync when we have enough idle time. Otherwise,
      // another idle request is made to schedule _saveStateAsync again.
      let saveStateAsyncWhenIdle = (deadline) => {
        // When looking at the telemetry data, the time it takes to execute
        // _saveStateAsync is around 5.9ms (median). Therefore,
        // we'll not execute the function when the idle time is less than 5ms.
        if (deadline.timeRemaining() < 5) {
186
          this._idleCallbackID = requestIdleCallback(saveStateAsyncWhenIdle);
187
188
189
190
191
          return;
        }
        this._saveStateAsync();
      };

192
      this._idleCallbackID = requestIdleCallback(saveStateAsyncWhenIdle);
193
    }, delay);
194
195
196
197
198
199
  },

  /**
   * Sets the last save time to the current time. This will cause us to wait for
   * at least the configured interval when runDelayed() is called next.
   */
200
  updateLastSaveTime() {
201
202
203
204
205
206
    this._lastSaveTime = Date.now();
  },

  /**
   * Cancels all pending session saves.
   */
207
  cancel() {
208
209
    clearTimeout(this._timeoutID);
    this._timeoutID = null;
210
    cancelIdleCallback(this._idleCallbackID);
211
    this._idleCallbackID = null;
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
  /**
   * Observe idle/ active notifications.
   */
  observe(subject, topic, data) {
    switch (topic) {
      case "idle":
        this._isIdle = true;
        break;
      case "active":
        this._isIdle = false;
        if (this._timeoutID && this._wasIdle) {
          // A state save has been scheduled while we were idle.
          // Replace it by an active save.
          clearTimeout(this._timeoutID);
          this._timeoutID = null;
          this.runDelayed();
        }
        break;
      default:
        throw new Error(`Unexpected change value ${topic}`);
    }
  },

237
238
239
240
241
242
243
  /**
   * Saves the current session state. Collects data and writes to disk.
   *
   * @param forceUpdateAllWindows (optional)
   *        Forces us to recollect data for all windows and will bypass and
   *        update the corresponding caches.
   */
244
  _saveState(forceUpdateAllWindows = false) {
245
    // Cancel any pending timeouts.
246
247
    this.cancel();

248
249
250
251
252
253
254
255
    if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
      // Don't save (or even collect) anything in permanent private
      // browsing mode

      this.updateLastSaveTime();
      return Promise.resolve();
    }

256
    stopWatchStart("COLLECT_DATA_MS");
257
    let state = SessionStore.getCurrentState(forceUpdateAllWindows);
258
    PrivacyFilter.filterPrivateWindowsAndTabs(state);
259

260
261
262
    // Make sure we only write worth saving tabs to disk.
    SessionStore.keepOnlyWorthSavingTabs(state);

263
264
265
266
267
268
269
    // Make sure that we keep the previous session if we started with a single
    // private window and no non-private windows have been opened, yet.
    if (state.deferredInitialState) {
      state.windows = state.deferredInitialState.windows || [];
      delete state.deferredInitialState;
    }

270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
    if (AppConstants.platform != "macosx") {
      // We want to restore closed windows that are marked with _shouldRestore.
      // We're doing this here because we want to control this only when saving
      // the file.
      while (state._closedWindows.length) {
        let i = state._closedWindows.length - 1;

        if (!state._closedWindows[i]._shouldRestore) {
          // We only need to go until _shouldRestore
          // is falsy since we're going in reverse.
          break;
        }

        delete state._closedWindows[i]._shouldRestore;
        state.windows.unshift(state._closedWindows.pop());
285
      }
286
287
    }

288
289
    // Clear cookies and storage on clean shutdown.
    this._maybeClearCookiesAndStorage(state);
290

291
    stopWatchFinish("COLLECT_DATA_MS");
292
    return this._writeState(state);
293
294
  },

295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
  /**
   * Purges cookies and DOMSessionStorage data from the session on clean
   * shutdown, only if requested by the user's preferences.
   */
  _maybeClearCookiesAndStorage(state) {
    // Only do this on shutdown.
    if (!RunState.isClosing) {
      return;
    }

    // Don't clear when restarting.
    if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) {
      return;
    }

    let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
                        Services.cookies.QueryInterface(Ci.nsICookieService).ACCEPT_SESSION;
    let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") &&
                          Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies");

    if (expireCookies || sanitizeCookies) {
      // Remove cookies.
      delete state.cookies;

      // Remove DOMSessionStorage data.
      for (let window of state.windows) {
        for (let tab of window.tabs) {
          delete tab.storage;
        }
      }
    }
  },

328
329
330
331
332
  /**
   * Saves the current session state. Collects data asynchronously and calls
   * _saveState() to collect data again (with a cache hit rate of hopefully
   * 100%) and write to disk afterwards.
   */
333
  _saveStateAsync() {
334
335
336
    // Allow scheduling delayed saves again.
    this._timeoutID = null;

337
338
    // Write to disk.
    this._saveState();
339
340
  },

341
342
343
  /**
   * Write the given state object to disk.
   */
344
  _writeState(state) {
345
346
347
348
349
350
    // We update the time stamp before writing so that we don't write again
    // too soon, if saving is requested before the write completes. Without
    // this update we may save repeatedly if actions cause a runDelayed
    // before writing has completed. See Bug 902280
    this.updateLastSaveTime();

351
352
353
    // Write (atomically) to a session file, using a tmp file. Once the session
    // file is successfully updated, save the time stamp of the last save and
    // notify the observers.
354
    return SessionFile.write(state).then(() => {
355
356
      this.updateLastSaveTime();
      notify(null, "sessionstore-state-write-complete");
357
    }, console.error);
358
359
  },
};
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388

XPCOMUtils.defineLazyPreferenceGetter(SessionSaverInternal, "_intervalWhileActive", PREF_INTERVAL_ACTIVE,
  15000 /* 15 seconds */, () => {
  // Cancel any pending runs and call runDelayed() with
  // zero to apply the newly configured interval.
  SessionSaverInternal.cancel();
  SessionSaverInternal.runDelayed(0);
});

XPCOMUtils.defineLazyPreferenceGetter(SessionSaverInternal, "_intervalWhileIdle", PREF_INTERVAL_IDLE,
  3600000 /* 1 h */);

XPCOMUtils.defineLazyPreferenceGetter(SessionSaverInternal, "_idleDelay", PREF_IDLE_DELAY,
  180000 /* 3 minutes */, (key, previous, latest) => {
  // Update the idle observer for the new `PREF_IDLE_DELAY` value. Here we need
  // to re-fetch the service instead of the original one in use; This is for a
  // case that the Mock service in the unit test needs to be fetched to
  // replace the original one.
  var idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService);
  if (previous != undefined) {
    idleService.removeIdleObserver(SessionSaverInternal, previous);
  }
  if (latest != undefined) {
    idleService.addIdleObserver(SessionSaverInternal, latest);
  }
});

var idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService);
idleService.addIdleObserver(SessionSaverInternal, SessionSaverInternal._idleDelay);