mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1133536 - Detect & report aborted sessions in Telemetry. r=vladan
This commit is contained in:
parent
2e3be5fc6b
commit
0b6208db94
@ -123,6 +123,28 @@ this.TelemetryFile = {
|
||||
return Promise.all(p);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a ping to the saved pings directory so that it gets along with other pings. Note
|
||||
* that the original ping file will not be modified.
|
||||
*
|
||||
* @param {String} aFilePath The path to the ping file that needs to be added to the
|
||||
* saved pings directory.
|
||||
* @return {Promise} A promise resolved when the ping is saved to the pings directory.
|
||||
*/
|
||||
addPendingPing: function(aPingPath) {
|
||||
// Pings in the saved ping directory need to have the ping id or slug (old format) as
|
||||
// the file name. We load the ping content, check that it is valid, and use it to save
|
||||
// the ping file with the correct file name.
|
||||
return loadPingFile(aPingPath).then(ping => {
|
||||
// Append the ping to the pending list.
|
||||
pendingPings.push(ping);
|
||||
// Since we read a ping successfully, update the related histogram.
|
||||
Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS").add(1);
|
||||
// Save the ping to the saved pings directory.
|
||||
return this.savePing(ping, false);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the file for a ping
|
||||
*
|
||||
@ -277,29 +299,37 @@ function getPingDirectory() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a ping file.
|
||||
* @param {String} aFilePath The path of the ping file.
|
||||
* @return {Promise<Object>} A promise resolved with the ping content or rejected if the
|
||||
* ping contains invalid data.
|
||||
*/
|
||||
let loadPingFile = Task.async(function* (aFilePath) {
|
||||
let array = yield OS.File.read(aFilePath);
|
||||
let decoder = new TextDecoder();
|
||||
let string = decoder.decode(array);
|
||||
|
||||
let ping = JSON.parse(string);
|
||||
// The ping's payload used to be stringified JSON. Deal with that.
|
||||
if (typeof(ping.payload) == "string") {
|
||||
ping.payload = JSON.parse(ping.payload);
|
||||
}
|
||||
return ping;
|
||||
});
|
||||
|
||||
function addToPendingPings(file) {
|
||||
function onLoad(success) {
|
||||
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
|
||||
success_histogram.add(success);
|
||||
}
|
||||
|
||||
return Task.spawn(function*() {
|
||||
try {
|
||||
let array = yield OS.File.read(file);
|
||||
let decoder = new TextDecoder();
|
||||
let string = decoder.decode(array);
|
||||
|
||||
let ping = JSON.parse(string);
|
||||
// The ping's payload used to be stringified JSON. Deal with that.
|
||||
if (typeof(ping.payload) == "string") {
|
||||
ping.payload = JSON.parse(ping.payload);
|
||||
}
|
||||
|
||||
return loadPingFile(file).then(ping => {
|
||||
pendingPings.push(ping);
|
||||
onLoad(true);
|
||||
} catch (e) {
|
||||
},
|
||||
() => {
|
||||
onLoad(false);
|
||||
yield OS.File.remove(file);
|
||||
}
|
||||
});
|
||||
return OS.File.remove(file);
|
||||
});
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/debug.js", this);
|
||||
Cu.import("resource://gre/modules/Services.jsm", this);
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile.jsm", this);
|
||||
Cu.import("resource://gre/modules/Promise.jsm", this);
|
||||
Cu.import("resource://gre/modules/DeferredTask.jsm", this);
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
@ -145,6 +146,19 @@ this.TelemetryPing = Object.freeze({
|
||||
return Impl.setServer(aServer);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a ping to the pending ping list by moving it to the saved pings directory
|
||||
* and adding it to the pending ping list.
|
||||
*
|
||||
* @param {String} aPingPath The path of the ping to add to the pending ping list.
|
||||
* @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
|
||||
* it to the saved pings directory.
|
||||
* @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
|
||||
*/
|
||||
addPendingPing: function(aPingPath, aRemoveOriginal) {
|
||||
return Impl.addPendingPing(aPingPath, aRemoveOriginal);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send payloads to the server.
|
||||
*
|
||||
@ -205,8 +219,11 @@ this.TelemetryPing = Object.freeze({
|
||||
* environment data.
|
||||
* @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
|
||||
* if found.
|
||||
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
|
||||
* ping location if not provided.
|
||||
*
|
||||
* @returns {Promise} A promise that resolves when the ping is saved to disk.
|
||||
* @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
|
||||
* saved to disk.
|
||||
*/
|
||||
savePing: function(aType, aPayload, aOptions = {}) {
|
||||
let options = aOptions;
|
||||
@ -218,36 +235,6 @@ this.TelemetryPing = Object.freeze({
|
||||
return Impl.savePing(aType, aPayload, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Only used for testing. Saves a ping to disk and return the ping id once done.
|
||||
*
|
||||
* @param {String} aType The type of the ping.
|
||||
* @param {Object} aPayload The actual data payload for the ping.
|
||||
* @param {Object} [aOptions] Options object.
|
||||
* @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
|
||||
* if sending fails.
|
||||
* @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
|
||||
* id, false otherwise.
|
||||
* @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
|
||||
* environment data.
|
||||
* @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
|
||||
* if found.
|
||||
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
|
||||
* ping location if not provided.
|
||||
*
|
||||
* @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
|
||||
* saved to disk.
|
||||
*/
|
||||
testSavePingToFile: function(aType, aPayload, aOptions = {}) {
|
||||
let options = aOptions;
|
||||
options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
|
||||
options.addClientId = aOptions.addClientId || false;
|
||||
options.addEnvironment = aOptions.addEnvironment || false;
|
||||
options.overwrite = aOptions.overwrite || false;
|
||||
|
||||
return Impl.testSavePingToFile(aType, aPayload, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* The client id send with the telemetry ping.
|
||||
*
|
||||
@ -377,6 +364,23 @@ let Impl = {
|
||||
this._server = aServer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a ping to the pending ping list by moving it to the saved pings directory
|
||||
* and adding it to the pending ping list.
|
||||
*
|
||||
* @param {String} aPingPath The path of the ping to add to the pending ping list.
|
||||
* @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
|
||||
* it to the saved pings directory.
|
||||
* @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
|
||||
*/
|
||||
addPendingPing: function(aPingPath, aRemoveOriginal) {
|
||||
return TelemetryFile.addPendingPing(aPingPath).then(() => {
|
||||
if (aRemoveOriginal) {
|
||||
return OS.File.remove(aPingPath);
|
||||
}
|
||||
}, error => this._log.error("addPendingPing - Unable to add the pending ping", error));
|
||||
},
|
||||
|
||||
/**
|
||||
* Build a complete ping and send data to the server. Record success/send-time in
|
||||
* histograms.
|
||||
@ -458,50 +462,26 @@ let Impl = {
|
||||
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
|
||||
* environment data.
|
||||
* @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
|
||||
*
|
||||
* @returns {Promise} A promise that resolves when the ping is saved to disk.
|
||||
*/
|
||||
savePing: function savePing(aType, aPayload, aOptions) {
|
||||
this._log.trace("savePing - Type " + aType + ", Server " + this._server +
|
||||
", aOptions " + JSON.stringify(aOptions));
|
||||
|
||||
return this.assemblePing(aType, aPayload, aOptions)
|
||||
.then(pingData => TelemetryFile.savePing(pingData, aOptions.overwrite),
|
||||
error => this._log.error("savePing - Rejection", error));
|
||||
},
|
||||
|
||||
/**
|
||||
* Save a ping to disk and return the ping id when done.
|
||||
*
|
||||
* @param {String} aType The type of the ping.
|
||||
* @param {Object} aPayload The actual data payload for the ping.
|
||||
* @param {Object} aOptions Options object.
|
||||
* @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
|
||||
* if sending fails.
|
||||
* @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
|
||||
* false otherwise.
|
||||
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
|
||||
* environment data.
|
||||
* @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
|
||||
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
|
||||
* ping location if not provided.
|
||||
*
|
||||
* @returns {Promise} A promise that resolves with the ping id when the ping is saved to
|
||||
* disk.
|
||||
*/
|
||||
testSavePingToFile: function testSavePingToFile(aType, aPayload, aOptions) {
|
||||
this._log.trace("testSavePingToFile - Type " + aType + ", Server " + this._server +
|
||||
savePing: function savePing(aType, aPayload, aOptions) {
|
||||
this._log.trace("savePing - Type " + aType + ", Server " + this._server +
|
||||
", aOptions " + JSON.stringify(aOptions));
|
||||
|
||||
return this.assemblePing(aType, aPayload, aOptions)
|
||||
.then(pingData => {
|
||||
if (aOptions.filePath) {
|
||||
return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
|
||||
.then(() => { return pingData.id; });
|
||||
} else {
|
||||
return TelemetryFile.savePing(pingData, aOptions.overwrite)
|
||||
.then(() => { return pingData.id; });
|
||||
}
|
||||
}, error => this._log.error("testSavePing - Rejection", error));
|
||||
.then(pingData => {
|
||||
if ("filePath" in aOptions) {
|
||||
return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
|
||||
.then(() => { return pingData.id; });
|
||||
} else {
|
||||
return TelemetryFile.savePing(pingData, aOptions.overwrite)
|
||||
.then(() => { return pingData.id; });
|
||||
}
|
||||
}, error => this._log.error("savePing - Rejection", error));
|
||||
},
|
||||
|
||||
finishPingRequest: function finishPingRequest(success, startTime, ping, isPersisted) {
|
||||
|
@ -21,6 +21,8 @@ Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
const myScope = this;
|
||||
|
||||
const IS_CONTENT_PROCESS = (function() {
|
||||
// We cannot use Services.appinfo here because in telemetry xpcshell tests,
|
||||
// appinfo is initially unavailable, and becomes available only later on.
|
||||
@ -34,6 +36,7 @@ const PING_TYPE_MAIN = "main";
|
||||
const PING_TYPE_SAVED_SESSION = "saved-session";
|
||||
const RETENTION_DAYS = 14;
|
||||
|
||||
const REASON_ABORTED_SESSION = "aborted-session";
|
||||
const REASON_DAILY = "daily";
|
||||
const REASON_SAVED_SESSION = "saved-session";
|
||||
const REASON_IDLE_DAILY = "idle-daily";
|
||||
@ -67,6 +70,9 @@ const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit";
|
||||
const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
|
||||
const MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD = "Telemetry:GetChildPayload";
|
||||
|
||||
const DATAREPORTING_DIRECTORY = "datareporting";
|
||||
const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
|
||||
|
||||
const SESSION_STATE_FILE_NAME = "session-state.json";
|
||||
|
||||
// Maximum number of content payloads that we are willing to store.
|
||||
@ -78,12 +84,27 @@ const TELEMETRY_INTERVAL = 60000;
|
||||
const TELEMETRY_DELAY = 60000;
|
||||
// Delay before initializing telemetry if we're testing (ms)
|
||||
const TELEMETRY_TEST_DELAY = 100;
|
||||
// Execute a scheduler tick every 5 minutes.
|
||||
const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
|
||||
// The maximum number of times a scheduled operation can fail.
|
||||
const SCHEDULER_RETRY_ATTEMPTS = 3;
|
||||
|
||||
// The tolerance we have when checking if it's midnight (15 minutes).
|
||||
const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
|
||||
|
||||
// Coalesce the daily and aborted-session pings if they are both due within
|
||||
// two minutes from each other.
|
||||
const SCHEDULER_COALESCE_THRESHOLD_MS = 2 * 60 * 1000;
|
||||
|
||||
// Seconds of idle time before pinging.
|
||||
// On idle-daily a gather-telemetry notification is fired, during it probes can
|
||||
// start asynchronous tasks to gather data. On the next idle the data is sent.
|
||||
const IDLE_TIMEOUT_SECONDS = 5 * 60;
|
||||
|
||||
// The frequency at which we persist session data to the disk to prevent data loss
|
||||
// in case of aborted sessions (currently 5 minutes).
|
||||
const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
var gLastMemoryPoll = null;
|
||||
|
||||
let gWasDebuggerAttached = false;
|
||||
@ -147,8 +168,8 @@ let Policy = {
|
||||
now: () => new Date(),
|
||||
generateSessionUUID: () => generateUUID(),
|
||||
generateSubsessionUUID: () => generateUUID(),
|
||||
setDailyTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
||||
clearDailyTimeout: (id) => clearTimeout(id),
|
||||
setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
||||
clearSchedulerTickTimeout: id => clearTimeout(id),
|
||||
};
|
||||
|
||||
/**
|
||||
@ -161,6 +182,38 @@ function truncateToDays(date) {
|
||||
0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the difference between the times is within the provided tolerance.
|
||||
* @param {Number} t1 A time in milliseconds.
|
||||
* @param {Number} t2 A time in milliseconds.
|
||||
* @param {Number} tolerance The tolerance, in milliseconds.
|
||||
* @return {Boolean} True if the absolute time difference is within the tolerance, false
|
||||
* otherwise.
|
||||
*/
|
||||
function areTimesClose(t1, t2, tolerance) {
|
||||
return Math.abs(t1 - t2) <= tolerance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the midnight which is closer to the provided date.
|
||||
* @param {Object} date The date object to check.
|
||||
* @return {Object} The Date object representing the closes midnight, or null if midnight
|
||||
* is not within the midnight tolerance.
|
||||
*/
|
||||
function getNearestMidnight(date) {
|
||||
let lastMidnight = truncateToDays(date);
|
||||
if (areTimesClose(date.getTime(), lastMidnight.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
|
||||
return lastMidnight;
|
||||
}
|
||||
|
||||
let nextMidnightDate = new Date(lastMidnight);
|
||||
nextMidnightDate.setDate(nextMidnightDate.getDate() + 1);
|
||||
if (areTimesClose(date.getTime(), nextMidnightDate.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
|
||||
return nextMidnightDate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ping type based on the payload.
|
||||
* @param {Object} aPayload The ping payload.
|
||||
@ -258,11 +311,13 @@ let processInfo = {
|
||||
* We are using this to synchronize saving to the file that TelemetrySession persists
|
||||
* its state in.
|
||||
*/
|
||||
let gStateSaveSerializer = {
|
||||
_queuedOperations: [],
|
||||
_queuedInProgress: false,
|
||||
_log: Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX),
|
||||
function SaveSerializer() {
|
||||
this._queuedOperations = [];
|
||||
this._queuedInProgress = false;
|
||||
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
|
||||
}
|
||||
|
||||
SaveSerializer.prototype = {
|
||||
/**
|
||||
* Enqueues an operation to a list to serialise their execution in order to prevent race
|
||||
* conditions. Useful to serialise access to disk.
|
||||
@ -340,6 +395,265 @@ let gStateSaveSerializer = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* TelemetryScheduler contains a single timer driving all regularly-scheduled
|
||||
* Telemetry related jobs. Having a single place with this logic simplifies
|
||||
* reasoning about scheduling actions in a single place, making it easier to
|
||||
* coordinate jobs and coalesce them.
|
||||
*/
|
||||
let TelemetryScheduler = {
|
||||
_lastDailyPingTime: 0,
|
||||
_lastSessionCheckpointTime: 0,
|
||||
|
||||
// For sanity checking.
|
||||
_lastAdhocPingTime: 0,
|
||||
_lastTickTime: 0,
|
||||
|
||||
_log: null,
|
||||
|
||||
// The number of times a daily ping fails.
|
||||
_dailyPingRetryAttempts: 0,
|
||||
|
||||
// The timer which drives the scheduler.
|
||||
_schedulerTimer: null,
|
||||
_shuttingDown: true,
|
||||
|
||||
/**
|
||||
* Initialises the scheduler and schedules the first daily/aborted session pings.
|
||||
*/
|
||||
init: function() {
|
||||
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "TelemetryScheduler::");
|
||||
this._log.trace("init");
|
||||
this._shuttingDown = false;
|
||||
// Initialize the last daily ping and aborted session last due times to the current time.
|
||||
// Otherwise, we might end up sending daily pings even if the subsession is not long enough.
|
||||
let now = Policy.now();
|
||||
this._lastDailyPingTime = now.getTime();
|
||||
this._lastSessionCheckpointTime = now.getTime();
|
||||
this._rescheduleTimeout();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reschedules the tick timer.
|
||||
*/
|
||||
_rescheduleTimeout: function() {
|
||||
this._log.trace("_rescheduleTimeout");
|
||||
if (this._shuttingDown) {
|
||||
this._log.warn("_rescheduleTimeout - already shutdown");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._schedulerTimer) {
|
||||
Policy.clearSchedulerTickTimeout(this._schedulerTimer);
|
||||
}
|
||||
|
||||
this._schedulerTimer =
|
||||
Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), SCHEDULER_TICK_INTERVAL_MS);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if we can send a daily ping or not.
|
||||
* @param {Object} nowDate A date object.
|
||||
* @return {Boolean} True if we can send the daily ping, false otherwise.
|
||||
*/
|
||||
_isDailyPingDue: function(nowDate) {
|
||||
let nearestMidnight = getNearestMidnight(nowDate);
|
||||
if (nearestMidnight) {
|
||||
let subsessionLength = Math.abs(nowDate.getTime() - this._lastDailyPingTime);
|
||||
if (subsessionLength < MIN_SUBSESSION_LENGTH_MS) {
|
||||
// Generating a daily ping now would create a very short subsession.
|
||||
return false;
|
||||
} else if (areTimesClose(this._lastDailyPingTime, nearestMidnight.getTime(),
|
||||
SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
|
||||
// We've already sent a ping for this midnight.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let lastDailyPingDate = truncateToDays(new Date(this._lastDailyPingTime));
|
||||
// This is today's date and also the previous midnight (0:00).
|
||||
let todayDate = truncateToDays(nowDate);
|
||||
// Check that _lastDailyPingTime isn't today nor within SCHEDULER_MIDNIGHT_TOLERANCE_MS of the
|
||||
// *previous* midnight.
|
||||
if ((lastDailyPingDate.getTime() != todayDate.getTime()) &&
|
||||
!areTimesClose(this._lastDailyPingTime, todayDate.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
|
||||
// Computer must have gone to sleep, the daily ping is overdue.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* An helper function to save an aborted-session ping.
|
||||
* @param {Number} now The current time, in milliseconds.
|
||||
* @param {Object} [competingPayload=null] If we are coalescing the daily and the
|
||||
* aborted-session pings, this is the payload for the former. Note
|
||||
* that the reason field of this payload will be changed.
|
||||
* @return {Promise} A promise resolved when the ping is saved.
|
||||
*/
|
||||
_saveAbortedPing: function(now, competingPayload=null) {
|
||||
this._lastSessionCheckpointTime = now;
|
||||
return Impl._saveAbortedSessionPing(competingPayload)
|
||||
.catch(e => this._log.error("_saveAbortedPing - Failed", e));
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs a scheduler tick. This function manages Telemetry recurring operations.
|
||||
* @return {Promise} A promise, only used when testing, resolved when the scheduled
|
||||
* operation completes.
|
||||
*/
|
||||
_onSchedulerTick: function() {
|
||||
if (this._shuttingDown) {
|
||||
this._log.warn("_onSchedulerTick - already shutdown.");
|
||||
return;
|
||||
}
|
||||
|
||||
let promise = Promise.resolve();
|
||||
try {
|
||||
promise = this._schedulerTickLogic();
|
||||
} catch (e) {
|
||||
this._log.error("_onSchedulerTick - There was an exception", e);
|
||||
} finally {
|
||||
this._rescheduleTimeout();
|
||||
}
|
||||
|
||||
// This promise is returned to make testing easier.
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements the scheduler logic.
|
||||
* @return {Promise} Resolved when the scheduled task completes. Only used in tests.
|
||||
*/
|
||||
_schedulerTickLogic: function() {
|
||||
this._log.trace("_schedulerTickLogic");
|
||||
|
||||
let nowDate = Policy.now();
|
||||
let now = nowDate.getTime();
|
||||
|
||||
if (now - this._lastTickTime > 1.1 * SCHEDULER_TICK_INTERVAL_MS) {
|
||||
this._log.trace("_schedulerTickLogic - First scheduler tick after sleep or startup.");
|
||||
}
|
||||
this._lastTickTime = now;
|
||||
|
||||
// Check if aborted-session ping is due.
|
||||
let isAbortedPingDue =
|
||||
(now - this._lastSessionCheckpointTime) >= ABORTED_SESSION_UPDATE_INTERVAL_MS;
|
||||
// Check if daily ping is due.
|
||||
let shouldSendDaily = this._isDailyPingDue(nowDate);
|
||||
// We can combine the daily-ping and the aborted-session ping in the following cases:
|
||||
// - If both the daily and the aborted session pings are due (a laptop that wakes
|
||||
// up after a few hours).
|
||||
// - If either the daily ping is due and the other one would follow up shortly
|
||||
// (whithin the coalescence threshold).
|
||||
let nextSessionCheckpoint =
|
||||
this._lastSessionCheckpointTime + ABORTED_SESSION_UPDATE_INTERVAL_MS;
|
||||
let combineActions = (shouldSendDaily && isAbortedPingDue) || (shouldSendDaily &&
|
||||
areTimesClose(now, nextSessionCheckpoint, SCHEDULER_COALESCE_THRESHOLD_MS));
|
||||
|
||||
if (combineActions) {
|
||||
this._log.trace("_schedulerTickLogic - Combining pings.");
|
||||
// Send the daily ping and also save its payload as an aborted-session ping.
|
||||
return Impl._sendDailyPing(true).then(() => this._dailyPingSucceeded(now),
|
||||
() => this._dailyPingFailed(now));
|
||||
} else if (shouldSendDaily) {
|
||||
this._log.trace("_schedulerTickLogic - Daily ping due.");
|
||||
return Impl._sendDailyPing().then(() => this._dailyPingSucceeded(now),
|
||||
() => this._dailyPingFailed(now));
|
||||
} else if (isAbortedPingDue) {
|
||||
this._log.trace("_schedulerTickLogic - Aborted session ping due.");
|
||||
return this._saveAbortedPing(now);
|
||||
}
|
||||
|
||||
// No ping is due.
|
||||
this._log.trace("_schedulerTickLogic - No ping due.");
|
||||
// It's possible, because of sleeps, that we're no longer within midnight tolerance for
|
||||
// daily pings. Because of that, daily retry attempts would not be 0 on the next midnight.
|
||||
// Reset that count on do-nothing ticks.
|
||||
this._dailyPingRetryAttempts = 0;
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the scheduled pings if some other ping was sent.
|
||||
* @param {String} reason The reason of the ping that was sent.
|
||||
* @param {Object} [competingPayload=null] The payload of the ping that was sent. The
|
||||
* reason of this payload will be changed.
|
||||
*/
|
||||
reschedulePings: function(reason, competingPayload = null) {
|
||||
if (this._shuttingDown) {
|
||||
this._log.error("reschedulePings - already shutdown");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.trace("reschedulePings - reason: " + reason);
|
||||
let now = Policy.now();
|
||||
this._lastAdhocPingTime = now.getTime();
|
||||
if (reason == REASON_ENVIRONMENT_CHANGE) {
|
||||
// We just generated an environment-changed ping, save it as an aborted session and
|
||||
// update the schedules.
|
||||
this._saveAbortedPing(now.getTime(), competingPayload);
|
||||
// If we're close to midnight, skip today's daily ping and reschedule it for tomorrow.
|
||||
let nearestMidnight = getNearestMidnight(now);
|
||||
if (nearestMidnight) {
|
||||
this._lastDailyPingTime = now.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
this._rescheduleTimeout();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a scheduled operation successfully completes (ping sent or saved).
|
||||
* @param {Number} now The current time, in milliseconds.
|
||||
*/
|
||||
_dailyPingSucceeded: function(now) {
|
||||
this._log.trace("_dailyPingSucceeded");
|
||||
this._lastDailyPingTime = now;
|
||||
this._dailyPingRetryAttempts = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a scheduled operation fails (ping sent or saved).
|
||||
* @param {Number} now The current time, in milliseconds.
|
||||
*/
|
||||
_dailyPingFailed: function(now) {
|
||||
this._log.error("_dailyPingFailed");
|
||||
this._dailyPingRetryAttempts++;
|
||||
|
||||
// If we reach the maximum number of retry attempts for a daily ping, log the error
|
||||
// and skip this daily ping.
|
||||
if (this._dailyPingRetryAttempts >= SCHEDULER_RETRY_ATTEMPTS) {
|
||||
this._log.error("_pingFailed - The daily ping failed too many times. Skipping it.");
|
||||
this._dailyPingRetryAttempts = 0;
|
||||
this._lastDailyPingTime = now;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops the scheduler.
|
||||
*/
|
||||
shutdown: function() {
|
||||
if (this._shuttingDown) {
|
||||
if (this._log) {
|
||||
this._log.error("shutdown - Already shut down");
|
||||
} else {
|
||||
Cu.reportError("TelemetryScheduler.shutdown - Already shut down");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.trace("shutdown");
|
||||
if (this._schedulerTimer) {
|
||||
Policy.clearSchedulerTickTimeout(this._schedulerTimer);
|
||||
this._schedulerTimer = null;
|
||||
}
|
||||
|
||||
this._shuttingDown = true;
|
||||
}
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["TelemetrySession"];
|
||||
|
||||
this.TelemetrySession = Object.freeze({
|
||||
@ -490,12 +804,14 @@ let Impl = {
|
||||
_profileSubsessionCounter: 0,
|
||||
// Date of the last session split
|
||||
_subsessionStartDate: null,
|
||||
// The timer used for daily collections.
|
||||
_dailyTimerId: null,
|
||||
// A task performing delayed initialization of the chrome process
|
||||
_delayedInitTask: null,
|
||||
// The deferred promise resolved when the initialization task completes.
|
||||
_delayedInitTaskDeferred: null,
|
||||
// Used to serialize session state writes to disk.
|
||||
_stateSaveSerializer: new SaveSerializer(),
|
||||
// Used to serialize aborted session ping writes to disk.
|
||||
_abortedSessionSerializer: new SaveSerializer(),
|
||||
|
||||
/**
|
||||
* Gets a series of simple measurements (counters). At the moment, this
|
||||
@ -1014,8 +1330,7 @@ let Impl = {
|
||||
this.startNewSubsession();
|
||||
// Persist session data to disk (don't wait until it completes).
|
||||
let sessionData = this._getSessionDataObject();
|
||||
gStateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
|
||||
this._rescheduleDailyTimer();
|
||||
this._stateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
|
||||
}
|
||||
|
||||
return payload;
|
||||
@ -1168,9 +1483,20 @@ let Impl = {
|
||||
Telemetry.asyncFetchTelemetryData(function () {});
|
||||
|
||||
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
|
||||
this._rescheduleDailyTimer();
|
||||
// Check for a previously written aborted session ping.
|
||||
yield this._checkAbortedSessionPing();
|
||||
|
||||
TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
|
||||
() => this._onEnvironmentChange());
|
||||
// Write the first aborted-session ping as early as possible. Just do that
|
||||
// if we are not testing, since calling Telemetry.reset() will make a previous
|
||||
// aborted ping a pending ping.
|
||||
if (!testing) {
|
||||
yield this._saveAbortedSessionPing();
|
||||
}
|
||||
|
||||
// Start the scheduler.
|
||||
TelemetryScheduler.init();
|
||||
#endif
|
||||
|
||||
this._delayedInitTaskDeferred.resolve();
|
||||
@ -1321,7 +1647,7 @@ let Impl = {
|
||||
overwrite: true,
|
||||
filePath: file.path,
|
||||
};
|
||||
return TelemetryPing.testSavePingToFile(getPingType(payload), payload, options);
|
||||
return TelemetryPing.savePing(getPingType(payload), payload, options);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1501,11 +1827,8 @@ let Impl = {
|
||||
let cleanup = () => {
|
||||
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
|
||||
TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER);
|
||||
TelemetryScheduler.shutdown();
|
||||
#endif
|
||||
if (this._dailyTimerId) {
|
||||
Policy.clearDailyTimeout(this._dailyTimerId);
|
||||
this._dailyTimerId = null;
|
||||
}
|
||||
this.uninstall();
|
||||
|
||||
let reset = () => {
|
||||
@ -1515,7 +1838,11 @@ let Impl = {
|
||||
|
||||
if (Telemetry.canSend || testing) {
|
||||
return this.savePendingPings()
|
||||
.then(() => gStateSaveSerializer.flushTasks())
|
||||
.then(() => this._stateSaveSerializer.flushTasks())
|
||||
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
|
||||
.then(() => this._abortedSessionSerializer
|
||||
.enqueueTask(() => this._removeAbortedSessionPing()))
|
||||
#endif
|
||||
.then(reset);
|
||||
}
|
||||
|
||||
@ -1545,35 +1872,14 @@ let Impl = {
|
||||
return this._delayedInitTask.finalize().then(cleanup);
|
||||
},
|
||||
|
||||
_rescheduleDailyTimer: function() {
|
||||
if (this._dailyTimerId) {
|
||||
this._log.trace("_rescheduleDailyTimer - clearing existing timeout");
|
||||
Policy.clearDailyTimeout(this._dailyTimerId);
|
||||
}
|
||||
|
||||
let now = Policy.now();
|
||||
let midnight = truncateToDays(now).getTime() + MS_IN_ONE_DAY;
|
||||
let msUntilCollection = midnight - now.getTime();
|
||||
if (msUntilCollection < MIN_SUBSESSION_LENGTH_MS) {
|
||||
msUntilCollection += MS_IN_ONE_DAY;
|
||||
}
|
||||
|
||||
this._log.trace("_rescheduleDailyTimer - now: " + now
|
||||
+ ", scheduled: " + new Date(now.getTime() + msUntilCollection));
|
||||
this._dailyTimerId = Policy.setDailyTimeout(() => this._onDailyTimer(), msUntilCollection);
|
||||
},
|
||||
|
||||
_onDailyTimer: function() {
|
||||
if (!this._initStarted) {
|
||||
if (this._log) {
|
||||
this._log.warn("_onDailyTimer - not initialized");
|
||||
} else {
|
||||
Cu.reportError("TelemetrySession._onDailyTimer - not initialized");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.trace("_onDailyTimer");
|
||||
/**
|
||||
* Gather and send a daily ping.
|
||||
* @param {Boolean} [saveAsAborted=false] Also saves the payload as an aborted-session
|
||||
* ping.
|
||||
* @return {Promise} Resolved when the ping is sent.
|
||||
*/
|
||||
_sendDailyPing: function(saveAsAborted = false) {
|
||||
this._log.trace("_sendDailyPing");
|
||||
let payload = this.getSessionPayload(REASON_DAILY, true);
|
||||
|
||||
let options = {
|
||||
@ -1581,10 +1887,14 @@ let Impl = {
|
||||
addClientId: true,
|
||||
addEnvironment: true,
|
||||
};
|
||||
let promise = TelemetryPing.send(getPingType(payload), payload, options);
|
||||
|
||||
this._rescheduleDailyTimer();
|
||||
// Return the promise so tests can wait on the ping submission.
|
||||
let promise = TelemetryPing.send(getPingType(payload), payload, options);
|
||||
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
|
||||
// If required, also save the payload as an aborted session.
|
||||
if (saveAsAborted) {
|
||||
return promise.then(() => this._saveAbortedSessionPing(payload));
|
||||
}
|
||||
#endif
|
||||
return promise;
|
||||
},
|
||||
|
||||
@ -1594,7 +1904,7 @@ let Impl = {
|
||||
* loading has completed, with false otherwise.
|
||||
*/
|
||||
_loadSessionData: Task.async(function* () {
|
||||
let dataFile = OS.Path.join(OS.Constants.Path.profileDir, "datareporting",
|
||||
let dataFile = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
|
||||
SESSION_STATE_FILE_NAME);
|
||||
|
||||
// Try to load the "profileSubsessionCounter" from the state file.
|
||||
@ -1632,7 +1942,7 @@ let Impl = {
|
||||
* Saves session data to disk.
|
||||
*/
|
||||
_saveSessionData: Task.async(function* (sessionData) {
|
||||
let dataDir = OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
|
||||
let dataDir = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY);
|
||||
yield OS.File.makeDir(dataDir);
|
||||
|
||||
let filePath = OS.Path.join(dataDir, SESSION_STATE_FILE_NAME);
|
||||
@ -1647,12 +1957,15 @@ let Impl = {
|
||||
this._log.trace("_onEnvironmentChange");
|
||||
let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
|
||||
|
||||
let clonedPayload = Cu.cloneInto(payload, myScope);
|
||||
TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, clonedPayload);
|
||||
|
||||
let options = {
|
||||
retentionDays: RETENTION_DAYS,
|
||||
addClientId: true,
|
||||
addEnvironment: true,
|
||||
};
|
||||
let promise = TelemetryPing.send(getPingType(payload), payload, options);
|
||||
TelemetryPing.send(getPingType(payload), payload, options);
|
||||
},
|
||||
|
||||
_isClassicReason: function(reason) {
|
||||
@ -1673,7 +1986,73 @@ let Impl = {
|
||||
initialized: this._initialized,
|
||||
initStarted: this._initStarted,
|
||||
haveDelayedInitTask: !!this._delayedInitTask,
|
||||
dailyTimerScheduled: !!this._dailyTimerId,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the aborted session ping. This is called during shutdown.
|
||||
* @return {Promise} Resolved when the aborted session ping is removed or if it doesn't
|
||||
* exist.
|
||||
*/
|
||||
_removeAbortedSessionPing: function() {
|
||||
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
|
||||
ABORTED_SESSION_FILE_NAME);
|
||||
try {
|
||||
return OS.File.remove(FILE_PATH);
|
||||
} catch (ex if ex.becauseNoSuchFile) { }
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if there's any aborted session ping available. If so, tell TelemetryPing about
|
||||
* it.
|
||||
*/
|
||||
_checkAbortedSessionPing: Task.async(function* () {
|
||||
// Create the subdirectory that will contain te aborted session ping. We put it in a
|
||||
// subdirectory so that it doesn't get picked up as a pending ping. Please note that
|
||||
// this does nothing if the directory does not already exist.
|
||||
const ABORTED_SESSIONS_DIR =
|
||||
OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY);
|
||||
yield OS.File.makeDir(ABORTED_SESSIONS_DIR, { ignoreExisting: true });
|
||||
|
||||
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
|
||||
ABORTED_SESSION_FILE_NAME);
|
||||
let abortedExists = yield OS.File.exists(FILE_PATH);
|
||||
if (abortedExists) {
|
||||
this._log.trace("_checkAbortedSessionPing - aborted session found: " + FILE_PATH);
|
||||
yield this._abortedSessionSerializer.enqueueTask(
|
||||
() => TelemetryPing.addPendingPing(FILE_PATH, true));
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Saves the aborted session ping to disk.
|
||||
* @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted
|
||||
* session ping. The reason of this payload is changed to aborted-session.
|
||||
* If not provided, a new payload is gathered.
|
||||
*/
|
||||
_saveAbortedSessionPing: function(aProvidedPayload = null) {
|
||||
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
|
||||
ABORTED_SESSION_FILE_NAME);
|
||||
this._log.trace("_saveAbortedSessionPing - ping path: " + FILE_PATH);
|
||||
|
||||
let payload = null;
|
||||
if (aProvidedPayload) {
|
||||
payload = aProvidedPayload;
|
||||
// Overwrite the original reason.
|
||||
payload.info.reason = REASON_ABORTED_SESSION;
|
||||
} else {
|
||||
payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
|
||||
}
|
||||
|
||||
let options = {
|
||||
retentionDays: RETENTION_DAYS,
|
||||
addClientId: true,
|
||||
addEnvironment: true,
|
||||
overwrite: true,
|
||||
filePath: FILE_PATH,
|
||||
};
|
||||
return this._abortedSessionSerializer.enqueueTask(() =>
|
||||
TelemetryPing.savePing(getPingType(payload), payload, options));
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user