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