summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/geckoview/GeckoViewProcessHangMonitor.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/modules/geckoview/GeckoViewProcessHangMonitor.sys.mjs')
-rw-r--r--mobile/android/modules/geckoview/GeckoViewProcessHangMonitor.sys.mjs210
1 files changed, 210 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/GeckoViewProcessHangMonitor.sys.mjs b/mobile/android/modules/geckoview/GeckoViewProcessHangMonitor.sys.mjs
new file mode 100644
index 0000000000..7f6f14a29e
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewProcessHangMonitor.sys.mjs
@@ -0,0 +1,210 @@
+/* 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, aCallback) {
+ 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, aData) {
+ 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"
+);