summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutprocesses/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutprocesses/content')
-rw-r--r--toolkit/components/aboutprocesses/content/aboutProcesses.css270
-rw-r--r--toolkit/components/aboutprocesses/content/aboutProcesses.html54
-rw-r--r--toolkit/components/aboutprocesses/content/aboutProcesses.js1570
3 files changed, 1894 insertions, 0 deletions
diff --git a/toolkit/components/aboutprocesses/content/aboutProcesses.css b/toolkit/components/aboutprocesses/content/aboutProcesses.css
new file mode 100644
index 0000000000..39848813b6
--- /dev/null
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.css
@@ -0,0 +1,270 @@
+/* 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/. */
+
+@import url("chrome://global/skin/in-content/common.css");
+
+html {
+ background-color: var(--in-content-page-background);
+}
+body {
+ overflow-x: hidden;
+}
+
+#process-table {
+ user-select: none;
+ font-size: 1em;
+ border-spacing: 0;
+ background-color: var(--in-content-box-background);
+ margin: 0;
+ position: absolute;
+ top: 0;
+ inset-inline-start: 0;
+ width: 100%;
+ height: 100%;
+}
+
+/* Avoid scrolling the header */
+#process-tbody {
+ display: block;
+ margin-top: 2em;
+}
+#process-thead {
+ position: fixed;
+ z-index: 1;
+ height: 2em;
+ border-bottom: 1px solid var(--in-content-border-color);
+ width: 100%;
+ background-color: var(--in-content-box-background);
+}
+tr {
+ display: grid;
+ /* Flexible width for the name column, 15% width for the memory and CPU column,
+ * then fixed width (16px icon + 2*10px margin) for the actions column. */
+ grid-template-columns: 1fr 15% 15% 36px;
+ grid-auto-rows: 2em;
+ width: 100%;
+}
+
+.cpu, .memory {
+ text-align: end;
+}
+
+#process-thead > tr {
+ height: inherit;
+}
+
+th {
+ font-weight: normal;
+ text-align: start;
+ background-color: var(--in-content-button-background);
+}
+th:not(:first-child) {
+ border-inline-start: 1px solid;
+ border-image: linear-gradient(transparent 0%, transparent 20%, var(--in-content-box-border-color) 20%, var(--in-content-box-border-color) 80%, transparent 80%, transparent 100%) 1 1;
+}
+th, td {
+ padding: 5px 10px;
+ min-height: 16px;
+ max-height: 2em;
+ overflow: hidden;
+ white-space: nowrap;
+}
+td.type, td.favicon {
+ background-repeat: no-repeat;
+ background-origin: border-box;
+ background-size: 16px 16px;
+ background-position: 11px center;
+ padding-inline-start: 38px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+td.type:dir(rtl), td.favicon:dir(rtl) {
+ background-position-x: right 11px;
+}
+td:first-child {
+ text-overflow: ellipsis;
+}
+
+.profiler-icon {
+ background: url("chrome://devtools/skin/images/tool-profiler.svg") no-repeat center;
+ display: inline-block;
+ height: 100%;
+ width: 16px;
+ padding: 5px 7px;
+ /* The -10px is -5 to undo the td padding and -5 to undo the padding here. */
+ margin: -10px 3px;
+ -moz-context-properties: fill, fill-opacity;
+ fill-opacity: 0.9;
+ fill: currentColor;
+}
+.profiler-icon:hover {
+ background-color: var(--in-content-button-background-hover);
+ color: var(--in-content-button-text-color-hover);
+}
+
+.profiler-icon:hover:active {
+ background-color: var(--in-content-button-background-active);
+ color: var(--in-content-button-text-color-active);
+ fill-opacity: 1;
+}
+
+.profiler-icon:not(.profiler-active) {
+ transform: scaleX(-1);
+}
+
+.profiler-active {
+ fill: var(--in-content-accent-color);
+}
+td:not(:hover) > .profiler-icon:not(.profiler-active) {
+ display: none;
+}
+
+.twisty {
+ position: relative;
+}
+/* Putting the background image in a positioned pseudo element lets us
+* use CSS transforms on the background image, which we need for rtl. */
+.twisty::before {
+ content: url("chrome://global/skin/icons/arrow-right-12.svg");
+ position: absolute;
+ display: block;
+ line-height: 50%;
+ top: 4px; /* Half the image's height */
+ inset-inline-start: -16px;
+ width: 12px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+.twisty:dir(rtl)::before {
+ content: url("chrome://global/skin/icons/arrow-left-12.svg");
+}
+.twisty.open::before {
+ content: url("chrome://global/skin/icons/arrow-down-12.svg");
+}
+.twisty:-moz-focusring {
+ outline: none;
+}
+.twisty:-moz-focusring::before {
+ outline: var(--in-content-focus-outline);
+}
+.indent {
+ padding-inline: 48px 0;
+}
+.double_indent {
+ padding-inline: 58px 0;
+}
+
+tr[selected] > td {
+ background-color: var(--in-content-item-selected);
+ color: var(--in-content-item-selected-text);
+}
+#process-tbody > tr:hover {
+ background-color: var(--in-content-item-hover);
+ color: var(--in-content-item-hover-text);
+}
+
+/* Tab names and thread summary text can extend into memory and CPU columns. */
+.window > :first-child,
+.thread-summary > :first-child {
+ grid-column: 1 / 4;
+}
+
+/* Thread names can extend into the memory column. */
+.thread > :first-child {
+ grid-column: 1 / 3;
+}
+
+.clickable {
+ background-repeat: no-repeat;
+ background-position: right 4px center;
+}
+.clickable:dir(rtl) {
+ background-position-x: left 4px;
+}
+
+.arrow-up,
+.arrow-down {
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.arrow-up {
+ background-image: url("chrome://global/skin/icons/arrow-up-12.svg");
+}
+.arrow-down {
+ background-image: url("chrome://global/skin/icons/arrow-down-12.svg");
+}
+
+th.clickable:hover {
+ background-color: var(--in-content-button-background-hover);
+ color: var(--in-content-button-text-color-hover);
+}
+th.clickable:hover:active {
+ background-color: var(--in-content-button-background-active);
+ color: var(--in-content-button-text-color-active);
+}
+
+tr.process > td.type {
+ font-weight: bold;
+}
+tr.thread {
+ font-size-adjust: 0.5;
+}
+
+.killing {
+ opacity: 0.3;
+ transition-property: opacity;
+ transition-duration: 1s;
+}
+
+.killed {
+ opacity: 0.3;
+}
+
+/* icons */
+.close-icon {
+ background: url("chrome://global/skin/icons/close.svg") no-repeat center;
+ opacity: 0; /* Start out as transparent */
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+tr:is([selected], :hover):not(.killing) > .close-icon {
+ opacity: 1;
+}
+
+.close-icon:hover {
+ background-color: var(--in-content-button-background-hover);
+ color: var(--in-content-button-text-color-hover);
+}
+
+.close-icon:hover:active {
+ background-color: var(--in-content-button-background-active);
+ color: var(--in-content-button-text-color-active);
+}
+
+/* column-name */
+
+/* When the process is reported as frozen, we display an hourglass before its name. */
+.process.hung > :first-child > :not(.twisty)::before {
+ content: "⌛️";
+}
+
+/*
+ Show a separation between process groups.
+ */
+
+tr.separate-from-previous-process-group {
+ border-top: dotted 1px var(--in-content-box-border-color);
+ margin-top: -1px;
+}
+
+/* Graphical view of CPU use. */
+.cpu {
+ --bar-width: 0;
+ background: linear-gradient(to left, var(--blue-40) calc(var(--bar-width) * 1%), transparent calc(var(--bar-width) * 1%));
+}
+.cpu:dir(rtl) {
+ background: linear-gradient(to right, var(--blue-40) calc(var(--bar-width) * 1%), transparent calc(var(--bar-width) * 1%));
+}
diff --git a/toolkit/components/aboutprocesses/content/aboutProcesses.html b/toolkit/components/aboutprocesses/content/aboutProcesses.html
new file mode 100644
index 0000000000..76a672cdc2
--- /dev/null
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.html
@@ -0,0 +1,54 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:;img-src data:; object-src 'none'"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <title data-l10n-id="about-processes-title"></title>
+ <link
+ rel="icon"
+ id="favicon"
+ href="chrome://global/skin/icons/performance.svg"
+ />
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link rel="localization" href="toolkit/about/aboutProcesses.ftl" />
+ <link rel="localization" href="branding/brand.ftl" />
+ <script src="chrome://global/content/aboutProcesses.js"></script>
+ <link rel="stylesheet" href="chrome://global/content/aboutProcesses.css" />
+ </head>
+ <body>
+ <table id="process-table">
+ <thead id="process-thead">
+ <tr>
+ <th
+ class="clickable"
+ id="column-name"
+ data-l10n-id="about-processes-column-name"
+ ></th>
+ <th
+ class="clickable"
+ id="column-memory-resident"
+ data-l10n-id="about-processes-column-memory-resident"
+ ></th>
+ <!-- Memory usage. -->
+ <th
+ class="clickable"
+ id="column-cpu-total"
+ data-l10n-id="about-processes-column-cpu-total"
+ ></th>
+ <!--CPU time-->
+ <th id="column-kill" data-l10n-id="about-processes-column-action">
+ ⚙
+ </th>
+ <!-- Kill button. -->
+ </tr>
+ </thead>
+ <tbody id="process-tbody"></tbody>
+ </table>
+ </body>
+</html>
diff --git a/toolkit/components/aboutprocesses/content/aboutProcesses.js b/toolkit/components/aboutprocesses/content/aboutProcesses.js
new file mode 100644
index 0000000000..4ad00b2840
--- /dev/null
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.js
@@ -0,0 +1,1570 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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";
+
+// Time in ms before we start changing the sort order again after receiving a
+// mousemove event.
+const TIME_BEFORE_SORTING_AGAIN = 5000;
+
+// How long we should wait between samples.
+const MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS = 1000;
+
+// How often we should update
+const UPDATE_INTERVAL_MS = 2000;
+
+const NS_PER_US = 1000;
+const NS_PER_MS = 1000 * 1000;
+const NS_PER_S = 1000 * 1000 * 1000;
+const NS_PER_MIN = NS_PER_S * 60;
+const NS_PER_HOUR = NS_PER_MIN * 60;
+const NS_PER_DAY = NS_PER_HOUR * 24;
+
+const ONE_GIGA = 1024 * 1024 * 1024;
+const ONE_MEGA = 1024 * 1024;
+const ONE_KILO = 1024;
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ContextualIdentityService:
+ "resource://gre/modules/ContextualIdentityService.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(this, "ProfilerPopupBackground", function () {
+ return ChromeUtils.importESModule(
+ "resource://devtools/client/performance-new/shared/background.sys.mjs"
+ );
+});
+
+const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
+
+const SHOW_THREADS = Services.prefs.getBoolPref(
+ "toolkit.aboutProcesses.showThreads"
+);
+const SHOW_ALL_SUBFRAMES = Services.prefs.getBoolPref(
+ "toolkit.aboutProcesses.showAllSubframes"
+);
+const SHOW_PROFILER_ICONS = Services.prefs.getBoolPref(
+ "toolkit.aboutProcesses.showProfilerIcons"
+);
+const PROFILE_DURATION = Math.max(
+ 1,
+ Services.prefs.getIntPref("toolkit.aboutProcesses.profileDuration")
+);
+
+/**
+ * For the time being, Fluent doesn't support duration or memory formats, so we need
+ * to fetch units from Fluent. To avoid re-fetching at each update, we prefetch these
+ * units during initialization, asynchronously, and keep them.
+ *
+ * @type {
+ * duration: { ns: String, us: String, ms: String, s: String, m: String, h: String, d: String },
+ * memory: { B: String, KB: String, MB: String, GB: String, TB: String, PB: String, EB: String }
+ * }.
+ */
+let gLocalizedUnits;
+
+let tabFinder = {
+ update() {
+ this._map = new Map();
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ let tabbrowser = win.gBrowser;
+ for (let browser of tabbrowser.browsers) {
+ let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet
+ if (id != null) {
+ this._map.set(id, browser);
+ }
+ }
+ if (tabbrowser.preloadedBrowser) {
+ let browser = tabbrowser.preloadedBrowser;
+ if (browser.outerWindowID) {
+ this._map.set(browser.outerWindowID, browser);
+ }
+ }
+ }
+ },
+
+ /**
+ * Find the <xul:tab> for a window id.
+ *
+ * This is useful e.g. for reloading or closing tabs.
+ *
+ * @return null If the xul:tab could not be found, e.g. if the
+ * windowId is that of a chrome window.
+ * @return {{tabbrowser: <xul:tabbrowser>, tab: <xul.tab>}} The
+ * tabbrowser and tab if the latter could be found.
+ */
+ get(id) {
+ let browser = this._map.get(id);
+ if (!browser) {
+ return null;
+ }
+ let tabbrowser = browser.getTabBrowser();
+ if (!tabbrowser) {
+ return {
+ tabbrowser: null,
+ tab: {
+ getAttribute() {
+ return "";
+ },
+ linkedBrowser: browser,
+ },
+ };
+ }
+ return { tabbrowser, tab: tabbrowser.getTabForBrowser(browser) };
+ },
+};
+
+/**
+ * Utilities for dealing with state
+ */
+var State = {
+ // Store the previous and current samples so they can be compared.
+ _previous: null,
+ _latest: null,
+
+ async _promiseSnapshot() {
+ let date = Cu.now();
+ let main = await ChromeUtils.requestProcInfo();
+ main.date = date;
+
+ let processes = new Map();
+ processes.set(main.pid, main);
+ for (let child of main.children) {
+ child.date = date;
+ processes.set(child.pid, child);
+ }
+
+ return { processes, date };
+ },
+
+ /**
+ * Update the internal state.
+ *
+ * @return {Promise}
+ */
+ async update(force = false) {
+ if (
+ force ||
+ !this._latest ||
+ Cu.now() - this._latest.date > MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS
+ ) {
+ // Replacing this._previous before we are done awaiting
+ // this._promiseSnapshot can cause this._previous and this._latest to be
+ // equal for a short amount of time, which can cause test failures when
+ // a forced update of the display is triggered in the meantime.
+ let newSnapshot = await this._promiseSnapshot();
+ this._previous = this._latest;
+ this._latest = newSnapshot;
+ }
+ },
+
+ _getThreadDelta(cur, prev, deltaT) {
+ let result = {
+ tid: cur.tid,
+ name: cur.name || `(${cur.tid})`,
+ // Total amount of CPU used, in ns.
+ totalCpu: cur.cpuTime,
+ slopeCpu: null,
+ active: null,
+ };
+ if (!deltaT) {
+ return result;
+ }
+ result.slopeCpu = (result.totalCpu - (prev ? prev.cpuTime : 0)) / deltaT;
+ result.active =
+ !!result.slopeCpu || cur.cpuCycleCount > (prev ? prev.cpuCycleCount : 0);
+ return result;
+ },
+
+ _getDOMWindows(process) {
+ if (!process.windows) {
+ return [];
+ }
+ if (!process.type == "extensions") {
+ return [];
+ }
+ let windows = process.windows.map(win => {
+ let tab = tabFinder.get(win.outerWindowId);
+ let addon =
+ process.type == "extension"
+ ? WebExtensionPolicy.getByURI(win.documentURI)
+ : null;
+ let displayRank;
+ if (tab) {
+ displayRank = 1;
+ } else if (win.isProcessRoot) {
+ displayRank = 2;
+ } else if (win.documentTitle) {
+ displayRank = 3;
+ } else {
+ displayRank = 4;
+ }
+ return {
+ outerWindowId: win.outerWindowId,
+ documentURI: win.documentURI,
+ documentTitle: win.documentTitle,
+ isProcessRoot: win.isProcessRoot,
+ isInProcess: win.isInProcess,
+ tab,
+ addon,
+ // The number of instances we have collapsed.
+ count: 1,
+ // A rank used to quickly sort windows.
+ displayRank,
+ };
+ });
+
+ // We keep all tabs and addons but we collapse subframes that have the same host.
+
+ // A map from host -> subframe.
+ let collapsible = new Map();
+ let result = [];
+ for (let win of windows) {
+ if (win.tab || win.addon) {
+ result.push(win);
+ continue;
+ }
+ let prev = collapsible.get(win.documentURI.prePath);
+ if (prev) {
+ prev.count += 1;
+ } else {
+ collapsible.set(win.documentURI.prePath, win);
+ result.push(win);
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Compute the delta between two process snapshots.
+ *
+ * @param {ProcessSnapshot} cur
+ * @param {ProcessSnapshot?} prev
+ */
+ _getProcessDelta(cur, prev) {
+ let windows = this._getDOMWindows(cur);
+ let result = {
+ pid: cur.pid,
+ childID: cur.childID,
+ totalRamSize: cur.memory,
+ deltaRamSize: null,
+ totalCpu: cur.cpuTime,
+ slopeCpu: null,
+ active: null,
+ type: cur.type,
+ origin: cur.origin || "",
+ threads: null,
+ displayRank: Control._getDisplayGroupRank(cur, windows),
+ windows,
+ utilityActors: cur.utilityActors,
+ // If this process has an unambiguous title, store it here.
+ title: null,
+ };
+ // Attempt to determine a title for this process.
+ let titles = [
+ ...new Set(
+ result.windows
+ .filter(win => win.documentTitle)
+ .map(win => win.documentTitle)
+ ),
+ ];
+ if (titles.length == 1) {
+ result.title = titles[0];
+ }
+ if (!prev) {
+ if (SHOW_THREADS) {
+ result.threads = cur.threads.map(data => this._getThreadDelta(data));
+ }
+ return result;
+ }
+ if (prev.pid != cur.pid) {
+ throw new Error("Assertion failed: A process cannot change pid.");
+ }
+ let deltaT = (cur.date - prev.date) * NS_PER_MS;
+ let threads = null;
+ if (SHOW_THREADS) {
+ let prevThreads = new Map();
+ for (let thread of prev.threads) {
+ prevThreads.set(thread.tid, thread);
+ }
+ threads = cur.threads.map(curThread =>
+ this._getThreadDelta(curThread, prevThreads.get(curThread.tid), deltaT)
+ );
+ }
+ result.deltaRamSize = cur.memory - prev.memory;
+ result.slopeCpu = (cur.cpuTime - prev.cpuTime) / deltaT;
+ result.active = !!result.slopeCpu || cur.cpuCycleCount > prev.cpuCycleCount;
+ result.threads = threads;
+ return result;
+ },
+
+ getCounters() {
+ tabFinder.update();
+
+ let counters = [];
+
+ for (let cur of this._latest.processes.values()) {
+ let prev = this._previous?.processes.get(cur.pid);
+ counters.push(this._getProcessDelta(cur, prev));
+ }
+
+ return counters;
+ },
+};
+
+var View = {
+ // Processes, tabs and subframes that we killed during the previous iteration.
+ // Array<{pid:Number} | {windowId:Number}>
+ _killedRecently: [],
+ commit() {
+ this._killedRecently.length = 0;
+ let tbody = document.getElementById("process-tbody");
+
+ let insertPoint = tbody.firstChild;
+ let nextRow;
+ while ((nextRow = this._orderedRows.shift())) {
+ if (insertPoint && insertPoint === nextRow) {
+ insertPoint = insertPoint.nextSibling;
+ } else {
+ tbody.insertBefore(nextRow, insertPoint);
+ }
+ }
+
+ if (insertPoint) {
+ while ((nextRow = insertPoint.nextSibling)) {
+ this._removeRow(nextRow);
+ }
+ this._removeRow(insertPoint);
+ }
+ },
+ // If we are not going to display the updated list of rows, drop references
+ // to rows that haven't been inserted in the DOM tree.
+ discardUpdate() {
+ for (let row of this._orderedRows) {
+ if (!row.parentNode) {
+ this._rowsById.delete(row.rowId);
+ }
+ }
+ this._orderedRows = [];
+ },
+ insertAfterRow(row) {
+ let tbody = row.parentNode;
+ let nextRow;
+ while ((nextRow = this._orderedRows.pop())) {
+ 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;
+ },
+
+ displayCpu(data, cpuCell, maxSlopeCpu) {
+ // Put a value < 0% when we really don't want to see a bar as
+ // otherwise it sometimes appears due to rounding errors when we
+ // don't have an integer number of pixels.
+ let barWidth = -0.5;
+ 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) {
+ // A thread having used exactly 0ns of CPU time is not possible.
+ // When we get 0 it means the thread used less than the precision of
+ // the measurement, and it makes more sense to show '0ms' than '0ns'.
+ // This is useful on Linux where the minimum non-zero CPU time value
+ // for threads of child processes is 10ms, and on Windows ARM64 where
+ // the minimum non-zero value is 16ms.
+ 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"],
+ });
+
+ let cpuPercent = data.slopeCpu * 100;
+ if (maxSlopeCpu > 1) {
+ cpuPercent /= maxSlopeCpu;
+ }
+ // Ensure we always have a visible bar for non-0 values.
+ barWidth = Math.max(0.5, cpuPercent);
+ }
+ }
+ cpuCell.style.setProperty("--bar-width", barWidth);
+ },
+
+ /**
+ * Display a row showing a single process (without its threads).
+ *
+ * @param {ProcessDelta} data The data to display.
+ * @param {Number} maxSlopeCpu The largest slopeCpu value.
+ * @return {DOMElement} The row displaying the process.
+ */
+ displayProcessRow(data, maxSlopeCpu) {
+ 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 classNames = [];
+ let fluentName;
+ let fluentArgs = {
+ pid: "" + data.pid, // Make sure that this number is not localized
+ };
+ switch (data.type) {
+ case "web":
+ fluentName = "about-processes-web-process";
+ break;
+ case "webIsolated":
+ fluentName = "about-processes-web-isolated-process";
+ fluentArgs.origin = data.origin;
+ break;
+ case "webServiceWorker":
+ fluentName = "about-processes-web-serviceworker";
+ fluentArgs.origin = data.origin;
+ break;
+ case "file":
+ fluentName = "about-processes-file-process";
+ break;
+ case "extension":
+ fluentName = "about-processes-extension-process";
+ classNames = ["extensions"];
+ break;
+ case "privilegedabout":
+ fluentName = "about-processes-privilegedabout-process";
+ break;
+ case "privilegedmozilla":
+ fluentName = "about-processes-privilegedmozilla-process";
+ break;
+ case "withCoopCoep":
+ fluentName = "about-processes-with-coop-coep-process";
+ fluentArgs.origin = data.origin;
+ break;
+ case "browser":
+ fluentName = "about-processes-browser-process";
+ break;
+ case "plugin":
+ fluentName = "about-processes-plugin-process";
+ break;
+ case "gmpPlugin":
+ fluentName = "about-processes-gmp-plugin-process";
+ break;
+ case "gpu":
+ fluentName = "about-processes-gpu-process";
+ break;
+ case "vr":
+ fluentName = "about-processes-vr-process";
+ break;
+ case "rdd":
+ fluentName = "about-processes-rdd-process";
+ break;
+ case "socket":
+ fluentName = "about-processes-socket-process";
+ break;
+ case "remoteSandboxBroker":
+ fluentName = "about-processes-remote-sandbox-broker-process";
+ break;
+ case "forkServer":
+ fluentName = "about-processes-fork-server-process";
+ break;
+ case "preallocated":
+ fluentName = "about-processes-preallocated-process";
+ break;
+ case "utility":
+ fluentName = "about-processes-utility-process";
+ break;
+ // 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";
+ fluentArgs.type = data.type;
+ break;
+ }
+
+ // Show container names instead of raw origin attribute suffixes.
+ if (fluentArgs.origin?.includes("^")) {
+ let origin = fluentArgs.origin;
+ let privateBrowsingId, userContextId;
+ try {
+ ({ privateBrowsingId, userContextId } =
+ ChromeUtils.createOriginAttributesFromOrigin(origin));
+ fluentArgs.origin = origin.slice(0, origin.indexOf("^"));
+ } catch (e) {
+ // createOriginAttributesFromOrigin can throw NS_ERROR_FAILURE for incorrect origin strings.
+ }
+ if (userContextId) {
+ let identityLabel =
+ ContextualIdentityService.getUserContextLabel(userContextId);
+ if (identityLabel) {
+ fluentArgs.origin += ` — ${identityLabel}`;
+ }
+ }
+ if (privateBrowsingId) {
+ fluentName += "-private";
+ }
+ }
+
+ let processNameElement = nameCell;
+ if (SHOW_PROFILER_ICONS) {
+ if (!nameCell.firstChild) {
+ processNameElement = document.createElement("span");
+ nameCell.appendChild(processNameElement);
+
+ let profilerIcon = document.createElement("span");
+ profilerIcon.className = "profiler-icon";
+ document.l10n.setAttributes(
+ profilerIcon,
+ "about-processes-profile-process",
+ { duration: PROFILE_DURATION }
+ );
+ nameCell.appendChild(profilerIcon);
+ } else {
+ processNameElement = nameCell.firstChild;
+ }
+ }
+ document.l10n.setAttributes(processNameElement, fluentName, fluentArgs);
+ nameCell.className = ["type", "favicon", ...classNames].join(" ");
+ nameCell.setAttribute("id", data.pid + "-label");
+
+ let image;
+ switch (data.type) {
+ case "browser":
+ case "privilegedabout":
+ image = "chrome://branding/content/icon32.png";
+ break;
+ case "extension":
+ image = "chrome://mozapps/skin/extensions/extension.svg";
+ break;
+ default:
+ // If all favicons match, pick the shared favicon.
+ // Otherwise, pick a default icon.
+ // If some tabs have no favicon, we ignore them.
+ for (let win of data.windows || []) {
+ if (!win.tab) {
+ continue;
+ }
+ let favicon = win.tab.tab.getAttribute("image");
+ if (!favicon) {
+ // No favicon here, let's ignore the tab.
+ } else if (!image) {
+ // Let's pick a first favicon.
+ // We'll remove it later if we find conflicting favicons.
+ image = favicon;
+ } else if (image == favicon) {
+ // So far, no conflict, keep the favicon.
+ } else {
+ // Conflicting favicons, fallback to default.
+ image = null;
+ break;
+ }
+ }
+ if (!image) {
+ image = "chrome://global/skin/icons/link.svg";
+ }
+ }
+ nameCell.style.backgroundImage = `url('${image}')`;
+ }
+
+ // Column: Memory
+ let memoryCell = nameCell.nextSibling;
+ {
+ let formattedTotal = this._formatMemory(data.totalRamSize);
+ if (data.deltaRamSize) {
+ let formattedDelta = this._formatMemory(data.deltaRamSize);
+ this._fillCell(memoryCell, {
+ fluentName: "about-processes-total-memory-size-changed",
+ fluentArgs: {
+ total: formattedTotal.amount,
+ totalUnit: gLocalizedUnits.memory[formattedTotal.unit],
+ delta: Math.abs(formattedDelta.amount),
+ deltaUnit: gLocalizedUnits.memory[formattedDelta.unit],
+ deltaSign: data.deltaRamSize > 0 ? "+" : "-",
+ },
+ classes: ["memory"],
+ });
+ } else {
+ this._fillCell(memoryCell, {
+ fluentName: "about-processes-total-memory-size-no-change",
+ fluentArgs: {
+ total: formattedTotal.amount,
+ totalUnit: gLocalizedUnits.memory[formattedTotal.unit],
+ },
+ classes: ["memory"],
+ });
+ }
+ }
+
+ // Column: CPU
+ let cpuCell = memoryCell.nextSibling;
+ this.displayCpu(data, cpuCell, maxSlopeCpu);
+
+ // Column: Kill button – but not for all processes.
+ let killButton = cpuCell.nextSibling;
+ killButton.className = "action-icon";
+
+ if (data.type.startsWith("web")) {
+ // 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.
+ //
+ // We still want to display the process in case something actually
+ // went bad and the user needs the information to realize this.
+ // But we also want to make it visible that the process is being
+ // 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-process"
+ );
+ }
+ }
+
+ return row;
+ },
+
+ /**
+ * Display a thread summary row with the thread count and a twisty to
+ * open/close the list.
+ *
+ * @param {ProcessDelta} data The data to display.
+ * @return {boolean} Whether the full thread list should be displayed.
+ */
+ displayThreadSummaryRow(data) {
+ const cellCount = 2;
+ let rowId = "ts:" + data.pid;
+ let row = this._getOrCreateRow(rowId, cellCount);
+ row.process = data;
+ row.className = "thread-summary";
+ 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.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);
+ thread.count++;
+ thread.slopeCpu += t.slopeCpu;
+ }
+ }
+ let fluentName, fluentArgs;
+ if (activeThreadCount) {
+ let percentFormatter = new Intl.NumberFormat(undefined, {
+ style: "percent",
+ minimumSignificantDigits: 1,
+ });
+
+ let threadList = Array.from(activeThreads.values());
+ threadList.sort((t1, t2) => t2.slopeCpu - t1.slopeCpu);
+
+ fluentName = "about-processes-active-threads";
+ fluentArgs = {
+ number: threads.length,
+ active: activeThreadCount,
+ list: new Intl.ListFormat(undefined, { style: "narrow" }).format(
+ threadList.map(t => {
+ let name = t.count > 1 ? `${t.count} × ${t.name}` : t.name;
+ let percent = Math.round(t.slopeCpu * 1000) / 1000;
+ if (percent) {
+ return `${name} ${percentFormatter.format(percent)}`;
+ }
+ return name;
+ })
+ ),
+ };
+ } else {
+ fluentName = "about-processes-inactive-threads";
+ fluentArgs = {
+ number: threads.length,
+ };
+ }
+
+ let span;
+ if (!nameCell.firstChild) {
+ nameCell.className = "name indent";
+ // Create the nodes:
+ let imgBtn = document.createElement("span");
+ // Provide markup for an accessible disclosure button:
+ imgBtn.className = "twisty";
+ imgBtn.setAttribute("role", "button");
+ imgBtn.setAttribute("tabindex", "0");
+ // Label to include both summary and details texts
+ imgBtn.setAttribute("aria-labelledby", `${data.pid}-label ${rowId}`);
+ if (!imgBtn.hasAttribute("aria-expanded")) {
+ imgBtn.setAttribute("aria-expanded", "false");
+ }
+ nameCell.appendChild(imgBtn);
+
+ span = document.createElement("span");
+ span.setAttribute("id", rowId);
+ nameCell.appendChild(span);
+ } else {
+ // The only thing that can change is the thread count.
+ let imgBtn = nameCell.firstChild;
+ isOpen = imgBtn.classList.contains("open");
+ span = imgBtn.nextSibling;
+ }
+ document.l10n.setAttributes(span, fluentName, fluentArgs);
+
+ // Column: action
+ let actionCell = nameCell.nextSibling;
+ actionCell.className = "action-icon";
+
+ return isOpen;
+ },
+
+ displayDOMWindowRow(data, parent) {
+ const cellCount = 2;
+ let rowId = "w:" + data.outerWindowId;
+ let row = this._getOrCreateRow(rowId, cellCount);
+ row.win = data;
+ row.className = "window";
+
+ // Column: name
+ let nameCell = row.firstChild;
+ let tab = tabFinder.get(data.outerWindowId);
+ let fluentName;
+ let fluentArgs = {};
+ let className;
+ if (tab && tab.tabbrowser) {
+ fluentName = "about-processes-tab-name";
+ fluentArgs.name = tab.tab.label;
+ className = "tab";
+ } else if (tab) {
+ fluentName = "about-processes-preloaded-tab";
+ className = "preloaded-tab";
+ } else if (data.count == 1) {
+ fluentName = "about-processes-frame-name-one";
+ fluentArgs.url = data.documentURI.spec;
+ className = "frame-one";
+ } else {
+ fluentName = "about-processes-frame-name-many";
+ fluentArgs.number = data.count;
+ fluentArgs.shortUrl =
+ data.documentURI.scheme == "about"
+ ? data.documentURI.spec
+ : data.documentURI.prePath;
+ className = "frame-many";
+ }
+ this._fillCell(nameCell, {
+ fluentName,
+ fluentArgs,
+ classes: ["name", "indent", "favicon", className],
+ });
+ let image = tab?.tab.getAttribute("image");
+ if (image) {
+ nameCell.style.backgroundImage = `url('${image}')`;
+ }
+
+ // Column: action
+ 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
+ )
+ ) {
+ // 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 window as un-killed while we actually just killed it.
+ //
+ // We still want to display the window in case something actually
+ // went bad and the user needs the information to realize this.
+ // But we also want to make it visible that the window is being
+ // 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");
+ }
+ }
+ },
+
+ utilityActorNameToFluentName(actorName) {
+ let fluentName;
+ switch (actorName) {
+ case "audioDecoder_Generic":
+ fluentName = "about-processes-utility-actor-audio-decoder-generic";
+ break;
+
+ case "audioDecoder_AppleMedia":
+ fluentName = "about-processes-utility-actor-audio-decoder-applemedia";
+ break;
+
+ case "audioDecoder_WMF":
+ fluentName = "about-processes-utility-actor-audio-decoder-wmf";
+ break;
+
+ case "mfMediaEngineCDM":
+ fluentName = "about-processes-utility-actor-mf-media-engine";
+ break;
+
+ case "jSOracle":
+ fluentName = "about-processes-utility-actor-js-oracle";
+ break;
+
+ case "windowsUtils":
+ fluentName = "about-processes-utility-actor-windows-utils";
+ break;
+
+ case "windowsFileDialog":
+ fluentName = "about-processes-utility-actor-windows-file-dialog";
+ break;
+
+ default:
+ fluentName = "about-processes-utility-actor-unknown";
+ break;
+ }
+ return fluentName;
+ },
+
+ displayUtilityActorRow(data, parent) {
+ const cellCount = 2;
+ // The actor name is expected to be unique within a given utility process.
+ let rowId = "u:" + parent.pid + data.actorName;
+ let row = this._getOrCreateRow(rowId, cellCount);
+ row.actor = data;
+ row.className = "actor";
+
+ // Column: name
+ let nameCell = row.firstChild;
+ let fluentName = this.utilityActorNameToFluentName(data.actorName);
+ let fluentArgs = {};
+ this._fillCell(nameCell, {
+ fluentName,
+ fluentArgs,
+ classes: ["name", "indent", "favicon"],
+ });
+ },
+
+ /**
+ * Display a row showing a single thread.
+ *
+ * @param {ThreadDelta} data The data to display.
+ * @param {Number} maxSlopeCpu The largest slopeCpu value.
+ */
+ displayThreadRow(data, maxSlopeCpu) {
+ const cellCount = 3;
+ let rowId = "t:" + data.tid;
+ let row = this._getOrCreateRow(rowId, cellCount);
+ row.thread = data;
+ row.className = "thread";
+
+ // Column: name
+ let nameCell = row.firstChild;
+ this._fillCell(nameCell, {
+ 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
+ this.displayCpu(data, nameCell.nextSibling, maxSlopeCpu);
+
+ // Third column (Buttons) is empty, nothing to do.
+ },
+
+ _orderedRows: [],
+ _fillCell(elt, { classes, fluentName, fluentArgs }) {
+ document.l10n.setAttributes(elt, 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" };
+ }
+ if (rawDurationNS <= NS_PER_S) {
+ return { duration: rawDurationNS / NS_PER_MS, unit: "ms" };
+ }
+ if (rawDurationNS <= NS_PER_MIN) {
+ return { duration: rawDurationNS / NS_PER_S, unit: "s" };
+ }
+ if (rawDurationNS <= NS_PER_HOUR) {
+ return { duration: rawDurationNS / NS_PER_MIN, unit: "m" };
+ }
+ if (rawDurationNS <= NS_PER_DAY) {
+ return { duration: rawDurationNS / NS_PER_HOUR, unit: "h" };
+ }
+ return { duration: rawDurationNS / NS_PER_DAY, unit: "d" };
+ },
+
+ /**
+ * Format a value representing an amount of memory.
+ *
+ * As a special case, we also handle `null`, which represents the case in which we do
+ * not have sufficient information to compute an amount of memory.
+ *
+ * @param {Number?} value The value to format. Must be either `null` or a non-negative number.
+ * @return { {unit: "GB" | "MB" | "KB" | B" | "?"}, amount: Number } The formated amount and its
+ * unit, which may be used for e.g. additional CSS formating.
+ */
+ _formatMemory(value) {
+ if (value == null) {
+ return { unit: "?", amount: 0 };
+ }
+ if (typeof value != "number") {
+ throw new Error(`Invalid memory value ${value}`);
+ }
+ let abs = Math.abs(value);
+ if (abs >= ONE_GIGA) {
+ return {
+ unit: "GB",
+ amount: value / ONE_GIGA,
+ };
+ }
+ if (abs >= ONE_MEGA) {
+ return {
+ unit: "MB",
+ amount: value / ONE_MEGA,
+ };
+ }
+ if (abs >= ONE_KILO) {
+ return {
+ unit: "KB",
+ amount: value / ONE_KILO,
+ };
+ }
+ return {
+ unit: "B",
+ amount: value,
+ };
+ },
+};
+
+var Control = {
+ // 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) {
+ let sibling = row.nextSibling;
+ while (sibling && !sibling.classList.contains("process")) {
+ let next = sibling.nextSibling;
+ if (sibling.classList.contains("thread")) {
+ View._removeRow(sibling);
+ }
+ sibling = next;
+ }
+ },
+ init() {
+ this._initHangReports();
+
+ // Start prefetching units.
+ this._promisePrefetchedUnits = (async function () {
+ let [ns, us, ms, s, m, h, d, B, KB, MB, GB, TB, PB, EB] =
+ await document.l10n.formatValues([
+ { id: "duration-unit-ns" },
+ { id: "duration-unit-us" },
+ { id: "duration-unit-ms" },
+ { id: "duration-unit-s" },
+ { id: "duration-unit-m" },
+ { id: "duration-unit-h" },
+ { id: "duration-unit-d" },
+ { id: "memory-unit-B" },
+ { id: "memory-unit-KB" },
+ { id: "memory-unit-MB" },
+ { id: "memory-unit-GB" },
+ { id: "memory-unit-TB" },
+ { id: "memory-unit-PB" },
+ { id: "memory-unit-EB" },
+ ]);
+ return {
+ duration: { ns, us, ms, s, m, h, d },
+ memory: { B, KB, MB, GB, TB, PB, EB },
+ };
+ })();
+
+ let tbody = document.getElementById("process-tbody");
+
+ // Single click:
+ // - show or hide the contents of a twisty;
+ // - close a process;
+ // - profile a process;
+ // - change selection.
+ tbody.addEventListener("click", event => {
+ this._updateLastMouseEvent();
+
+ this._handleActivate(event.target);
+ });
+
+ // Enter or Space keypress:
+ // - show or hide the contents of a twisty;
+ // - close a process;
+ // - profile a process;
+ // - change selection.
+ tbody.addEventListener("keypress", event => {
+ // Handle showing or hiding subitems of a row, when keyboard is used.
+ if (event.key === "Enter" || event.key === " ") {
+ this._handleActivate(event.target);
+ }
+ });
+
+ // Double click:
+ // - navigate to tab;
+ // - navigate to about:addons.
+ tbody.addEventListener("dblclick", event => {
+ this._updateLastMouseEvent();
+ event.stopPropagation();
+
+ // Bubble up the doubleclick manually.
+ for (
+ let target = event.target;
+ target && target.getAttribute("id") != "process-tbody";
+ target = target.parentNode
+ ) {
+ if (target.classList.contains("tab")) {
+ // We've clicked on a tab, navigate.
+ let { tab, tabbrowser } = target.parentNode.win.tab;
+ tabbrowser.selectedTab = tab;
+ tabbrowser.ownerGlobal.focus();
+ return;
+ }
+ if (target.classList.contains("extensions")) {
+ // We've clicked on the extensions process, open or reuse window.
+ let parentWin =
+ window.docShell.browsingContext.embedderElement.ownerGlobal;
+ parentWin.BrowserOpenAddonsMgr();
+ return;
+ }
+ // Otherwise, proceed.
+ }
+ });
+
+ tbody.addEventListener("mousemove", () => {
+ this._updateLastMouseEvent();
+ });
+
+ // Visibility change:
+ // - stop updating while the user isn't looking;
+ // - resume updating when the user returns.
+ window.addEventListener("visibilitychange", event => {
+ if (!document.hidden) {
+ this._updateDisplay(true);
+ }
+ });
+
+ document
+ .getElementById("process-thead")
+ .addEventListener("click", async event => {
+ if (!event.target.classList.contains("clickable")) {
+ return;
+ }
+ // Linux has conventions opposite to Windows and macOS on the direction of arrows
+ // when sorting.
+ const platformIsLinux = AppConstants.platform == "linux";
+ const ascArrow = platformIsLinux ? "arrow-up" : "arrow-down";
+ const descArrow = platformIsLinux ? "arrow-down" : "arrow-up";
+
+ if (this._sortColumn) {
+ const td = document.getElementById(this._sortColumn);
+ td.classList.remove(ascArrow, descArrow);
+ }
+
+ const columnId = event.target.id;
+ if (columnId == this._sortColumn) {
+ // Reverse sorting order.
+ this._sortAscendent = !this._sortAscendent;
+ } else {
+ this._sortColumn = columnId;
+ this._sortAscendent = true;
+ }
+
+ event.target.classList.toggle(ascArrow, this._sortAscendent);
+ event.target.classList.toggle(descArrow, !this._sortAscendent);
+
+ 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(force = false) {
+ await State.update(force);
+
+ if (document.hidden) {
+ return;
+ }
+
+ await this._updateDisplay(force);
+ },
+
+ // The force parameter can force a full update even when the mouse has been
+ // moved recently.
+ async _updateDisplay(force = false) {
+ let counters = State.getCounters();
+ if (this._promisePrefetchedUnits) {
+ gLocalizedUnits = await this._promisePrefetchedUnits;
+ this._promisePrefetchedUnits = null;
+ }
+
+ // 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);
+
+ // Stored because it is used when opening the list of threads.
+ this._maxSlopeCpu = Math.max(...counters.map(process => process.slopeCpu));
+
+ let previousProcess = null;
+ for (let process of counters) {
+ this._sortDOMWindows(process.windows);
+
+ process.isHung = process.childID && hungItems.has(process.childID);
+
+ let processRow = View.displayProcessRow(process, this._maxSlopeCpu);
+
+ if (process.type != "extension") {
+ // We do not want to display extensions.
+ for (let win of process.windows) {
+ if (SHOW_ALL_SUBFRAMES || win.tab || win.isProcessRoot) {
+ View.displayDOMWindowRow(win, process);
+ }
+ }
+ }
+
+ if (process.type === "utility") {
+ for (let actor of process.utilityActors) {
+ View.displayUtilityActorRow(actor, process);
+ }
+ }
+
+ if (SHOW_THREADS) {
+ if (View.displayThreadSummaryRow(process)) {
+ this._showThreads(processRow, this._maxSlopeCpu);
+ }
+ }
+ 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;
+ }
+
+ if (
+ !force &&
+ Date.now() - this._lastMouseEvent < TIME_BEFORE_SORTING_AGAIN
+ ) {
+ // If there has been a recent mouse event, we don't want to reorder,
+ // add or remove rows so that the table content under the mouse pointer
+ // doesn't change when the user might be about to click to close a tab
+ // or kill a process.
+ // We didn't return earlier because updating CPU and memory values is
+ // still valuable.
+ View.discardUpdate();
+ return;
+ }
+
+ 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;
+ }
+
+ // 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, maxSlopeCpu) {
+ let process = row.process;
+ this._sortThreads(process.threads);
+ for (let thread of process.threads) {
+ View.displayThreadRow(thread, maxSlopeCpu);
+ }
+ },
+ _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 = 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);
+ }
+ if (!this._sortAscendent) {
+ order = -order;
+ }
+ return order;
+ });
+ },
+ _sortProcesses(counters) {
+ return counters.sort((a, b) => {
+ let order;
+ 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 = 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 ||
+ // Other processes are ordered by origin.
+ String(a.origin).localeCompare(b.origin);
+ break;
+ default:
+ throw new Error("Unsupported order: " + this._sortColumn);
+ }
+ if (!this._sortAscendent) {
+ order = -order;
+ }
+ return order;
+ });
+ },
+ _sortDOMWindows(windows) {
+ return windows.sort((a, b) => {
+ let order =
+ a.displayRank - b.displayRank ||
+ a.documentTitle.localeCompare(b.documentTitle) ||
+ a.documentURI.spec.localeCompare(b.documentURI.spec);
+ if (!this._sortAscendent) {
+ order = -order;
+ }
+ return order;
+ });
+ },
+
+ // Assign a display rank to a process.
+ //
+ // The `browser` process comes first (rank 0).
+ // Then come web tabs (rank 1).
+ // Then come web frames (rank 2).
+ // Then come special processes (minus preallocated) (rank 3).
+ // Then come preallocated processes (rank 4).
+ _getDisplayGroupRank(data, windows) {
+ const RANK_BROWSER = 0;
+ const RANK_WEB_TABS = 1;
+ const RANK_WEB_FRAMES = 2;
+ const RANK_UTILITY = 3;
+ const RANK_PREALLOCATED = 4;
+ let type = data.type;
+ switch (type) {
+ // Browser comes first.
+ case "browser":
+ return RANK_BROWSER;
+ // Web content comes next.
+ case "webIsolated":
+ case "webServiceWorker":
+ case "withCoopCoep": {
+ if (windows.some(w => w.tab)) {
+ return RANK_WEB_TABS;
+ }
+ return RANK_WEB_FRAMES;
+ }
+ // Preallocated processes come last.
+ case "preallocated":
+ return RANK_PREALLOCATED;
+ // "web" is special, as it could be one of:
+ // - web content currently loading/unloading/...
+ // - a preallocated process.
+ case "web":
+ if (windows.some(w => w.tab)) {
+ return RANK_WEB_TABS;
+ }
+ if (windows.length >= 1) {
+ return RANK_WEB_FRAMES;
+ }
+ // For the time being, we do not display DOM workers
+ // (and there's no API to get information on them).
+ // Once the blockers for bug 1663737 have landed, we'll be able
+ // to find out whether this process has DOM workers. If so, we'll
+ // count this process as a content process.
+ return RANK_PREALLOCATED;
+ // Other special processes before preallocated.
+ default:
+ return RANK_UTILITY;
+ }
+ },
+
+ // Handle events on image controls.
+ _handleActivate(target) {
+ if (target.classList.contains("twisty")) {
+ this._handleTwisty(target);
+ return;
+ }
+ if (target.classList.contains("close-icon")) {
+ this._handleKill(target);
+ return;
+ }
+
+ if (target.classList.contains("profiler-icon")) {
+ this._handleProfiling(target);
+ return;
+ }
+
+ this._handleSelection(target);
+ },
+
+ // Open/close list of threads.
+ _handleTwisty(target) {
+ let row = target.parentNode.parentNode;
+ if (target.classList.toggle("open")) {
+ target.setAttribute("aria-expanded", "true");
+ this._showThreads(row, this._maxSlopeCpu);
+ View.insertAfterRow(row);
+ } else {
+ target.setAttribute("aria-expanded", "false");
+ this._removeSubtree(row);
+ }
+ },
+
+ // Kill process/close tab/close subframe.
+ _handleKill(target) {
+ let row = target.parentNode;
+ if (row.process) {
+ // Kill process immediately.
+ let pid = row.process.pid;
+
+ // Make sure that the user can't click twice on the kill button.
+ // Otherwise, chaos might ensue. Plus we risk crashing under Windows.
+ View._killedRecently.push({ pid });
+
+ // Discard tab contents and show that the process and all its contents are getting killed.
+ row.classList.add("killing");
+ for (
+ let childRow = row.nextSibling;
+ childRow && !childRow.classList.contains("process");
+ childRow = childRow.nextSibling
+ ) {
+ childRow.classList.add("killing");
+ let win = childRow.win;
+ if (win) {
+ View._killedRecently.push({ pid: win.outerWindowId });
+ if (win.tab && win.tab.tabbrowser) {
+ win.tab.tabbrowser.discardBrowser(
+ win.tab.tab,
+ /* aForceDiscard = */ true
+ );
+ }
+ }
+ }
+
+ // Finally, kill the process.
+ const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+ );
+ ProcessTools.kill(pid);
+ } else if (row.win && row.win.tab && row.win.tab.tabbrowser) {
+ // This is a tab, close it.
+ row.win.tab.tabbrowser.removeTab(row.win.tab.tab, {
+ skipPermitUnload: true,
+ animate: true,
+ });
+ View._killedRecently.push({ outerWindowId: row.win.outerWindowId });
+ row.classList.add("killing");
+
+ // If this was the only root window of the process, show that the process is also getting killed.
+ if (row.previousSibling.classList.contains("process")) {
+ let parentRow = row.previousSibling;
+ let roots = 0;
+ for (let win of parentRow.process.windows) {
+ if (win.isProcessRoot) {
+ roots += 1;
+ }
+ }
+ if (roots <= 1) {
+ // Yes, we're the only process root, so the process is dying.
+ //
+ // It might actually become a preloaded process rather than
+ // dying. That's an acceptable error. Even if we display incorrectly
+ // that the process is dying, this error will last only one refresh.
+ View._killedRecently.push({ pid: parentRow.process.pid });
+ parentRow.classList.add("killing");
+ }
+ }
+ }
+ },
+
+ // Handle profiling of a process.
+ _handleProfiling(target) {
+ if (Services.profiler.IsActive()) {
+ return;
+ }
+ Services.profiler.StartProfiler(
+ 10000000,
+ 1,
+ ["default", "ipcmessages", "power"],
+ ["pid:" + target.parentNode.parentNode.process.pid]
+ );
+ target.classList.add("profiler-active");
+ setTimeout(() => {
+ ProfilerPopupBackground.captureProfile("aboutprofiling");
+ target.classList.remove("profiler-active");
+ }, PROFILE_DURATION * 1000);
+ },
+
+ // Handle selection changes.
+ _handleSelection(target) {
+ let row = target.closest("tr");
+ if (!row) {
+ return;
+ }
+ if (this.selectedRow) {
+ this.selectedRow.removeAttribute("selected");
+ if (this.selectedRow.rowId == row.rowId) {
+ // Clicking the same row again clears the selection.
+ this.selectedRow = null;
+ return;
+ }
+ }
+ row.setAttribute("selected", "true");
+ this.selectedRow = row;
+ },
+};
+
+window.onload = async function () {
+ Control.init();
+
+ // Display immediately the list of processes. CPU values will be missing.
+ await Control.update();
+
+ // After the minimum interval between samples, force an update to show
+ // valid CPU values asap.
+ await new Promise(resolve =>
+ setTimeout(resolve, MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS)
+ );
+ await Control.update(true);
+
+ // Then update at the normal frequency.
+ window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS);
+};