1/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6"use strict"; 7 8var Cc = Components.classes; 9var Ci = Components.interfaces; 10var Cu = Components.utils; 11 12this.EXPORTED_SYMBOLS = ["ProcessHangMonitor"]; 13 14Cu.import("resource://gre/modules/AppConstants.jsm"); 15Cu.import("resource://gre/modules/Services.jsm"); 16 17/** 18 * This JSM is responsible for observing content process hang reports 19 * and asking the user what to do about them. See nsIHangReport for 20 * the platform interface. 21 */ 22 23var ProcessHangMonitor = { 24 /** 25 * This timeout is the wait period applied after a user selects "Wait" in 26 * an existing notification. 27 */ 28 get WAIT_EXPIRATION_TIME() { 29 try { 30 return Services.prefs.getIntPref("browser.hangNotification.waitPeriod"); 31 } catch (ex) { 32 return 10000; 33 } 34 }, 35 36 /** 37 * Collection of hang reports that haven't expired or been dismissed 38 * by the user. These are nsIHangReports. 39 */ 40 _activeReports: new Set(), 41 42 /** 43 * Collection of hang reports that have been suppressed for a short 44 * period of time. Value is an nsITimer for when the wait time 45 * expires. 46 */ 47 _pausedReports: new Map(), 48 49 /** 50 * Initialize hang reporting. Called once in the parent process. 51 */ 52 init: function() { 53 Services.obs.addObserver(this, "process-hang-report", false); 54 Services.obs.addObserver(this, "clear-hang-report", false); 55 Services.obs.addObserver(this, "xpcom-shutdown", false); 56 Services.ww.registerNotification(this); 57 }, 58 59 /** 60 * Terminate JavaScript associated with the hang being reported for 61 * the selected browser in |win|. 62 */ 63 terminateScript: function(win) { 64 this.handleUserInput(win, report => report.terminateScript()); 65 }, 66 67 /** 68 * Start devtools debugger for JavaScript associated with the hang 69 * being reported for the selected browser in |win|. 70 */ 71 debugScript: function(win) { 72 this.handleUserInput(win, report => { 73 function callback() { 74 report.endStartingDebugger(); 75 } 76 77 report.beginStartingDebugger(); 78 79 let svc = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(Ci.nsISlowScriptDebug); 80 let handler = svc.remoteActivationHandler; 81 handler.handleSlowScriptDebug(report.scriptBrowser, callback); 82 }); 83 }, 84 85 /** 86 * Terminate the plugin process associated with a hang being reported 87 * for the selected browser in |win|. Will attempt to generate a combined 88 * crash report for all processes. 89 */ 90 terminatePlugin: function(win) { 91 this.handleUserInput(win, report => report.terminatePlugin()); 92 }, 93 94 /** 95 * Dismiss the browser notification and invoke an appropriate action based on 96 * the hang type. 97 */ 98 stopIt: function (win) { 99 let report = this.findActiveReport(win.gBrowser.selectedBrowser); 100 if (!report) { 101 return; 102 } 103 104 switch (report.hangType) { 105 case report.SLOW_SCRIPT: 106 this.terminateScript(win); 107 break; 108 case report.PLUGIN_HANG: 109 this.terminatePlugin(win); 110 break; 111 } 112 }, 113 114 /** 115 * Dismiss the notification, clear the report from the active list and set up 116 * a new timer to track a wait period during which we won't notify. 117 */ 118 waitLonger: function(win) { 119 let report = this.findActiveReport(win.gBrowser.selectedBrowser); 120 if (!report) { 121 return; 122 } 123 // Remove the report from the active list. 124 this.removeActiveReport(report); 125 126 // NOTE, we didn't call userCanceled on nsIHangReport here. This insures 127 // we don't repeatedly generate and cache crash report data for this hang 128 // in the process hang reporter. It already has one report for the browser 129 // process we want it hold onto. 130 131 // Create a new wait timer with notify callback 132 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 133 timer.initWithCallback(() => { 134 for (let [stashedReport, otherTimer] of this._pausedReports) { 135 if (otherTimer === timer) { 136 this.removePausedReport(stashedReport); 137 138 // We're still hung, so move the report back to the active 139 // list and update the UI. 140 this._activeReports.add(report); 141 this.updateWindows(); 142 break; 143 } 144 } 145 }, this.WAIT_EXPIRATION_TIME, timer.TYPE_ONE_SHOT); 146 147 this._pausedReports.set(report, timer); 148 149 // remove the browser notification associated with this hang 150 this.updateWindows(); 151 }, 152 153 /** 154 * If there is a hang report associated with the selected browser in 155 * |win|, invoke |func| on that report and stop notifying the user 156 * about it. 157 */ 158 handleUserInput: function(win, func) { 159 let report = this.findActiveReport(win.gBrowser.selectedBrowser); 160 if (!report) { 161 return null; 162 } 163 this.removeActiveReport(report); 164 165 return func(report); 166 }, 167 168 observe: function(subject, topic, data) { 169 switch (topic) { 170 case "xpcom-shutdown": 171 Services.obs.removeObserver(this, "xpcom-shutdown"); 172 Services.obs.removeObserver(this, "process-hang-report"); 173 Services.obs.removeObserver(this, "clear-hang-report"); 174 Services.ww.unregisterNotification(this); 175 break; 176 177 case "process-hang-report": 178 this.reportHang(subject.QueryInterface(Ci.nsIHangReport)); 179 break; 180 181 case "clear-hang-report": 182 this.clearHang(subject.QueryInterface(Ci.nsIHangReport)); 183 break; 184 185 case "domwindowopened": 186 // Install event listeners on the new window in case one of 187 // its tabs is already hung. 188 let win = subject.QueryInterface(Ci.nsIDOMWindow); 189 let listener = (ev) => { 190 win.removeEventListener("load", listener, true); 191 this.updateWindows(); 192 }; 193 win.addEventListener("load", listener, true); 194 break; 195 } 196 }, 197 198 /** 199 * Find a active hang report for the given <browser> element. 200 */ 201 findActiveReport: function(browser) { 202 let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; 203 for (let report of this._activeReports) { 204 if (report.isReportForBrowser(frameLoader)) { 205 return report; 206 } 207 } 208 return null; 209 }, 210 211 /** 212 * Find a paused hang report for the given <browser> element. 213 */ 214 findPausedReport: function(browser) { 215 let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; 216 for (let [report, ] of this._pausedReports) { 217 if (report.isReportForBrowser(frameLoader)) { 218 return report; 219 } 220 } 221 return null; 222 }, 223 224 /** 225 * Remove an active hang report from the active list and cancel the timer 226 * associated with it. 227 */ 228 removeActiveReport: function(report) { 229 this._activeReports.delete(report); 230 this.updateWindows(); 231 }, 232 233 /** 234 * Remove a paused hang report from the paused list and cancel the timer 235 * associated with it. 236 */ 237 removePausedReport: function(report) { 238 let timer = this._pausedReports.get(report); 239 if (timer) { 240 timer.cancel(); 241 } 242 this._pausedReports.delete(report); 243 }, 244 245 /** 246 * Iterate over all XUL windows and ensure that the proper hang 247 * reports are shown for each one. Also install event handlers in 248 * each window to watch for events that would cause a different hang 249 * report to be displayed. 250 */ 251 updateWindows: function() { 252 let e = Services.wm.getEnumerator("navigator:browser"); 253 while (e.hasMoreElements()) { 254 let win = e.getNext(); 255 256 this.updateWindow(win); 257 258 // Only listen for these events if there are active hang reports. 259 if (this._activeReports.size) { 260 this.trackWindow(win); 261 } else { 262 this.untrackWindow(win); 263 } 264 } 265 }, 266 267 /** 268 * If there is a hang report for the current tab in |win|, display it. 269 */ 270 updateWindow: function(win) { 271 let report = this.findActiveReport(win.gBrowser.selectedBrowser); 272 273 if (report) { 274 this.showNotification(win, report); 275 } else { 276 this.hideNotification(win); 277 } 278 }, 279 280 /** 281 * Show the notification for a hang. 282 */ 283 showNotification: function(win, report) { 284 let nb = win.document.getElementById("high-priority-global-notificationbox"); 285 let notification = nb.getNotificationWithValue("process-hang"); 286 if (notification) { 287 return; 288 } 289 290 let bundle = win.gNavigatorBundle; 291 292 let buttons = [{ 293 label: bundle.getString("processHang.button_stop.label"), 294 accessKey: bundle.getString("processHang.button_stop.accessKey"), 295 callback: function() { 296 ProcessHangMonitor.stopIt(win); 297 } 298 }, 299 { 300 label: bundle.getString("processHang.button_wait.label"), 301 accessKey: bundle.getString("processHang.button_wait.accessKey"), 302 callback: function() { 303 ProcessHangMonitor.waitLonger(win); 304 } 305 }]; 306 307 if (AppConstants.MOZ_DEV_EDITION && report.hangType == report.SLOW_SCRIPT) { 308 buttons.push({ 309 label: bundle.getString("processHang.button_debug.label"), 310 accessKey: bundle.getString("processHang.button_debug.accessKey"), 311 callback: function() { 312 ProcessHangMonitor.debugScript(win); 313 } 314 }); 315 } 316 317 nb.appendNotification(bundle.getString("processHang.label"), 318 "process-hang", 319 "chrome://browser/content/aboutRobots-icon.png", 320 nb.PRIORITY_WARNING_HIGH, buttons); 321 }, 322 323 /** 324 * Ensure that no hang notifications are visible in |win|. 325 */ 326 hideNotification: function(win) { 327 let nb = win.document.getElementById("high-priority-global-notificationbox"); 328 let notification = nb.getNotificationWithValue("process-hang"); 329 if (notification) { 330 nb.removeNotification(notification); 331 } 332 }, 333 334 /** 335 * Install event handlers on |win| to watch for events that would 336 * cause a different hang report to be displayed. 337 */ 338 trackWindow: function(win) { 339 win.gBrowser.tabContainer.addEventListener("TabSelect", this, true); 340 win.gBrowser.tabContainer.addEventListener("TabRemotenessChange", this, true); 341 }, 342 343 untrackWindow: function(win) { 344 win.gBrowser.tabContainer.removeEventListener("TabSelect", this, true); 345 win.gBrowser.tabContainer.removeEventListener("TabRemotenessChange", this, true); 346 }, 347 348 handleEvent: function(event) { 349 let win = event.target.ownerGlobal; 350 351 // If a new tab is selected or if a tab changes remoteness, then 352 // we may need to show or hide a hang notification. 353 354 if (event.type == "TabSelect" || event.type == "TabRemotenessChange") { 355 this.updateWindow(win); 356 } 357 }, 358 359 /** 360 * Handle a potentially new hang report. If it hasn't been seen 361 * before, show a notification for it in all open XUL windows. 362 */ 363 reportHang: function(report) { 364 // If this hang was already reported reset the timer for it. 365 if (this._activeReports.has(report)) { 366 // if this report is in active but doesn't have a notification associated 367 // with it, display a notification. 368 this.updateWindows(); 369 return; 370 } 371 372 // If this hang was already reported and paused by the user ignore it. 373 if (this._pausedReports.has(report)) { 374 return; 375 } 376 377 // On e10s this counts slow-script/hanged-plugin notice only once. 378 // This code is not reached on non-e10s. 379 if (report.hangType == report.SLOW_SCRIPT) { 380 // On non-e10s, SLOW_SCRIPT_NOTICE_COUNT is probed at nsGlobalWindow.cpp 381 Services.telemetry.getHistogramById("SLOW_SCRIPT_NOTICE_COUNT").add(); 382 } else if (report.hangType == report.PLUGIN_HANG) { 383 // On non-e10s we have sufficient plugin telemetry probes, 384 // so PLUGIN_HANG_NOTICE_COUNT is only probed on e10s. 385 Services.telemetry.getHistogramById("PLUGIN_HANG_NOTICE_COUNT").add(); 386 } 387 388 this._activeReports.add(report); 389 this.updateWindows(); 390 }, 391 392 clearHang: function(report) { 393 this.removeActiveReport(report); 394 this.removePausedReport(report); 395 report.userCanceled(); 396 }, 397}; 398