Bug 875562 - Part 6: Implement initial crash events; r=bsmedberg

Support for main process crashes, plugin crashes, and plugin hangs is
added to the crash manager. This includes a JS API for reporting them as
well as support for reading the event files.

There is still an issue of unbound growth on the store. This will be
addressed in a subsequent patch.

--HG--
extra : rebase_source : e714bf5f9c2fd9c50f2e40659c3b1a89591f3b1a
This commit is contained in:
Gregory Szorc 2014-01-28 15:46:03 -08:00
parent 5bfce7e8fc
commit 94c17fd393
4 changed files with 319 additions and 35 deletions

View File

@ -346,19 +346,62 @@ this.CrashManager.prototype = Object.freeze({
let decoder = new TextDecoder();
data = decoder.decode(data);
let sepIndex = data.indexOf("\n");
if (sepIndex == -1) {
return this.EVENT_FILE_ERROR_MALFORMED;
let type, time, payload;
let start = 0;
for (let i = 0; i < 2; i++) {
let index = data.indexOf("\n", start);
if (index == -1) {
return this.EVENT_FILE_ERROR_MALFORMED;
}
let sub = data.substring(start, index);
switch (i) {
case 0:
type = sub;
break;
case 1:
time = sub;
try {
time = parseInt(time, 10);
} catch (ex) {
return this.EVENT_FILE_ERROR_MALFORMED;
}
}
start = index + 1;
}
let date = new Date(time * 1000);
let payload = data.substring(start);
let type = data.substring(0, sepIndex);
let payload = data.substring(sepIndex + 1);
return this._handleEventFilePayload(entry, type, payload);
return this._handleEventFilePayload(store, entry, type, date, payload);
}.bind(this));
},
_handleEventFilePayload: function (entry, type, payload) {
_handleEventFilePayload: function (store, entry, type, date, payload) {
// The payload types and formats are documented in docs/crash-events.rst.
// Do not change the format of an existing type. Instead, invent a new
// type.
let eventMap = {
"crash.main.1": "addMainProcessCrash",
"crash.plugin.1": "addPluginCrash",
"hang.plugin.1": "addPluginHang",
};
if (type in eventMap) {
let lines = payload.split("\n");
if (lines.length > 1) {
this._log.warn("Multiple lines unexpected in payload for " +
entry.path);
return this.EVENT_FILE_ERROR_MALFORMED;
}
store[eventMap[type]](payload, date);
return this.EVENT_FILE_SUCCESS;
}
// DO NOT ADD NEW TYPES WITHOUT DOCUMENTING!
return this.EVENT_FILE_ERROR_UNKNOWN_EVENT;
},
@ -494,6 +537,10 @@ function CrashStore(storeDir, telemetrySizeKey) {
}
CrashStore.prototype = Object.freeze({
TYPE_MAIN_CRASH: "main-crash",
TYPE_PLUGIN_CRASH: "plugin-crash",
TYPE_PLUGIN_HANG: "plugin-hang",
/**
* Load data from disk.
*
@ -681,6 +728,87 @@ CrashStore.prototype = Object.freeze({
return null;
},
_ensureCrashRecord: function (id) {
if (!this._data.crashes.has(id)) {
this._data.crashes.set(id, {
id: id,
type: null,
crashDate: null,
});
}
return this._data.crashes.get(id);
},
/**
* Record the occurrence of a crash in the main process.
*
* @param id (string) Crash ID. Likely a UUID.
* @param date (Date) When the crash occurred.
*/
addMainProcessCrash: function (id, date) {
let r = this._ensureCrashRecord(id);
r.type = this.TYPE_MAIN_CRASH;
r.crashDate = date;
},
/**
* Record the occurrence of a crash in a plugin process.
*
* @param id (string) Crash ID. Likely a UUID.
* @param date (Date) When the crash occurred.
*/
addPluginCrash: function (id, date) {
let r = this._ensureCrashRecord(id);
r.type = this.TYPE_PLUGIN_CRASH;
r.crashDate = date;
},
/**
* Record the occurrence of a hang in a plugin process.
*
* @param id (string) Crash ID. Likely a UUID.
* @param date (Date) When the hang was reported.
*/
addPluginHang: function (id, date) {
let r = this._ensureCrashRecord(id);
r.type = this.TYPE_PLUGIN_HANG;
r.crashDate = date;
},
get mainProcessCrashes() {
let crashes = [];
for (let crash of this.crashes) {
if (crash.isMainProcessCrash) {
crashes.push(crash);
}
}
return crashes;
},
get pluginCrashes() {
let crashes = [];
for (let crash of this.crashes) {
if (crash.isPluginCrash) {
crashes.push(crash);
}
}
return crashes;
},
get pluginHangs() {
let crashes = [];
for (let crash of this.crashes) {
if (crash.isPluginHang) {
crashes.push(crash);
}
}
return crashes;
},
});
/**
@ -718,6 +846,22 @@ CrashRecord.prototype = Object.freeze({
// We currently only have 1 date, so this is easy.
return this._o.crashDate;
},
get type() {
return this._o.type;
},
get isMainProcessCrash() {
return this._o.type == CrashStore.prototype.TYPE_MAIN_CRASH;
},
get isPluginCrash() {
return this._o.type == CrashStore.prototype.TYPE_PLUGIN_CRASH;
},
get isPluginHang() {
return this._o.type == CrashStore.prototype.TYPE_PLUGIN_HANG;
},
});
/**

View File

@ -87,10 +87,12 @@ this.TestingCrashManager.prototype = {
});
},
createEventsFile: function (filename, name, content, index=0, date=new Date()) {
createEventsFile: function (filename, type, date, content, index=0) {
let path = OS.Path.join(this._eventsDirs[index], filename);
let data = name + "\n" + content;
let data = type + "\n" +
Math.floor(date.getTime() / 1000) + "\n" +
content;
let encoder = new TextEncoder();
let array = encoder.encode(data);
@ -105,21 +107,22 @@ this.TestingCrashManager.prototype = {
*
* We can probably delete this once we have actual events defined.
*/
_handleEventFilePayload: function (entry, type, payload) {
_handleEventFilePayload: function (store, entry, type, date, payload) {
if (type == "test.1") {
if (payload == "malformed") {
return this.EVENT_FILE_ERROR_MALFORMED;
} else if (payload == "success") {
return this.EVENT_FILE_SUCCESS;
} else {
// Payload is crash ID. Create a duommy record.
this._store._data.crashes.set(payload, {id: payload, crashDate: entry.date});
return this.EVENT_FILE_SUCCESS;
return this.EVENT_FILE_ERROR_UNKNOWN_EVENT;
}
}
return CrashManager.prototype._handleEventFilePayload.call(this, type,
return CrashManager.prototype._handleEventFilePayload.call(this,
store,
entry,
type,
date,
payload);
},
};

View File

@ -12,6 +12,8 @@ Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://testing-common/CrashManagerTest.jsm", this);
const DUMMY_DATE = new Date(1391043519000);
function run_test() {
do_get_profile();
run_next_test();
@ -111,9 +113,9 @@ add_task(function* test_store_expires() {
// Ensure discovery of unprocessed events files works.
add_task(function* test_unprocessed_events_files() {
let m = yield getManager();
yield m.createEventsFile("1", "test.1", "foo", 0);
yield m.createEventsFile("2", "test.1", "bar", 0);
yield m.createEventsFile("1", "test.1", "baz", 1);
yield m.createEventsFile("1", "test.1", new Date(), "foo", 0);
yield m.createEventsFile("2", "test.1", new Date(), "bar", 0);
yield m.createEventsFile("1", "test.1", new Date(), "baz", 1);
let paths = yield m._getUnprocessedEventsFiles();
Assert.equal(paths.length, 3);
@ -133,7 +135,7 @@ add_task(function* test_aggregate_events_locking() {
add_task(function* test_malformed_files_deleted() {
let m = yield getManager();
yield m.createEventsFile("1", "test.1", "malformed");
yield m.createEventsFile("1", "crash.main.1", new Date(), "foo\nbar");
let count = yield m.aggregateEventsFiles();
Assert.equal(count, 1);
@ -148,8 +150,8 @@ add_task(function* test_malformed_files_deleted() {
add_task(function* test_aggregate_ignore_unknown_events() {
let m = yield getManager();
yield m.createEventsFile("1", "test.1", "success");
yield m.createEventsFile("2", "foobar.1", "dummy");
yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1");
yield m.createEventsFile("2", "foobar.1", new Date(), "dummy");
let count = yield m.aggregateEventsFiles();
Assert.equal(count, 2);
@ -165,8 +167,8 @@ add_task(function* test_prune_old() {
let m = yield getManager();
let oldDate = new Date(Date.now() - 86400000);
let newDate = new Date(Date.now() - 10000);
yield m.createEventsFile("1", "test.1", "id1", 0, oldDate);
yield m.createEventsFile("2", "test.1", "id2", 0, newDate);
yield m.createEventsFile("1", "crash.main.1", oldDate, "id1");
yield m.createEventsFile("2", "crash.plugin.1", newDate, "id2");
yield m.aggregateEventsFiles();
@ -190,13 +192,69 @@ add_task(function* test_prune_old() {
add_task(function* test_schedule_maintenance() {
let m = yield getManager();
yield m.createEventsFile("1", "test.1", "id1");
yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1");
let oldDate = new Date(Date.now() - m.PURGE_OLDER_THAN_DAYS * 2 * 24 * 60 * 60 * 1000);
yield m.createEventsFile("2", "test.1", "id2", 0, oldDate);
yield m.createEventsFile("2", "crash.main.1", oldDate, "id2");
yield m.scheduleMaintenance(25);
let crashes = yield m.getCrashes();
Assert.equal(crashes.length, 1);
Assert.equal(crashes[0].id, "id1");
});
add_task(function* test_main_crash_event_file() {
let m = yield getManager();
yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1");
let count = yield m.aggregateEventsFiles();
Assert.equal(count, 1);
let crashes = yield m.getCrashes();
Assert.equal(crashes.length, 1);
Assert.equal(crashes[0].id, "id1");
Assert.equal(crashes[0].type, "main-crash");
Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE);
count = yield m.aggregateEventsFiles();
Assert.equal(count, 0);
});
add_task(function* test_multiline_crash_id_rejected() {
let m = yield getManager();
yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1\nid2");
yield m.aggregateEventsFiles();
let crashes = yield m.getCrashes();
Assert.equal(crashes.length, 0);
});
add_task(function* test_plugin_crash_event_file() {
let m = yield getManager();
yield m.createEventsFile("1", "crash.plugin.1", DUMMY_DATE, "id1");
let count = yield m.aggregateEventsFiles();
Assert.equal(count, 1);
let crashes = yield m.getCrashes();
Assert.equal(crashes.length, 1);
Assert.equal(crashes[0].id, "id1");
Assert.equal(crashes[0].type, "plugin-crash");
Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE);
count = yield m.aggregateEventsFiles();
Assert.equal(count, 0);
});
add_task(function* test_plugin_hang_event_file() {
let m = yield getManager();
yield m.createEventsFile("1", "hang.plugin.1", DUMMY_DATE, "id1");
let count = yield m.aggregateEventsFiles();
Assert.equal(count, 1);
let crashes = yield m.getCrashes();
Assert.equal(crashes.length, 1);
Assert.equal(crashes[0].id, "id1");
Assert.equal(crashes[0].type, "plugin-hang");
Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE);
count = yield m.aggregateEventsFiles();
Assert.equal(count, 0);
});

View File

@ -25,10 +25,6 @@ function getStore() {
yield OS.File.makeDir(storeDir, {unixMode: OS.Constants.libc.S_IRWXU});
let s = new CrashStore(storeDir);
s._addCrash = (id, date) => {
s._data.crashes.set(id, {id: id, crashDate: date});
}
yield s.load();
return s;
@ -49,8 +45,7 @@ add_task(function test_add_crash() {
Assert.equal(s.crashesCount, 0);
let d = new Date(Date.now() - 5000);
// TODO use official APIs once they are implemented.
s._addCrash("id1", d);
s.addMainProcessCrash("id1", d);
Assert.equal(s.crashesCount, 1);
@ -61,7 +56,7 @@ add_task(function test_add_crash() {
Assert.equal(c.id, "id1", "ID set properly.");
Assert.equal(c.crashDate.getTime(), d.getTime(), "Date set.");
s._addCrash("id2", new Date());
s.addMainProcessCrash("id2", new Date());
Assert.equal(s.crashesCount, 2);
});
@ -72,8 +67,8 @@ add_task(function test_save_load() {
let d1 = new Date();
let d2 = new Date(d1.getTime() - 10000);
s._addCrash("id1", d1);
s._addCrash("id2", d2);
s.addMainProcessCrash("id1", d1);
s.addMainProcessCrash("id2", d2);
yield s.save();
@ -102,3 +97,87 @@ add_task(function test_corrupt_json() {
Assert.ok(s.corruptDate);
Assert.equal(date.getTime(), s.corruptDate.getTime());
});
add_task(function* test_add_main_crash() {
let s = yield getStore();
s.addMainProcessCrash("id1", new Date());
Assert.equal(s.crashesCount, 1);
let c = s.crashes[0];
Assert.ok(c.crashDate);
Assert.equal(c.type, bsp.CrashStore.prototype.TYPE_MAIN_CRASH);
Assert.ok(c.isMainProcessCrash);
s.addMainProcessCrash("id2", new Date());
Assert.equal(s.crashesCount, 2);
// Duplicate.
s.addMainProcessCrash("id1", new Date());
Assert.equal(s.crashesCount, 2);
Assert.equal(s.mainProcessCrashes.length, 2);
});
add_task(function* test_add_plugin_crash() {
let s = yield getStore();
s.addPluginCrash("id1", new Date());
Assert.equal(s.crashesCount, 1);
let c = s.crashes[0];
Assert.ok(c.crashDate);
Assert.equal(c.type, bsp.CrashStore.prototype.TYPE_PLUGIN_CRASH);
Assert.ok(c.isPluginCrash);
s.addPluginCrash("id2", new Date());
Assert.equal(s.crashesCount, 2);
s.addPluginCrash("id1", new Date());
Assert.equal(s.crashesCount, 2);
Assert.equal(s.pluginCrashes.length, 2);
});
add_task(function* test_add_plugin_hang() {
let s = yield getStore();
s.addPluginHang("id1", new Date());
Assert.equal(s.crashesCount, 1);
let c = s.crashes[0];
Assert.ok(c.crashDate);
Assert.equal(c.type, bsp.CrashStore.prototype.TYPE_PLUGIN_HANG);
Assert.ok(c.isPluginHang);
s.addPluginHang("id2", new Date());
Assert.equal(s.crashesCount, 2);
s.addPluginHang("id1", new Date());
Assert.equal(s.crashesCount, 2);
Assert.equal(s.pluginHangs.length, 2);
});
add_task(function* test_add_mixed_types() {
let s = yield getStore();
s.addMainProcessCrash("main", new Date());
s.addPluginCrash("pcrash", new Date());
s.addPluginHang("phang", new Date());
Assert.equal(s.crashesCount, 3);
yield s.save();
s._data.crashes.clear();
Assert.equal(s.crashesCount, 0);
yield s.load();
Assert.equal(s.crashesCount, 3);
Assert.equal(s.mainProcessCrashes.length, 1);
Assert.equal(s.pluginCrashes.length, 1);
Assert.equal(s.pluginHangs.length, 1);
});