services/datareporting/sessions.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 #ifndef MERGED_COMPARTMENT
michael@0 8
michael@0 9 this.EXPORTED_SYMBOLS = [
michael@0 10 "SessionRecorder",
michael@0 11 ];
michael@0 12
michael@0 13 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
michael@0 14
michael@0 15 #endif
michael@0 16
michael@0 17 Cu.import("resource://gre/modules/Preferences.jsm");
michael@0 18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 19 Cu.import("resource://gre/modules/Log.jsm");
michael@0 20 Cu.import("resource://services-common/utils.js");
michael@0 21
michael@0 22
michael@0 23 // We automatically prune sessions older than this.
michael@0 24 const MAX_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days.
michael@0 25 const STARTUP_RETRY_INTERVAL_MS = 5000;
michael@0 26
michael@0 27 // Wait up to 5 minutes for startup measurements before giving up.
michael@0 28 const MAX_STARTUP_TRIES = 300000 / STARTUP_RETRY_INTERVAL_MS;
michael@0 29
michael@0 30 /**
michael@0 31 * Records information about browser sessions.
michael@0 32 *
michael@0 33 * This serves as an interface to both current session information as
michael@0 34 * well as a history of previous sessions.
michael@0 35 *
michael@0 36 * Typically only one instance of this will be installed in an
michael@0 37 * application. It is typically managed by an XPCOM service. The
michael@0 38 * instance is instantiated at application start; onStartup is called
michael@0 39 * once the profile is installed; onShutdown is called during shutdown.
michael@0 40 *
michael@0 41 * We currently record state in preferences. However, this should be
michael@0 42 * invisible to external consumers. We could easily swap in a different
michael@0 43 * storage mechanism if desired.
michael@0 44 *
michael@0 45 * Please note the different semantics for storing times and dates in
michael@0 46 * preferences. Full dates (notably the session start time) are stored
michael@0 47 * as strings because preferences have a 32-bit limit on integer values
michael@0 48 * and milliseconds since UNIX epoch would overflow. Many times are
michael@0 49 * stored as integer offsets from the session start time because they
michael@0 50 * should not overflow 32 bits.
michael@0 51 *
michael@0 52 * Since this records history of all sessions, there is a possibility
michael@0 53 * for unbounded data aggregation. This is curtailed through:
michael@0 54 *
michael@0 55 * 1) An "idle-daily" observer which delete sessions older than
michael@0 56 * MAX_SESSION_AGE_MS.
michael@0 57 * 2) The creator of this instance explicitly calling
michael@0 58 * `pruneOldSessions`.
michael@0 59 *
michael@0 60 * @param branch
michael@0 61 * (string) Preferences branch on which to record state.
michael@0 62 */
michael@0 63 this.SessionRecorder = function (branch) {
michael@0 64 if (!branch) {
michael@0 65 throw new Error("branch argument must be defined.");
michael@0 66 }
michael@0 67
michael@0 68 if (!branch.endsWith(".")) {
michael@0 69 throw new Error("branch argument must end with '.': " + branch);
michael@0 70 }
michael@0 71
michael@0 72 this._log = Log.repository.getLogger("Services.DataReporting.SessionRecorder");
michael@0 73
michael@0 74 this._prefs = new Preferences(branch);
michael@0 75 this._lastActivityWasInactive = false;
michael@0 76 this._activeTicks = 0;
michael@0 77 this.fineTotalTime = 0;
michael@0 78 this._started = false;
michael@0 79 this._timer = null;
michael@0 80 this._startupFieldTries = 0;
michael@0 81
michael@0 82 this._os = Cc["@mozilla.org/observer-service;1"]
michael@0 83 .getService(Ci.nsIObserverService);
michael@0 84
michael@0 85 };
michael@0 86
michael@0 87 SessionRecorder.prototype = Object.freeze({
michael@0 88 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
michael@0 89
michael@0 90 STARTUP_RETRY_INTERVAL_MS: STARTUP_RETRY_INTERVAL_MS,
michael@0 91
michael@0 92 get _currentIndex() {
michael@0 93 return this._prefs.get("currentIndex", 0);
michael@0 94 },
michael@0 95
michael@0 96 set _currentIndex(value) {
michael@0 97 this._prefs.set("currentIndex", value);
michael@0 98 },
michael@0 99
michael@0 100 get _prunedIndex() {
michael@0 101 return this._prefs.get("prunedIndex", 0);
michael@0 102 },
michael@0 103
michael@0 104 set _prunedIndex(value) {
michael@0 105 this._prefs.set("prunedIndex", value);
michael@0 106 },
michael@0 107
michael@0 108 get startDate() {
michael@0 109 return CommonUtils.getDatePref(this._prefs, "current.startTime");
michael@0 110 },
michael@0 111
michael@0 112 set _startDate(value) {
michael@0 113 CommonUtils.setDatePref(this._prefs, "current.startTime", value);
michael@0 114 },
michael@0 115
michael@0 116 get activeTicks() {
michael@0 117 return this._prefs.get("current.activeTicks", 0);
michael@0 118 },
michael@0 119
michael@0 120 incrementActiveTicks: function () {
michael@0 121 this._prefs.set("current.activeTicks", ++this._activeTicks);
michael@0 122 },
michael@0 123
michael@0 124 /**
michael@0 125 * Total time of this session in integer seconds.
michael@0 126 *
michael@0 127 * See also fineTotalTime for the time in milliseconds.
michael@0 128 */
michael@0 129 get totalTime() {
michael@0 130 return this._prefs.get("current.totalTime", 0);
michael@0 131 },
michael@0 132
michael@0 133 updateTotalTime: function () {
michael@0 134 // We store millisecond precision internally to prevent drift from
michael@0 135 // repeated rounding.
michael@0 136 this.fineTotalTime = Date.now() - this.startDate;
michael@0 137 this._prefs.set("current.totalTime", Math.floor(this.fineTotalTime / 1000));
michael@0 138 },
michael@0 139
michael@0 140 get main() {
michael@0 141 return this._prefs.get("current.main", -1);
michael@0 142 },
michael@0 143
michael@0 144 set _main(value) {
michael@0 145 if (!Number.isInteger(value)) {
michael@0 146 throw new Error("main time must be an integer.");
michael@0 147 }
michael@0 148
michael@0 149 this._prefs.set("current.main", value);
michael@0 150 },
michael@0 151
michael@0 152 get firstPaint() {
michael@0 153 return this._prefs.get("current.firstPaint", -1);
michael@0 154 },
michael@0 155
michael@0 156 set _firstPaint(value) {
michael@0 157 if (!Number.isInteger(value)) {
michael@0 158 throw new Error("firstPaint must be an integer.");
michael@0 159 }
michael@0 160
michael@0 161 this._prefs.set("current.firstPaint", value);
michael@0 162 },
michael@0 163
michael@0 164 get sessionRestored() {
michael@0 165 return this._prefs.get("current.sessionRestored", -1);
michael@0 166 },
michael@0 167
michael@0 168 set _sessionRestored(value) {
michael@0 169 if (!Number.isInteger(value)) {
michael@0 170 throw new Error("sessionRestored must be an integer.");
michael@0 171 }
michael@0 172
michael@0 173 this._prefs.set("current.sessionRestored", value);
michael@0 174 },
michael@0 175
michael@0 176 getPreviousSessions: function () {
michael@0 177 let result = {};
michael@0 178
michael@0 179 for (let i = this._prunedIndex; i < this._currentIndex; i++) {
michael@0 180 let s = this.getPreviousSession(i);
michael@0 181 if (!s) {
michael@0 182 continue;
michael@0 183 }
michael@0 184
michael@0 185 result[i] = s;
michael@0 186 }
michael@0 187
michael@0 188 return result;
michael@0 189 },
michael@0 190
michael@0 191 getPreviousSession: function (index) {
michael@0 192 return this._deserialize(this._prefs.get("previous." + index));
michael@0 193 },
michael@0 194
michael@0 195 /**
michael@0 196 * Prunes old, completed sessions that started earlier than the
michael@0 197 * specified date.
michael@0 198 */
michael@0 199 pruneOldSessions: function (date) {
michael@0 200 for (let i = this._prunedIndex; i < this._currentIndex; i++) {
michael@0 201 let s = this.getPreviousSession(i);
michael@0 202 if (!s) {
michael@0 203 continue;
michael@0 204 }
michael@0 205
michael@0 206 if (s.startDate >= date) {
michael@0 207 continue;
michael@0 208 }
michael@0 209
michael@0 210 this._log.debug("Pruning session #" + i + ".");
michael@0 211 this._prefs.reset("previous." + i);
michael@0 212 this._prunedIndex = i;
michael@0 213 }
michael@0 214 },
michael@0 215
michael@0 216 recordStartupFields: function () {
michael@0 217 let si = this._getStartupInfo();
michael@0 218
michael@0 219 if (!si.process) {
michael@0 220 throw new Error("Startup info not available.");
michael@0 221 }
michael@0 222
michael@0 223 let missing = false;
michael@0 224
michael@0 225 for (let field of ["main", "firstPaint", "sessionRestored"]) {
michael@0 226 if (!(field in si)) {
michael@0 227 this._log.debug("Missing startup field: " + field);
michael@0 228 missing = true;
michael@0 229 continue;
michael@0 230 }
michael@0 231
michael@0 232 this["_" + field] = si[field].getTime() - si.process.getTime();
michael@0 233 }
michael@0 234
michael@0 235 if (!missing || this._startupFieldTries > MAX_STARTUP_TRIES) {
michael@0 236 this._clearStartupTimer();
michael@0 237 return;
michael@0 238 }
michael@0 239
michael@0 240 // If we have missing fields, install a timer and keep waiting for
michael@0 241 // data.
michael@0 242 this._startupFieldTries++;
michael@0 243
michael@0 244 if (!this._timer) {
michael@0 245 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 246 this._timer.initWithCallback({
michael@0 247 notify: this.recordStartupFields.bind(this),
michael@0 248 }, this.STARTUP_RETRY_INTERVAL_MS, this._timer.TYPE_REPEATING_SLACK);
michael@0 249 }
michael@0 250 },
michael@0 251
michael@0 252 _clearStartupTimer: function () {
michael@0 253 if (this._timer) {
michael@0 254 this._timer.cancel();
michael@0 255 delete this._timer;
michael@0 256 }
michael@0 257 },
michael@0 258
michael@0 259 /**
michael@0 260 * Perform functionality on application startup.
michael@0 261 *
michael@0 262 * This is typically called in a "profile-do-change" handler.
michael@0 263 */
michael@0 264 onStartup: function () {
michael@0 265 if (this._started) {
michael@0 266 throw new Error("onStartup has already been called.");
michael@0 267 }
michael@0 268
michael@0 269 let si = this._getStartupInfo();
michael@0 270 if (!si.process) {
michael@0 271 throw new Error("Process information not available. Misconfigured app?");
michael@0 272 }
michael@0 273
michael@0 274 this._started = true;
michael@0 275
michael@0 276 this._os.addObserver(this, "profile-before-change", false);
michael@0 277 this._os.addObserver(this, "user-interaction-active", false);
michael@0 278 this._os.addObserver(this, "user-interaction-inactive", false);
michael@0 279 this._os.addObserver(this, "idle-daily", false);
michael@0 280
michael@0 281 // This has the side-effect of clearing current session state.
michael@0 282 this._moveCurrentToPrevious();
michael@0 283
michael@0 284 this._startDate = si.process;
michael@0 285 this._prefs.set("current.activeTicks", 0);
michael@0 286 this.updateTotalTime();
michael@0 287
michael@0 288 this.recordStartupFields();
michael@0 289 },
michael@0 290
michael@0 291 /**
michael@0 292 * Record application activity.
michael@0 293 */
michael@0 294 onActivity: function (active) {
michael@0 295 let updateActive = active && !this._lastActivityWasInactive;
michael@0 296 this._lastActivityWasInactive = !active;
michael@0 297
michael@0 298 this.updateTotalTime();
michael@0 299
michael@0 300 if (updateActive) {
michael@0 301 this.incrementActiveTicks();
michael@0 302 }
michael@0 303 },
michael@0 304
michael@0 305 onShutdown: function () {
michael@0 306 this._log.info("Recording clean session shutdown.");
michael@0 307 this._prefs.set("current.clean", true);
michael@0 308 this.updateTotalTime();
michael@0 309 this._clearStartupTimer();
michael@0 310
michael@0 311 this._os.removeObserver(this, "profile-before-change");
michael@0 312 this._os.removeObserver(this, "user-interaction-active");
michael@0 313 this._os.removeObserver(this, "user-interaction-inactive");
michael@0 314 this._os.removeObserver(this, "idle-daily");
michael@0 315 },
michael@0 316
michael@0 317 _CURRENT_PREFS: [
michael@0 318 "current.startTime",
michael@0 319 "current.activeTicks",
michael@0 320 "current.totalTime",
michael@0 321 "current.main",
michael@0 322 "current.firstPaint",
michael@0 323 "current.sessionRestored",
michael@0 324 "current.clean",
michael@0 325 ],
michael@0 326
michael@0 327 // This is meant to be called only during onStartup().
michael@0 328 _moveCurrentToPrevious: function () {
michael@0 329 try {
michael@0 330 if (!this.startDate.getTime()) {
michael@0 331 this._log.info("No previous session. Is this first app run?");
michael@0 332 return;
michael@0 333 }
michael@0 334
michael@0 335 let clean = this._prefs.get("current.clean", false);
michael@0 336
michael@0 337 let count = this._currentIndex++;
michael@0 338 let obj = {
michael@0 339 s: this.startDate.getTime(),
michael@0 340 a: this.activeTicks,
michael@0 341 t: this.totalTime,
michael@0 342 c: clean,
michael@0 343 m: this.main,
michael@0 344 fp: this.firstPaint,
michael@0 345 sr: this.sessionRestored,
michael@0 346 };
michael@0 347
michael@0 348 this._log.debug("Recording last sessions as #" + count + ".");
michael@0 349 this._prefs.set("previous." + count, JSON.stringify(obj));
michael@0 350 } catch (ex) {
michael@0 351 this._log.warn("Exception when migrating last session: " +
michael@0 352 CommonUtils.exceptionStr(ex));
michael@0 353 } finally {
michael@0 354 this._log.debug("Resetting prefs from last session.");
michael@0 355 for (let pref of this._CURRENT_PREFS) {
michael@0 356 this._prefs.reset(pref);
michael@0 357 }
michael@0 358 }
michael@0 359 },
michael@0 360
michael@0 361 _deserialize: function (s) {
michael@0 362 let o;
michael@0 363 try {
michael@0 364 o = JSON.parse(s);
michael@0 365 } catch (ex) {
michael@0 366 return null;
michael@0 367 }
michael@0 368
michael@0 369 return {
michael@0 370 startDate: new Date(o.s),
michael@0 371 activeTicks: o.a,
michael@0 372 totalTime: o.t,
michael@0 373 clean: !!o.c,
michael@0 374 main: o.m,
michael@0 375 firstPaint: o.fp,
michael@0 376 sessionRestored: o.sr,
michael@0 377 };
michael@0 378 },
michael@0 379
michael@0 380 // Implemented as a function to allow for monkeypatching in tests.
michael@0 381 _getStartupInfo: function () {
michael@0 382 return Cc["@mozilla.org/toolkit/app-startup;1"]
michael@0 383 .getService(Ci.nsIAppStartup)
michael@0 384 .getStartupInfo();
michael@0 385 },
michael@0 386
michael@0 387 observe: function (subject, topic, data) {
michael@0 388 switch (topic) {
michael@0 389 case "profile-before-change":
michael@0 390 this.onShutdown();
michael@0 391 break;
michael@0 392
michael@0 393 case "user-interaction-active":
michael@0 394 this.onActivity(true);
michael@0 395 break;
michael@0 396
michael@0 397 case "user-interaction-inactive":
michael@0 398 this.onActivity(false);
michael@0 399 break;
michael@0 400
michael@0 401 case "idle-daily":
michael@0 402 this.pruneOldSessions(new Date(Date.now() - MAX_SESSION_AGE_MS));
michael@0 403 break;
michael@0 404 }
michael@0 405 },
michael@0 406 });

mercurial