diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 0bc459daccb..20dfcbb26db 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -319,6 +319,26 @@ orient="horizontal" hidden="true"/> + + + + + + + + + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index bc8d86f74c5..7ee0adc5e44 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -472,13 +472,9 @@ dataReportingNotification.button.label = Choose What I Share dataReportingNotification.button.accessKey = C # Process hang reporter -processHang.label = A web page is slowing down your browser. What would you like to do? -processHang.button_stop.label = Stop It -processHang.button_stop.accessKey = S -processHang.button_wait.label = Wait -processHang.button_wait.accessKey = W -processHang.button_debug.label = Debug Script -processHang.button_debug.accessKey = D +processHang.message = A web page is causing %1$S to run slowly. What would you like to do? +processHang.button.label = Options +processHang.button.accessKey = O # Webapps notification popup webapps.install = Install diff --git a/browser/modules/ProcessHangMonitor.jsm b/browser/modules/ProcessHangMonitor.jsm index 2989c614db3..7ee3875c17c 100644 --- a/browser/modules/ProcessHangMonitor.jsm +++ b/browser/modules/ProcessHangMonitor.jsm @@ -19,31 +19,13 @@ Cu.import("resource://gre/modules/Services.jsm"); * the platform interface. */ +/** + * If a hang hasn't been reported for more than 10 seconds, assume the + * content process has gotten unstuck (and hide the hang notification). + */ +const HANG_EXPIRATION_TIME = 10000; + var ProcessHangMonitor = { - /** - * If a hang hasn't been reported for more than 10 seconds, assume the - * content process has gotten unstuck (and hide the hang notification). - */ - get HANG_EXPIRATION_TIME() { - try { - return Services.prefs.getIntPref("browser.hangNotification.expiration"); - } catch (ex) { - return 10000; - } - }, - - /** - * 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; - } - }, - /** * Collection of hang reports that haven't expired or been dismissed * by the user. The keys are nsIHangReports and values keys are @@ -52,12 +34,6 @@ var ProcessHangMonitor = { */ _activeReports: new Map(), - /** - * Collection of hang reports that have been suppressed for a - * short period of time. - */ - _pausedReports: new Map(), - /** * Initialize hang reporting. Called once in the parent process. */ @@ -94,78 +70,46 @@ var ProcessHangMonitor = { }, /** - * Terminate the plugin process associated with a hang being reported - * for the selected browser in |win|. Will attempt to generate a combined - * crash report for all processes. + * Kill the plugin process causing the hang being reported for the + * selected browser in |win|. */ terminatePlugin: function(win) { this.handleUserInput(win, report => report.terminatePlugin()); }, /** - * Dismiss the browser notification and invoke an appropriate action based on - * the hang type. + * Kill the content process causing the hang being reported for the selected + * browser in |win|. */ - stopIt: function (win) { - let report = this.findActiveReport(win.gBrowser.selectedBrowser); - if (!report) { - return; - } - - switch (report.hangType) { - case report.SLOW_SCRIPT: - this.terminateScript(win); - break; - case report.PLUGIN_HANG: - this.terminatePlugin(win); - break; - } + terminateProcess: function(win) { + this.handleUserInput(win, report => report.terminateProcess()); }, /** - * 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. + * Update the "Options" pop-up menu for the hang notification + * associated with the selected browser in |win|. The menu should + * display only options that are relevant to the given report. */ - waitLonger: function(win) { - let report = this.findActiveReport(win.gBrowser.selectedBrowser); + refreshMenu: function(win) { + let report = this.findReport(win.gBrowser.selectedBrowser); if (!report) { return; } - // Remove the report from the active list and cancel its timer. - this.removeActiveReport(report); - // NOTE, we didn't call userCanceled on nsIHangReport here. This insures - // we don't repeatedly generate and cache crash report data for this hang - // in the process hang reporter. It already has one report for the browser - // process we want it hold onto. + function setVisible(id, visible) { + let item = win.document.getElementById(id); + item.hidden = !visible; + } - // Create a new wait timer with notify callback - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(() => { - for (let [stashedReport, otherTimer] of this._pausedReports) { - if (otherTimer === timer) { - this.removePausedReport(stashedReport); - - // Create a new notification display timeout timer - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(this, this.HANG_EXPIRATION_TIME, timer.TYPE_ONE_SHOT); - - // Store the timer in the active reports map. If we receive a new - // observer notification for this hang, we'll redisplay the browser - // notification in reportHang below. If we do not receive a new - // observer, timer will take care of cleaning up resources associated - // with this hang. The observer for active hangs fires about once - // a second. - this._activeReports.set(report, timer); - break; - } - } - }, this.WAIT_EXPIRATION_TIME, timer.TYPE_ONE_SHOT); - - this._pausedReports.set(report, timer); - - // remove the browser notification associated with this hang - this.updateWindows(); + if (report.hangType == report.SLOW_SCRIPT) { + setVisible("processHangTerminateScript", true); + setVisible("processHangDebugScript", true); + setVisible("processHangTerminatePlugin", false); + } else if (report.hangType == report.PLUGIN_HANG) { + setVisible("processHangTerminateScript", false); + setVisible("processHangDebugScript", false); + setVisible("processHangTerminatePlugin", true); + } }, /** @@ -174,11 +118,11 @@ var ProcessHangMonitor = { * about it. */ handleUserInput: function(win, func) { - let report = this.findActiveReport(win.gBrowser.selectedBrowser); + let report = this.findReport(win.gBrowser.selectedBrowser); if (!report) { return; } - this.removeActiveReport(report); + this.removeReport(report); return func(report); }, @@ -209,9 +153,9 @@ var ProcessHangMonitor = { }, /** - * Find a active hang report for the given element. + * Find any active hang reports for the given element. */ - findActiveReport: function(browser) { + findReport: function(browser) { let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; for (let [report, timer] of this._activeReports) { if (report.isReportForBrowser(frameLoader)) { @@ -221,44 +165,6 @@ var ProcessHangMonitor = { return null; }, - /** - * Find a paused hang report for the given element. - */ - findPausedReport: function(browser) { - let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; - for (let [report, timer] of this._pausedReports) { - if (report.isReportForBrowser(frameLoader)) { - return report; - } - } - return null; - }, - - /** - * Remove an active hang report from the active list and cancel the timer - * associated with it. - */ - removeActiveReport: function(report) { - let timer = this._activeReports.get(report); - if (timer) { - timer.cancel(); - } - this._activeReports.delete(report); - this.updateWindows(); - }, - - /** - * Remove a paused hang report from the paused list and cancel the timer - * associated with it. - */ - removePausedReport: function(report) { - let timer = this._pausedReports.get(report); - if (timer) { - timer.cancel(); - } - this._pausedReports.delete(report); - }, - /** * Iterate over all XUL windows and ensure that the proper hang * reports are shown for each one. Also install event handlers in @@ -285,7 +191,7 @@ var ProcessHangMonitor = { * If there is a hang report for the current tab in |win|, display it. */ updateWindow: function(win) { - let report = this.findActiveReport(win.gBrowser.selectedBrowser); + let report = this.findReport(win.gBrowser.selectedBrowser); if (report) { this.showNotification(win, report); @@ -306,36 +212,19 @@ var ProcessHangMonitor = { let bundle = win.gNavigatorBundle; let brandBundle = win.document.getElementById("bundle_brand"); + let appName = brandBundle.getString("brandShortName"); + let message = bundle.getFormattedString( + "processHang.message", + [appName]); let buttons = [{ - label: bundle.getString("processHang.button_stop.label"), - accessKey: bundle.getString("processHang.button_stop.accessKey"), - callback: function() { - ProcessHangMonitor.stopIt(win); - } - }, - { - label: bundle.getString("processHang.button_wait.label"), - accessKey: bundle.getString("processHang.button_wait.accessKey"), - callback: function() { - ProcessHangMonitor.waitLonger(win); - } - }]; + label: bundle.getString("processHang.button.label"), + accessKey: bundle.getString("processHang.button.accessKey"), + popup: "processHangOptions", + callback: null, + }]; -#ifdef MOZ_DEV_EDITION - if (report.hangType == report.SLOW_SCRIPT) { - buttons.push({ - label: bundle.getString("processHang.button_debug.label"), - accessKey: bundle.getString("processHang.button_debug.accessKey"), - callback: function() { - ProcessHangMonitor.debugScript(win); - } - }); - } -#endif - - nb.appendNotification(bundle.getString("processHang.label"), - "process-hang", + nb.appendNotification(message, "process-hang", "chrome://browser/content/aboutRobots-icon.png", nb.PRIORITY_WARNING_HIGH, buttons); }, @@ -381,19 +270,11 @@ var ProcessHangMonitor = { * before, show a notification for it in all open XUL windows. */ reportHang: function(report) { - // If this hang was already reported reset the timer for it. + // If this hang was already reported, then reset the timer for it. if (this._activeReports.has(report)) { let timer = this._activeReports.get(report); timer.cancel(); - timer.initWithCallback(this, this.HANG_EXPIRATION_TIME, timer.TYPE_ONE_SHOT); - // if this report is in active but doesn't have a notification associated - // with it, display a notification. - this.updateWindows(); - return; - } - - // If this hang was already reported and paused by the user ignore it. - if (this._pausedReports.has(report)) { + timer.initWithCallback(this, HANG_EXPIRATION_TIME, timer.TYPE_ONE_SHOT); return; } @@ -410,19 +291,28 @@ var ProcessHangMonitor = { // Otherwise create a new timer and display the report. let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(this, this.HANG_EXPIRATION_TIME, timer.TYPE_ONE_SHOT); + timer.initWithCallback(this, HANG_EXPIRATION_TIME, timer.TYPE_ONE_SHOT); this._activeReports.set(report, timer); this.updateWindows(); }, + /** + * Dismiss a hang report because the user closed the notification + * for it or the report expired. + */ + removeReport: function(report) { + this._activeReports.delete(report); + this.updateWindows(); + }, + /** * Callback for when HANG_EXPIRATION_TIME has elapsed. */ notify: function(timer) { for (let [otherReport, otherTimer] of this._activeReports) { if (otherTimer === timer) { - this.removeActiveReport(otherReport); + this.removeReport(otherReport); otherReport.userCanceled(); break; } diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 4e663ace5f7..4f6038e0cb6 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -33,6 +33,7 @@ EXTRA_JS_MODULES += [ 'offlineAppCache.jsm', 'PanelFrame.jsm', 'PluginContent.jsm', + 'ProcessHangMonitor.jsm', 'ReaderParent.jsm', 'RecentWindow.jsm', 'RemotePrompt.jsm', @@ -45,10 +46,6 @@ EXTRA_JS_MODULES += [ 'webrtcUI.jsm', ] -EXTRA_PP_JS_MODULES += [ - 'ProcessHangMonitor.jsm' -] - if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': EXTRA_JS_MODULES += [ 'Windows8WindowFrameColor.jsm', diff --git a/browser/modules/test/browser.ini b/browser/modules/test/browser.ini index 60b0ac299ca..927644f651f 100644 --- a/browser/modules/test/browser.ini +++ b/browser/modules/test/browser.ini @@ -3,8 +3,6 @@ support-files = head.js [browser_BrowserUITelemetry_buckets.js] -[browser_ProcessHangNotifications.js] -skip-if = !e10s [browser_ContentSearch.js] skip-if = e10s support-files = diff --git a/browser/modules/test/browser_ProcessHangNotifications.js b/browser/modules/test/browser_ProcessHangNotifications.js deleted file mode 100644 index 54fc5af47e4..00000000000 --- a/browser/modules/test/browser_ProcessHangNotifications.js +++ /dev/null @@ -1,185 +0,0 @@ - -Cu.import("resource://gre/modules/UpdateUtils.jsm"); - -function getNotificationBox(aWindow) { - return aWindow.document.getElementById("high-priority-global-notificationbox"); -} - -function promiseNotificationShown(aWindow, aName) { - return new Promise((resolve) => { - let notification = getNotificationBox(aWindow); - notification.addEventListener("AlertActive", function active() { - notification.removeEventListener("AlertActive", active, true); - is(notification.allNotifications.length, 1, "Notification Displayed."); - resolve(notification); - }); - }); -} - -function promiseReportCallMade(aValue) { - return new Promise((resolve) => { - let old = gTestHangReport.testCallback; - gTestHangReport.testCallback = function (val) { - gTestHangReport.testCallback = old; - is(aValue, val, "was the correct method call made on the hang report object?"); - resolve(); - }; - }); -} - -function pushPrefs(...aPrefs) { - return new Promise((resolve) => { - SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve); - resolve(); - }); -} - -function popPrefs() { - return new Promise((resolve) => { - SpecialPowers.popPrefEnv(resolve); - resolve(); - }); -} - -let gTestHangReport = { - SLOW_SCRIPT: 1, - PLUGIN_HANG: 2, - - TEST_CALLBACK_CANCELED: 1, - TEST_CALLBACK_TERMSCRIPT: 2, - TEST_CALLBACK_TERMPLUGIN: 3, - - _hangType: 1, - _tcb: function (aCallbackType) {}, - - get hangType() { - return this._hangType; - }, - - set hangType(aValue) { - this._hangType = aValue; - }, - - set testCallback(aValue) { - this._tcb = aValue; - }, - - QueryInterface: function (aIID) { - if (aIID.equals(Components.interfaces.nsIHangReport) || - aIID.equals(Components.interfaces.nsISupports)) - return this; - throw Components.results.NS_NOINTERFACE; - }, - - userCanceled: function () { - this._tcb(this.TEST_CALLBACK_CANCELED); - }, - - terminateScript: function () { - this._tcb(this.TEST_CALLBACK_TERMSCRIPT); - }, - - terminatePlugin: function () { - this._tcb(this.TEST_CALLBACK_TERMPLUGIN); - }, - - isReportForBrowser: function(aFrameLoader) { - return true; - } -}; - -// on dev edition we add a button for js debugging of hung scripts. -let buttonCount = (UpdateUtils.UpdateChannel == "aurora" ? 3 : 2); - -/** - * Test if hang reports receive a terminate script callback when the user selects - * stop in response to a script hang. - */ - -add_task(function* terminateScriptTest() { - let promise = promiseNotificationShown(window, "process-hang"); - Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); - let notification = yield promise; - - let buttons = notification.currentNotification.getElementsByTagName("button"); - is(buttons.length, buttonCount, "proper number of buttons"); - - // Click the "Stop It" button, we should get a terminate script callback - gTestHangReport.hangType = gTestHangReport.SLOW_SCRIPT; - promise = promiseReportCallMade(gTestHangReport.TEST_CALLBACK_TERMSCRIPT); - buttons[0].click(); - yield promise; -}); - -/** - * Test if hang reports receive user canceled callbacks after a user selects wait - * and the browser frees up from a script hang on its own. - */ - -add_task(function* waitForScriptTest() { - let promise = promiseNotificationShown(window, "process-hang"); - Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); - let notification = yield promise; - - let buttons = notification.currentNotification.getElementsByTagName("button"); - is(buttons.length, buttonCount, "proper number of buttons"); - - yield pushPrefs(["browser.hangNotification.waitPeriod", 1000], - ["browser.hangNotification.expiration", 2000]); - - function nocbcheck() { - ok(false, "received a callback?"); - } - let oldcb = gTestHangReport.testCallback; - gTestHangReport.testCallback = nocbcheck; - // Click the "Wait" button this time, we shouldn't get a callback at all. - buttons[1].click(); - gTestHangReport.testCallback = oldcb; - - // send another hang pulse, we should not get a notification here - Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); - is(notification.currentNotification, null, "no notification should be visible"); - - // After selecting Wait, we should get a userCanceled callback after - // HANG_EXPIRATION_TIME. - yield promiseReportCallMade(gTestHangReport.TEST_CALLBACK_CANCELED); - - yield popPrefs(); -}); - -/** - * Test if hang reports receive user canceled callbacks after the content - * process stops sending hang notifications. - */ - -add_task(function* hangGoesAwayTest() { - yield pushPrefs(["browser.hangNotification.expiration", 1000]); - - let promise = promiseNotificationShown(window, "process-hang"); - Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); - yield promise; - - yield promiseReportCallMade(gTestHangReport.TEST_CALLBACK_CANCELED); - - yield popPrefs(); -}); - -/** - * Tests if hang reports receive a terminate plugin callback when the user selects - * stop in response to a plugin hang. - */ - -add_task(function* terminatePluginTest() { - let promise = promiseNotificationShown(window, "process-hang"); - Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); - let notification = yield promise; - - let buttons = notification.currentNotification.getElementsByTagName("button"); - is(buttons.length, buttonCount, "proper number of buttons"); - - // Click the "Stop It" button, we should get a terminate script callback - gTestHangReport.hangType = gTestHangReport.PLUGIN_HANG; - promise = promiseReportCallMade(gTestHangReport.TEST_CALLBACK_TERMPLUGIN); - buttons[0].click(); - yield promise; -}); diff --git a/dom/ipc/ProcessHangMonitor.cpp b/dom/ipc/ProcessHangMonitor.cpp index b13792052cf..7c79a7e6738 100644 --- a/dom/ipc/ProcessHangMonitor.cpp +++ b/dom/ipc/ProcessHangMonitor.cpp @@ -151,6 +151,7 @@ public: NS_IMETHOD BeginStartingDebugger() override; NS_IMETHOD EndStartingDebugger() override; NS_IMETHOD TerminatePlugin() override; + NS_IMETHOD TerminateProcess() override; NS_IMETHOD UserCanceled() override; NS_IMETHOD IsReportForBrowser(nsIFrameLoader* aFrameLoader, bool* aResult) override; @@ -819,8 +820,6 @@ HangMonitoredProcess::TerminatePlugin() return NS_ERROR_UNEXPECTED; } - // generates a crash report that includes a browser report taken here - // earlier, the content process, and any plugin process(es). uint32_t id = mHangData.get_PluginHangData().pluginId(); plugins::TerminatePlugin(id, NS_LITERAL_CSTRING("HangMonitor"), mBrowserDumpId); @@ -831,6 +830,24 @@ HangMonitoredProcess::TerminatePlugin() return NS_OK; } +NS_IMETHODIMP +HangMonitoredProcess::TerminateProcess() +{ + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + if (!mContentParent) { + return NS_ERROR_UNEXPECTED; + } + + if (mActor && mHangData.type() == HangData::TPluginHangData) { + uint32_t id = mHangData.get_PluginHangData().pluginId(); + mActor->CleanupPluginHang(id, true); + } + + mContentParent->KillHard("HangMonitor"); + return NS_OK; +} + NS_IMETHODIMP HangMonitoredProcess::IsReportForBrowser(nsIFrameLoader* aFrameLoader, bool* aResult) { diff --git a/dom/ipc/nsIHangReport.idl b/dom/ipc/nsIHangReport.idl index 9ccbbbbad1b..565ad8a6e39 100644 --- a/dom/ipc/nsIHangReport.idl +++ b/dom/ipc/nsIHangReport.idl @@ -18,7 +18,7 @@ interface nsIFrameLoader; * process will continue to run uninhibitedly during this time. */ -[scriptable, uuid(5fcffbb9-be62-49b1-b8a1-36e820787a74)] +[scriptable, uuid(90cea731-dd3e-459e-b017-f9a14697b56e)] interface nsIHangReport : nsISupports { const unsigned long SLOW_SCRIPT = 1; @@ -50,6 +50,10 @@ interface nsIHangReport : nsISupports // Only valid for PLUGIN_HANG reports. void terminatePlugin(); + // Terminate the hung content process unconditionally. + // Valid for any type of hang. + void terminateProcess(); + // Ask the content process to start up the slow script debugger. // Only valid for SLOW_SCRIPT reports. void beginStartingDebugger();