Bug 1647695 - Display an hourglass in front of frozen processes;r=florian
authorDavid Teller <dteller@mozilla.com>
Thu, 06 Aug 2020 14:16:24 +0000
changeset 544073 b3e8e9466c0dc1c61aa8848b6c3b9e15851902fc
parent 544072 d025884043681815fbeda2c785295918a12c368d
child 544074 2facd744d7c36989f9f0cf58021cefe525731f0c
push id37687
push userapavel@mozilla.com
push dateMon, 10 Aug 2020 21:36:34 +0000
treeherdermozilla-central@ee09cb88af17 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian
bugs1647695
milestone81.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1647695 - Display an hourglass in front of frozen processes;r=florian Depends on D83665 Differential Revision: https://phabricator.services.mozilla.com/D83666
toolkit/components/aboutprocesses/content/aboutProcesses.css
toolkit/components/aboutprocesses/content/aboutProcesses.js
toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses.js
--- a/toolkit/components/aboutprocesses/content/aboutProcesses.css
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.css
@@ -37,29 +37,42 @@ body {
   min-width: 40em;
   background-color: var(--in-content-box-background);
 }
 tr {
   display: table;
   table-layout: fixed;
   width: 100%;
 }
+
+/* column-pid */
 td:nth-child(1) {
   width:  16%;
 }
 /* At least one column needs to have a flexible width,
-   so no width specified for td:nth-child(2) */
+   so no width specified for td:nth-child(2) aka column-name*/
+
+
+/* column-memory-resident */
 td:nth-child(3) {
     width: 10%;
 }
+
+/* column-cpu-user */
 td:nth-child(4) {
-    width: 10%;
+  width: 10%;
 }
+
+/* column-cpu-kernel */
 td:nth-child(5) {
-    width: 10%;
+  width: 10%;
+}
+/* column-threads */
+td:nth-child(6) {
+  width: 2%;
 }
 
 #process-thead > tr {
   height: inherit;
 }
 
 #process-thead > tr > td {
   border: none;
@@ -163,16 +176,23 @@ td {
 
 #process-tbody > tr.process {
   font-weight: bold;
 }
 #process-tbody > tr.thread {
   font-size-adjust: 0.5;
 }
 
+/* column-name */
+
+/* When the process is reported as frozen, we display an hourglass before its name. */
+.process.hung > :nth-child(2)::before {
+  content: "⌛️";
+}
+
 /*
   Show a the separation between process groups.
  */
 
 #process-tbody > tr.separate-from-next-process-group {
   border-bottom: dotted 1px var(--in-content-box-border-color);
   margin-bottom: -1px;
 }
