browser/modules/ProcessHangMonitor.jsm
author Gregory Szorc <gps@mozilla.com>
Mon, 08 May 2017 17:19:05 -0700
changeset 360045 a0e257e346ccf3c1db332ec5903241f4eeb9a7ee
parent 353316 56bf52d4ddeb1dc7019c4f9e23961670d0a31414
child 375657 b83aea215a82d44bec7443b69e60feef32f5fb2c
permissions -rw-r--r--
Bug 1359965 - Support and generate tar.gz WPT archive; r=glandium Several years ago there was a single zip file for all test files. Clients would only extract the files they needed. Thus, zip was a reasonable archive format because it allowed direct access to members without having to decompress the entirety of the stream. We have since split up that monolithic archive into separate, domain-specific archives. e.g. 1 archive for mochitests and one for xpcshell tests. This drastically cut down on network I/O required on testers because they only fetched archives/data that was relevant. It also enabled parallel generation of test archives, we shaved dozens of seconds off builds due to compression being a long pole. Despite the architectural changes to test archive management, we still used zip files. This is not ideal because we no longer access specific files in test archives and thus don't care about single/partial member access performance. This commit implements support for generating tar.gz test archives. And it switches the web-platform archive to a tar.gz file. The performance implications for archive generation are significant: before: 48,321,250 bytes; 6.05s after: 31,844,267 bytes; 4.57s The size is reduced because we have a single compression context so data from 1 file can benefit compression in a subsequent file. CPU usage is reduced because the compressor has to work less with 1 context than it does with N. While I didn't measure it, decompression performance should also be improved for the same reasons. And of course network I/O will be reduced. mozharness consumers use a generic method for handling unarchiving. This method automagically handles multiple file extensions. So as long as downstream consumers aren't hard coding ".zip" this change should "just work." MozReview-Commit-ID: LQa5MIHLsms

/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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/. */

"use strict";

var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;

this.EXPORTED_SYMBOLS = ["ProcessHangMonitor"];

Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");

/**
 * This JSM is responsible for observing content process hang reports
 * and asking the user what to do about them. See nsIHangReport for
 * the platform interface.
 */

