Bug 1705827 - update the about:processes table without rebuilding the rows that have not changed, r=dthayer.
authorFlorian Quèze <florian@queze.net>
Wed, 21 Apr 2021 18:05:22 +0000
changeset 576987 57a6210227b60109dc5a207c589056e1aef64d82
parent 576986 2cf5cde0ea50b0dd5fbf507e509253e40be12f3c
child 576988 e5aa7733e1aea8c6319eae58e084a145c57b6e59
push id38397
push userbtara@mozilla.com
push dateThu, 22 Apr 2021 03:04:50 +0000
treeherdermozilla-central@a74febc3cc23 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdthayer
bugs1705827
milestone90.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 1705827 - update the about:processes table without rebuilding the rows that have not changed, r=dthayer. Differential Revision: https://phabricator.services.mozilla.com/D112421
toolkit/components/aboutprocesses/content/aboutProcesses.js
toolkit/components/aboutprocesses/tests/browser/head.js
--- a/toolkit/components/aboutprocesses/content/aboutProcesses.js
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.js
@@ -393,59 +393,89 @@ var State = {
       counters.push(delta);
     }
 
     return counters;
   },
 };
 
 var View = {
-  _fragment: document.createDocumentFragment(),
   // Processes, tabs and subframes that we killed during the previous iteration.
   // Array<{pid:Number} | {windowId:Number}>
   _killedRecently: [],
-  async commit() {
+  commit() {
     this._killedRecently.length = 0;
     let tbody = document.getElementById("process-tbody");
 
-    // Force translation to happen before we insert the new content in the DOM
-    // to avoid flicker when resizing.
-    await document.l10n.translateFragment(this._fragment);
+    let insertPoint = tbody.firstChild;
+    let nextRow;
+    while ((nextRow = this._orderedRows.shift())) {
+      if (insertPoint && insertPoint === nextRow) {
+        insertPoint = insertPoint.nextSibling;
+      } else {
+        tbody.insertBefore(nextRow, insertPoint);
+      }
+    }
 
-    // Pause the DOMLocalization mutation observer, or the already translated
-    // content will be translated a second time at the next tick.
-    document.l10n.pauseObserving();
-    while (tbody.firstChild) {
-      tbody.firstChild.remove();
+    if (insertPoint) {
+      while ((nextRow = insertPoint.nextSibling)) {
+        this._removeRow(nextRow);
+      }
+      this._removeRow(insertPoint);
     }
-    tbody.appendChild(this._fragment);
-    document.l10n.resumeObserving();
-
-    this._fragment = document.createDocumentFragment();
   },
   insertAfterRow(row) {
-    row.parentNode.insertBefore(this._fragment, row.nextSibling);
-    this._fragment = document.createDocumentFragment();
+    let tbody = row.parentNode;
+    let nextRow;
+    while ((nextRow = this._orderedRows.shift())) {
+      tbody.insertBefore(nextRow, row.nextSibling);
+    }
+  },
+
+  _rowsById: new Map(),
+  _removeRow(row) {
+    this._rowsById.delete(row.rowId);
+
+    row.remove();
+  },
+  _getOrCreateRow(rowId, cellCount) {
+    let row = this._rowsById.get(rowId);
+    if (!row) {
+      row = document.createElement("tr");
+      while (cellCount--) {
+        row.appendChild(document.createElement("td"));
+      }
+      row.rowId = rowId;
+      this._rowsById.set(rowId, row);
+    }
+    this._orderedRows.push(row);
+    return row;
   },
 
   /**
-   * Append a row showing a single process (without its threads).
+   * Display a row showing a single process (without its threads).
    *
    * @param {ProcessDelta} data The data to display.
    * @return {DOMElement} The row displaying the process.
    */
-  appendProcessRow(data, units) {
-    let row = document.createElement("tr");
-    row.classList.add("process");
-
-    if (data.isHung) {
-      row.classList.add("hung");
+  displayProcessRow(data, units) {
+    const cellCount = 4;
+    let rowId = "p:" + data.pid;
+    let row = this._getOrCreateRow(rowId, cellCount);
+    row.process = data;
+    {
+      let classNames = "process";
+      if (data.isHung) {
+        classNames += " hung";
+      }
+      row.className = classNames;
     }
 
     // Column: Name
+    let nameCell = row.firstChild;
     {
       let fluentName;
       let classNames = [];
       switch (data.type) {
         case "web":
           fluentName = "about-processes-web-process-name";
           break;
         case "webIsolated":
@@ -500,17 +530,17 @@ var View = {
         // The following are probably not going to show up for users
         // but let's handle the case anyway to avoid heisenoranges
         // during tests in case of a leftover process from a previous
         // test.
         default:
           fluentName = "about-processes-unknown-process-name";
           break;
       }
-      let elt = this._addCell(row, {
+      this._fillCell(nameCell, {
         fluentName,
         fluentArgs: {
           pid: "" + data.pid, // Make sure that this number is not localized
           origin: data.origin,
           type: data.type,
         },
         classes: ["type", "favicon", ...classNames],
       });
@@ -546,83 +576,83 @@ var View = {
               image = null;
               break;
             }
           }
           if (!image) {
             image = "chrome://browser/skin/link.svg";
           }
       }
-      elt.style.backgroundImage = `url('${image}')`;
+      nameCell.style.backgroundImage = `url('${image}')`;
     }
 
-    // Column: Resident size
+    // Column: Memory
+    let memoryCell = nameCell.nextSibling;
     {
       let formattedTotal = this._formatMemory(data.totalRamSize);
       if (data.deltaRamSize) {
         let formattedDelta = this._formatMemory(data.deltaRamSize);
-        this._addCell(row, {
+        this._fillCell(memoryCell, {
           fluentName: "about-processes-total-memory-size",
           fluentArgs: {
             total: formattedTotal.amount,
             totalUnit: units.memory[formattedTotal.unit],
             delta: Math.abs(formattedDelta.amount),
             deltaUnit: units.memory[formattedDelta.unit],
             deltaSign: data.deltaRamSize > 0 ? "+" : "-",
           },
           classes: ["memory"],
         });
       } else {
-        this._addCell(row, {
+        this._fillCell(memoryCell, {
           fluentName: "about-processes-total-memory-size-no-change",
           fluentArgs: {
             total: formattedTotal.amount,
             totalUnit: units.memory[formattedTotal.unit],
           },
           classes: ["memory"],
         });
       }
     }
 
     // Column: CPU: User and Kernel
+    let cpuCell = memoryCell.nextSibling;
     if (data.slopeCpu == null) {
-      this._addCell(row, {
+      this._fillCell(cpuCell, {
         fluentName: "about-processes-cpu-user-and-kernel-not-ready",
         classes: ["cpu"],
       });
     } else {
       let { duration, unit } = this._getDuration(data.totalCpu);
       let localizedUnit = units.duration[unit];
       if (data.slopeCpu == 0) {
-        this._addCell(row, {
+        this._fillCell(cpuCell, {
           fluentName: "about-processes-cpu-user-and-kernel-idle",
           fluentArgs: {
             total: duration,
             unit: localizedUnit,
           },
           classes: ["cpu"],
         });
       } else {
-        this._addCell(row, {
+        this._fillCell(cpuCell, {
           fluentName: "about-processes-cpu-user-and-kernel",
           fluentArgs: {
             percent: data.slopeCpu,
             total: duration,
             unit: localizedUnit,
           },
           classes: ["cpu"],
         });
       }
     }
 
     // Column: Kill button – but not for all processes.
-    let killButton = this._addCell(row, {
-      content: "",
-      classes: ["action-icon"],
-    });
+    let killButton = cpuCell.nextSibling;
+    killButton.className = "action-icon";
 
     if (["web", "webIsolated", "webLargeAllocation"].includes(data.type)) {
       // This type of process can be killed.
       if (this._killedRecently.some(kill => kill.pid && kill.pid == data.pid)) {
         // We're racing between the "kill" action and the visual refresh.
         // In a few cases, we could end up with the visual refresh showing
         // a process as un-killed while we actually just killed it.
         //
@@ -636,54 +666,65 @@ var View = {
         killButton.classList.add("close-icon");
         document.l10n.setAttributes(
           killButton,
           "about-processes-shutdown-process"
         );
       }
     }
 
-    this._fragment.appendChild(row);
     return row;
   },
 
-  appendThreadSummaryRow(data, isOpen) {
-    let row = document.createElement("tr");
-    row.classList.add("thread-summary");
+  displayThreadSummaryRow(data, isOpen) {
+    const cellCount = 2;
+    let rowId = "ts:" + data.pid;
+    let row = this._getOrCreateRow(rowId, cellCount);
+    row.process = data;
+    row.className = "thread-summary";
 
     // Column: Name
-    let elt = this._addCell(row, {
-      fluentName: "about-processes-thread-summary",
-      fluentArgs: { number: data.threads.length },
-      classes: ["name", "indent"],
-    });
-    if (data.threads.length) {
-      let img = document.createElement("span");
-      img.classList.add("twisty");
-      if (data.isOpen) {
-        img.classList.add("open");
+    let nameCell = row.firstChild;
+    let fluentName = "about-processes-thread-summary";
+    let fluentArgs = { number: data.threads.length };
+    if (!nameCell.firstChild) {
+      // Create the nodes
+      this._fillCell(nameCell, {
+        fluentName,
+        fluentArgs,
+        classes: ["name", "indent"],
+      });
+      if (data.threads.length) {
+        let img = document.createElement("span");
+        img.classList.add("twisty");
+        if (data.isOpen) {
+          img.classList.add("open");
+        }
+        nameCell.insertBefore(img, nameCell.firstChild);
       }
-      elt.insertBefore(img, elt.firstChild);
+    } else {
+      // The only thing that can change is the thread count.
+      let span = nameCell.firstChild.nextSibling;
+      document.l10n.setAttributes(span, fluentName, fluentArgs);
     }
 
     // Column: action
-    this._addCell(row, {
-      content: "",
-      classes: ["action-icon"],
-    });
-
-    this._fragment.appendChild(row);
-    return row;
+    let actionCell = nameCell.nextSibling;
+    actionCell.className = "action-icon";
   },
 
-  appendDOMWindowRow(data, parent) {
-    let row = document.createElement("tr");
-    row.classList.add("window");
+  displayDOMWindowRow(data, parent) {
+    const cellCount = 2;
+    let rowId = "w:" + data.outerWindowId;
+    let row = this._getOrCreateRow(rowId, cellCount);
+    row.win = data;
+    row.className = "window";
 
     // Column: filename
+    let nameCell = row.firstChild;
     let tab = tabFinder.get(data.outerWindowId);
     let fluentName;
     let name;
     let className;
     if (parent.type == "extension") {
       fluentName = "about-processes-extension-name";
       if (data.addon) {
         name = data.addon.name;
@@ -705,39 +746,37 @@ var View = {
       fluentName = "about-processes-frame-name-one";
       name = data.prePath;
       className = "frame-one";
     } else {
       fluentName = "about-processes-frame-name-many";
       name = data.prePath;
       className = "frame-many";
     }
-    let elt = this._addCell(row, {
+    this._fillCell(nameCell, {
       fluentName,
       fluentArgs: {
         name,
         url: data.documentURI.spec,
         number: data.count,
         shortUrl:
           data.documentURI.scheme == "about"
             ? data.documentURI.spec
             : data.documentURI.prePath,
       },
       classes: ["name", "indent", "favicon", className],
     });
     let image = tab?.tab.getAttribute("image");
     if (image) {
-      elt.style.backgroundImage = `url('${image}')`;
+      nameCell.style.backgroundImage = `url('${image}')`;
     }
 
     // Column: action
-    let killButton = this._addCell(row, {
-      content: "",
-      classes: ["action-icon"],
-    });
+    let killButton = nameCell.nextSibling;
+    killButton.className = "action-icon";
 
     if (data.tab && data.tab.tabbrowser) {
       // A tab. We want to be able to close it.
       if (
         this._killedRecently.some(
           kill => kill.windowId && kill.windowId == data.outerWindowId
         )
       ) {
@@ -751,94 +790,85 @@ var View = {
         // killed.
         row.classList.add("killed");
       } else {
         // Otherwise, let's display the kill button.
         killButton.classList.add("close-icon");
         document.l10n.setAttributes(killButton, "about-processes-shutdown-tab");
       }
     }
-    this._fragment.appendChild(row);
-    return row;
   },
 
   /**
-   * Append a row showing a single thread.
+   * Display a row showing a single thread.
    *
    * @param {ThreadDelta} data The data to display.
-   * @return {DOMElement} The row displaying the thread.
    */
-  appendThreadRow(data, units) {
-    let row = document.createElement("tr");
-    row.classList.add("thread");
+  displayThreadRow(data, units) {
+    const cellCount = 3;
+    let rowId = "t:" + data.tid;
+    let row = this._getOrCreateRow(rowId, cellCount);
+    row.thread = data;
+    row.className = "thread";
 
     // Column: filename
-    this._addCell(row, {
+    let nameCell = row.firstChild;
+    this._fillCell(nameCell, {
       fluentName: "about-processes-thread-name",
       fluentArgs: {
         name: data.name,
         tid: "" + data.tid /* Make sure that this number is not localized */,
       },
       classes: ["name", "double_indent"],
     });
 
     // Column: CPU: User and Kernel
+    let cpuCell = nameCell.nextSibling;
     if (data.slopeCpu == null) {
-      this._addCell(row, {
+      this._fillCell(cpuCell, {
         fluentName: "about-processes-cpu-user-and-kernel-not-ready",
         classes: ["cpu"],
       });
     } else {
       let { duration, unit } = this._getDuration(data.totalCpu);
       let localizedUnit = units.duration[unit];
       if (data.slopeCpu == 0) {
-        this._addCell(row, {
+        this._fillCell(cpuCell, {
           fluentName: "about-processes-cpu-user-and-kernel-idle",
           fluentArgs: {
             total: duration,
             unit: localizedUnit,
           },
           classes: ["cpu"],
         });
       } else {
-        this._addCell(row, {
+        this._fillCell(cpuCell, {
           fluentName: "about-processes-cpu-user-and-kernel",
           fluentArgs: {
             percent: data.slopeCpu,
             total: duration,
             unit: localizedUnit,
           },
           classes: ["cpu"],
         });
       }
     }
 
-    // Column: Buttons (empty)
-    this._addCell(row, {
-      content: "",
-      classes: [],
-    });
-
-    this._fragment.appendChild(row);
-    return row;
+    // Third column (Buttons) is empty, nothing to do.
   },
 
-  _addCell(row, { content, classes, fluentName, fluentArgs }) {
-    let elt = document.createElement("td");
-    if (fluentName) {
-      let span = document.createElement("span");
-      document.l10n.setAttributes(span, fluentName, fluentArgs);
+  _orderedRows: [],
+  _fillCell(elt, { classes, fluentName, fluentArgs }) {
+    let span = elt.firstChild;
+    if (!span) {
+      span = document.createElement("span");
       elt.appendChild(span);
-    } else {
-      elt.textContent = content;
-      elt.setAttribute("title", content);
     }
-    elt.classList.add(...classes);
-    row.appendChild(elt);
-    return elt;
+    document.l10n.setAttributes(span, fluentName, fluentArgs);
+    elt.className = classes.join(" ");
   },
 
   _getDuration(rawDurationNS) {
     if (rawDurationNS <= NS_PER_US) {
       return { duration: rawDurationNS, unit: "ns" };
     }
     if (rawDurationNS <= NS_PER_MS) {
       return { duration: rawDurationNS / NS_PER_US, unit: "us" };
@@ -909,17 +939,17 @@ var Control = {
   _hungItems: new Set(),
   _sortColumn: null,
   _sortAscendent: true,
   _removeSubtree(row) {
     let sibling = row.nextSibling;
     while (sibling && !sibling.classList.contains("process")) {
       let next = sibling.nextSibling;
       if (sibling.classList.contains("thread")) {
-        sibling.remove();
+        View._removeRow(sibling);
       }
       sibling = next;
     }
   },
   init() {
     this._initHangReports();
 
     // Start prefetching units.
@@ -1107,44 +1137,32 @@ var Control = {
     if (document.hidden) {
       return;
     }
 
     await wait(0);
 
     await this._updateDisplay(force);
   },
-  _setRowId(row, id, selectedId) {
-    row.rowId = id;
-    if (id == selectedId) {
-      row.setAttribute("selected", "true");
-      this.selectedRow = row;
-    }
-  },
 
   // The force parameter can force a full update even when the mouse has been
   // moved recently.
   async _updateDisplay(force = false) {
     if (
       !force &&
       Date.now() - this._lastMouseEvent < TIME_BEFORE_SORTING_AGAIN
     ) {
       return;
     }
 
     let counters = State.getCounters();
     let units = await gPromisePrefetchedUnits;
 
-    // Reset the selectedRow field and the _openItems set each time we redraw
-    // to avoid keeping forever references to dead processes.
-    let selectedRowId;
-    if (this.selectedRow) {
-      selectedRowId = this.selectedRow.rowId;
-      this.selectedRow = null;
-    }
+    // Reset 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;
@@ -1156,66 +1174,59 @@ var Control = {
       this._sortDOMWindows(process.windows);
 
       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, units);
-      this._setRowId(processRow, "p:" + process.pid, selectedRowId);
-      processRow.process = process;
+      let processRow = View.displayProcessRow(process, units);
 
       if (process.type != "extension") {
         // We do not want to display extensions.
-        let winRow;
         for (let win of process.windows) {
           if (SHOW_ALL_SUBFRAMES || win.tab || win.isProcessRoot) {
-            winRow = View.appendDOMWindowRow(win, process);
-            this._setRowId(winRow, "w:" + win.outerWindowId, selectedRowId);
-            winRow.win = win;
+            View.displayDOMWindowRow(win, process);
           }
         }
       }
 
       if (SHOW_THREADS) {
-        let threadSummaryRow = View.appendThreadSummaryRow(process, isOpen);
-        this._setRowId(threadSummaryRow, "ts:" + process.pid, selectedRowId);
-        threadSummaryRow.process = process;
-
+        View.displayThreadSummaryRow(process, isOpen);
         if (isOpen) {
           this._openItems.add(process.pid);
-          this._showThreads(processRow, units, selectedRowId);
+          this._showThreads(processRow, units);
         }
       }
       if (
         this._sortColumn == null &&
         previousProcess &&
         previousProcess.displayRank != process.displayRank
       ) {
         // Add a separation between successive categories of processes.
         processRow.classList.add("separate-from-previous-process-group");
       }
       previousProcess = process;
     }
 
-    await View.commit();
+    View.commit();
+
+    // Reset the selectedRow field if that row is no longer in the DOM
+    // to avoid keeping forever references to dead processes.
+    if (this.selectedRow && !this.selectedRow.parentNode) {
+      this.selectedRow = null;
+    }
   },
-  _showThreads(row, units, selectedRowId) {
+  _showThreads(row, units) {
     let process = row.process;
     this._sortThreads(process.threads);
-    let elt = row;
     for (let thread of process.threads) {
-      elt = View.appendThreadRow(thread, units);
-      this._setRowId(elt, "t:" + thread.tid, selectedRowId);
-      // Enrich `elt` with a property `thread`, used for testing.
-      elt.thread = thread;
+      View.displayThreadRow(thread, units);
     }
-    return elt;
   },
   _sortThreads(threads) {
     return threads.sort((a, b) => {
       let order;
       switch (this._sortColumn) {
         case "column-name":
           order = a.name.localeCompare(b.name) || a.pid - b.pid;
           break;
--- a/toolkit/components/aboutprocesses/tests/browser/head.js
+++ b/toolkit/components/aboutprocesses/tests/browser/head.js
@@ -46,23 +46,27 @@ async function promiseAboutProcessesUpda
 }) {
   let startTime = performance.now();
   let mutationPromise = new Promise(resolve => {
     let observer = new doc.ownerGlobal.MutationObserver(() => {
       info("Observed about:processes tbody childList change");
       observer.disconnect();
       resolve();
     });
-    observer.observe(tbody, { childList: true });
+    observer.observe(tbody, {
+      childList: true,
+      attributes: true,
+      subtree: true,
+    });
   });
 
   if (force) {
     await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => {
       info("Forcing about:processes refresh");
-      content.Control.update(/* force = */ true);
+      await content.Control.update(/* force = */ true);
     });
   }
 
   await mutationPromise;
 
   // Fluent will update the visible table content during the next
   // refresh driver tick, wait for it.
   await new Promise(doc.defaultView.requestAnimationFrame);