Bug 839794 - Use OS.File in Telemetry. r=Yoric

This commit is contained in:
Roberto A. Vitillo 2014-01-23 17:47:53 +00:00
parent 8b4bc729bc
commit 2a6f143916
5 changed files with 413 additions and 536 deletions

View File

@ -1,3 +1,8 @@
/* -*- 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"];
@ -7,20 +12,13 @@ const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
let imports = {};
Cu.import("resource://gre/modules/Services.jsm", imports);
Cu.import("resource://gre/modules/Deprecated.jsm", imports);
Cu.import("resource://gre/modules/NetUtil.jsm", imports);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/Deprecated.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);
let {Services, Deprecated, NetUtil} = imports;
// Constants from prio.h for nsIFileOutputStream.init
const PR_WRONLY = 0x2;
const PR_CREATE_FILE = 0x8;
const PR_TRUNCATE = 0x20;
const PR_EXCL = 0x80;
const RW_OWNER = parseInt("0600", 8);
const RWX_OWNER = parseInt("0700", 8);
const Telemetry = Services.telemetry;
// Files that have been lying around for longer than MAX_PING_FILE_AGE are
// deleted without being loaded.
@ -34,9 +32,6 @@ const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
// requests for.
let pingsLoaded = 0;
// The number of those requests that have actually completed.
let pingLoadsCompleted = 0;
// The number of pings that we have destroyed due to being older
// than MAX_PING_FILE_AGE.
let pingsDiscarded = 0;
@ -45,13 +40,11 @@ let pingsDiscarded = 0;
// but younger than MAX_PING_FILE_AGE.
let pingsOverdue = 0;
// If |true|, send notifications "telemetry-test-save-complete"
// and "telemetry-test-load-complete" once save/load is complete.
let shouldNotifyUponSave = false;
// Data that has neither been saved nor sent by ping
let pendingPings = [];
let isPingDirectoryCreated = false;
this.TelemetryFile = {
get MAX_PING_FILE_AGE() {
@ -62,94 +55,64 @@ this.TelemetryFile = {
return OVERDUE_PING_FILE_AGE;
},
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 {nsIFile} file The destination file.
* @param {bool} sync If |true|, write synchronously. Deprecated.
* This argument should be |false|.
* @param {string} file The destination file.
* @param {bool} overwrite If |true|, the file will be overwritten
* if it exists.
* @returns {promise}
*/
savePingToFile: function(ping, file, sync, overwrite) {
savePingToFile: function(ping, file, overwrite) {
let pingString = JSON.stringify(ping);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let ostream = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
let initFlags = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE;
if (!overwrite) {
initFlags |= PR_EXCL;
}
try {
ostream.init(file, initFlags, RW_OWNER, 0);
} catch (e) {
// Probably due to PR_EXCL.
return;
}
if (sync) {
let utf8String = converter.ConvertFromUnicode(pingString);
utf8String += converter.Finish();
let success = false;
try {
let amount = ostream.write(utf8String, utf8String.length);
success = amount == utf8String.length;
} catch (e) {
}
finishTelemetrySave(success, ostream);
} else {
let istream = converter.convertToInputStream(pingString);
let self = this;
NetUtil.asyncCopy(istream, ostream,
function(result) {
finishTelemetrySave(Components.isSuccessCode(result),
ostream);
});
}
return OS.File.writeAtomic(file, pingString, {tmpPath: file + ".tmp",
noOverwrite: !overwrite});
},
/**
* Save a ping to its file, synchronously.
* 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) {
this.savePingToFile(ping,
getSaveFileForPing(ping), true, overwrite);
return Task.spawn(function*() {
yield getPingDirectory();
let file = pingFilePath(ping);
return this.savePingToFile(ping, file, overwrite);
}.bind(this));
},
/**
* Save all pending pings, synchronously.
* Save all pending pings.
*
* @param {object} sessionPing The additional session ping.
* @returns {promise}
*/
savePendingPings: function(sessionPing) {
this.savePing(sessionPing, true);
pendingPings.forEach(function sppcb(e, i, a) {
this.savePing(e, false);
}, this);
let p = pendingPings.reduce((p, ping) => {
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) {
// FIXME: We shouldn't create the directory just to remove the file.
let file = getSaveFileForPing(ping);
try {
file.remove(true); // FIXME: Should be |false|, isn't it?
} catch(e) {
}
return OS.File.remove(pingFilePath(ping));
},
/**
@ -158,24 +121,26 @@ this.TelemetryFile = {
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @param {bool} sync If |true|, loading takes place synchronously.
* @param {function*} onLoad A function called upon loading of each
* ping. It is passed |true| in case of success, |false| in case of
* format error.
* @returns {promise}
*/
loadSavedPings: function(sync, onLoad = null, onDone = null) {
let directory = ensurePingDirectory();
let entries = directory.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
pingsLoaded = 0;
pingLoadsCompleted = 0;
try {
while (entries.hasMoreElements()) {
this.loadHistograms(entries.nextFile, sync, onLoad, onDone);
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();
yield iter.close();
let p = [e for (e of entries) if (!e.isDir)].
map((e) => this.loadHistograms(e.path));
yield Promise.all(p);
}
} finally {
entries.close();
}
yield iter.close();
}.bind(this));
},
/**
@ -184,43 +149,26 @@ this.TelemetryFile = {
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @param {nsIFile} file The file to load.
* @param {bool} sync If |true|, loading takes place synchronously.
* @param {function*} onLoad A function called upon loading of the
* ping. It is passed |true| in case of success, |false| in case of
* format error.
* @param {string} file The file to load.
* @returns {promise}
*/
loadHistograms: function loadHistograms(file, sync, onLoad = null, onDone = null) {
let now = Date.now();
if (now - file.lastModifiedTime > MAX_PING_FILE_AGE) {
// We haven't had much luck in sending this file; delete it.
file.remove(true);
pingsDiscarded++;
return;
}
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 - file.lastModifiedTime > OVERDUE_PING_FILE_AGE) {
pingsOverdue++;
}
// This file is a bit stale, and overdue for sending.
if (now - info.lastModificationDate > OVERDUE_PING_FILE_AGE) {
pingsOverdue++;
}
pingsLoaded++;
if (sync) {
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
stream.init(file, -1, -1, 0);
addToPendingPings(file, stream, onLoad, onDone);
} else {
let channel = NetUtil.newChannel(file);
channel.contentType = "application/json";
NetUtil.asyncFetch(channel, (function(stream, result) {
if (!Components.isSuccessCode(result)) {
return;
}
addToPendingPings(file, stream, onLoad, onDone);
}).bind(this));
}
pingsLoaded++;
return addToPendingPings(file);
});
},
/**
@ -251,7 +199,7 @@ this.TelemetryFile = {
*
* @return {iterator}
*/
popPendingPings: function(reason) {
popPendingPings: function*(reason) {
while (pendingPings.length > 0) {
let data = pendingPings.pop();
// Send persisted pings to the test URL too.
@ -262,74 +210,53 @@ this.TelemetryFile = {
}
},
set shouldNotifyUponSave(value) {
shouldNotifyUponSave = value;
},
testLoadHistograms: function(file, sync, onLoad) {
testLoadHistograms: function(file) {
pingsLoaded = 0;
pingLoadsCompleted = 0;
this.loadHistograms(file, sync, onLoad);
return this.loadHistograms(file.path);
}
};
///// Utility functions
function pingFilePath(ping) {
return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug);
}
function getSaveFileForPing(ping) {
let file = ensurePingDirectory();
file.append(ping.slug);
return file;
};
function getPingDirectory() {
return Task.spawn(function*() {
let directory = TelemetryFile.pingDirectoryPath;
function ensurePingDirectory() {
let directory = Services.dirsvc.get("ProfD", Ci.nsILocalFile).clone();
directory.append("saved-telemetry-pings");
try {
directory.create(Ci.nsIFile.DIRECTORY_TYPE, RWX_OWNER);
} catch (e) {
// Already exists, just ignore this.
}
return directory;
};
function addToPendingPings(file, stream, onLoad, onDone) {
let success = false;
try {
let string = NetUtil.readInputStreamToString(stream, stream.available(),
{ charset: "UTF-8" });
stream.close();
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);
if (!isPingDirectoryCreated) {
yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });
isPingDirectoryCreated = true;
}
pingLoadsCompleted++;
pendingPings.push(ping);
success = true;
} catch (e) {
// An error reading the file, or an error parsing the contents.
stream.close(); // close is idempotent.
file.remove(true); // FIXME: Should be false, isn't it?
return directory;
});
}
function addToPendingPings(file) {
function onLoad(success) {
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
}
if (onLoad) {
onLoad(success);
}
return Task.spawn(function*() {
try {
let array = yield OS.File.read(file);
let decoder = new TextDecoder();
let string = decoder.decode(array);
if (pingLoadsCompleted == pingsLoaded) {
if (onDone) {
onDone();
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);
}
if (shouldNotifyUponSave) {
Services.obs.notifyObservers(null, "telemetry-test-load-complete", null);
}
}
};
function finishTelemetrySave(ok, stream) {
stream.close();
if (shouldNotifyUponSave && ok) {
Services.obs.notifyObservers(null, "telemetry-test-save-complete", null);
}
};
});
}

