Bug 1647695 - Display an hourglass in front of frozen processes;r=florian
☠☠ backed out by ea03df7a1554 ☠ ☠
authorDavid Teller <dteller@mozilla.com>
Mon, 03 Aug 2020 14:54:25 +0000
changeset 543133 1b1fa5dbd9bdf7d3579af6bcdb3a727e6ec9a777
parent 543132 0f097362abf86343710620aa61113434c294dc0e
child 543134 01b6951d0aaee040d5c51ef737046ea88cd9f7bb
push id123248
push userdteller@mozilla.com
push dateMon, 03 Aug 2020 15:05:58 +0000
treeherderautoland@01b6951d0aae [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 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,26 +228,58 @@ 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"
   ));
 
+  // Another tab that we'll pretend is hung.
+  let tabHung = BrowserTestUtils.addTab(gBrowser, "https://example.org");
+
   await BrowserTestUtils.browserLoaded(tabAboutProcesses.linkedBrowser);
+  await BrowserTestUtils.browserLoaded(tabHung.linkedBrowser);
+
+  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();
+
   // Wait until the table has first been populated.
   await TestUtils.waitForCondition(() => tbody.childElementCount);
 
   // 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();
@@ -344,13 +376,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);
 });