--- a/toolkit/components/aboutprocesses/content/aboutProcesses.js
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.js
@@ -21,16 +21,18 @@ const UPDATE_INTERVAL_MS = 2000;
 
 const MS_PER_NS = 1000000;
 const NS_PER_S = 1000000000;
 
 const ONE_GIGA = 1024 * 1024 * 1024;
 const ONE_MEGA = 1024 * 1024;
 const ONE_KILO = 1024;
 
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
 /**
  * Returns a Promise that's resolved after the next turn of the event loop.
  *
  * Just returning a resolved Promise would mean that any `then` callbacks
  * would be called right after the end of the current turn, so `setTimeout`
  * is used to delay Promise resolution until the next turn.
  *
  * In mochi tests, it's possible for this to be called after the
@@ -143,16 +145,17 @@ var State = {
    * Compute the delta between two process snapshots.
    *
    * @param {ProcessSnapshot} cur
    * @param {ProcessSnapshot?} prev
    */
   _getProcessDelta(cur, prev) {
     let result = {
       pid: cur.pid,
+      childID: cur.childID,
       filename: cur.filename,
       totalVirtualMemorySize: cur.virtualMemorySize,
       deltaVirtualMemorySize: null,
       totalResidentSize: cur.residentSetSize,
       deltaResidentSize: null,
       totalCpuUser: cur.cpuUser,
       slopeCpuUser: null,
       totalCpuKernel: cur.cpuKernel,
@@ -244,41 +247,44 @@ var View = {
     row.parentNode.insertBefore(this._fragment, row.nextSibling);
     this._fragment = document.createDocumentFragment();
   },
 
   /**
    * Append a row showing a single process (without its threads).
    *
    * @param {ProcessDelta} data The data to display.
-   * @param {bool} isOpen `true` if we're also displaying the threads of this process, `false` otherwise.
    * @return {DOMElement} The row displaying the process.
    */
-  appendProcessRow(data, isOpen) {
+  appendProcessRow(data) {
     let row = document.createElement("tr");
     row.classList.add("process");
 
+    if (data.isHung) {
+      row.classList.add("hung");
+    }
+
     // Column: pid / twisty image
     {
       let elt = this._addCell(row, {
         content: data.pid,
         classes: ["pid", "root"],
       });
 
       if (data.threads.length) {
         let img = document.createElement("span");
         img.classList.add("twisty", "process");
-        if (isOpen) {
+        if (data.isOpen) {
           img.classList.add("open");
         }
         elt.insertBefore(img, elt.firstChild);
       }
     }
 
-    // Column: type
+    // Column: name/type
     {
       let content = data.origin ? `${data.origin} (${data.type})` : data.type;
       this._addCell(row, {
         content,
         classes: ["type"],
       });
     }
 
@@ -522,24 +528,30 @@ var View = {
   _setTextAndTooltip(elt, text, tooltip = text) {
     elt.textContent = text;
     elt.setAttribute("title", tooltip);
   },
 };
 
 var Control = {
   _openItems: new Set(),
+  // The set of all processes reported as "hung" by the process hang monitor.
+  //
+  // type: Set<ChildID>
+  _hungItems: new Set(),
   _sortColumn: null,
   _sortAscendent: true,
   _removeSubtree(row) {
     while (row.nextSibling && row.nextSibling.classList.contains("thread")) {
       row.nextSibling.remove();
     }
   },
   init() {
+    this._initHangReports();
+
     let tbody = document.getElementById("process-tbody");
     tbody.addEventListener("click", event => {
       this._updateLastMouseEvent();
 
       // Handle showing or hiding subitems of a row.
       let target = event.target;
       if (target.classList.contains("twisty")) {
         let row = target.parentNode.parentNode;
@@ -610,16 +622,39 @@ var Control = {
 
         await this._updateDisplay(true);
       });
   },
   _lastMouseEvent: 0,
   _updateLastMouseEvent() {
     this._lastMouseEvent = Date.now();
   },
+  _initHangReports() {
+    const PROCESS_HANG_REPORT_NOTIFICATION = "process-hang-report";
+
+    // Receiving report of a hung child.
+    // Let's store if for our next update.
+    let hangReporter = report => {
+      report.QueryInterface(Ci.nsIHangReport);
+      this._hungItems.add(report.childID);
+    };
+    Services.obs.addObserver(hangReporter, PROCESS_HANG_REPORT_NOTIFICATION);
+
+    // Don't forget to unregister the reporter.
+    window.addEventListener(
+      "unload",
+      () => {
+        Services.obs.removeObserver(
+          hangReporter,
+          PROCESS_HANG_REPORT_NOTIFICATION
+        );
+      },
+      { once: true }
+    );
+  },
   async update() {
     await State.update();
 
     if (document.hidden) {
       return;
     }
 
     await wait(0);
@@ -639,23 +674,36 @@ var Control = {
 
     let counters = State.getCounters();
 
     // Reset the selectedRow field and the _openItems set each time we redraw
     // to avoid keeping forever references to dead processes.
     let openItems = this._openItems;
     this._openItems = new Set();
 
+    // Similarly, we reset `_hungItems`, based on the assumption that the process hang
+    // monitor will inform us again before the next update. Since the process hang monitor
+    // pings its clients about once per second and we update about once per 2 seconds
+    // (or more if the mouse moves), we should be ok.
+    let hungItems = this._hungItems;
+    this._hungItems = new Set();
+
     counters = this._sortProcesses(counters);
     let previousRow = null;
     let previousProcess = null;
     for (let process of counters) {
       let isOpen = openItems.has(process.pid);
+      process.isOpen = isOpen;
+
+      let isHung = process.childID && hungItems.has(process.childID);
+      process.isHung = isHung;
+
       let processRow = View.appendProcessRow(process, isOpen);
       processRow.process = process;
+
       let latestRow = processRow;
       if (isOpen) {
         this._openItems.add(process.pid);
         latestRow = this._showChildren(processRow);
       }
       if (
         this._sortColumn == null &&
         previousProcess &&
--- a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses.js
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-let { AppConstants } = ChromeUtils.import(
+const { AppConstants } = ChromeUtils.import(
   "resource://gre/modules/AppConstants.jsm"
 );
 
 // A bunch of assumptions we make about the behavior of the parent process,
 // and which we use as sanity checks. If Firefox evolves, we will need to
 // update these values.
 const HARDCODED_ASSUMPTIONS_PROCESS = {
   minimalNumberOfThreads: 10,
@@ -228,39 +228,75 @@ function testMemory(string, total, delta
   Assert.ok(
     isCloseEnough(Math.abs(computedDelta), Math.abs(delta)),
     `The displayed approximation of the delta amount of memory is reasonable: ${computedDelta} vs ${delta}`
   );
 }
 
 add_task(async function testAboutProcesses() {
   info("Setting up about:processes");
+
+  // The tab we're testing.
   let tabAboutProcesses = (gBrowser.selectedTab = BrowserTestUtils.addTab(
     gBrowser,
     "about:processes"
   ));
+  await BrowserTestUtils.browserLoaded(tabAboutProcesses.linkedBrowser);
 
-  await BrowserTestUtils.browserLoaded(tabAboutProcesses.linkedBrowser);
+  info("Setting up example.com");
+  // Another tab that we'll pretend is hung.
+  let tabHung = BrowserTestUtils.addTab(gBrowser, "http://example.com");
+  await BrowserTestUtils.browserLoaded(tabHung.linkedBrowser);
+
+  info("Setting up fake process hang detector");
+  let hungChildID = tabHung.linkedBrowser.frameLoader.childID;
 
   let doc = tabAboutProcesses.linkedBrowser.contentDocument;
   let tbody = doc.getElementById("process-tbody");
 
+  // Keep informing about:processes that `tabHung` is hung.
+  // Note: this is a background task, do not `await` it.
+  let isProcessHangDetected = false;
+  let fakeProcessHangMonitor = async function() {
+    for (let i = 0; i < 100; ++i) {
+      if (isProcessHangDetected || !tabHung.linkedBrowser) {
+        // Let's stop spamming as soon as we can.
+        return;
+      }
+      // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+      await new Promise(resolve => setTimeout(resolve, 300));
+      Services.obs.notifyObservers(
+        {
+          childID: hungChildID,
+          hangType: Ci.nsIHangReport.PLUGIN_HANG,
+          pluginName: "Fake plug-in",
+          QueryInterface: ChromeUtils.generateQI(["nsIHangReport"]),
+        },
+        "process-hang-report"
+      );
+    }
+  };
+  fakeProcessHangMonitor();
+
+  info("Waiting for the first update of about:processes");
   // Wait until the table has first been populated.
   await TestUtils.waitForCondition(() => tbody.childElementCount);
 
+  info("Waiting for the second update of about:processes");
   // And wait for another update using a mutation observer, to give our newly created test tab some time
   // to burn some CPU.
   await new Promise(resolve => {
     let observer = new doc.ownerGlobal.MutationObserver(() => {
       observer.disconnect();
       resolve();
     });
     observer.observe(tbody, { childList: true });
   });
 
+  info("Looking at the contents of about:processes");
   // Find the row for the browser process.
   let row = tbody.firstChild;
   while (row && row.children[1].textContent != "browser") {
     row = row.nextSibling;
   }
 
   Assert.ok(row, "found a table row for the browser");
   let children = row.children;
@@ -344,13 +380,34 @@ add_task(async function testAboutProcess
     info("Sanity checks: CPU (kernel)");
     testCpu(
       cpuKernelContent,
       threadRow.thread.totalCpuKernel,
       threadRow.thread.slopeCpuKernel,
       HARDCODED_ASSUMPTIONS_THREAD
     );
   }
-
   Assert.equal(numberOfThreads, numberOfThreadsFound);
 
+  info("Ensuring that the hung process is marked as hung");
+  let isOneNonHungProcessDetected = false;
+  for (let row of tbody.getElementsByClassName("process")) {
+    if (row.classList.contains("hung")) {
+      if (row.process.childID == hungChildID) {
+        isProcessHangDetected = true;
+      }
+    } else {
+      isOneNonHungProcessDetected = true;
+    }
+    if (isProcessHangDetected && isOneNonHungProcessDetected) {
+      break;
+    }
+  }
+
+  Assert.ok(isProcessHangDetected, "We have found our hung process");
+  Assert.ok(
+    isOneNonHungProcessDetected,
+    "We have found at least one non-hung process"
+  );
+
   BrowserTestUtils.removeTab(tabAboutProcesses);
+  BrowserTestUtils.removeTab(tabHung);
 });