Bug 1391594 - Unilaterally terminate script and plugin hangs during shutdown or when there are no windows. r?billm draft
authorMike Conley <mconley@mozilla.com>
Mon, 02 Oct 2017 18:38:27 -0400
changeset 682772 3ea3490286365321e793ae2a2dc53f0f3bad5bef
parent 680782 c6a2643362a67cdf7a87ac165454fce4b383debb
child 682773 5e5733884c26d3e854bf1e1b36cf38aa54267731
push id85140
push usermconley@mozilla.com
push dateWed, 18 Oct 2017 18:30:30 +0000
reviewersbillm
bugs1391594
milestone58.0a1
Bug 1391594 - Unilaterally terminate script and plugin hangs during shutdown or when there are no windows. r?billm MozReview-Commit-ID: 6DYDYTRVrYu
browser/modules/ProcessHangMonitor.jsm
--- a/browser/modules/ProcessHangMonitor.jsm
+++ b/browser/modules/ProcessHangMonitor.jsm
@@ -29,16 +29,22 @@ var ProcessHangMonitor = {
     try {
       return Services.prefs.getIntPref("browser.hangNotification.waitPeriod");
     } catch (ex) {
       return 10000;
     }
   },
 
   /**
+   * Should only be set to true once the quit-application-granted notification
+   * has been fired.
+   */
+  _shuttingDown: false,
+
+  /**
    * Collection of hang reports that haven't expired or been dismissed
    * by the user. These are nsIHangReports.
    */
   _activeReports: new Set(),
 
   /**
    * Collection of hang reports that have been suppressed for a short
    * period of time. Value is an nsITimer for when the wait time
@@ -47,16 +53,17 @@ var ProcessHangMonitor = {
   _pausedReports: new Map(),
 
   /**
    * Initialize hang reporting. Called once in the parent process.
    */
   init() {
     Services.obs.addObserver(this, "process-hang-report");
     Services.obs.addObserver(this, "clear-hang-report");
+    Services.obs.addObserver(this, "quit-application-granted");
     Services.obs.addObserver(this, "xpcom-shutdown");
     Services.ww.registerNotification(this);
   },
 
   /**
    * Terminate JavaScript associated with the hang being reported for
    * the selected browser in |win|.
    */
@@ -132,16 +139,37 @@ var ProcessHangMonitor = {
     switch (report.hangType) {
       case report.SLOW_SCRIPT:
         this.terminateGlobal(win);
         break;
     }
   },
 
   /**
+   * Terminate whatever is causing this report, be it an add-on, page script,
+   * or plug-in. This is done without updating any report notifications.
+   */
+  stopHang(report) {
+    switch (report.hangType) {
+      case report.SLOW_SCRIPT: {
+        if (report.addonId) {
+          report.terminateGlobal();
+        } else {
+          report.terminateScript();
+        }
+        break;
+      }
+      case report.PLUGIN_HANG: {
+        report.terminatePlugin();
+        break;
+      }
+    }
+  },
+
+  /**
    * Dismiss the notification, clear the report from the active list and set up
    * a new timer to track a wait period during which we won't notify.
    */
   waitLonger(win) {
     let report = this.findActiveReport(win.gBrowser.selectedBrowser);
     if (!report) {
       return;
     }
@@ -187,41 +215,76 @@ var ProcessHangMonitor = {
     }
     this.removeActiveReport(report);
 
     return func(report);
   },
 
   observe(subject, topic, data) {
     switch (topic) {
-      case "xpcom-shutdown":
+      case "xpcom-shutdown": {
         Services.obs.removeObserver(this, "xpcom-shutdown");
         Services.obs.removeObserver(this, "process-hang-report");
         Services.obs.removeObserver(this, "clear-hang-report");
+        Services.obs.removeObserver(this, "quit-application-granted");
         Services.ww.unregisterNotification(this);
         break;
+      }
 
-      case "process-hang-report":
+      case "quit-application-granted": {
+        this.onQuitApplicationGranted();
+        break;
+      }
+
+      case "process-hang-report": {
         this.reportHang(subject.QueryInterface(Ci.nsIHangReport));
         break;
+      }
 
-      case "clear-hang-report":
+      case "clear-hang-report": {
         this.clearHang(subject.QueryInterface(Ci.nsIHangReport));
         break;
+      }
 
-      case "domwindowopened":
+      case "domwindowopened": {
         // Install event listeners on the new window in case one of
         // its tabs is already hung.
         let win = subject.QueryInterface(Ci.nsIDOMWindow);
         let listener = (ev) => {
           win.removeEventListener("load", listener, true);
           this.updateWindows();
         };
         win.addEventListener("load", listener, true);
         break;
+      }
+    }
+  },
+
+  /**
+   * Called early on in the shutdown sequence. We take this opportunity to
+   * take any pre-existing hang reports, and terminate them. We also put
+   * ourselves in a state so that if any more hang reports show up while
+   * we're shutting down, we terminate them immediately.
+   */
+  onQuitApplicationGranted() {
+    this._shuttingDown = true;
+    this.stopAllHangs();
+    this.updateWindows();
+  },
+
+  stopAllHangs() {
+    for (let report of this._activeReports) {
+      this.stopHang(report);
+    }
+
+    this._activeReports = new Set();
+
+    for (let [pausedReport, ] of this._pausedReports) {
+      this.stopHang(pausedReport);
+      this.removePausedReport(pausedReport);
     }
   },
 
   /**
    * Find a active hang report for the given <browser> element.
    */
   findActiveReport(browser) {
     let frameLoader = browser.frameLoader;
@@ -270,16 +333,25 @@ var ProcessHangMonitor = {
   /**
    * Iterate over all XUL windows and ensure that the proper hang
    * reports are shown for each one. Also install event handlers in
    * each window to watch for events that would cause a different hang
    * report to be displayed.
    */
   updateWindows() {
     let e = Services.wm.getEnumerator("navigator:browser");
+
+    // If it turns out we have no windows (this can happen on macOS),
+    // we have no opportunity to ask the user whether or not they want
+    // to stop the hang or wait, so we'll opt for stopping the hang.
+    if (!e.hasMoreElements()) {
+      this.stopAllHangs();
+      return;
+    }
+
     while (e.hasMoreElements()) {
       let win = e.getNext();
 
       this.updateWindow(win);
 
       // Only listen for these events if there are active hang reports.
       if (this._activeReports.size) {
         this.trackWindow(win);
@@ -414,16 +486,21 @@ var ProcessHangMonitor = {
     }
   },
 
   /**
    * Handle a potentially new hang report. If it hasn't been seen
    * before, show a notification for it in all open XUL windows.
    */
   reportHang(report) {
+    if (this._shuttingDown) {
+      this.stopHang(report);
+      return;
+    }
+
     // If this hang was already reported reset the timer for it.
     if (this._activeReports.has(report)) {
       // if this report is in active but doesn't have a notification associated
       // with it, display a notification.
       this.updateWindows();
       return;
     }