mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-16 12:17:34 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
403
toolkit/modules/SessionRecorder.jsm
Normal file
403
toolkit/modules/SessionRecorder.jsm
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"SessionRecorder",
|
||||
];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
// We automatically prune sessions older than this.
|
||||
const MAX_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days.
|
||||
const STARTUP_RETRY_INTERVAL_MS = 5000;
|
||||
|
||||
// Wait up to 5 minutes for startup measurements before giving up.
|
||||
const MAX_STARTUP_TRIES = 300000 / STARTUP_RETRY_INTERVAL_MS;
|
||||
|
||||
const LOGGER_NAME = "Toolkit.Telemetry";
|
||||
const LOGGER_PREFIX = "SessionRecorder::";
|
||||
|
||||
/**
|
||||
* Records information about browser sessions.
|
||||
*
|
||||
* This serves as an interface to both current session information as
|
||||
* well as a history of previous sessions.
|
||||
*
|
||||
* Typically only one instance of this will be installed in an
|
||||
* application. It is typically managed by an XPCOM service. The
|
||||
* instance is instantiated at application start; onStartup is called
|
||||
* once the profile is installed; onShutdown is called during shutdown.
|
||||
*
|
||||
* We currently record state in preferences. However, this should be
|
||||
* invisible to external consumers. We could easily swap in a different
|
||||
* storage mechanism if desired.
|
||||
*
|
||||
* Please note the different semantics for storing times and dates in
|
||||
* preferences. Full dates (notably the session start time) are stored
|
||||
* as strings because preferences have a 32-bit limit on integer values
|
||||
* and milliseconds since UNIX epoch would overflow. Many times are
|
||||
* stored as integer offsets from the session start time because they
|
||||
* should not overflow 32 bits.
|
||||
*
|
||||
* Since this records history of all sessions, there is a possibility
|
||||
* for unbounded data aggregation. This is curtailed through:
|
||||
*
|
||||
* 1) An "idle-daily" observer which delete sessions older than
|
||||
* MAX_SESSION_AGE_MS.
|
||||
* 2) The creator of this instance explicitly calling
|
||||
* `pruneOldSessions`.
|
||||
*
|
||||
* @param branch
|
||||
* (string) Preferences branch on which to record state.
|
||||
*/
|
||||
this.SessionRecorder = function (branch) {
|
||||
if (!branch) {
|
||||
throw new Error("branch argument must be defined.");
|
||||
}
|
||||
|
||||
if (!branch.endsWith(".")) {
|
||||
throw new Error("branch argument must end with '.': " + branch);
|
||||
}
|
||||
|
||||
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
|
||||
|
||||
this._prefs = new Preferences(branch);
|
||||
this._lastActivityWasInactive = false;
|
||||
this._activeTicks = 0;
|
||||
this.fineTotalTime = 0;
|
||||
this._started = false;
|
||||
this._timer = null;
|
||||
this._startupFieldTries = 0;
|
||||
|
||||
this._os = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
|
||||
};
|
||||
|
||||
SessionRecorder.prototype = Object.freeze({
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
|
||||
|
||||
STARTUP_RETRY_INTERVAL_MS: STARTUP_RETRY_INTERVAL_MS,
|
||||
|
||||
get _currentIndex() {
|
||||
return this._prefs.get("currentIndex", 0);
|
||||
},
|
||||
|
||||
set _currentIndex(value) {
|
||||
this._prefs.set("currentIndex", value);
|
||||
},
|
||||
|
||||
get _prunedIndex() {
|
||||
return this._prefs.get("prunedIndex", 0);
|
||||
},
|
||||
|
||||
set _prunedIndex(value) {
|
||||
this._prefs.set("prunedIndex", value);
|
||||
},
|
||||
|
||||
get startDate() {
|
||||
return CommonUtils.getDatePref(this._prefs, "current.startTime");
|
||||
},
|
||||
|
||||
set _startDate(value) {
|
||||
CommonUtils.setDatePref(this._prefs, "current.startTime", value);
|
||||
},
|
||||
|
||||
get activeTicks() {
|
||||
return this._prefs.get("current.activeTicks", 0);
|
||||
},
|
||||
|
||||
incrementActiveTicks: function () {
|
||||
this._prefs.set("current.activeTicks", ++this._activeTicks);
|
||||
},
|
||||
|
||||
/**
|
||||
* Total time of this session in integer seconds.
|
||||
*
|
||||
* See also fineTotalTime for the time in milliseconds.
|
||||
*/
|
||||
get totalTime() {
|
||||
return this._prefs.get("current.totalTime", 0);
|
||||
},
|
||||
|
||||
updateTotalTime: function () {
|
||||
// We store millisecond precision internally to prevent drift from
|
||||
// repeated rounding.
|
||||
this.fineTotalTime = Date.now() - this.startDate;
|
||||
this._prefs.set("current.totalTime", Math.floor(this.fineTotalTime / 1000));
|
||||
},
|
||||
|
||||
get main() {
|
||||
return this._prefs.get("current.main", -1);
|
||||
},
|
||||
|
||||
set _main(value) {
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error("main time must be an integer.");
|
||||
}
|
||||
|
||||
this._prefs.set("current.main", value);
|
||||
},
|
||||
|
||||
get firstPaint() {
|
||||
return this._prefs.get("current.firstPaint", -1);
|
||||
},
|
||||
|
||||
set _firstPaint(value) {
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error("firstPaint must be an integer.");
|
||||
}
|
||||
|
||||
this._prefs.set("current.firstPaint", value);
|
||||
},
|
||||
|
||||
get sessionRestored() {
|
||||
return this._prefs.get("current.sessionRestored", -1);
|
||||
},
|
||||
|
||||
set _sessionRestored(value) {
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error("sessionRestored must be an integer.");
|
||||
}
|
||||
|
||||
this._prefs.set("current.sessionRestored", value);
|
||||
},
|
||||
|
||||
getPreviousSessions: function () {
|
||||
let result = {};
|
||||
|
||||
for (let i = this._prunedIndex; i < this._currentIndex; i++) {
|
||||
let s = this.getPreviousSession(i);
|
||||
if (!s) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[i] = s;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
getPreviousSession: function (index) {
|
||||
return this._deserialize(this._prefs.get("previous." + index));
|
||||
},
|
||||
|
||||
/**
|
||||
* Prunes old, completed sessions that started earlier than the
|
||||
* specified date.
|
||||
*/
|
||||
pruneOldSessions: function (date) {
|
||||
for (let i = this._prunedIndex; i < this._currentIndex; i++) {
|
||||
let s = this.getPreviousSession(i);
|
||||
if (!s) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s.startDate >= date) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._log.debug("Pruning session #" + i + ".");
|
||||
this._prefs.reset("previous." + i);
|
||||
this._prunedIndex = i;
|
||||
}
|
||||
},
|
||||
|
||||
recordStartupFields: function () {
|
||||
let si = this._getStartupInfo();
|
||||
|
||||
if (!si.process) {
|
||||
throw new Error("Startup info not available.");
|
||||
}
|
||||
|
||||
let missing = false;
|
||||
|
||||
for (let field of ["main", "firstPaint", "sessionRestored"]) {
|
||||
if (!(field in si)) {
|
||||
this._log.debug("Missing startup field: " + field);
|
||||
missing = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
this["_" + field] = si[field].getTime() - si.process.getTime();
|
||||
}
|
||||
|
||||
if (!missing || this._startupFieldTries > MAX_STARTUP_TRIES) {
|
||||
this._clearStartupTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have missing fields, install a timer and keep waiting for
|
||||
// data.
|
||||
this._startupFieldTries++;
|
||||
|
||||
if (!this._timer) {
|
||||
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this._timer.initWithCallback({
|
||||
notify: this.recordStartupFields.bind(this),
|
||||
}, this.STARTUP_RETRY_INTERVAL_MS, this._timer.TYPE_REPEATING_SLACK);
|
||||
}
|
||||
},
|
||||
|
||||
_clearStartupTimer: function () {
|
||||
if (this._timer) {
|
||||
this._timer.cancel();
|
||||
delete this._timer;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform functionality on application startup.
|
||||
*
|
||||
* This is typically called in a "profile-do-change" handler.
|
||||
*/
|
||||
onStartup: function () {
|
||||
if (this._started) {
|
||||
throw new Error("onStartup has already been called.");
|
||||
}
|
||||
|
||||
let si = this._getStartupInfo();
|
||||
if (!si.process) {
|
||||
throw new Error("Process information not available. Misconfigured app?");
|
||||
}
|
||||
|
||||
this._started = true;
|
||||
|
||||
this._os.addObserver(this, "profile-before-change", false);
|
||||
this._os.addObserver(this, "user-interaction-active", false);
|
||||
this._os.addObserver(this, "user-interaction-inactive", false);
|
||||
this._os.addObserver(this, "idle-daily", false);
|
||||
|
||||
// This has the side-effect of clearing current session state.
|
||||
this._moveCurrentToPrevious();
|
||||
|
||||
this._startDate = si.process;
|
||||
this._prefs.set("current.activeTicks", 0);
|
||||
this.updateTotalTime();
|
||||
|
||||
this.recordStartupFields();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record application activity.
|
||||
*/
|
||||
onActivity: function (active) {
|
||||
let updateActive = active && !this._lastActivityWasInactive;
|
||||
this._lastActivityWasInactive = !active;
|
||||
|
||||
this.updateTotalTime();
|
||||
|
||||
if (updateActive) {
|
||||
this.incrementActiveTicks();
|
||||
}
|
||||
},
|
||||
|
||||
onShutdown: function () {
|
||||
this._log.info("Recording clean session shutdown.");
|
||||
this._prefs.set("current.clean", true);
|
||||
this.updateTotalTime();
|
||||
this._clearStartupTimer();
|
||||
|
||||
this._os.removeObserver(this, "profile-before-change");
|
||||
this._os.removeObserver(this, "user-interaction-active");
|
||||
this._os.removeObserver(this, "user-interaction-inactive");
|
||||
this._os.removeObserver(this, "idle-daily");
|
||||
},
|
||||
|
||||
_CURRENT_PREFS: [
|
||||
"current.startTime",
|
||||
"current.activeTicks",
|
||||
"current.totalTime",
|
||||
"current.main",
|
||||
"current.firstPaint",
|
||||
"current.sessionRestored",
|
||||
"current.clean",
|
||||
],
|
||||
|
||||
// This is meant to be called only during onStartup().
|
||||
_moveCurrentToPrevious: function () {
|
||||
try {
|
||||
if (!this.startDate.getTime()) {
|
||||
this._log.info("No previous session. Is this first app run?");
|
||||
return;
|
||||
}
|
||||
|
||||
let clean = this._prefs.get("current.clean", false);
|
||||
|
||||
let count = this._currentIndex++;
|
||||
let obj = {
|
||||
s: this.startDate.getTime(),
|
||||
a: this.activeTicks,
|
||||
t: this.totalTime,
|
||||
c: clean,
|
||||
m: this.main,
|
||||
fp: this.firstPaint,
|
||||
sr: this.sessionRestored,
|
||||
};
|
||||
|
||||
this._log.debug("Recording last sessions as #" + count + ".");
|
||||
this._prefs.set("previous." + count, JSON.stringify(obj));
|
||||
} catch (ex) {
|
||||
this._log.warn("Exception when migrating last session", ex);
|
||||
} finally {
|
||||
this._log.debug("Resetting prefs from last session.");
|
||||
for (let pref of this._CURRENT_PREFS) {
|
||||
this._prefs.reset(pref);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_deserialize: function (s) {
|
||||
let o;
|
||||
try {
|
||||
o = JSON.parse(s);
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: new Date(o.s),
|
||||
activeTicks: o.a,
|
||||
totalTime: o.t,
|
||||
clean: !!o.c,
|
||||
main: o.m,
|
||||
firstPaint: o.fp,
|
||||
sessionRestored: o.sr,
|
||||
};
|
||||
},
|
||||
|
||||
// Implemented as a function to allow for monkeypatching in tests.
|
||||
_getStartupInfo: function () {
|
||||
return Cc["@mozilla.org/toolkit/app-startup;1"]
|
||||
.getService(Ci.nsIAppStartup)
|
||||
.getStartupInfo();
|
||||
},
|
||||
|
||||
observe: function (subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "profile-before-change":
|
||||
this.onShutdown();
|
||||
break;
|
||||
|
||||
case "user-interaction-active":
|
||||
this.onActivity(true);
|
||||
break;
|
||||
|
||||
case "user-interaction-inactive":
|
||||
this.onActivity(false);
|
||||
break;
|
||||
|
||||
case "idle-daily":
|
||||
this.pruneOldSessions(new Date(Date.now() - MAX_SESSION_AGE_MS));
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue