Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
export class GeckoViewProcessHangMonitor extends GeckoViewModule {
constructor(aModuleInfo) {
super(aModuleInfo);
/**
* Collection of hang reports that haven't expired or been dismissed
* by the user. These are nsIHangReports.
*/
this._activeReports = new Set();
/**
* Collection of hang reports that have been suppressed for a short
* period of time. Keys are nsIHangReports. Values are timeouts for
* when the wait time expires.
*/
this._pausedReports = new Map();
/**
* Simple index used for report identification
*/
this._nextIndex = 0;
/**
* Map of report IDs to report objects.
* Keys are numbers. Values are nsIHangReports.
*/
this._reportIndex = new Map();
/**
* Map of report objects to report IDs.
* Keys are nsIHangReports. Values are numbers.
*/
this._reportLookupIndex = new Map();
}
onInit() {
debug`onInit`;
Services.obs.addObserver(this, "process-hang-report");
Services.obs.addObserver(this, "clear-hang-report");
}
onDestroy() {
debug`onDestroy`;
Services.obs.removeObserver(this, "process-hang-report");
Services.obs.removeObserver(this, "clear-hang-report");
}
onEnable() {
debug`onEnable`;
this.registerListener([
"GeckoView:HangReportStop",
"GeckoView:HangReportWait",
]);
}
onDisable() {
debug`onDisable`;
this.unregisterListener();
}
// Bundle event handler.
onEvent(aEvent, aData) {
debug`onEvent: event=${aEvent}, data=${aData}`;
if (this._reportIndex.has(aData.hangId)) {
const report = this._reportIndex.get(aData.hangId);
switch (aEvent) {
case "GeckoView:HangReportStop":
this.stopHang(report);
break;
case "GeckoView:HangReportWait":
this.pauseHang(report);
break;
}
} else {
debug`Report not found: reportIndex=${this._reportIndex}`;
}
}
// nsIObserver event handler
observe(aSubject, aTopic) {
debug`observe(aTopic=${aTopic})`;
aSubject.QueryInterface(Ci.nsIHangReport);
if (!aSubject.isReportForBrowserOrChildren(this.browser.frameLoader)) {
return;
}
switch (aTopic) {
case "process-hang-report": {
this.reportHang(aSubject);
break;
}
case "clear-hang-report": {
this.clearHang(aSubject);
break;
}
}
}
/**
* This timeout is the wait period applied after a user selects "Wait" in
* an existing notification.
*/
get WAIT_EXPIRATION_TIME() {
try {
return Services.prefs.getIntPref("browser.hangNotification.waitPeriod");
} catch (ex) {
return 10000;
}
}
/**
* Terminate whatever is causing this report, be it an add-on or page script.
* This is done without updating any report notifications.
*/
stopHang(report) {
report.terminateScript();
}
/**
*
*/
pauseHang(report) {
this._activeReports.delete(report);
// Create a new timeout with notify callback
const timer = this.window.setTimeout(() => {
for (const [stashedReport, otherTimer] of this._pausedReports) {
if (otherTimer === timer) {
this._pausedReports.delete(stashedReport);
// We're still hung, so move the report back to the active
// list.
this._activeReports.add(report);
break;
}
}
}, this.WAIT_EXPIRATION_TIME);
this._pausedReports.set(report, timer);
}
/**
* construct an information bundle
*/
notifyReport(report) {
this.eventDispatcher.sendRequest({
type: "GeckoView:HangReport",
hangId: this._reportLookupIndex.get(report),
scriptFileName: report.scriptFileName,
});
}
/**
* Handle a potentially new hang report.
*/
reportHang(report) {
// if we aren't enabled then default to stopping the script
if (!this.enabled) {
this.stopHang(report);
return;
}
// if we have already notified, remind
if (this._activeReports.has(report)) {
this.notifyReport(report);
return;
}
// If this hang was already reported and paused by the user then ignore it.
if (this._pausedReports.has(report)) {
return;
}
const index = this._nextIndex++;
this._reportLookupIndex.set(report, index);
this._reportIndex.set(index, report);
this._activeReports.add(report);
// Actually notify the new report
this.notifyReport(report);
}
clearHang(report) {
this._activeReports.delete(report);
const timer = this._pausedReports.get(report);
if (timer) {
this.window.clearTimeout(timer);
}
this._pausedReports.delete(report);
if (this._reportLookupIndex.has(report)) {
const index = this._reportLookupIndex.get(report);
this._reportIndex.delete(index);
}
this._reportLookupIndex.delete(report);
report.userCanceled();
}
}
const { debug, warn } = GeckoViewProcessHangMonitor.initLogging(
"GeckoViewProcessHangMonitor"
);