Bug 1734597 - Make about:processes fallback to cycle count to decide if a thread or process is active to workaround low CPU timing precision on Windows, r=dthayer,fluent-reviewers,flod.
authorFlorian Queze <florian@queze.net>
Mon, 11 Oct 2021 12:46:28 +0000
changeset 595355 df62726f3d16bb27d0c408e204151148035871dd
parent 595354 6e92568e4216c7a9cae83eadcc02b24347f43afc
child 595356 49f06640fdee2aa53ce8d9f0bc5959f95f376e88
push id151218
push userfqueze@mozilla.com
push dateMon, 11 Oct 2021 12:48:52 +0000
treeherderautoland@df62726f3d16 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdthayer, fluent-reviewers, flod
bugs1734597
milestone95.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 1734597 - Make about:processes fallback to cycle count to decide if a thread or process is active to workaround low CPU timing precision on Windows, r=dthayer,fluent-reviewers,flod. Differential Revision: https://phabricator.services.mozilla.com/D127813
dom/chrome-webidl/ChromeUtils.webidl
toolkit/components/aboutprocesses/content/aboutProcesses.js
toolkit/components/aboutprocesses/tests/browser/head.js
toolkit/components/processtools/ProcInfo.h
toolkit/components/processtools/ProcInfo_win.cpp
toolkit/locales/en-US/toolkit/about/aboutProcesses.ftl
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -626,16 +626,17 @@ enum WebIDLProcType {
  * These dictionaries hold information about Firefox running processes and
  * threads.
  *
  * See widget/ProcInfo.h for fields documentation.
  */
 dictionary ThreadInfoDictionary {
   long long tid = 0;
   DOMString name = "";
+  unsigned long long cpuCycleCount = 0;
   unsigned long long cpuUser = 0;
   unsigned long long cpuKernel = 0;
 };
 
 dictionary WindowInfoDictionary {
   // Window ID, as known to the parent process.
   unsigned long long outerWindowId = 0;
 
@@ -679,16 +680,21 @@ dictionary ChildProcInfoDictionary {
   unsigned long long memory = 0;
 
   // Time spent by the process in user mode, in ns.
   unsigned long long cpuUser = 0;
 
   // Time spent by the process in kernel mode, in ns.
   unsigned long long cpuKernel = 0;
 
+  // Total CPU cycles used by this process.
+  // On Windows where the resolution of CPU timings is 16ms, this can
+  // be used to determine if a process is idle or slightly active.
+  unsigned long long cpuCycleCount = 0;
+
   // Thread information for this process.
   sequence<ThreadInfoDictionary> threads = [];
 
   // --- Firefox info
 
   // Internal-to-Firefox process identifier.
   unsigned long long childID = 0;
 
@@ -722,16 +728,21 @@ dictionary ParentProcInfoDictionary {
   unsigned long long memory = 0;
 
   // Time spent by the process in user mode, in ns.
   unsigned long long cpuUser = 0;
 
   // Time spent by the process in kernel mode, in ns.
   unsigned long long cpuKernel = 0;
 
+  // Total CPU cycles used by this process.
+  // On Windows where the resolution of CPU timings is 16ms, this can
+  // be used to determine if a process is idle or slightly active.
+  unsigned long long cpuCycleCount = 0;
+
   // Thread information for this process.
   sequence<ThreadInfoDictionary> threads = [];
 
   // Information on children processes.
   sequence<ChildProcInfoDictionary> children = [];
 
   // --- Firefox info
   // Type of this parent process.
--- a/toolkit/components/aboutprocesses/content/aboutProcesses.js
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.js
@@ -29,16 +29,19 @@ const NS_PER_DAY = NS_PER_HOUR * 24;
 const ONE_GIGA = 1024 * 1024 * 1024;
 const ONE_MEGA = 1024 * 1024;
 const ONE_KILO = 1024;
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
+const { AppConstants } = ChromeUtils.import(
+  "resource://gre/modules/AppConstants.jsm"
+);
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ContextualIdentityService:
     "resource://gre/modules/ContextualIdentityService.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(this, "ProfilerPopupBackground", function() {
   return ChromeUtils.import(
@@ -214,35 +217,30 @@ var State = {
       this._buffer.shift();
     }
   },
 
   _getThreadDelta(cur, prev, deltaT) {
     let result = {
       tid: cur.tid,
       name: cur.name || `(${cur.tid})`,
-      // Total amount of CPU used, in ns (user).
-      totalCpuUser: cur.cpuUser,
-      slopeCpuUser: null,
-      // Total amount of CPU used, in ns (kernel).
-      totalCpuKernel: cur.cpuKernel,
-      slopeCpuKernel: null,
       // Total amount of CPU used, in ns (user + kernel).
       totalCpu: cur.cpuUser + cur.cpuKernel,
       slopeCpu: null,
+      active: null,
     };
     if (!prev) {
       return result;
     }
     if (prev.tid != cur.tid) {
       throw new Error("Assertion failed: A thread cannot change tid.");
     }
-    result.slopeCpuUser = (cur.cpuUser - prev.cpuUser) / deltaT;
-    result.slopeCpuKernel = (cur.cpuKernel - prev.cpuKernel) / deltaT;
-    result.slopeCpu = result.slopeCpuKernel + result.slopeCpuUser;
+    result.slopeCpu =
+      (cur.cpuUser + cur.cpuKernel - prev.cpuUser - prev.cpuKernel) / deltaT;
+    result.active = !!result.slopeCpu || cur.cpuCycleCount > prev.cpuCycleCount;
     return result;
   },
 
   _getDOMWindows(process) {
     if (!process.windows) {
       return [];
     }
     if (!process.type == "extensions") {
@@ -309,22 +307,19 @@ var State = {
   _getProcessDelta(cur, prev) {
     let windows = this._getDOMWindows(cur);
     let result = {
       pid: cur.pid,
       childID: cur.childID,
       filename: cur.filename,
       totalRamSize: cur.memory,
       deltaRamSize: null,
-      totalCpuUser: cur.cpuUser,
-      slopeCpuUser: null,
-      totalCpuKernel: cur.cpuKernel,
-      slopeCpuKernel: null,
       totalCpu: cur.cpuUser + cur.cpuKernel,
       slopeCpu: null,
+      active: null,
       type: cur.type,
       origin: cur.origin || "",
       threads: null,
       displayRank: Control._getDisplayGroupRank(cur, windows),
       windows,
       // If this process has an unambiguous title, store it here.
       title: null,
     };
@@ -361,19 +356,19 @@ var State = {
         let prevThread = prevThreads.get(curThread.tid);
         if (!prevThread) {
           return this._getThreadDelta(curThread);
         }
         return this._getThreadDelta(curThread, prevThread, deltaT);
       });
     }
     result.deltaRamSize = cur.memory - prev.memory;
-    result.slopeCpuUser = (cur.cpuUser - prev.cpuUser) / deltaT;
-    result.slopeCpuKernel = (cur.cpuKernel - prev.cpuKernel) / deltaT;
-    result.slopeCpu = result.slopeCpuUser + result.slopeCpuKernel;
+    result.slopeCpu =
+      (cur.cpuUser + cur.cpuKernel - prev.cpuUser - prev.cpuKernel) / deltaT;
+    result.active = !!result.slopeCpu || cur.cpuCycleCount > prev.cpuCycleCount;
     result.threads = threads;
     return result;
   },
 
   getCounters() {
     tabFinder.update();
 
     // We rebuild the maps during each iteration to make sure that
@@ -466,16 +461,56 @@ var View = {
       }
       row.rowId = rowId;
       this._rowsById.set(rowId, row);
     }
     this._orderedRows.push(row);
     return row;
   },
 
+  displayCpu(data, cpuCell) {
+    if (data.slopeCpu == null) {
+      this._fillCell(cpuCell, {
+        fluentName: "about-processes-cpu-user-and-kernel-not-ready",
+        classes: ["cpu"],
+      });
+    } else {
+      let { duration, unit } = this._getDuration(data.totalCpu);
+      if (data.totalCpu == 0 && AppConstants.platform == "win") {
+        // The minimum non zero CPU time we can get on Windows is 16ms
+        // so avoid displaying '0ns'.
+        unit = "ms";
+      }
+      let localizedUnit = gLocalizedUnits.duration[unit];
+      if (data.slopeCpu == 0) {
+        let fluentName = data.active
+          ? "about-processes-cpu-almost-idle"
+          : "about-processes-cpu-fully-idle";
+        this._fillCell(cpuCell, {
+          fluentName,
+          fluentArgs: {
+            total: duration,
+            unit: localizedUnit,
+          },
+          classes: ["cpu"],
+        });
+      } else {
+        this._fillCell(cpuCell, {
+          fluentName: "about-processes-cpu",
+          fluentArgs: {
+            percent: data.slopeCpu,
+            total: duration,
+            unit: localizedUnit,
+          },
+          classes: ["cpu"],
+        });
+      }
+    }
+  },
+
   /**
    * Display a row showing a single process (without its threads).
    *
    * @param {ProcessDelta} data The data to display.
    * @return {DOMElement} The row displaying the process.
    */
   displayProcessRow(data) {
     const cellCount = 4;
@@ -677,47 +712,19 @@ var View = {
             total: formattedTotal.amount,
             totalUnit: gLocalizedUnits.memory[formattedTotal.unit],
           },
           classes: ["memory"],
         });
       }
     }
 
-    // Column: CPU: User and Kernel
+    // Column: CPU
     let cpuCell = memoryCell.nextSibling;
-    if (data.slopeCpu == null) {
-      this._fillCell(cpuCell, {
-        fluentName: "about-processes-cpu-user-and-kernel-not-ready",
-        classes: ["cpu"],
-      });
-    } else {
-      let { duration, unit } = this._getDuration(data.totalCpu);
-      let localizedUnit = gLocalizedUnits.duration[unit];
-      if (data.slopeCpu == 0) {
-        this._fillCell(cpuCell, {
-          fluentName: "about-processes-cpu-idle",
-          fluentArgs: {
-            total: duration,
-            unit: localizedUnit,
-          },
-          classes: ["cpu"],
-        });
-      } else {
-        this._fillCell(cpuCell, {
-          fluentName: "about-processes-cpu",
-          fluentArgs: {
-            percent: data.slopeCpu,
-            total: duration,
-            unit: localizedUnit,
-          },
-          classes: ["cpu"],
-        });
-      }
-    }
+    this.displayCpu(data, cpuCell);
 
     // Column: Kill button – but not for all processes.
     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)) {
@@ -759,17 +766,17 @@ var View = {
     let isOpen = false;
 
     // Column: Name
     let nameCell = row.firstChild;
     let threads = data.threads;
     let activeThreads = new Map();
     let activeThreadCount = 0;
     for (let t of data.threads) {
-      if (!t.slopeCpu) {
+      if (!t.active) {
         continue;
       }
       ++activeThreadCount;
       let name = t.name.replace(/ ?#[0-9]+$/, "");
       if (!activeThreads.has(name)) {
         activeThreads.set(name, { name, slopeCpu: t.slopeCpu, count: 1 });
       } else {
         let thread = activeThreads.get(name);
@@ -923,47 +930,18 @@ var View = {
       fluentName: "about-processes-thread-name-and-id",
       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._fillCell(cpuCell, {
-        fluentName: "about-processes-cpu-user-and-kernel-not-ready",
-        classes: ["cpu"],
-      });
-    } else {
-      let { duration, unit } = this._getDuration(data.totalCpu);
-      let localizedUnit = gLocalizedUnits.duration[unit];
-      if (data.slopeCpu == 0) {
-        this._fillCell(cpuCell, {
-          fluentName: "about-processes-cpu-idle",
-          fluentArgs: {
-            total: duration,
-            unit: localizedUnit,
-          },
-          classes: ["cpu"],
-        });
-      } else {
-        this._fillCell(cpuCell, {
-          fluentName: "about-processes-cpu",
-          fluentArgs: {
-            percent: data.slopeCpu,
-            total: duration,
-            unit: localizedUnit,
-          },
-          classes: ["cpu"],
-        });
-      }
-    }
+    // Column: CPU
+    this.displayCpu(data, nameCell.nextSibling);
 
     // Third column (Buttons) is empty, nothing to do.
   },
 
   _orderedRows: [],
   _fillCell(elt, { classes, fluentName, fluentArgs }) {
     document.l10n.setAttributes(elt, fluentName, fluentArgs);
     elt.className = classes.join(" ");
@@ -1333,32 +1311,37 @@ var Control = {
     // to avoid keeping forever references to dead processes.
     if (this.selectedRow && !this.selectedRow.parentNode) {
       this.selectedRow = null;
     }
 
     // Used by tests to differentiate full updates from l10n updates.
     document.dispatchEvent(new CustomEvent("AboutProcessesUpdated"));
   },
+  _compareCpu(a, b) {
+    return (
+      b.slopeCpu - a.slopeCpu || b.active - a.active || b.totalCpu - a.totalCpu
+    );
+  },
   _showThreads(row) {
     let process = row.process;
     this._sortThreads(process.threads);
     for (let thread of process.threads) {
       View.displayThreadRow(thread);
     }
   },
   _sortThreads(threads) {
     return threads.sort((a, b) => {
       let order;
       switch (this._sortColumn) {
         case "column-name":
           order = a.name.localeCompare(b.name) || a.tid - b.tid;
           break;
         case "column-cpu-total":
-          order = b.slopeCpu - a.slopeCpu;
+          order = this._compareCpu(a, b);
           break;
         case "column-memory-resident":
         case null:
           order = a.tid - b.tid;
           break;
         default:
           throw new Error("Unsupported order: " + this._sortColumn);
       }
@@ -1374,17 +1357,17 @@ var Control = {
       switch (this._sortColumn) {
         case "column-name":
           order =
             String(a.origin).localeCompare(b.origin) ||
             String(a.type).localeCompare(b.type) ||
             a.pid - b.pid;
           break;
         case "column-cpu-total":
-          order = b.slopeCpu - a.slopeCpu;
+          order = this._compareCpu(a, b);
           break;
         case "column-memory-resident":
           order = b.totalRamSize - a.totalRamSize;
           break;
         case null:
           // Default order: classify processes by group.
           order =
             a.displayRank - b.displayRank ||
--- a/toolkit/components/aboutprocesses/tests/browser/head.js
+++ b/toolkit/components/aboutprocesses/tests/browser/head.js
@@ -629,17 +629,17 @@ async function testAboutProcessesWithCon
         0,
         "The number of active threads should never be negative"
       );
       Assert.lessOrEqual(
         active,
         number,
         "The number of active threads should not exceed the total number of threads"
       );
-      let activeThreads = row.process.threads.filter(t => t.slopeCpu);
+      let activeThreads = row.process.threads.filter(t => t.active);
       Assert.equal(
         active,
         activeThreads.length,
         "The displayed number of active threads should be correct"
       );
 
       let activeSet = new Set();
       for (let t of activeThreads) {
--- a/toolkit/components/processtools/ProcInfo.h
+++ b/toolkit/components/processtools/ProcInfo.h
@@ -55,16 +55,17 @@ struct ThreadInfo {
   // Thread Id.
   base::ProcessId tid = 0;
   // Thread name, if any.
   nsString name;
   // User time in ns.
   uint64_t cpuUser = 0;
   // System time in ns.
   uint64_t cpuKernel = 0;
+  uint64_t cpuCycleCount = 0;
 };
 
 // Info on a DOM window.
 struct WindowInfo {
   explicit WindowInfo()
       : outerWindowId(0),
         documentURI(nullptr),
         documentTitle(u""_ns),
@@ -106,16 +107,17 @@ struct ProcInfo {
   // Process filename (without the path name).
   nsString filename;
   // Memory size in bytes.
   uint64_t memory = 0;
   // User time in ns.
   uint64_t cpuUser = 0;
   // System time in ns.
   uint64_t cpuKernel = 0;
+  uint64_t cpuCycleCount = 0;
   // Threads owned by this process.
   CopyableTArray<ThreadInfo> threads;
   // DOM windows represented by this process.
   CopyableTArray<WindowInfo> windows;
 };
 
 typedef MozPromise<mozilla::HashMap<base::ProcessId, ProcInfo>, nsresult, true>
     ProcInfoPromise;
@@ -208,27 +210,29 @@ RefPtr<ProcInfoPromise> GetProcInfo(nsTA
 template <typename T>
 nsresult CopySysProcInfoToDOM(const ProcInfo& source, T* dest) {
   // Copy system info.
   dest->mPid = source.pid;
   dest->mFilename.Assign(source.filename);
   dest->mMemory = source.memory;
   dest->mCpuUser = source.cpuUser;
   dest->mCpuKernel = source.cpuKernel;
+  dest->mCpuCycleCount = source.cpuCycleCount;
 
   // Copy thread info.
   mozilla::dom::Sequence<mozilla::dom::ThreadInfoDictionary> threads;
   for (const ThreadInfo& entry : source.threads) {
     mozilla::dom::ThreadInfoDictionary* thread =
         threads.AppendElement(fallible);
     if (NS_WARN_IF(!thread)) {
       return NS_ERROR_OUT_OF_MEMORY;
     }
     thread->mCpuUser = entry.cpuUser;
     thread->mCpuKernel = entry.cpuKernel;
+    thread->mCpuCycleCount = entry.cpuCycleCount;
     thread->mTid = entry.tid;
     thread->mName.Assign(entry.name);
   }
   dest->mThreads = std::move(threads);
   return NS_OK;
 }
 
 }  // namespace mozilla
--- a/toolkit/components/processtools/ProcInfo_win.cpp
+++ b/toolkit/components/processtools/ProcInfo_win.cpp
@@ -86,16 +86,18 @@ RefPtr<ProcInfoPromise> GetProcInfo(nsTA
           info.pid = request.pid;
           info.childId = request.childId;
           info.type = request.processType;
           info.origin = request.origin;
           info.windows = std::move(request.windowInfo);
           info.filename.Assign(filename);
           info.cpuKernel = ToNanoSeconds(kernelTime);
           info.cpuUser = ToNanoSeconds(userTime);
+          QueryProcessCycleTime(handle.get(), &info.cpuCycleCount);
+
           info.memory = memoryCounters.PrivateUsage;
 
           if (!gathered.put(request.pid, std::move(info))) {
             holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
             return;
           }
         }
 
@@ -152,16 +154,18 @@ RefPtr<ProcInfoPromise> GetProcInfo(nsTA
           // If we fail, continue without this piece of information.
           FILETIME createTime, exitTime, kernelTime, userTime;
           if (GetThreadTimes(hThread.get(), &createTime, &exitTime, &kernelTime,
                              &userTime)) {
             threadInfo->cpuKernel = ToNanoSeconds(kernelTime);
             threadInfo->cpuUser = ToNanoSeconds(userTime);
           }
 
+          QueryThreadCycleTime(hThread.get(), &threadInfo->cpuCycleCount);
+
           // Attempt to get thread name.
           // If we fail, continue without this piece of information.
           if (getThreadDescription) {
             PWSTR threadName = nullptr;
             if (getThreadDescription(hThread.get(), &threadName) &&
                 threadName) {
               threadInfo->name = threadName;
             }
--- a/toolkit/locales/en-US/toolkit/about/aboutProcesses.ftl
+++ b/toolkit/locales/en-US/toolkit/about/aboutProcesses.ftl
@@ -133,19 +133,24 @@ about-processes-frame-name-many = Subfra
 
 # Common case.
 about-processes-cpu = { NUMBER($percent, maximumSignificantDigits: 2, style: "percent") }
     .title = Total CPU time: { NUMBER($total, maximumFractionDigits: 0) }{ $unit }
 
 # Special case: data is not available yet.
 about-processes-cpu-user-and-kernel-not-ready = (measuring)
 
+# Special case: process or thread is almost idle (using less than 0.1% of a CPU core).
+# This case only occurs on Windows where the precision of the CPU times is low.
+about-processes-cpu-almost-idle = < 0.1%
+    .title = Total CPU time: { NUMBER($total, maximumFractionDigits: 0) }{ $unit }
+
 # Special case: process or thread is currently idle.
-about-processes-cpu-idle = idle
-    .title = Total CPU time: { NUMBER($total, maximumFractionDigits: 2) }{ $unit }
+about-processes-cpu-fully-idle = idle
+    .title = Total CPU time: { NUMBER($total, maximumFractionDigits: 0) }{ $unit }
 
 ## Displaying Memory (total and delta)
 ## Variables:
 ##    $total (Number) The amount of memory currently used by the process.
 ##    $totalUnit (String) The unit in which to display $total. See the definitions
 ##                        of `memory-unit-*`.
 ##    $delta (Number) The absolute value of the amount of memory added recently.
 ##    $deltaSign (String) Either "+" if the amount of memory has increased