gecko/toolkit/components/telemetry/TelemetryFile.jsm

308 lines
8.3 KiB
JavaScript

/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
/* 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 = ["TelemetryFile"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
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/Task.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated',
'resource://gre/modules/Deprecated.jsm');
const Telemetry = Services.telemetry;
// Files that have been lying around for longer than MAX_PING_FILE_AGE are
// deleted without being loaded.
const MAX_PING_FILE_AGE = 14 * 24 * 60 * 60 * 1000; // 2 weeks
// Files that are older than OVERDUE_PING_FILE_AGE, but younger than
// MAX_PING_FILE_AGE indicate that we need to send all of our pings ASAP.
const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
// Maximum number of pings to save.
const MAX_LRU_PINGS = 17;
// The number of outstanding saved pings that we have issued loading
// requests for.
let pingsLoaded = 0;
// The number of pings that we have destroyed due to being older
// than MAX_PING_FILE_AGE.
let pingsDiscarded = 0;
// The number of pings that are older than OVERDUE_PING_FILE_AGE
// but younger than MAX_PING_FILE_AGE.
let pingsOverdue = 0;
// Data that has neither been saved nor sent by ping
let pendingPings = [];
let isPingDirectoryCreated = false;
this.TelemetryFile = {
get MAX_PING_FILE_AGE() {
return MAX_PING_FILE_AGE;
},
get OVERDUE_PING_FILE_AGE() {
return OVERDUE_PING_FILE_AGE;
},
get MAX_LRU_PINGS() {
return MAX_LRU_PINGS;
},
get pingDirectoryPath() {
return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings");
},
/**
* Save a single ping to a file.
*
* @param {object} ping The content of the ping to save.
* @param {string} file The destination file.
* @param {bool} overwrite If |true|, the file will be overwritten if it exists,
* if |false| the file will not be overwritten and no error will be reported if
* the file exists.
* @returns {promise}
*/
savePingToFile: function(ping, file, overwrite) {
return Task.spawn(function*() {
try {
let pingString = JSON.stringify(ping);
yield OS.File.writeAtomic(file, pingString, {tmpPath: file + ".tmp",
noOverwrite: !overwrite});
} catch(e if e.becauseExists) {
}
})
},
/**
* Save a ping to its file.
*
* @param {object} ping The content of the ping to save.
* @param {bool} overwrite If |true|, the file will be overwritten
* if it exists.
* @returns {promise}
*/
savePing: function(ping, overwrite) {
return Task.spawn(function*() {
yield getPingDirectory();
let file = pingFilePath(ping);
yield this.savePingToFile(ping, file, overwrite);
}.bind(this));
},
/**
* Save all pending pings.
*
* @param {object} sessionPing The additional session ping.
* @returns {promise}
*/
savePendingPings: function(sessionPing) {
let p = pendingPings.reduce((p, ping) => {
// Restore the files with the previous pings if for some reason they have
// been deleted, don't overwrite them otherwise.
p.push(this.savePing(ping, false));
return p;}, [this.savePing(sessionPing, true)]);
pendingPings = [];
return Promise.all(p);
},
/**
* Remove the file for a ping
*
* @param {object} ping The ping.
* @returns {promise}
*/
cleanupPingFile: function(ping) {
return OS.File.remove(pingFilePath(ping));
},
/**
* Load all saved pings.
*
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @returns {promise}
*/
loadSavedPings: function() {
return Task.spawn(function*() {
let directory = TelemetryFile.pingDirectoryPath;
let iter = new OS.File.DirectoryIterator(directory);
let exists = yield iter.exists();
if (exists) {
let entries = yield iter.nextBatch();
let sortedEntries = [];
for (let entry of entries) {
if (entry.isDir) {
continue;
}
let info = yield OS.File.stat(entry.path);
sortedEntries.push({entry:entry, lastModificationDate: info.lastModificationDate});
}
sortedEntries.sort(function compare(a, b) {
return b.lastModificationDate - a.lastModificationDate;
});
let count = 0;
let result = [];
// Keep only the last MAX_LRU_PINGS entries to avoid that the backlog overgrows.
for (let i = 0; i < MAX_LRU_PINGS && i < sortedEntries.length; i++) {
let entry = sortedEntries[i].entry;
result.push(this.loadHistograms(entry.path))
}
for (let i = MAX_LRU_PINGS; i < sortedEntries.length; i++) {
let entry = sortedEntries[i].entry;
OS.File.remove(entry.path);
}
yield Promise.all(result);
Services.telemetry.getHistogramById('TELEMETRY_FILES_EVICTED').
add(sortedEntries.length - MAX_LRU_PINGS);
}
yield iter.close();
}.bind(this));
},
/**
* Load the histograms from a file.
*
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @param {string} file The file to load.
* @returns {promise}
*/
loadHistograms: function loadHistograms(file) {
return OS.File.stat(file).then(function(info){
let now = Date.now();
if (now - info.lastModificationDate > MAX_PING_FILE_AGE) {
// We haven't had much luck in sending this file; delete it.
pingsDiscarded++;
return OS.File.remove(file);
}
// This file is a bit stale, and overdue for sending.
if (now - info.lastModificationDate > OVERDUE_PING_FILE_AGE) {
pingsOverdue++;
}
pingsLoaded++;
return addToPendingPings(file);
});
},
/**
* The number of pings loaded since the beginning of time.
*/
get pingsLoaded() {
return pingsLoaded;
},
/**
* The number of pings loaded that are older than OVERDUE_PING_FILE_AGE
* but younger than MAX_PING_FILE_AGE.
*/
get pingsOverdue() {
return pingsOverdue;
},
/**
* The number of pings that we just tossed out for being older than
* MAX_PING_FILE_AGE.
*/
get pingsDiscarded() {
return pingsDiscarded;
},
/**
* Iterate destructively through the pending pings.
*
* @return {iterator}
*/
popPendingPings: function*(reason) {
while (pendingPings.length > 0) {
let data = pendingPings.pop();
// Send persisted pings to the test URL too.
if (reason == "test-ping") {
data.reason = reason;
}
yield data;
}
},
testLoadHistograms: function(file) {
pingsLoaded = 0;
return this.loadHistograms(file.path);
}
};
///// Utility functions
function pingFilePath(ping) {
return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug);
}
function getPingDirectory() {
return Task.spawn(function*() {
let directory = TelemetryFile.pingDirectoryPath;
if (!isPingDirectoryCreated) {
yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });
isPingDirectoryCreated = true;
}
return directory;
});
}
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);
}
pendingPings.push(ping);
onLoad(true);
} catch (e) {
onLoad(false);
yield OS.File.remove(file);
}
});
}