var ProcessHangMonitor = {
  /**
   * 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. 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
   * expires.
   */
  _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, "xpcom-shutdown");
    Services.ww.registerNotification(this);
  },

  /**
   * Terminate JavaScript associated with the hang being reported for
   * the selected browser in |win|.
   */
  terminateScript(win) {
    this.handleUserInput(win, report => report.terminateScript());
  },

  /**
   * Start devtools debugger for JavaScript associated with the hang
   * being reported for the selected browser in |win|.
   */
  debugScript(win) {
    this.handleUserInput(win, report => {
      function callback() {
        report.endStartingDebugger();
      }

      report.beginStartingDebugger();

      let svc = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(Ci.nsISlowScriptDebug);
      let handler = svc.remoteActivationHandler;
      handler.handleSlowScriptDebug(report.scriptBrowser, callback);
    });
  },

  /**
   * 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.
   */
  terminatePlugin(win) {
    this.handleUserInput(win, report => report.terminatePlugin());
  },

  /**
   * Dismiss the browser notification and invoke an appropriate action based on
   * the hang type.
   */
  stopIt(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;
    }
  },

  /**
   * 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;
    }
    // Remove the report from the active list.
    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.

    // 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);

          // We're still hung, so move the report back to the active
          // list and update the UI.
          this._activeReports.add(report);
          this.updateWindows();
          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 there is a hang report associated with the selected browser in
   * |win|, invoke |func| on that report and stop notifying the user
   * about it.
   */
  handleUserInput(win, func) {
    let report = this.findActiveReport(win.gBrowser.selectedBrowser);
    if (!report) {
      return null;
    }
    this.removeActiveReport(report);

    return func(report);
  },

  observe(subject, topic, data) {
    switch (topic) {
      case "xpcom-shutdown":
        Services.obs.removeObserver(this, "xpcom-shutdown");
        Services.obs.removeObserver(this, "process-hang-report");
        Services.obs.removeObserver(this, "clear-hang-report");
        Services.ww.unregisterNotification(this);
        break;

      case "process-hang-report":
        this.reportHang(subject.QueryInterface(Ci.nsIHangReport));
        break;

      case "clear-hang-report":
        this.clearHang(subject.QueryInterface(Ci.nsIHangReport));
        break;

      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;
    }
  },

  /**
   * Find a active hang report for the given <browser> element.
   */
  findActiveReport(browser) {
    let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
    for (let report of this._activeReports) {
      if (report.isReportForBrowser(frameLoader)) {
        return report;
      }
    }
    return null;
  },

  /**
   * Find a paused hang report for the given <browser> element.
   */
  findPausedReport(browser) {
    let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
    for (let [report, ] 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(report) {
    this._activeReports.delete(report);
    this.updateWindows();
  },

  /**
   * Remove a paused hang report from the paused list and cancel the timer
   * associated with it.
   */
  removePausedReport(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
   * each window to watch for events that would cause a different hang
   * report to be displayed.
   */
  updateWindows() {
    let e = Services.wm.getEnumerator("navigator:browser");
    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);
      } else {
        this.untrackWindow(win);
      }
    }
  },

  /**
   * If there is a hang report for the current tab in |win|, display it.
   */
  updateWindow(win) {
    let report = this.findActiveReport(win.gBrowser.selectedBrowser);

    if (report) {
      this.showNotification(win, report);
    } else {
      this.hideNotification(win);
    }
  },

  /**
   * Show the notification for a hang.
   */
  showNotification(win, report) {
    let nb = win.document.getElementById("high-priority-global-notificationbox");
    let notification = nb.getNotificationWithValue("process-hang");
    if (notification) {
      return;
    }

    let bundle = win.gNavigatorBundle;

    let buttons = [{
        label: bundle.getString("processHang.button_stop.label"),
        accessKey: bundle.getString("processHang.button_stop.accessKey"),
        callback() {
          ProcessHangMonitor.stopIt(win);
        }
      },
      {
        label: bundle.getString("processHang.button_wait.label"),
        accessKey: bundle.getString("processHang.button_wait.accessKey"),
        callback() {
          ProcessHangMonitor.waitLonger(win);
        }
      }];

    if (AppConstants.MOZ_DEV_EDITION && report.hangType == report.SLOW_SCRIPT) {
      buttons.push({
        label: bundle.getString("processHang.button_debug.label"),
        accessKey: bundle.getString("processHang.button_debug.accessKey"),
        callback() {
          ProcessHangMonitor.debugScript(win);
        }
      });
    }

    nb.appendNotification(bundle.getString("processHang.label"),
                          "process-hang",
                          "chrome://browser/content/aboutRobots-icon.png",
                          nb.PRIORITY_WARNING_HIGH, buttons);
  },

  /**
   * Ensure that no hang notifications are visible in |win|.
   */
  hideNotification(win) {
    let nb = win.document.getElementById("high-priority-global-notificationbox");
    let notification = nb.getNotificationWithValue("process-hang");
    if (notification) {
      nb.removeNotification(notification);
    }
  },

  /**
   * Install event handlers on |win| to watch for events that would
   * cause a different hang report to be displayed.
   */
  trackWindow(win) {
    win.gBrowser.tabContainer.addEventListener("TabSelect", this, true);
    win.gBrowser.tabContainer.addEventListener("TabRemotenessChange", this, true);
  },

  untrackWindow(win) {
    win.gBrowser.tabContainer.removeEventListener("TabSelect", this, true);
    win.gBrowser.tabContainer.removeEventListener("TabRemotenessChange", this, true);
  },

  handleEvent(event) {
    let win = event.target.ownerGlobal;

    // If a new tab is selected or if a tab changes remoteness, then
    // we may need to show or hide a hang notification.

    if (event.type == "TabSelect" || event.type == "TabRemotenessChange") {
      this.updateWindow(win);
    }
  },

  /**
   * 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 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;
    }

    // If this hang was already reported and paused by the user ignore it.
    if (this._pausedReports.has(report)) {
      return;
    }

    // On e10s this counts slow-script/hanged-plugin notice only once.
    // This code is not reached on non-e10s.
    if (report.hangType == report.SLOW_SCRIPT) {
      // On non-e10s, SLOW_SCRIPT_NOTICE_COUNT is probed at nsGlobalWindow.cpp
      Services.telemetry.getHistogramById("SLOW_SCRIPT_NOTICE_COUNT").add();
    } else if (report.hangType == report.PLUGIN_HANG) {
      // On non-e10s we have sufficient plugin telemetry probes,
      // so PLUGIN_HANG_NOTICE_COUNT is only probed on e10s.
      Services.telemetry.getHistogramById("PLUGIN_HANG_NOTICE_COUNT").add();
    }

    this._activeReports.add(report);
    this.updateWindows();
  },

  clearHang(report) {
    this.removeActiveReport(report);
    this.removePausedReport(report);
    report.userCanceled();
  },
};