View File

@ -10,13 +10,15 @@ const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/debug.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.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);
#ifndef MOZ_WIDGET_GONK
Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
#endif
Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm");
Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
// When modifying the payload in incompatible ways, please bump this version number
const PAYLOAD_VERSION = 1;
@ -127,30 +129,57 @@ let processInfo = {
this.EXPORTED_SYMBOLS = ["TelemetryPing"];
this.TelemetryPing = Object.freeze({
/**
* Returns the current telemetry payload.
* @returns Object
*/
getPayload: function() {
return Impl.getPayload();
},
saveHistograms: function(aFile, aSync) {
return Impl.saveHistograms(aFile, aSync);
/**
* Save histograms to a file.
* Used only for testing purposes.
*
* @param {nsIFile} aFile The file to load from.
*/
testSaveHistograms: function(aFile) {
return Impl.testSaveHistograms(aFile);
},
/**
* Collect and store information about startup.
*/
gatherStartup: function() {
return Impl.gatherStartup();
},
enableLoadSaveNotifications: function() {
return Impl.enableLoadSaveNotifications();
},
cacheProfileDirectory: function() {
return Impl.cacheProfileDirectory();
},
/**
* Inform the ping which AddOns are installed.
*
* @param aAddOns - The AddOns.
*/
setAddOns: function(aAddOns) {
return Impl.setAddOns(aAddOns);
},
/**
* Send a ping to a test server. Used only for testing.
*
* @param aServer - The server.
*/
testPing: function(aServer) {
return Impl.testPing(aServer);
},
testLoadHistograms: function(aFile, aSync) {
return Impl.testLoadHistograms(aFile, aSync);
/**
* Load histograms from a file.
* Used only for testing purposes.
*
* @param aFile - File to load from.
*/
testLoadHistograms: function(aFile) {
return Impl.testLoadHistograms(aFile);
},
/**
* Returns the path component of the current submission URL.
* @returns String
*/
submissionPath: function() {
return Impl.submissionPath();
},
@ -163,24 +192,22 @@ this.TelemetryPing = Object.freeze({
* Used only for testing purposes.
*/
reset: function() {
this.uninstall();
this.setup();
return Task.spawn(function*(){
yield this.uninstall();
yield this.setup();
}.bind(this));
},
/**
* Used only for testing purposes.
*/
setup: function() {
Impl.setup(true);
return Impl.setup(true);
},
/**
* Used only for testing purposes.
*/
uninstall: function() {
try {
Impl.uninstall();
} catch (ex) {
// Ignore errors
}
return Impl.uninstall();
},
/**
* Descriptive metadata
@ -699,46 +726,15 @@ let Impl = {
send: function send(reason, server) {
// populate histograms one last time
this.gatherMemory();
this.sendPingsFromIterator(server, reason,
return this.sendPingsFromIterator(server, reason,
Iterator(this.popPayloads(reason)));
},
/**
* What we want to do is the following:
*
* for data in getPayloads(reason):
* if sending ping data to server failed:
* break;
*
* but we can't do that, since XMLHttpRequest is async. What we do
* instead is let this function control the essential looping logic
* and provide callbacks for XMLHttpRequest when a request has
* finished.
*/
sendPingsFromIterator: function sendPingsFromIterator(server, reason, i) {
function finishPings(reason) {
if (reason == "test-ping") {
Services.obs.notifyObservers(null, "telemetry-test-xhr-complete", null);
}
}
let p = [data for (data in i)].map((data) =>
this.doPing(server, data).then(null, () => TelemetryFile.savePing(data, true)));
let data = null;
try {
data = i.next();
} catch (e if e instanceof StopIteration) {
finishPings(reason);
return;
}
function onSuccess() {
this.sendPingsFromIterator(server, reason, i);
}
function onError() {
TelemetryFile.savePing(data, true);
// Notify that testing is complete, even if we didn't send everything.
finishPings(reason);
}
this.doPing(server, data,
onSuccess.bind(this), onError.bind(this));
return Promise.all(p);
},
finishPingRequest: function finishPingRequest(success, startTime, ping) {
@ -749,7 +745,9 @@ let Impl = {
hping.add(new Date() - startTime);
if (success) {
TelemetryFile.cleanupPingFile(ping);
return TelemetryFile.cleanupPingFile(ping);
} else {
return Promise.resolve();
}
},
@ -767,7 +765,8 @@ let Impl = {
return "/submit/telemetry/" + slug;
},
doPing: function doPing(server, ping, onSuccess, onError) {
doPing: function doPing(server, ping) {
let deferred = Promise.defer();
let url = server + this.submissionPath(ping);
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
@ -778,14 +777,19 @@ let Impl = {
let startTime = new Date();
function handler(success, callback) {
function handler(success) {
return function(event) {
this.finishPingRequest(success, startTime, ping);
callback();
if (success) {
deferred.resolve();
} else {
deferred.reject(event);
}
};
}
request.addEventListener("error", handler(false, onError).bind(this), false);
request.addEventListener("load", handler(true, onSuccess).bind(this), false);
request.addEventListener("error", handler(false).bind(this), false);
request.addEventListener("load", handler(true).bind(this), false);
request.setRequestHeader("Content-Encoding", "gzip");
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
@ -797,6 +801,7 @@ let Impl = {
.createInstance(Ci.nsIStringInputStream);
payloadStream.data = this.gzipCompressString(utf8Payload);
request.send(payloadStream);
return deferred.promise;
},
gzipCompressString: function gzipCompressString(string) {
@ -901,41 +906,39 @@ let Impl = {
// run various late initializers. Otherwise our gathered memory
// footprint and other numbers would be too optimistic.
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
function timerCallback() {
this._initialized = true;
TelemetryFile.loadSavedPings(false, (success =>
{
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
}), () =>
{
// If we have any TelemetryPings lying around, we'll be aggressive
// and try to send them all off ASAP.
if (TelemetryFile.pingsOverdue > 0) {
// It doesn't really matter what we pass to this.send as a reason,
// since it's never sent to the server. All that this.send does with
// the reason is check to make sure it's not a test-ping.
this.send("overdue-flush", this._server);
}
});
this.attachObservers();
this.gatherMemory();
let deferred = Promise.defer();
Telemetry.asyncFetchTelemetryData(function () {
});
delete this._timer;
function timerCallback() {
Task.spawn(function*(){
this._initialized = true;
yield TelemetryFile.loadSavedPings();
// If we have any TelemetryPings lying around, we'll be aggressive
// and try to send them all off ASAP.
if (TelemetryFile.pingsOverdue > 0) {
// It doesn't really matter what we pass to this.send as a reason,
// since it's never sent to the server. All that this.send does with
// the reason is check to make sure it's not a test-ping.
yield this.send("overdue-flush", this._server);
}
this.attachObservers();
this.gatherMemory();
Telemetry.asyncFetchTelemetryData(function () {});
delete this._timer;
deferred.resolve();
}.bind(this));
}
this._timer.initWithCallback(timerCallback.bind(this),
aTesting ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY,
Ci.nsITimer.TYPE_ONE_SHOT);
return deferred.promise;
},
testLoadHistograms: function testLoadHistograms(file, sync) {
TelemetryFile.testLoadHistograms(file, sync, (success =>
{
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
}));
testLoadHistograms: function testLoadHistograms(file) {
return TelemetryFile.testLoadHistograms(file);
},
getFlashVersion: function getFlashVersion() {
@ -952,13 +955,12 @@ let Impl = {
savePendingPings: function savePendingPings() {
let sessionPing = this.getSessionPayloadAndSlug("saved-session");
TelemetryFile.savePendingPings(sessionPing);
return TelemetryFile.savePendingPings(sessionPing);
},
saveHistograms: function saveHistograms(file, sync) {
TelemetryFile.savePingToFile(
this.getSessionPayloadAndSlug("saved-session"),
file, sync, true);
testSaveHistograms: function testSaveHistograms(file) {
return TelemetryFile.savePingToFile(this.getSessionPayloadAndSlug("saved-session"),
file.path, true);
},
/**
@ -979,6 +981,11 @@ let Impl = {
#ifdef MOZ_WIDGET_ANDROID
Services.obs.removeObserver(this, "application-background", false);
#endif
if (Telemetry.canSend) {
return this.savePendingPings();
} else {
Promise.resolve();
}
},
getPayload: function getPayload() {
@ -1002,10 +1009,6 @@ let Impl = {
this._slowSQLStartup = Telemetry.slowSQL;
},
enableLoadSaveNotifications: function enableLoadSaveNotifications() {
TelemetryFile.shouldNotifyUponSave = true;
},
setAddOns: function setAddOns(aAddOns) {
this._addons = aAddOns;
},
@ -1016,19 +1019,14 @@ let Impl = {
this._isIdleObserver = false;
}
if (aTest) {
this.send("test-ping", aServer);
return this.send("test-ping", aServer);
} else if (Telemetry.canSend) {
this.send("idle-daily", aServer);
return this.send("idle-daily", aServer);
}
},
testPing: function testPing(server) {
this.sendIdlePing(true, server);
},
cacheProfileDirectory: function cacheProfileDirectory() {
// This method doesn't do anything anymore
return;
return this.sendIdlePing(true, server);
},
/**
@ -1080,9 +1078,6 @@ let Impl = {
break;
case "profile-before-change2":
this.uninstall();
if (Telemetry.canSend) {
this.savePendingPings();
}
break;
#ifdef MOZ_WIDGET_ANDROID
@ -1109,5 +1104,5 @@ let Impl = {
break;
#endif
}
}
},
};

View File

@ -13,11 +13,13 @@ const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource://testing-common/httpd.js", this);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/TelemetryPing.jsm");
Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
const IGNORE_HISTOGRAM = "test::ignore_me";
const IGNORE_HISTOGRAM_TO_CLONE = "MEMORY_HEAP_ALLOCATED";
@ -36,45 +38,27 @@ const PR_TRUNCATE = 0x20;
const RW_OWNER = 0600;
const NUMBER_OF_THREADS_TO_LAUNCH = 30;
var gNumberOfThreadsLaunched = 0;
let gNumberOfThreadsLaunched = 0;
const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
var httpserver = new HttpServer();
var serverStarted = false;
var gFinished = false;
let gHttpServer = new HttpServer();
let gServerStarted = false;
let gRequestIterator = null;
function test_expired_histogram() {
var histogram_id = "FOOBAR";
var dummy = Telemetry.newHistogram(histogram_id, "30", 1, 2, 3, Telemetry.HISTOGRAM_EXPONENTIAL);
dummy.add(1);
do_check_eq(TelemetryPing.getPayload()["histograms"][histogram_id], undefined);
do_check_eq(TelemetryPing.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined);
}
function telemetry_ping () {
function sendPing () {
TelemetryPing.gatherStartup();
TelemetryPing.enableLoadSaveNotifications();
TelemetryPing.cacheProfileDirectory();
if (serverStarted) {
TelemetryPing.testPing("http://localhost:" + httpserver.identity.primaryPort);
if (gServerStarted) {
return TelemetryPing.testPing("http://localhost:" + gHttpServer.identity.primaryPort);
} else {
TelemetryPing.testPing("http://doesnotexist");
return TelemetryPing.testPing("http://doesnotexist");
}
}
// Mostly useful so that you can dump payloads from decodeRequestPayload.
function dummyHandler(request, response) {
let p = decodeRequestPayload(request);
return p;
}
function wrapWithExceptionHandler(f) {
function wrapper() {
function wrapper(...args) {
try {
f.apply(null, arguments);
f(...args);
} catch (ex if typeof(ex) == 'object') {
dump("Caught exception: " + ex.message + "\n");
dump(ex.stack);
@ -84,29 +68,11 @@ function wrapWithExceptionHandler(f) {
return wrapper;
}
function addWrappedObserver(f, topic) {
let wrappedObserver = wrapWithExceptionHandler(f);
Services.obs.addObserver(function g(aSubject, aTopic, aData) {
Services.obs.removeObserver(g, aTopic);
wrappedObserver(aSubject, aTopic, aData);
}, topic, false);
}
function registerPingHandler(handler) {
httpserver.registerPrefixHandler("/submit/telemetry/",
gHttpServer.registerPrefixHandler("/submit/telemetry/",
wrapWithExceptionHandler(handler));
}
function nonexistentServerObserver(aSubject, aTopic, aData) {
httpserver.start(-1);
serverStarted = true;
// Provide a dummy function so it returns 200 instead of 404 to telemetry.
registerPingHandler(dummyHandler);
addWrappedObserver(telemetryObserver, "telemetry-test-xhr-complete");
telemetry_ping();
}
function setupTestData() {
Telemetry.newHistogram(IGNORE_HISTOGRAM, "never", 1, 2, 3, Telemetry.HISTOGRAM_BOOLEAN);
Telemetry.histogramFrom(IGNORE_CLONED_HISTOGRAM, IGNORE_HISTOGRAM_TO_CLONE);
@ -133,16 +99,6 @@ function getSavedHistogramsFile(basename) {
return histogramsFile;
}
function telemetryObserver(aSubject, aTopic, aData) {
registerPingHandler(checkHistogramsSync);
let histogramsFile = getSavedHistogramsFile("saved-histograms.dat");
setupTestData();
TelemetryPing.saveHistograms(histogramsFile, true);
TelemetryPing.testLoadHistograms(histogramsFile, true);
telemetry_ping();
}
function decodeRequestPayload(request) {
let s = request.bodyInputStream;
let payload = null;
@ -204,9 +160,9 @@ function checkPayloadInfo(payload, reason) {
try {
// If we've not got nsIGfxInfoDebug, then this will throw and stop us doing
// this test.
var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
var isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
var isOSX = ("nsILocalFileMac" in Components.interfaces);
let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
let isOSX = ("nsILocalFileMac" in Components.interfaces);
if (isWindows || isOSX) {
do_check_true("adapterVendorID" in payload.info);
@ -240,7 +196,7 @@ function checkPayload(request, reason, successfulPings) {
do_check_true(!failedProfileLocksFile.exists());
var isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
if (isWindows) {
do_check_true(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0);
do_check_true(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0);
@ -312,72 +268,6 @@ function checkPayload(request, reason, successfulPings) {
("otherThreads" in payload.slowSQL));
}
function checkPersistedHistogramsSync(request, response) {
// Even though we have had two successful pings when this handler is
// run, we only had one successful ping when the histograms were
// saved.
checkPayload(request, "saved-session", 1);
addWrappedObserver(runAsyncTestObserver, "telemetry-test-xhr-complete");
}
function checkHistogramsSync(request, response) {
registerPingHandler(checkPersistedHistogramsSync);
checkPayload(request, "test-ping", 1);
}
function runAsyncTestObserver(aSubject, aTopic, aData) {
registerPingHandler(checkHistogramsAsync);
let histogramsFile = getSavedHistogramsFile("saved-histograms2.dat");
addWrappedObserver(function(aSubject, aTopic, aData) {
addWrappedObserver(function(aSubject, aTopic, aData) {
telemetry_ping();
}, "telemetry-test-load-complete");
TelemetryPing.testLoadHistograms(histogramsFile, false);
}, "telemetry-test-save-complete");
TelemetryPing.saveHistograms(histogramsFile, false);
}
function checkPersistedHistogramsAsync(request, response) {
// do not need the http server anymore
httpserver.stop(do_test_finished);
// Even though we have had four successful pings when this handler is
// run, we only had three successful pings when the histograms were
// saved.
checkPayload(request, "saved-session", 3);
runOldPingFileTest();
gFinished = true;
}
function checkHistogramsAsync(request, response) {
registerPingHandler(checkPersistedHistogramsAsync);
checkPayload(request, "test-ping", 3);
}
function runInvalidJSONTest() {
let histogramsFile = getSavedHistogramsFile("invalid-histograms.dat");
writeStringToFile(histogramsFile, "this.is.invalid.JSON");
do_check_true(histogramsFile.exists());
TelemetryPing.testLoadHistograms(histogramsFile, true);
do_check_false(histogramsFile.exists());
}
function runOldPingFileTest() {
let histogramsFile = getSavedHistogramsFile("old-histograms.dat");
TelemetryPing.saveHistograms(histogramsFile, true);
do_check_true(histogramsFile.exists());
let mtime = histogramsFile.lastModifiedTime;
histogramsFile.lastModifiedTime = mtime - (14 * 24 * 60 * 60 * 1000 + 60000); // 14 days, 1m
TelemetryPing.testLoadHistograms(histogramsFile, true);
do_check_false(histogramsFile.exists());
}
function dummyTheme(id) {
return {
id: id,
@ -390,7 +280,7 @@ function dummyTheme(id) {
}
// A fake plugin host for testing flash version telemetry
var PluginHost = {
let PluginHost = {
getPluginTags: function(countRef) {
let plugins = [{name: "Shockwave Flash", version: FLASH_VERSION}];
countRef.value = plugins.length;
@ -406,7 +296,7 @@ var PluginHost = {
}
}
var PluginHostFactory = {
let PluginHostFactory = {
createInstance: function (outer, iid) {
if (outer != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
@ -418,7 +308,7 @@ const PLUGINHOST_CONTRACTID = "@mozilla.org/plugin/host;1";
const PLUGINHOST_CID = Components.ID("{2329e6ea-1f15-4cbe-9ded-6e98e842de0e}");
function registerFakePluginHost() {
var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(PLUGINHOST_CID, "Fake Plugin Host",
PLUGINHOST_CONTRACTID, PluginHostFactory);
}
@ -452,7 +342,7 @@ function write_fake_failedprofilelocks_file() {
function run_test() {
do_test_pending();
try {
var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
gfxInfo.spoofVendorID("0xabcd");
gfxInfo.spoofDeviceID("0x1234");
} catch (x) {
@ -495,8 +385,6 @@ function run_test() {
}
function actualTest() {
// ensure that test runs to completion
do_register_cleanup(function () do_check_true(gFinished));
// try to make LightweightThemeManager do stuff
let gInternalManager = Cc["@mozilla.org/addons/integration;1"]
.getService(Ci.nsIObserver)
@ -508,12 +396,99 @@ function actualTest() {
// fake plugin host for consistent flash version data
registerFakePluginHost();
runInvalidJSONTest();
test_expired_histogram();
addWrappedObserver(nonexistentServerObserver, "telemetry-test-xhr-complete");
telemetry_ping();
// spin the event loop
do_test_pending();
do_test_finished();
run_next_test();
}
// Ensures that expired histograms are not part of the payload.
add_task(function* test_expiredHistogram() {
let histogram_id = "FOOBAR";
let dummy = Telemetry.newHistogram(histogram_id, "30", 1, 2, 3, Telemetry.HISTOGRAM_EXPONENTIAL);
dummy.add(1);
do_check_eq(TelemetryPing.getPayload()["histograms"][histogram_id], undefined);
do_check_eq(TelemetryPing.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined);
});
// Checks that an invalid histogram file is deleted if TelemetryFile fails to parse it.
add_task(function* test_runInvalidJSON() {
let histogramsFile = getSavedHistogramsFile("invalid-histograms.dat");
writeStringToFile(histogramsFile, "this.is.invalid.JSON");
do_check_true(histogramsFile.exists());
yield TelemetryPing.testLoadHistograms(histogramsFile);
do_check_false(histogramsFile.exists());
});
// Sends a ping to a non existing server.
add_task(function* test_noServerPing() {
yield sendPing();
});
// Checks that a sent ping is correctly received by a dummy http server.
add_task(function* test_simplePing() {
gHttpServer.start(-1);
gServerStarted = true;
gRequestIterator = Iterator(new Request());
yield sendPing();
decodeRequestPayload(yield gRequestIterator.next());
});
// Saves the current session histograms, reloads them, perfoms a ping
// and checks that the dummy http server received both the previously
// saved histograms and the new ones.
add_task(function* test_saveLoadPing() {
let histogramsFile = getSavedHistogramsFile("saved-histograms.dat");
setupTestData();
yield TelemetryPing.testSaveHistograms(histogramsFile);
yield TelemetryPing.testLoadHistograms(histogramsFile);
yield sendPing();
checkPayload((yield gRequestIterator.next()), "test-ping", 1);
checkPayload((yield gRequestIterator.next()), "saved-session", 1);
});
// Checks that an expired histogram file is deleted when loaded.
add_task(function* test_runOldPingFile() {
let histogramsFile = getSavedHistogramsFile("old-histograms.dat");
yield TelemetryPing.testSaveHistograms(histogramsFile);
do_check_true(histogramsFile.exists());
let mtime = histogramsFile.lastModifiedTime;
histogramsFile.lastModifiedTime = mtime - (14 * 24 * 60 * 60 * 1000 + 60000); // 14 days, 1m
yield TelemetryPing.testLoadHistograms(histogramsFile);
do_check_false(histogramsFile.exists());
});
add_task(function* stopServer(){
gHttpServer.stop(do_test_finished);
});
// An iterable sequence of http requests
function Request() {
let defers = [];
let current = 0;
function RequestIterator() {}
// Returns a promise that resolves to the next http request
RequestIterator.prototype.next = function() {
let deferred = defers[current++];
return deferred.promise;
}
this.__iterator__ = function(){
return new RequestIterator();
}
registerPingHandler((request, response) => {
let deferred = defers[defers.length - 1];
defers.push(Promise.defer());
deferred.resolve(request);
});
defers.push(Promise.defer());
}

View File

@ -13,57 +13,58 @@
* -> previousBuildID in telemetry, new value set in prefs.
*/
"use strict"
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/TelemetryPing.jsm");
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
// Force the Telemetry enabled preference so that TelemetryPing.reset() doesn't exit early.
Services.prefs.setBoolPref(TelemetryPing.Constants.PREF_ENABLED, true);
// Set up our dummy AppInfo object so we can control the appBuildID.
Cu.import("resource://testing-common/AppInfo.jsm");
Cu.import("resource://testing-common/AppInfo.jsm", this);
updateAppInfo();
// Check that when run with no previous build ID stored, we update the pref but do not
// put anything into the metadata.
function testFirstRun() {
TelemetryPing.reset();
add_task(function* test_firstRun() {
yield TelemetryPing.setup()
yield TelemetryPing.reset();
let metadata = TelemetryPing.getMetadata();
do_check_false("previousBuildID" in metadata);
let appBuildID = getAppInfo().appBuildID;
let buildIDPref = Services.prefs.getCharPref(TelemetryPing.Constants.PREF_PREVIOUS_BUILDID);
do_check_eq(appBuildID, buildIDPref);
}
});
// Check that a subsequent run with the same build ID does not put prev build ID in
// metadata. Assumes testFirstRun() has already been called to set the previousBuildID pref.
function testSecondRun() {
TelemetryPing.reset();
add_task(function* test_secondRun() {
yield TelemetryPing.reset();
let metadata = TelemetryPing.getMetadata();
do_check_false("previousBuildID" in metadata);
}
});
// Set up telemetry with a different app build ID and check that the old build ID
// is returned in the metadata and the pref is updated to the new build ID.
// Assumes testFirstRun() has been called to set the previousBuildID pref.
const NEW_BUILD_ID = "20130314";
function testNewBuild() {
add_task(function* test_newBuild() {
let info = getAppInfo();
let oldBuildID = info.appBuildID;
info.appBuildID = NEW_BUILD_ID;
TelemetryPing.reset();
yield TelemetryPing.reset();
let metadata = TelemetryPing.getMetadata();
do_check_eq(metadata.previousBuildID, oldBuildID);
let buildIDPref = Services.prefs.getCharPref(TelemetryPing.Constants.PREF_PREVIOUS_BUILDID);
do_check_eq(NEW_BUILD_ID, buildIDPref);
}
});
function run_test() {
// Make sure we have a profile directory.
do_get_profile();
testFirstRun();
testSecondRun();
testNewBuild();
run_next_test();
}

View File

@ -10,16 +10,20 @@
* overdue and recent pings.
*/
"use strict"
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/TelemetryFile.jsm");
Cu.import("resource://gre/modules/TelemetryPing.jsm");
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://testing-common/httpd.js", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/TelemetryFile.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
// We increment TelemetryFile's MAX_PING_FILE_AGE and
// OVERDUE_PING_FILE_AGE by 1ms so that our test pings exceed
@ -51,25 +55,30 @@ let gSeenPings = 0;
* @returns an Array with the created pings.
*/
function createSavedPings(aNum, aAge) {
// Create a TelemetryPing service that we can generate payloads from.
// Luckily, the TelemetryPing constructor does nothing that we need to
// clean up.
let pings = [];
let age = Date.now() - aAge;
for (let i = 0; i < aNum; ++i) {
let payload = TelemetryPing.getPayload();
let ping = { slug: "test-ping-" + gCreatedPings, reason: "test", payload: payload };
TelemetryFile.savePing(ping);
if (aAge) {
// savePing writes to the file synchronously, so we're good to
// modify the lastModifedTime now.
let file = getSaveFileForPing(ping);
file.lastModifiedTime = age;
return Task.spawn(function*(){
// Create a TelemetryPing service that we can generate payloads from.
// Luckily, the TelemetryPing constructor does nothing that we need to
// clean up.
let pings = [];
let age = Date.now() - aAge;
for (let i = 0; i < aNum; ++i) {
let payload = TelemetryPing.getPayload();
let ping = { slug: "test-ping-" + gCreatedPings, reason: "test", payload: payload };
yield TelemetryFile.savePing(ping);
if (aAge) {
// savePing writes to the file synchronously, so we're good to
// modify the lastModifedTime now.
let file = getSaveFileForPing(ping);
file.lastModifiedTime = age;
}
gCreatedPings++;
pings.push(ping);
}
gCreatedPings++;
pings.push(ping);
}
return pings;
return pings;
});
}
/**
@ -101,45 +110,13 @@ function getSaveFileForPing(aPing) {
}
/**
* Wait for PING_TIMEOUT_LENGTH ms, and make sure we didn't receive
* TelemetryPings in that time.
*
* @returns Promise
*/
function assertReceivedNoPings() {
let deferred = Promise.defer();
do_timeout(PING_TIMEOUT_LENGTH, function() {
if (gSeenPings > 0) {
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}
/**
* Returns a Promise that rejects if the number of TelemetryPings
* received by the HttpServer is not equal to aExpectedNum.
* Check if the number of TelemetryPings received by the
* HttpServer is not equal to aExpectedNum.
*
* @param aExpectedNum the number of pings we expect to receive.
* @returns Promise
*/
function assertReceivedPings(aExpectedNum) {
let deferred = Promise.defer();
do_timeout(PING_TIMEOUT_LENGTH, function() {
if (gSeenPings == aExpectedNum) {
deferred.resolve();
} else {
deferred.reject("Saw " + gSeenPings + " TelemetryPings, " +
"but expected " + aExpectedNum);
}
})
return deferred.promise;
do_check_eq(gSeenPings, aExpectedNum);
}
/**
@ -190,11 +167,13 @@ function stopHttpServer() {
* pings to put as back in the starting state.
*/
function resetTelemetry() {
TelemetryPing.uninstall();
// Quick and dirty way to clear TelemetryFile's pendingPings
// collection, and put it back in its initial state.
let gen = TelemetryFile.popPendingPings();
for (let item of gen) {};
return Task.spawn(function*(){
yield TelemetryPing.uninstall();
// Quick and dirty way to clear TelemetryFile's pendingPings
// collection, and put it back in its initial state.
let gen = TelemetryFile.popPendingPings();
for (let item of gen) {};
});
}
/**
@ -202,7 +181,7 @@ function resetTelemetry() {
* mode.
*/
function startTelemetry() {
TelemetryPing.setup();
return TelemetryPing.setup();
}
function run_test() {
@ -220,21 +199,21 @@ function run_test() {
* immediately and never sent.
*/
add_task(function test_expired_pings_are_deleted() {
let expiredPings = createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
startTelemetry();
yield assertReceivedNoPings();
let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
yield startTelemetry();
assertReceivedPings(0);
assertNotSaved(expiredPings);
resetTelemetry();
yield resetTelemetry();
});
/**
* Test that really recent pings are not sent on Telemetry initialization.
*/
add_task(function test_recent_pings_not_sent() {
let recentPings = createSavedPings(RECENT_PINGS);
startTelemetry();
yield assertReceivedNoPings();
resetTelemetry();
let recentPings = yield createSavedPings(RECENT_PINGS);
yield startTelemetry();
assertReceivedPings(0);
yield resetTelemetry();
clearPings(recentPings);
});
@ -244,17 +223,17 @@ add_task(function test_recent_pings_not_sent() {
* should just be deleted.
*/
add_task(function test_overdue_pings_trigger_send() {
let recentPings = createSavedPings(RECENT_PINGS);
let expiredPings = createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
let overduePings = createSavedPings(OVERDUE_PINGS, OVERDUE_PING_FILE_AGE);
let recentPings = yield createSavedPings(RECENT_PINGS);
let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
let overduePings = yield createSavedPings(OVERDUE_PINGS, OVERDUE_PING_FILE_AGE);
startTelemetry();
yield assertReceivedPings(TOTAL_EXPECTED_PINGS);
yield startTelemetry();
assertReceivedPings(TOTAL_EXPECTED_PINGS);
assertNotSaved(recentPings);
assertNotSaved(expiredPings);
assertNotSaved(overduePings);
resetTelemetry();
yield resetTelemetry();
});
add_task(function teardown() {