Backed out changeset bafe9571f3e8 (bug 839794) for xpcshell failures.

This commit is contained in:
Ryan VanderMeulen 2014-01-27 10:02:13 -05:00
parent a9e24185ca
commit db3da3f56c
5 changed files with 535 additions and 412 deletions

View File

@ -1,8 +1,3 @@
/* -*- 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"];
@ -12,13 +7,20 @@ 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/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 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);
const Telemetry = Services.telemetry;
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);
// Files that have been lying around for longer than MAX_PING_FILE_AGE are
// deleted without being loaded.
@ -32,6 +34,9 @@ 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;
@ -40,11 +45,13 @@ 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() {
@ -55,64 +62,94 @@ 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 {string} file The destination file.
* @param {nsIFile} file The destination file.
* @param {bool} sync If |true|, write synchronously. Deprecated.
* This argument should be |false|.
* @param {bool} overwrite If |true|, the file will be overwritten
* if it exists.
* @returns {promise}
*/
savePingToFile: function(ping, file, overwrite) {
savePingToFile: function(ping, file, sync, overwrite) {
let pingString = JSON.stringify(ping);
return OS.File.writeAtomic(file, pingString, {tmpPath: file + ".tmp",
noOverwrite: !overwrite});
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);
});
}
},
/**
* Save a ping to its file.
* Save a ping to its file, synchronously.
*
* @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);
return this.savePingToFile(ping, file, overwrite);
}.bind(this));
this.savePingToFile(ping,
getSaveFileForPing(ping), true, overwrite);
},
/**
* Save all pending pings.
* Save all pending pings, synchronously.
*
* @param {object} sessionPing The additional session ping.
* @returns {promise}
*/
savePendingPings: function(sessionPing) {
let p = pendingPings.reduce((p, ping) => {
p.push(this.savePing(ping, false));
return p;}, [this.savePing(sessionPing, true)]);
this.savePing(sessionPing, true);
pendingPings.forEach(function sppcb(e, i, a) {
this.savePing(e, false);
}, this);
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));
// 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) {
}
},
/**
@ -121,26 +158,24 @@ this.TelemetryFile = {
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @returns {promise}
* @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.
*/
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);
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);
}
yield iter.close();
}.bind(this));
} finally {
entries.close();
}
},
/**
@ -149,26 +184,43 @@ this.TelemetryFile = {
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @param {string} file The file to load.
* @returns {promise}
* @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.
*/
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);
}
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;
}
// This file is a bit stale, and overdue for sending.
if (now - info.lastModificationDate > OVERDUE_PING_FILE_AGE) {
pingsOverdue++;
}
// This file is a bit stale, and overdue for sending.
if (now - file.lastModifiedTime > OVERDUE_PING_FILE_AGE) {
pingsOverdue++;
}
pingsLoaded++;
return addToPendingPings(file);
});
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));
}
},
/**
@ -199,7 +251,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.
@ -210,53 +262,74 @@ this.TelemetryFile = {
}
},
testLoadHistograms: function(file) {
set shouldNotifyUponSave(value) {
shouldNotifyUponSave = value;
},
testLoadHistograms: function(file, sync, onLoad) {
pingsLoaded = 0;
return this.loadHistograms(file.path);
pingLoadsCompleted = 0;
this.loadHistograms(file, sync, onLoad);
}
};
///// Utility functions
function pingFilePath(ping) {
return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug);
}
function getPingDirectory() {
return Task.spawn(function*() {
let directory = TelemetryFile.pingDirectoryPath;
function getSaveFileForPing(ping) {
let file = ensurePingDirectory();
file.append(ping.slug);
return file;
};
if (!isPingDirectoryCreated) {
yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });
isPingDirectoryCreated = true;
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);
}
return directory;
});
}
function addToPendingPings(file) {
function onLoad(success) {
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
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 Task.spawn(function*() {
try {
let array = yield OS.File.read(file);
let decoder = new TextDecoder();
let string = decoder.decode(array);
if (onLoad) {
onLoad(success);
}
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 (pingLoadsCompleted == pingsLoaded) {
if (onDone) {
onDone();
}
});
}
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,15 +10,13 @@ const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
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/debug.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
#ifndef MOZ_WIDGET_GONK
Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
#endif
Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm");
// When modifying the payload in incompatible ways, please bump this version number
const PAYLOAD_VERSION = 1;
@ -129,57 +127,30 @@ let processInfo = {
this.EXPORTED_SYMBOLS = ["TelemetryPing"];
this.TelemetryPing = Object.freeze({
/**
* Returns the current telemetry payload.
* @returns Object
*/
getPayload: function() {
return Impl.getPayload();
},
/**
* 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);
saveHistograms: function(aFile, aSync) {
return Impl.saveHistograms(aFile, aSync);
},
/**
* Collect and store information about startup.
*/
gatherStartup: function() {
return Impl.gatherStartup();
},
/**
* Inform the ping which AddOns are installed.
*
* @param aAddOns - The AddOns.
*/
enableLoadSaveNotifications: function() {
return Impl.enableLoadSaveNotifications();
},
cacheProfileDirectory: function() {
return Impl.cacheProfileDirectory();
},
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);
},
/**
* Load histograms from a file.
* Used only for testing purposes.
*
* @param aFile - File to load from.
*/
testLoadHistograms: function(aFile) {
return Impl.testLoadHistograms(aFile);
testLoadHistograms: function(aFile, aSync) {
return Impl.testLoadHistograms(aFile, aSync);
},
/**
* Returns the path component of the current submission URL.
* @returns String
*/
submissionPath: function() {
return Impl.submissionPath();
},
@ -192,22 +163,24 @@ this.TelemetryPing = Object.freeze({
* Used only for testing purposes.
*/
reset: function() {
return Task.spawn(function*(){
yield this.uninstall();
yield this.setup();
}.bind(this));
this.uninstall();
this.setup();
},
/**
* Used only for testing purposes.
*/
setup: function() {
return Impl.setup(true);
Impl.setup(true);
},
/**
* Used only for testing purposes.
*/
uninstall: function() {
return Impl.uninstall();
try {
Impl.uninstall();
} catch (ex) {
// Ignore errors
}
},
/**
* Descriptive metadata
@ -724,15 +697,46 @@ let Impl = {
send: function send(reason, server) {
// populate histograms one last time
this.gatherMemory();
return this.sendPingsFromIterator(server, reason,
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) {
let p = [data for (data in i)].map((data) =>
this.doPing(server, data).then(null, () => TelemetryFile.savePing(data, true)));
function finishPings(reason) {
if (reason == "test-ping") {
Services.obs.notifyObservers(null, "telemetry-test-xhr-complete", null);
}
}
return Promise.all(p);
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));
},
finishPingRequest: function finishPingRequest(success, startTime, ping) {
@ -743,9 +747,7 @@ let Impl = {
hping.add(new Date() - startTime);
if (success) {
return TelemetryFile.cleanupPingFile(ping);
} else {
return Promise.resolve();
TelemetryFile.cleanupPingFile(ping);
}
},
@ -763,8 +765,7 @@ let Impl = {
return "/submit/telemetry/" + slug;
},
doPing: function doPing(server, ping) {
let deferred = Promise.defer();
doPing: function doPing(server, ping, onSuccess, onError) {
let url = server + this.submissionPath(ping);
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
@ -775,19 +776,14 @@ let Impl = {
let startTime = new Date();
function handler(success) {
function handler(success, callback) {
return function(event) {
this.finishPingRequest(success, startTime, ping);
if (success) {
deferred.resolve();
} else {
deferred.reject(event);
}
callback();
};
}
request.addEventListener("error", handler(false).bind(this), false);
request.addEventListener("load", handler(true).bind(this), false);
request.addEventListener("error", handler(false, onError).bind(this), false);
request.addEventListener("load", handler(true, onSuccess).bind(this), false);
request.setRequestHeader("Content-Encoding", "gzip");
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
@ -799,7 +795,6 @@ let Impl = {
.createInstance(Ci.nsIStringInputStream);
payloadStream.data = this.gzipCompressString(utf8Payload);
request.send(payloadStream);
return deferred.promise;
},
gzipCompressString: function gzipCompressString(string) {
@ -904,39 +899,41 @@ 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);
let deferred = Promise.defer();
function timerCallback() {
Task.spawn(function*(){
this._initialized = true;
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();
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));
Telemetry.asyncFetchTelemetryData(function () {
});
delete this._timer;
}
this._timer.initWithCallback(timerCallback.bind(this),
aTesting ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY,
Ci.nsITimer.TYPE_ONE_SHOT);
return deferred.promise;
},
testLoadHistograms: function testLoadHistograms(file) {
return TelemetryFile.testLoadHistograms(file);
testLoadHistograms: function testLoadHistograms(file, sync) {
TelemetryFile.testLoadHistograms(file, sync, (success =>
{
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
}));
},
getFlashVersion: function getFlashVersion() {
@ -953,12 +950,13 @@ let Impl = {
savePendingPings: function savePendingPings() {
let sessionPing = this.getSessionPayloadAndSlug("saved-session");
return TelemetryFile.savePendingPings(sessionPing);
TelemetryFile.savePendingPings(sessionPing);
},
testSaveHistograms: function testSaveHistograms(file) {
return TelemetryFile.savePingToFile(this.getSessionPayloadAndSlug("saved-session"),
file.path, true);
saveHistograms: function saveHistograms(file, sync) {
TelemetryFile.savePingToFile(
this.getSessionPayloadAndSlug("saved-session"),
file, sync, true);
},
/**
@ -979,11 +977,6 @@ 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() {
@ -1007,6 +1000,10 @@ let Impl = {
this._slowSQLStartup = Telemetry.slowSQL;
},
enableLoadSaveNotifications: function enableLoadSaveNotifications() {
TelemetryFile.shouldNotifyUponSave = true;
},
setAddOns: function setAddOns(aAddOns) {
this._addons = aAddOns;
},
@ -1017,14 +1014,19 @@ let Impl = {
this._isIdleObserver = false;
}
if (aTest) {
return this.send("test-ping", aServer);
this.send("test-ping", aServer);
} else if (Telemetry.canSend) {
return this.send("idle-daily", aServer);
this.send("idle-daily", aServer);
}
},
testPing: function testPing(server) {
return this.sendIdlePing(true, server);
this.sendIdlePing(true, server);
},
cacheProfileDirectory: function cacheProfileDirectory() {
// This method doesn't do anything anymore
return;
},
/**
@ -1076,6 +1078,9 @@ let Impl = {
break;
case "profile-before-change2":
this.uninstall();
if (Telemetry.canSend) {
this.savePendingPings();
}
break;
#ifdef MOZ_WIDGET_ANDROID
@ -1102,5 +1107,5 @@ let Impl = {
break;
#endif
}
},
}
};

View File

@ -13,13 +13,11 @@ const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://testing-common/httpd.js", this);
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource://gre/modules/Services.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);
Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/TelemetryPing.jsm");
const IGNORE_HISTOGRAM = "test::ignore_me";
const IGNORE_HISTOGRAM_TO_CLONE = "MEMORY_HEAP_ALLOCATED";
@ -38,27 +36,45 @@ const PR_TRUNCATE = 0x20;
const RW_OWNER = 0600;
const NUMBER_OF_THREADS_TO_LAUNCH = 30;
let gNumberOfThreadsLaunched = 0;
var gNumberOfThreadsLaunched = 0;
const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
let gHttpServer = new HttpServer();
let gServerStarted = false;
let gRequestIterator = null;
var httpserver = new HttpServer();
var serverStarted = false;
var gFinished = false;
function sendPing () {
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 () {
TelemetryPing.gatherStartup();
if (gServerStarted) {
return TelemetryPing.testPing("http://localhost:" + gHttpServer.identity.primaryPort);
TelemetryPing.enableLoadSaveNotifications();
TelemetryPing.cacheProfileDirectory();
if (serverStarted) {
TelemetryPing.testPing("http://localhost:" + httpserver.identity.primaryPort);
} else {
return TelemetryPing.testPing("http://doesnotexist");
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(...args) {
function wrapper() {
try {
f(...args);
f.apply(null, arguments);
} catch (ex if typeof(ex) == 'object') {
dump("Caught exception: " + ex.message + "\n");
dump(ex.stack);
@ -68,11 +84,29 @@ 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) {
gHttpServer.registerPrefixHandler("/submit/telemetry/",
httpserver.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);
@ -99,6 +133,16 @@ 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;
@ -160,9 +204,9 @@ function checkPayloadInfo(payload, reason) {
try {
// If we've not got nsIGfxInfoDebug, then this will throw and stop us doing
// this test.
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);
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);
if (isWindows || isOSX) {
do_check_true("adapterVendorID" in payload.info);
@ -196,7 +240,7 @@ function checkPayload(request, reason, successfulPings) {
do_check_true(!failedProfileLocksFile.exists());
let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
var 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);
@ -268,6 +312,72 @@ 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,
@ -280,7 +390,7 @@ function dummyTheme(id) {
}
// A fake plugin host for testing flash version telemetry
let PluginHost = {
var PluginHost = {
getPluginTags: function(countRef) {
let plugins = [{name: "Shockwave Flash", version: FLASH_VERSION}];
countRef.value = plugins.length;
@ -296,7 +406,7 @@ let PluginHost = {
}
}
let PluginHostFactory = {
var PluginHostFactory = {
createInstance: function (outer, iid) {
if (outer != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
@ -308,7 +418,7 @@ const PLUGINHOST_CONTRACTID = "@mozilla.org/plugin/host;1";
const PLUGINHOST_CID = Components.ID("{2329e6ea-1f15-4cbe-9ded-6e98e842de0e}");
function registerFakePluginHost() {
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(PLUGINHOST_CID, "Fake Plugin Host",
PLUGINHOST_CONTRACTID, PluginHostFactory);
}
@ -342,7 +452,7 @@ function write_fake_failedprofilelocks_file() {
function run_test() {
do_test_pending();
try {
let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
gfxInfo.spoofVendorID("0xabcd");
gfxInfo.spoofDeviceID("0x1234");
} catch (x) {
@ -385,6 +495,8 @@ 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)
@ -396,99 +508,12 @@ function actualTest() {
// fake plugin host for consistent flash version data
registerFakePluginHost();
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());
runInvalidJSONTest();
test_expired_histogram();
addWrappedObserver(nonexistentServerObserver, "telemetry-test-xhr-complete");
telemetry_ping();
// spin the event loop
do_test_pending();
do_test_finished();
}

View File

@ -13,58 +13,57 @@
* -> previousBuildID in telemetry, new value set in prefs.
*/
"use strict"
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/TelemetryPing.jsm");
// 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", this);
Cu.import("resource://testing-common/AppInfo.jsm");
updateAppInfo();
// Check that when run with no previous build ID stored, we update the pref but do not
// put anything into the metadata.
add_task(function* test_firstRun() {
yield TelemetryPing.setup()
yield TelemetryPing.reset();
function testFirstRun() {
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.
add_task(function* test_secondRun() {
yield TelemetryPing.reset();
function testSecondRun() {
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";
add_task(function* test_newBuild() {
function testNewBuild() {
let info = getAppInfo();
let oldBuildID = info.appBuildID;
info.appBuildID = NEW_BUILD_ID;
yield TelemetryPing.reset();
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();
run_next_test();
testFirstRun();
testSecondRun();
testNewBuild();
}

View File

@ -10,20 +10,16 @@
* 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", 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);
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");
// We increment TelemetryFile's MAX_PING_FILE_AGE and
// OVERDUE_PING_FILE_AGE by 1ms so that our test pings exceed
@ -55,30 +51,25 @@ let gSeenPings = 0;
* @returns an Array with the created pings.
*/
function createSavedPings(aNum, aAge) {
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);
// 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 pings;
});
gCreatedPings++;
pings.push(ping);
}
return pings;
}
/**
@ -110,13 +101,45 @@ function getSaveFileForPing(aPing) {
}
/**
* Check if the number of TelemetryPings received by the
* HttpServer is not equal to aExpectedNum.
* 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.
*
* @param aExpectedNum the number of pings we expect to receive.
* @returns Promise
*/
function assertReceivedPings(aExpectedNum) {
do_check_eq(gSeenPings, 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;
}
/**
@ -167,13 +190,11 @@ function stopHttpServer() {
* pings to put as back in the starting state.
*/
function resetTelemetry() {
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) {};
});
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) {};
}
/**
@ -181,7 +202,7 @@ function resetTelemetry() {
* mode.
*/
function startTelemetry() {
return TelemetryPing.setup();
TelemetryPing.setup();
}
function run_test() {
@ -199,21 +220,21 @@ function run_test() {
* immediately and never sent.
*/
add_task(function test_expired_pings_are_deleted() {
let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
yield startTelemetry();
assertReceivedPings(0);
let expiredPings = createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
startTelemetry();
yield assertReceivedNoPings();
assertNotSaved(expiredPings);
yield resetTelemetry();
resetTelemetry();
});
/**
* Test that really recent pings are not sent on Telemetry initialization.
*/
add_task(function test_recent_pings_not_sent() {
let recentPings = yield createSavedPings(RECENT_PINGS);
yield startTelemetry();
assertReceivedPings(0);
yield resetTelemetry();
let recentPings = createSavedPings(RECENT_PINGS);
startTelemetry();
yield assertReceivedNoPings();
resetTelemetry();
clearPings(recentPings);
});
@ -223,17 +244,17 @@ add_task(function test_recent_pings_not_sent() {
* should just be deleted.
*/
add_task(function test_overdue_pings_trigger_send() {
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);
let recentPings = createSavedPings(RECENT_PINGS);
let expiredPings = createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
let overduePings = createSavedPings(OVERDUE_PINGS, OVERDUE_PING_FILE_AGE);
yield startTelemetry();
assertReceivedPings(TOTAL_EXPECTED_PINGS);
startTelemetry();
yield assertReceivedPings(TOTAL_EXPECTED_PINGS);
assertNotSaved(recentPings);
assertNotSaved(expiredPings);
assertNotSaved(overduePings);
yield resetTelemetry();
resetTelemetry();
});
add_task(function teardown() {