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
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #ifndef MERGED_COMPARTMENT
9 this.EXPORTED_SYMBOLS = [
13 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
17 Cu.import("resource://gre/modules/Preferences.jsm");
18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
19 Cu.import("resource://gre/modules/Log.jsm");
20 Cu.import("resource://services-common/utils.js");
23 // We automatically prune sessions older than this.
24 const MAX_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days.
25 const STARTUP_RETRY_INTERVAL_MS = 5000;
27 // Wait up to 5 minutes for startup measurements before giving up.
28 const MAX_STARTUP_TRIES = 300000 / STARTUP_RETRY_INTERVAL_MS;
31 * Records information about browser sessions.
33 * This serves as an interface to both current session information as
34 * well as a history of previous sessions.
36 * Typically only one instance of this will be installed in an
37 * application. It is typically managed by an XPCOM service. The
38 * instance is instantiated at application start; onStartup is called
39 * once the profile is installed; onShutdown is called during shutdown.
41 * We currently record state in preferences. However, this should be
42 * invisible to external consumers. We could easily swap in a different
43 * storage mechanism if desired.
45 * Please note the different semantics for storing times and dates in
46 * preferences. Full dates (notably the session start time) are stored
47 * as strings because preferences have a 32-bit limit on integer values
48 * and milliseconds since UNIX epoch would overflow. Many times are
49 * stored as integer offsets from the session start time because they
50 * should not overflow 32 bits.
52 * Since this records history of all sessions, there is a possibility
53 * for unbounded data aggregation. This is curtailed through:
55 * 1) An "idle-daily" observer which delete sessions older than
57 * 2) The creator of this instance explicitly calling
61 * (string) Preferences branch on which to record state.
63 this.SessionRecorder = function (branch) {
65 throw new Error("branch argument must be defined.");
68 if (!branch.endsWith(".")) {
69 throw new Error("branch argument must end with '.': " + branch);
72 this._log = Log.repository.getLogger("Services.DataReporting.SessionRecorder");
74 this._prefs = new Preferences(branch);
75 this._lastActivityWasInactive = false;
76 this._activeTicks = 0;
77 this.fineTotalTime = 0;
78 this._started = false;
80 this._startupFieldTries = 0;
82 this._os = Cc["@mozilla.org/observer-service;1"]
83 .getService(Ci.nsIObserverService);
87 SessionRecorder.prototype = Object.freeze({
88 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
90 STARTUP_RETRY_INTERVAL_MS: STARTUP_RETRY_INTERVAL_MS,
93 return this._prefs.get("currentIndex", 0);
96 set _currentIndex(value) {
97 this._prefs.set("currentIndex", value);
101 return this._prefs.get("prunedIndex", 0);
104 set _prunedIndex(value) {
105 this._prefs.set("prunedIndex", value);
109 return CommonUtils.getDatePref(this._prefs, "current.startTime");
112 set _startDate(value) {
113 CommonUtils.setDatePref(this._prefs, "current.startTime", value);
117 return this._prefs.get("current.activeTicks", 0);
120 incrementActiveTicks: function () {
121 this._prefs.set("current.activeTicks", ++this._activeTicks);
125 * Total time of this session in integer seconds.
127 * See also fineTotalTime for the time in milliseconds.
130 return this._prefs.get("current.totalTime", 0);
133 updateTotalTime: function () {
134 // We store millisecond precision internally to prevent drift from
135 // repeated rounding.
136 this.fineTotalTime = Date.now() - this.startDate;
137 this._prefs.set("current.totalTime", Math.floor(this.fineTotalTime / 1000));
141 return this._prefs.get("current.main", -1);
145 if (!Number.isInteger(value)) {
146 throw new Error("main time must be an integer.");
149 this._prefs.set("current.main", value);
153 return this._prefs.get("current.firstPaint", -1);
156 set _firstPaint(value) {
157 if (!Number.isInteger(value)) {
158 throw new Error("firstPaint must be an integer.");
161 this._prefs.set("current.firstPaint", value);
164 get sessionRestored() {
165 return this._prefs.get("current.sessionRestored", -1);
168 set _sessionRestored(value) {
169 if (!Number.isInteger(value)) {
170 throw new Error("sessionRestored must be an integer.");
173 this._prefs.set("current.sessionRestored", value);
176 getPreviousSessions: function () {
179 for (let i = this._prunedIndex; i < this._currentIndex; i++) {
180 let s = this.getPreviousSession(i);
191 getPreviousSession: function (index) {
192 return this._deserialize(this._prefs.get("previous." + index));
196 * Prunes old, completed sessions that started earlier than the
199 pruneOldSessions: function (date) {
200 for (let i = this._prunedIndex; i < this._currentIndex; i++) {
201 let s = this.getPreviousSession(i);
206 if (s.startDate >= date) {
210 this._log.debug("Pruning session #" + i + ".");
211 this._prefs.reset("previous." + i);
212 this._prunedIndex = i;
216 recordStartupFields: function () {
217 let si = this._getStartupInfo();
220 throw new Error("Startup info not available.");
225 for (let field of ["main", "firstPaint", "sessionRestored"]) {
226 if (!(field in si)) {
227 this._log.debug("Missing startup field: " + field);
232 this["_" + field] = si[field].getTime() - si.process.getTime();
235 if (!missing || this._startupFieldTries > MAX_STARTUP_TRIES) {
236 this._clearStartupTimer();
240 // If we have missing fields, install a timer and keep waiting for
242 this._startupFieldTries++;
245 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
246 this._timer.initWithCallback({
247 notify: this.recordStartupFields.bind(this),
248 }, this.STARTUP_RETRY_INTERVAL_MS, this._timer.TYPE_REPEATING_SLACK);
252 _clearStartupTimer: function () {
254 this._timer.cancel();
260 * Perform functionality on application startup.
262 * This is typically called in a "profile-do-change" handler.
264 onStartup: function () {
266 throw new Error("onStartup has already been called.");
269 let si = this._getStartupInfo();
271 throw new Error("Process information not available. Misconfigured app?");
274 this._started = true;
276 this._os.addObserver(this, "profile-before-change", false);
277 this._os.addObserver(this, "user-interaction-active", false);
278 this._os.addObserver(this, "user-interaction-inactive", false);
279 this._os.addObserver(this, "idle-daily", false);
281 // This has the side-effect of clearing current session state.
282 this._moveCurrentToPrevious();
284 this._startDate = si.process;
285 this._prefs.set("current.activeTicks", 0);
286 this.updateTotalTime();
288 this.recordStartupFields();
292 * Record application activity.
294 onActivity: function (active) {
295 let updateActive = active && !this._lastActivityWasInactive;
296 this._lastActivityWasInactive = !active;
298 this.updateTotalTime();
301 this.incrementActiveTicks();
305 onShutdown: function () {
306 this._log.info("Recording clean session shutdown.");
307 this._prefs.set("current.clean", true);
308 this.updateTotalTime();
309 this._clearStartupTimer();
311 this._os.removeObserver(this, "profile-before-change");
312 this._os.removeObserver(this, "user-interaction-active");
313 this._os.removeObserver(this, "user-interaction-inactive");
314 this._os.removeObserver(this, "idle-daily");
319 "current.activeTicks",
322 "current.firstPaint",
323 "current.sessionRestored",
327 // This is meant to be called only during onStartup().
328 _moveCurrentToPrevious: function () {
330 if (!this.startDate.getTime()) {
331 this._log.info("No previous session. Is this first app run?");
335 let clean = this._prefs.get("current.clean", false);
337 let count = this._currentIndex++;
339 s: this.startDate.getTime(),
345 sr: this.sessionRestored,
348 this._log.debug("Recording last sessions as #" + count + ".");
349 this._prefs.set("previous." + count, JSON.stringify(obj));
351 this._log.warn("Exception when migrating last session: " +
352 CommonUtils.exceptionStr(ex));
354 this._log.debug("Resetting prefs from last session.");
355 for (let pref of this._CURRENT_PREFS) {
356 this._prefs.reset(pref);
361 _deserialize: function (s) {
370 startDate: new Date(o.s),
376 sessionRestored: o.sr,
380 // Implemented as a function to allow for monkeypatching in tests.
381 _getStartupInfo: function () {
382 return Cc["@mozilla.org/toolkit/app-startup;1"]
383 .getService(Ci.nsIAppStartup)
387 observe: function (subject, topic, data) {
389 case "profile-before-change":
393 case "user-interaction-active":
394 this.onActivity(true);
397 case "user-interaction-inactive":
398 this.onActivity(false);
402 this.pruneOldSessions(new Date(Date.now() - MAX_SESSION_AGE_MS));