177 lines
5.2 KiB
JavaScript
177 lines
5.2 KiB
JavaScript
/* 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
|
|
});
|
|
|
|
export function BHRTelemetryService() {
|
|
// Allow tests to get access to this object to verify it works correctly.
|
|
this.wrappedJSObject = this;
|
|
|
|
Services.obs.addObserver(this, "profile-before-change");
|
|
Services.obs.addObserver(this, "bhr-thread-hang");
|
|
Services.obs.addObserver(this, "idle-daily");
|
|
|
|
this.resetPayload();
|
|
}
|
|
|
|
BHRTelemetryService.prototype = Object.freeze({
|
|
classID: Components.ID("{117c8cdf-69e6-4f31-a439-b8a654c67127}"),
|
|
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
|
|
|
|
TRANSMIT_HANG_COUNT: 50,
|
|
|
|
resetPayload() {
|
|
this.startTime = +new Date();
|
|
this.payload = {
|
|
modules: [],
|
|
hangs: [],
|
|
};
|
|
this.clearPermahangFile = false;
|
|
},
|
|
|
|
recordHang({
|
|
duration,
|
|
thread,
|
|
runnableName,
|
|
process,
|
|
stack,
|
|
remoteType,
|
|
modules,
|
|
annotations,
|
|
wasPersisted,
|
|
}) {
|
|
if (!Services.telemetry.canRecordExtended) {
|
|
return;
|
|
}
|
|
|
|
// Create a mapping from module indicies in the original nsIHangDetails
|
|
// object to this.payload.modules indicies.
|
|
let moduleIdxs = modules.map(module => {
|
|
let idx = this.payload.modules.findIndex(m => {
|
|
return m[0] === module[0] && m[1] === module[1];
|
|
});
|
|
if (idx === -1) {
|
|
idx = this.payload.modules.length;
|
|
this.payload.modules.push(module);
|
|
}
|
|
return idx;
|
|
});
|
|
|
|
// Native stack frames are [modIdx, offset] arrays. If we have a valid
|
|
// module index, we want to map it to the this.payload.modules array.
|
|
for (let i = 0; i < stack.length; ++i) {
|
|
if (Array.isArray(stack[i]) && stack[i][0] !== -1) {
|
|
stack[i][0] = moduleIdxs[stack[i][0]];
|
|
} else if (typeof stack[i] == "string") {
|
|
// This is just a precaution - we don't currently know of sensitive
|
|
// URLs being included in label frames' dynamic strings which we
|
|
// include here, but this is just an added guard. Here we strip any
|
|
// string with a :// in it that isn't a chrome:// or resource://
|
|
// URL. This is not completely robust, but we are already trying to
|
|
// protect against this by only including dynamic strings from the
|
|
// opt-in AUTO_PROFILER_..._NONSENSITIVE macros.
|
|
let match = /[^\s]+:\/\/.*/.exec(stack[i]);
|
|
if (
|
|
match &&
|
|
!match[0].startsWith("chrome://") &&
|
|
!match[0].startsWith("resource://")
|
|
) {
|
|
stack[i] = stack[i].replace(match[0], "(excluded)");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create the hang object to record in the payload.
|
|
this.payload.hangs.push({
|
|
duration,
|
|
thread,
|
|
runnableName,
|
|
process,
|
|
remoteType,
|
|
annotations,
|
|
stack,
|
|
});
|
|
|
|
if (wasPersisted) {
|
|
this.clearPermahangFile = true;
|
|
}
|
|
|
|
// If we have collected enough hangs, we can submit the hangs we have
|
|
// collected to telemetry.
|
|
if (this.payload.hangs.length > this.TRANSMIT_HANG_COUNT) {
|
|
this.submit();
|
|
}
|
|
},
|
|
|
|
submit() {
|
|
if (this.clearPermahangFile) {
|
|
// NB: This is async but it is called from an Observer callback.
|
|
IOUtils.remove(
|
|
PathUtils.join(PathUtils.profileDir, "last_permahang.bin")
|
|
);
|
|
}
|
|
|
|
if (!Services.telemetry.canRecordExtended) {
|
|
return;
|
|
}
|
|
|
|
// NOTE: We check a separate bhrPing.enabled pref here. This pref is unset
|
|
// when running tests so that we run as much of BHR as possible (to catch
|
|
// errors) while avoiding timeouts caused by invoking `pingsender` during
|
|
// testing.
|
|
if (
|
|
Services.prefs.getBoolPref("toolkit.telemetry.bhrPing.enabled", false) &&
|
|
this.payload.hangs.length
|
|
) {
|
|
this.payload.timeSinceLastPing = new Date() - this.startTime;
|
|
lazy.TelemetryController.submitExternalPing("bhr", this.payload, {
|
|
addEnvironment: true,
|
|
});
|
|
|
|
Glean.hangs.modules.set(this.payload.modules);
|
|
let gleanHangs = this.payload.hangs.map(
|
|
({ stack, duration, ...restOfHang }) => ({
|
|
stack: stack.map(frame =>
|
|
typeof frame == "string"
|
|
? { frame }
|
|
: { module: frame[0], frame: frame[1] }
|
|
),
|
|
duration: Math.round(duration),
|
|
...restOfHang,
|
|
})
|
|
);
|
|
Glean.hangs.reports.set(gleanHangs);
|
|
GleanPings.hangReport.submit();
|
|
}
|
|
this.resetPayload();
|
|
},
|
|
|
|
shutdown() {
|
|
Services.obs.removeObserver(this, "profile-before-change");
|
|
Services.obs.removeObserver(this, "bhr-thread-hang");
|
|
Services.obs.removeObserver(this, "idle-daily");
|
|
this.submit();
|
|
},
|
|
|
|
observe(aSubject, aTopic) {
|
|
switch (aTopic) {
|
|
case "profile-after-change":
|
|
this.resetPayload();
|
|
break;
|
|
case "bhr-thread-hang":
|
|
this.recordHang(aSubject.QueryInterface(Ci.nsIHangDetails));
|
|
break;
|
|
case "profile-before-change":
|
|
this.shutdown();
|
|
break;
|
|
case "idle-daily":
|
|
this.submit();
|
|
break;
|
|
}
|
|
},
|
|
});
|