summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutprocesses
diff options
context:
space:
mode:
Diffstat (limited to '')
-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
-rw-r--r--toolkit/components/aboutprocesses/jar.mn8
-rw-r--r--toolkit/components/aboutprocesses/moz.build12
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser.toml32
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js7
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_selection.js124
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_shortcut.js16
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_all_frames.js6
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_frames_without_threads.js6
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_threads.js7
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_twisty.js134
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_utility_actors.js44
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/head.js1066
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/small-shot.mp3bin0 -> 6825 bytes
16 files changed, 3356 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);
+};
diff --git a/toolkit/components/aboutprocesses/jar.mn b/toolkit/components/aboutprocesses/jar.mn
new file mode 100644
index 0000000000..724a747306
--- /dev/null
+++ b/toolkit/components/aboutprocesses/jar.mn
@@ -0,0 +1,8 @@
+# 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/.
+
+toolkit.jar:
+ content/global/aboutProcesses.html (content/aboutProcesses.html)
+ content/global/aboutProcesses.js (content/aboutProcesses.js)
+ content/global/aboutProcesses.css (content/aboutProcesses.css)
diff --git a/toolkit/components/aboutprocesses/moz.build b/toolkit/components/aboutprocesses/moz.build
new file mode 100644
index 0000000000..5206b02fe0
--- /dev/null
+++ b/toolkit/components/aboutprocesses/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Performance Monitoring")
+
+JAR_MANIFESTS += ["jar.mn"]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser.toml b/toolkit/components/aboutprocesses/tests/browser/browser.toml
new file mode 100644
index 0000000000..07bec9de5e
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser.toml
@@ -0,0 +1,32 @@
+[DEFAULT]
+head = "head.js"
+skip-if = [
+ "asan",
+ "tsan"
+] # With sanitizers, we regularly hit internal timeouts.
+support-files = ["small-shot.mp3"]
+
+["browser_aboutprocesses_default_options.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 td.action-icon, td.close-icon may not be focusable
+https_first_disabled = true
+
+["browser_aboutprocesses_selection.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 tr.process, tr.window, tr.thread, tr.thread-summary may not be focusable
+
+["browser_aboutprocesses_shortcut.js"]
+
+["browser_aboutprocesses_show_all_frames.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 td.action-icon, td.close-icon may not be focusable
+https_first_disabled = true
+
+["browser_aboutprocesses_show_frames_without_threads.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 td.action-icon, td.close-icon may not be focusable
+https_first_disabled = true
+
+["browser_aboutprocesses_show_threads.js"]
+fail-if = ["a11y_checks"] # Bug 1854228 td.action-icon, td.close-icon may not be focusable
+https_first_disabled = true
+
+["browser_aboutprocesses_twisty.js"]
+
+["browser_aboutprocesses_utility_actors.js"]
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js
new file mode 100644
index 0000000000..15ed1c4501
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js
@@ -0,0 +1,7 @@
+// Test about:processes with default options.
+add_task(async function testDefaultOptions() {
+ return testAboutProcessesWithConfig({
+ showAllFrames: false,
+ showThreads: false,
+ });
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_selection.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_selection.js
new file mode 100644
index 0000000000..f208fb095d
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_selection.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var doc, tbody, tabAboutProcesses;
+
+const rowTypes = ["process", "window", "thread-summary", "thread"];
+
+function promiseUpdate() {
+ return promiseAboutProcessesUpdated({
+ doc,
+ tbody,
+ force: true,
+ tabAboutProcesses,
+ });
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("toolkit.aboutProcesses.showThreads", true);
+
+ info("Setting up about:processes");
+ tabAboutProcesses = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:processes",
+ waitForLoad: true,
+ });
+
+ doc = tabAboutProcesses.linkedBrowser.contentDocument;
+ tbody = doc.getElementById("process-tbody");
+ await promiseUpdate();
+
+ info("Open a list of threads to have thread rows displayed");
+ let twisty = doc.querySelector("tr.thread-summary .twisty");
+ twisty.click();
+ await promiseUpdate();
+});
+
+add_task(async function testSelectionPersistedAfterUpdate() {
+ for (let rowType of rowTypes) {
+ let row = doc.querySelector(`tr.${rowType}`);
+ Assert.ok(!!row, `Found ${rowType} row`);
+ Assert.ok(!row.hasAttribute("selected"), "The row should not be selected");
+
+ info("Click in the row to select it.");
+ row.click();
+ Assert.equal(
+ row.getAttribute("selected"),
+ "true",
+ "The row should be selected"
+ );
+ Assert.equal(
+ doc.querySelectorAll("[selected]").length,
+ 1,
+ "There should be only one selected row"
+ );
+
+ info("Wait for an update and ensure the selected row is still the same");
+ let rowId = row.rowId;
+ let findRowsWithId = rowId =>
+ [...doc.querySelectorAll("tr")].filter(r => r.rowId == rowId);
+ Assert.equal(
+ findRowsWithId(rowId).length,
+ 1,
+ "There should be only one row with id " + rowId
+ );
+ await promiseUpdate();
+ let selectedRow = doc.querySelector("[selected]");
+ if (rowType == "thread" && !selectedRow) {
+ info("The thread row might have disappeared if the thread has ended");
+ Assert.equal(
+ findRowsWithId(rowId).length,
+ 0,
+ "There should no longer be a row with id " + rowId
+ );
+ continue;
+ }
+ Assert.ok(
+ !!selectedRow,
+ "There should still be a selected row after an update"
+ );
+ Assert.equal(
+ selectedRow.rowId,
+ rowId,
+ "The selected row should have the same id as the row we clicked"
+ );
+ }
+});
+
+add_task(function testClickAgainToRemoveSelection() {
+ for (let rowType of rowTypes) {
+ let row = doc.querySelector(`tr.${rowType}`);
+ Assert.ok(!!row, `Found ${rowType} row`);
+ Assert.ok(!row.hasAttribute("selected"), "The row should not be selected");
+ info("Click in the row to select it.");
+ row.click();
+ Assert.equal(
+ row.getAttribute("selected"),
+ "true",
+ "The row should now be selected"
+ );
+ Assert.equal(
+ doc.querySelectorAll("[selected]").length,
+ 1,
+ "There should be only one selected row"
+ );
+
+ info("Click the row again to remove the selection.");
+ row.click();
+ Assert.ok(
+ !row.hasAttribute("selected"),
+ "The row should no longer be selected"
+ );
+ Assert.ok(
+ !doc.querySelector("[selected]"),
+ "There should be no selected row"
+ );
+ }
+});
+
+add_task(function cleanup() {
+ BrowserTestUtils.removeTab(tabAboutProcesses);
+ Services.prefs.clearUserPref("toolkit.aboutProcesses.showThreads");
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_shortcut.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_shortcut.js
new file mode 100644
index 0000000000..61b8be1fcd
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_shortcut.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ // Wait for the browser to be ready to receive keyboard events.
+ if (!gBrowser.selectedBrowser.hasLayers) {
+ await BrowserTestUtils.waitForEvent(window, "MozLayerTreeReady");
+ }
+
+ EventUtils.synthesizeKey("KEY_Escape", { shiftKey: true });
+
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ Assert.equal(gBrowser.selectedBrowser.currentURI.spec, "about:processes");
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_all_frames.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_all_frames.js
new file mode 100644
index 0000000000..2c437f9dc4
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_all_frames.js
@@ -0,0 +1,6 @@
+add_task(async function testShowFramesAndThreads() {
+ await testAboutProcessesWithConfig({
+ showAllFrames: true,
+ showThreads: true,
+ });
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_frames_without_threads.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_frames_without_threads.js
new file mode 100644
index 0000000000..178ceec9ed
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_frames_without_threads.js
@@ -0,0 +1,6 @@
+add_task(async function testShowFramesWithoutThreads() {
+ await testAboutProcessesWithConfig({
+ showAllFrames: true,
+ showThreads: false,
+ });
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_threads.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_threads.js
new file mode 100644
index 0000000000..10e630ab31
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_show_threads.js
@@ -0,0 +1,7 @@
+// Test about:processes with showThreads: true, showAllFrames: false.
+add_task(async function testShowThreads() {
+ return testAboutProcessesWithConfig({
+ showAllFrames: false,
+ showThreads: true,
+ });
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_twisty.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_twisty.js
new file mode 100644
index 0000000000..77fb0fbfbb
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_twisty.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let doc, tbody, tabAboutProcesses;
+
+const rowTypes = ["process", "window", "thread-summary", "thread"];
+
+function promiseUpdate() {
+ return promiseAboutProcessesUpdated({
+ doc,
+ tbody,
+ force: true,
+ tabAboutProcesses,
+ });
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("toolkit.aboutProcesses.showThreads", true);
+
+ info("Setting up about:processes");
+ tabAboutProcesses = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:processes",
+ waitForLoad: true,
+ });
+
+ doc = tabAboutProcesses.linkedBrowser.contentDocument;
+ tbody = doc.getElementById("process-tbody");
+ await promiseUpdate();
+});
+
+add_task(function testTwistyImageButtonSetup() {
+ let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
+ let groupRow = twistyBtn.parentNode.parentNode;
+ let groupRowId = groupRow.firstChild.children[1].getAttribute("id");
+ let groupRowLabelId = groupRowId.split(":")[1];
+
+ info("Verify twisty button is properly set up.");
+ Assert.ok(
+ twistyBtn.hasAttribute("aria-labelledby"),
+ "the Twisty image button has an aria-labelledby"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-labelledby"),
+ `${groupRowLabelId}-label ${groupRowId}`,
+ "the Twisty image button's aria-labelledby refers to a valid 'id' that is the Name of its row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("role"),
+ "button",
+ "the Twisty image is programmatically a button"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("tabindex"),
+ "0",
+ "the Twisty image button is included in the focus order"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "false",
+ "the Twisty image button is collapsed by default"
+ );
+});
+
+add_task(function testTwistyImageButtonClicking() {
+ let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
+ let groupRow = twistyBtn.parentNode.parentNode;
+
+ info(
+ "Verify we can toggle/open a list of threads by clicking the twisty button."
+ );
+ twistyBtn.click();
+
+ Assert.ok(
+ groupRow.nextSibling.classList.contains("thread") &&
+ !groupRow.nextSibling.classList.contains("thread-summary"),
+ "clicking a collapsed Twisty adds subitems after the row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "true",
+ "the Twisty image button is expanded after a click"
+ );
+});
+
+add_task(function testTwistyImageButtonKeypressing() {
+ let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
+ let groupRow = twistyBtn.parentNode.parentNode;
+
+ info(
+ `Verify we can toggle/close a list of threads by pressing Enter or
+ Space on the twisty button.`
+ );
+ // Verify the twisty button can be focused with a keyboard.
+ twistyBtn.focus();
+ Assert.equal(
+ twistyBtn,
+ doc.activeElement,
+ "the Twisty image button can be focused"
+ );
+
+ // Verify we can toggle subitems with a keyboard.
+ // Twisty is expanded
+ EventUtils.synthesizeKey("KEY_Enter");
+ Assert.ok(
+ !groupRow.nextSibling.classList.contains("thread") ||
+ groupRow.nextSibling.classList.contains("thread-summary"),
+ "pressing Enter on expanded Twisty hides a list of threads after the row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "false",
+ "the Twisty image button is collapsed after an Enter keypress"
+ );
+ // Twisty is collapsed
+ EventUtils.synthesizeKey(" ");
+ Assert.ok(
+ groupRow.nextSibling.classList.contains("thread") &&
+ !groupRow.nextSibling.classList.contains("thread-summary"),
+ "pressing Space on collapsed Twisty shows a list of threads after the row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "true",
+ "the Twisty image button is expanded after a Space keypress"
+ );
+});
+
+add_task(function cleanup() {
+ BrowserTestUtils.removeTab(tabAboutProcesses);
+ Services.prefs.clearUserPref("toolkit.aboutProcesses.showThreads");
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_utility_actors.js b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_utility_actors.js
new file mode 100644
index 0000000000..8ab7dcb747
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_utility_actors.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test about:processes preparation of utility actor names.
+add_task(async function testUtilityActorNames() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ opening: "about:processes",
+ waitForLoad: true,
+ },
+ browser => {
+ const View = browser.contentWindow.View;
+ const unknownActorName = "unknown";
+ const kDontExistFluentName =
+ View.utilityActorNameToFluentName("i-dont-exist");
+ const unknownFluentName =
+ View.utilityActorNameToFluentName(unknownActorName);
+
+ Assert.equal(
+ unknownFluentName,
+ kDontExistFluentName,
+ "Anything is unknown"
+ );
+
+ for (let actorName of ChromeUtils.getAllPossibleUtilityActorNames()) {
+ const fluentName = View.utilityActorNameToFluentName(actorName);
+ if (actorName === unknownActorName) {
+ Assert.ok(
+ fluentName === unknownFluentName,
+ `Actor name ${actorName} is expected unknown ${fluentName}`
+ );
+ } else {
+ Assert.ok(
+ fluentName !== unknownFluentName,
+ `Actor name ${actorName} is known ${fluentName}`
+ );
+ }
+ }
+ }
+ );
+});
diff --git a/toolkit/components/aboutprocesses/tests/browser/head.js b/toolkit/components/aboutprocesses/tests/browser/head.js
new file mode 100644
index 0000000000..7f22825fa2
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/head.js
@@ -0,0 +1,1066 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// A bunch of assumptions we make about the behavior of the parent process,
+// and which we use as sanity checks. If Firefox evolves, we will need to
+// update these values.
+// Note that Test Verify can really stress the cpu durations.
+const HARDCODED_ASSUMPTIONS_PROCESS = {
+ minimalNumberOfThreads: 6,
+ maximalNumberOfThreads: 1000,
+ minimalCPUPercentage: 0,
+ maximalCPUPercentage: 1000,
+ minimalCPUTotalDurationMS: 10,
+ maximalCPUTotalDurationMS: 10000000,
+ minimalRAMBytesUsage: 1024 * 1024 /* 1 Megabyte */,
+ maximalRAMBytesUsage: 1024 * 1024 * 1024 * 1024 * 1 /* 1 Tb */,
+};
+
+const HARDCODED_ASSUMPTIONS_THREAD = {
+ minimalCPUPercentage: 0,
+ maximalCPUPercentage: 100,
+ minimalCPUTotalDurationMS: 0,
+ maximalCPUTotalDurationMS: 10000000,
+};
+
+// How close we accept our rounding up/down.
+const APPROX_FACTOR = 1.51;
+const MS_PER_NS = 1000000;
+
+// Wait for `about:processes` to be updated.
+async function promiseAboutProcessesUpdated({ doc, force, tabAboutProcesses }) {
+ let startTime = performance.now();
+
+ let updatePromise = new Promise(resolve => {
+ doc.addEventListener("AboutProcessesUpdated", resolve, { once: true });
+ });
+
+ if (force) {
+ await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => {
+ info("Forcing about:processes refresh");
+ await content.Control.update(/* force = */ true);
+ });
+ }
+
+ await updatePromise;
+
+ // Fluent will update the visible table content during the next
+ // refresh driver tick, wait for it.
+ // requestAnimationFrame calls us at the begining of the tick, we use
+ // dispatchToMainThread to execute our code after the end of it.
+ //XXX: Replace with proper wait for l10n completion once bug 1520659 is fixed.
+ await new Promise(doc.defaultView.requestAnimationFrame);
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ ChromeUtils.addProfilerMarker(
+ "promiseAboutProcessesUpdated",
+ { startTime, category: "Test" },
+ force ? "force" : undefined
+ );
+}
+
+function promiseProcessDied({ childID }) {
+ return new Promise(resolve => {
+ let observer = properties => {
+ properties.QueryInterface(Ci.nsIPropertyBag2);
+ let subjectChildID = properties.get("childID");
+ if (subjectChildID == childID) {
+ Services.obs.removeObserver(observer, "ipc:content-shutdown");
+ resolve();
+ }
+ };
+ Services.obs.addObserver(observer, "ipc:content-shutdown");
+ });
+}
+
+function isCloseEnough(value, expected) {
+ if (value < 0 || expected < 0) {
+ throw new Error(`Invalid isCloseEnough(${value}, ${expected})`);
+ }
+ if (Math.round(value) == Math.round(expected)) {
+ return true;
+ }
+ if (expected == 0) {
+ return false;
+ }
+ let ratio = value / expected;
+ if (ratio <= APPROX_FACTOR && ratio >= 1 / APPROX_FACTOR) {
+ return true;
+ }
+ return false;
+}
+
+function getMemoryMultiplier(unit, sign = "+") {
+ let multiplier;
+ switch (sign) {
+ case "+":
+ multiplier = 1;
+ break;
+ case "-":
+ multiplier = -1;
+ break;
+ default:
+ throw new Error("Invalid sign: " + sign);
+ }
+ switch (unit) {
+ case "B":
+ break;
+ case "KB":
+ multiplier *= 1024;
+ break;
+ case "MB":
+ multiplier *= 1024 * 1024;
+ break;
+ case "GB":
+ multiplier *= 1024 * 1024 * 1024;
+ break;
+ case "TB":
+ multiplier *= 1024 * 1024 * 1024 * 1024;
+ break;
+ default:
+ throw new Error("Invalid memory unit: " + unit);
+ }
+ return multiplier;
+}
+
+function getTimeMultiplier(unit) {
+ switch (unit) {
+ case "ns":
+ return 1 / (1000 * 1000);
+ case "µs":
+ return 1 / 1000;
+ case "ms":
+ return 1;
+ case "s":
+ return 1000;
+ case "m":
+ return 60000;
+ }
+ throw new Error("Invalid time unit: " + unit);
+}
+async function testCpu(element, total, slope, assumptions) {
+ info(
+ `Testing CPU display ${element.textContent} - ${element.title} vs total ${total}, slope ${slope}`
+ );
+ let barWidth = getComputedStyle(element).getPropertyValue("--bar-width");
+ if (slope) {
+ Assert.greater(
+ Number.parseFloat(barWidth),
+ 0,
+ "The bar width should be > 0 when there is some CPU use"
+ );
+ } else {
+ Assert.equal(barWidth, "-0.5", "There should be no CPU bar displayed");
+ }
+
+ if (element.textContent == "(measuring)") {
+ info("Still measuring");
+ return;
+ }
+
+ const CPU_TEXT_CONTENT_REGEXP = /\~0%|idle|[0-9.,]+%|[?]/;
+ let extractedPercentage = CPU_TEXT_CONTENT_REGEXP.exec(
+ element.textContent
+ )[0];
+ switch (extractedPercentage) {
+ case "idle":
+ Assert.equal(slope, 0, "Idle means exactly 0%");
+ // Nothing else to do here.
+ return;
+ case "~0%":
+ Assert.ok(slope > 0 && slope < 0.0001);
+ break;
+ case "?":
+ Assert.ok(slope == null);
+ // Nothing else to do here.
+ return;
+ default: {
+ // `Number.parseFloat("99%")` returns `99`.
+ let computedPercentage = Number.parseFloat(extractedPercentage);
+ Assert.ok(
+ isCloseEnough(computedPercentage, slope * 100),
+ `The displayed approximation of the slope is reasonable: ${computedPercentage} vs ${
+ slope * 100
+ }`
+ );
+ // Also, sanity checks.
+ Assert.ok(
+ computedPercentage / 100 >= assumptions.minimalCPUPercentage,
+ `Not too little: ${computedPercentage / 100} >=? ${
+ assumptions.minimalCPUPercentage
+ } `
+ );
+ Assert.ok(
+ computedPercentage / 100 <= assumptions.maximalCPUPercentage,
+ `Not too much: ${computedPercentage / 100} <=? ${
+ assumptions.maximalCPUPercentage
+ } `
+ );
+ break;
+ }
+ }
+
+ const CPU_TOOLTIP_REGEXP = /(?:.*: ([0-9.,]+) ?(ns|µs|ms|s|m|h|d))/;
+ // Example: "Total CPU time: 4,470ms"
+
+ let [, extractedTotal, extractedUnit] = CPU_TOOLTIP_REGEXP.exec(
+ element.title
+ );
+
+ let totalMS = total / MS_PER_NS;
+ let computedTotal =
+ // We produce localized numbers, with "," as a thousands separator in en-US builds,
+ // but `parseFloat` doesn't understand the ",", so we need to remove it
+ // before parsing.
+ Number.parseFloat(extractedTotal.replace(/,/g, "")) *
+ getTimeMultiplier(extractedUnit);
+ Assert.ok(
+ isCloseEnough(computedTotal, totalMS),
+ `The displayed approximation of the total duration is reasonable: ${computedTotal} vs ${totalMS}`
+ );
+ Assert.ok(
+ totalMS <= assumptions.maximalCPUTotalDurationMS &&
+ totalMS >= assumptions.minimalCPUTotalDurationMS,
+ `The total number of MS is reasonable ${totalMS}: [${assumptions.minimalCPUTotalDurationMS}, ${assumptions.maximalCPUTotalDurationMS}]`
+ );
+}
+
+async function testMemory(element, total, delta, assumptions) {
+ info(
+ `Testing memory display ${element.textContent} - ${element.title} vs total ${total}, delta ${delta}`
+ );
+ const MEMORY_TEXT_CONTENT_REGEXP = /([0-9.,]+)(TB|GB|MB|KB|B)/;
+ // Example: "383.55MB"
+ let extracted = MEMORY_TEXT_CONTENT_REGEXP.exec(element.textContent);
+ Assert.notEqual(
+ extracted,
+ null,
+ `Can we parse ${element.textContent} with ${MEMORY_TEXT_CONTENT_REGEXP}?`
+ );
+ let [, extractedTotal, extractedUnit] = extracted;
+
+ let extractedTotalNumber = Number.parseFloat(extractedTotal);
+ Assert.ok(
+ extractedTotalNumber > 0,
+ `Unitless total memory use is greater than 0: ${extractedTotal}`
+ );
+ if (extractedUnit != "GB") {
+ Assert.ok(
+ extractedTotalNumber <= 1024,
+ `Unitless total memory use is less than 1024: ${extractedTotal}`
+ );
+ }
+
+ // Now check that the conversion was meaningful.
+ let computedTotal = getMemoryMultiplier(extractedUnit) * extractedTotalNumber;
+ Assert.ok(
+ isCloseEnough(computedTotal, total),
+ `The displayed approximation of the total amount of memory is reasonable: ${computedTotal} vs ${total}`
+ );
+ if (!AppConstants.ASAN) {
+ // ASAN plays tricks with RAM (e.g. allocates the entirety of virtual memory),
+ // which makes this test unrealistic.
+ Assert.ok(
+ assumptions.minimalRAMBytesUsage <= computedTotal &&
+ computedTotal <= assumptions.maximalRAMBytesUsage,
+ `The total amount amount of memory is reasonable: ${computedTotal} in [${assumptions.minimalRAMBytesUsage}, ${assumptions.maximalRAMBytesUsage}]`
+ );
+ }
+
+ const MEMORY_TOOLTIP_REGEXP = /(?:.*: ([-+]?)([0-9.,]+)(GB|MB|KB|B))?/;
+ // Example: "Evolution: -12.5MB"
+ extracted = MEMORY_TOOLTIP_REGEXP.exec(element.title);
+ Assert.notEqual(
+ extracted,
+ null,
+ `Can we parse ${element.title} with ${MEMORY_TOOLTIP_REGEXP}?`
+ );
+ let [, extractedDeltaSign, extractedDeltaTotal, extractedDeltaUnit] =
+ extracted;
+ if (extractedDeltaSign == null) {
+ Assert.equal(delta || 0, 0);
+ return;
+ }
+ let deltaTotalNumber = Number.parseFloat(
+ // Remove the thousands separator that breaks parseFloat.
+ extractedDeltaTotal.replace(/,/g, "")
+ );
+ // Note: displaying 1024KB can happen if the value is slightly less than
+ // 1024*1024B but rounded to 1024KB.
+ Assert.ok(
+ deltaTotalNumber > 0 && deltaTotalNumber <= 1024,
+ `Unitless delta memory use is in (0, 1024): ${extractedDeltaTotal}`
+ );
+ Assert.ok(
+ ["B", "KB", "MB"].includes(extractedDeltaUnit),
+ `Delta unit is reasonable: ${extractedDeltaUnit}`
+ );
+
+ // Now check that the conversion was meaningful.
+ // Let's just check that the number displayed is within 10% of `delta`.
+ let computedDelta =
+ getMemoryMultiplier(extractedDeltaUnit, extractedDeltaSign) *
+ deltaTotalNumber;
+ Assert.equal(
+ computedDelta >= 0,
+ delta >= 0,
+ `Delta has the right sign: ${computedDelta} vs ${delta}`
+ );
+}
+
+function extractProcessDetails(row) {
+ let children = row.children;
+ let name = children[0];
+ let memory = children[1];
+ let cpu = children[2];
+ if (Services.prefs.getBoolPref("toolkit.aboutProcesses.showProfilerIcons")) {
+ name = name.firstChild;
+ Assert.ok(
+ name.nextSibling.classList.contains("profiler-icon"),
+ "The profiler icon should be shown"
+ );
+ }
+ let fluentArgs = row.ownerDocument.l10n.getAttributes(name).args;
+ let threadDetailsRow = row.nextSibling;
+ while (threadDetailsRow) {
+ if (threadDetailsRow.classList.contains("process")) {
+ threadDetailsRow = null;
+ break;
+ }
+ if (threadDetailsRow.classList.contains("thread-summary")) {
+ break;
+ }
+ threadDetailsRow = threadDetailsRow.nextSibling;
+ }
+
+ return {
+ memory,
+ cpu,
+ pidContent: fluentArgs.pid,
+ threads: threadDetailsRow,
+ };
+}
+
+function findTabRowByName(doc, name) {
+ for (let row of doc.getElementsByClassName("name")) {
+ if (!row.parentNode.classList.contains("window")) {
+ continue;
+ }
+ let foundName = document.l10n.getAttributes(row).args.name;
+ if (foundName != name) {
+ continue;
+ }
+ return row.parentNode;
+ }
+ return null;
+}
+
+function findProcessRowByOrigin(doc, origin) {
+ for (let row of doc.getElementsByClassName("process")) {
+ if (row.process.origin == origin) {
+ return row;
+ }
+ }
+ return null;
+}
+
+async function setupTabWithOriginAndTitle(origin, title) {
+ let tab = BrowserTestUtils.addTab(gBrowser, origin, { skipAnimation: true });
+ tab.testTitle = title;
+ tab.testOrigin = origin;
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [title], async title => {
+ content.document.title = title;
+ });
+ return tab;
+}
+
+async function setupAudioTab() {
+ let origin = "about:blank";
+ let title = "utility audio";
+ let tab = BrowserTestUtils.addTab(gBrowser, origin, { skipAnimation: true });
+ tab.testTitle = title;
+ tab.testOrigin = origin;
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [title], async title => {
+ content.document.title = title;
+ const ROOT =
+ "https://example.com/browser/toolkit/components/aboutprocesses/tests/browser";
+ let audio = content.document.createElement("audio");
+ audio.setAttribute("controls", "true");
+ audio.setAttribute("loop", true);
+ audio.src = `${ROOT}/small-shot.mp3`;
+ content.document.body.appendChild(audio);
+ await audio.play();
+ });
+ return tab;
+}
+
+async function testAboutProcessesWithConfig({ showAllFrames, showThreads }) {
+ const isFission = gFissionBrowser;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["toolkit.aboutProcesses.showAllSubframes", showAllFrames],
+ ["toolkit.aboutProcesses.showThreads", showThreads],
+ // Force same-origin tabs to share a single process, to properly test
+ // functionality involving multiple tabs within a single process with Fission.
+ ["dom.ipc.processCount.webIsolated", 1],
+ // Ensure utility audio decoder is enabled
+ ["media.utility-process.enabled", true],
+ ],
+ });
+
+ // Install a test extension to also cover processes and sub-frames related to the
+ // extension process.
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "test-aboutprocesses@mochi.test" },
+ },
+ },
+ background() {
+ // Creates an about:blank iframe in the extension process to make sure that
+ // Bug 1665099 doesn't regress.
+ document.body.appendChild(document.createElement("iframe"));
+
+ this.browser.test.sendMessage("bg-page-loaded");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bg-page-loaded");
+
+ // Setup tabs asynchronously.
+
+ // The about:processes tab.
+ info("Setting up about:processes");
+ let promiseTabAboutProcesses = BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:processes",
+ waitForLoad: true,
+ });
+
+ info("Setting up example.com");
+ // Another tab that we'll pretend is hung.
+ let promiseTabHung = (async function () {
+ let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com", {
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let p = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ true /* includeSubFrames */
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // Open an in-process iframe to test toolkit.aboutProcesses.showAllSubframes
+ let frame = content.document.createElement("iframe");
+ content.document.body.appendChild(frame);
+ });
+ await p;
+ return tab;
+ })();
+
+ let promiseAudioPlayback = setupAudioTab();
+
+ let promiseUserContextTab = (async function () {
+ let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com", {
+ userContextId: 1,
+ skipAnimation: true,
+ });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.title = "Tab with User Context";
+ });
+ return tab;
+ })();
+
+ info("Setting up tabs we intend to close");
+
+ // The two following tabs share the same domain.
+ // We use them to check that closing one doesn't close the other.
+ let promiseTabCloseSeparately1 = setupTabWithOriginAndTitle(
+ "http://example.org",
+ "Close me 1 (separately)"
+ );
+ let promiseTabCloseSeparately2 = setupTabWithOriginAndTitle(
+ "http://example.org",
+ "Close me 2 (separately)"
+ );
+
+ // The two following tabs share the same domain.
+ // We use them to check that closing the process kills them both.
+ let promiseTabCloseProcess1 = setupTabWithOriginAndTitle(
+ "http://example.net",
+ "Close me 1 (process)"
+ );
+
+ let promiseTabCloseProcess2 = setupTabWithOriginAndTitle(
+ "http://example.net",
+ "Close me 2 (process)"
+ );
+
+ // The two following tabs share the same domain.
+ // We use them to check that closing the process kills them both.
+ let promiseTabCloseTogether1 = setupTabWithOriginAndTitle(
+ "https://example.org",
+ "Close me 1 (together)"
+ );
+
+ let promiseTabCloseTogether2 = setupTabWithOriginAndTitle(
+ "https://example.org",
+ "Close me 2 (together)"
+ );
+
+ // Wait for initialization to finish.
+ let tabAboutProcesses = await promiseTabAboutProcesses;
+ let tabHung = await promiseTabHung;
+ let audioPlayback = await promiseAudioPlayback;
+ let tabUserContext = await promiseUserContextTab;
+ let tabCloseSeparately1 = await promiseTabCloseSeparately1;
+ let tabCloseSeparately2 = await promiseTabCloseSeparately2;
+ let tabCloseProcess1 = await promiseTabCloseProcess1;
+ let tabCloseProcess2 = await promiseTabCloseProcess2;
+ let tabCloseTogether1 = await promiseTabCloseTogether1;
+ let tabCloseTogether2 = await promiseTabCloseTogether2;
+
+ let doc = tabAboutProcesses.linkedBrowser.contentDocument;
+ let tbody = doc.getElementById("process-tbody");
+ Assert.ok(!!tbody, "Found the #process-tbody element");
+
+ if (isFission) {
+ // We're going to kill this process later, so tell it to add an
+ // annotation so the leak checker knows it is okay there is no
+ // leak log.
+ await SpecialPowers.spawn(tabCloseProcess1.linkedBrowser, [], () => {
+ ChromeUtils.privateNoteIntentionalCrash();
+ });
+ }
+
+ info("Setting up fake process hang detector");
+ let hungChildID = tabHung.linkedBrowser.frameLoader.childID;
+
+ // Keep informing about:processes that `tabHung` is hung.
+ // Note: this is a background task, do not `await` it.
+ let fakeProcessHangMonitor = async function () {
+ for (let i = 0; i < 100; ++i) {
+ if (!tabHung.linkedBrowser) {
+ // Let's stop spamming as soon as we can.
+ return;
+ }
+
+ Services.obs.notifyObservers(
+ {
+ childID: hungChildID,
+ scriptBrowser: tabHung.linkedBrowser,
+ scriptFileName: "chrome://browser/content/browser.js",
+ QueryInterface: ChromeUtils.generateQI(["nsIHangReport"]),
+ },
+ "process-hang-report"
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ }
+ };
+ fakeProcessHangMonitor();
+
+ // about:processes will take a little time to appear and be populated.
+ await promiseAboutProcessesUpdated({ doc, tabAboutProcesses });
+ Assert.ok(tbody.childElementCount, "The table should be populated");
+ Assert.ok(
+ !!tbody.getElementsByClassName("hung").length,
+ "The hung process should appear"
+ );
+
+ info("Looking at the contents of about:processes");
+ let processesToBeFound = [
+ // The browser process.
+ {
+ name: "browser",
+ predicate: row => row.process.type == "browser",
+ },
+ // The hung process.
+ {
+ name: "hung",
+ predicate: row =>
+ row.classList.contains("hung") &&
+ row.classList.contains("process") &&
+ ["web", "webIsolated"].includes(row.process.type),
+ },
+ // Any non-hung process
+ {
+ name: "non-hung",
+ predicate: row =>
+ !row.classList.contains("hung") &&
+ row.classList.contains("process") &&
+ ["web", "webIsolated"].includes(row.process.type),
+ },
+ // A utility process with at least one actor.
+ {
+ name: "utility",
+ predicate: row =>
+ row.process &&
+ row.process.type == "utility" &&
+ row.classList.contains("process") &&
+ row.nextSibling &&
+ row.nextSibling.classList.contains("actor"),
+ },
+ ];
+ for (let finder of processesToBeFound) {
+ info(`Running sanity tests on ${finder.name}`);
+ let row = tbody.firstChild;
+ while (row) {
+ if (finder.predicate(row)) {
+ break;
+ }
+ row = row.nextSibling;
+ }
+ Assert.ok(!!row, `found a table row for ${finder.name}`);
+ let { memory, cpu, pidContent, threads } = extractProcessDetails(row);
+
+ info("Sanity checks: pid");
+ let pid = Number.parseInt(pidContent);
+ Assert.ok(pid > 0, `Checking pid ${pidContent}`);
+ Assert.equal(pid, row.process.pid);
+
+ info("Sanity checks: memory resident");
+ await testMemory(
+ memory,
+ row.process.totalRamSize,
+ row.process.deltaRamSize,
+ HARDCODED_ASSUMPTIONS_PROCESS
+ );
+
+ info("Sanity checks: CPU (Total)");
+ await testCpu(
+ cpu,
+ row.process.totalCpu,
+ row.process.slopeCpu,
+ HARDCODED_ASSUMPTIONS_PROCESS
+ );
+
+ // Testing threads.
+ if (!showThreads) {
+ info("In this mode, we shouldn't display any threads");
+ Assert.equal(
+ threads,
+ null,
+ "In hidden threads mode, we shouldn't have any thread summary"
+ );
+ } else {
+ Assert.ok(threads, "We have a thread summary row");
+
+ let {
+ number,
+ active = 0,
+ list,
+ } = doc.l10n.getAttributes(threads.children[0].children[1]).args;
+
+ info("Sanity checks: number of threads");
+ Assert.greaterOrEqual(
+ number,
+ HARDCODED_ASSUMPTIONS_PROCESS.minimalNumberOfThreads
+ );
+ Assert.lessOrEqual(
+ number,
+ HARDCODED_ASSUMPTIONS_PROCESS.maximalNumberOfThreads
+ );
+ Assert.equal(
+ number,
+ row.process.threads.length,
+ "The number we display should be the number of threads"
+ );
+
+ info("Sanity checks: number of active threads");
+ Assert.greaterOrEqual(
+ active,
+ 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.active);
+ Assert.equal(
+ active,
+ activeThreads.length,
+ "The displayed number of active threads should be correct"
+ );
+
+ let activeSet = new Set();
+ for (let t of activeThreads) {
+ activeSet.add(t.name.replace(/ ?#[0-9]+$/, ""));
+ }
+ info("Sanity checks: thread list");
+ Assert.equal(
+ list ? list.split(", ").length : 0,
+ activeSet.size,
+ "The thread summary list of active threads should have the expected length"
+ );
+
+ info("Testing that we can open the list of threads");
+ let twisty = threads.getElementsByClassName("twisty")[0];
+ twisty.click();
+
+ // Fluent will update the text content of new rows during the
+ // next refresh driver tick, wait for it.
+ // requestAnimationFrame calls us at the begining of the tick, we use
+ // dispatchToMainThread to execute our code after the end of it.
+ //XXX: Replace with proper wait for l10n completion once bug 1520659 is fixed.
+ await new Promise(doc.defaultView.requestAnimationFrame);
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ let numberOfThreadsFound = 0;
+ for (
+ let threadRow = threads.nextSibling;
+ threadRow && threadRow.classList.contains("thread");
+ threadRow = threadRow.nextSibling
+ ) {
+ numberOfThreadsFound++;
+ }
+ Assert.equal(
+ numberOfThreadsFound,
+ number,
+ `We should see ${number} threads, found ${numberOfThreadsFound}`
+ );
+ let threadIds = [];
+ for (
+ let threadRow = threads.nextSibling;
+ threadRow && threadRow.classList.contains("thread");
+ threadRow = threadRow.nextSibling
+ ) {
+ Assert.ok(
+ threadRow.children.length >= 3 && threadRow.children[1].textContent,
+ "The thread row should be populated"
+ );
+ let children = threadRow.children;
+ let cpu = children[1];
+ let l10nArgs = doc.l10n.getAttributes(children[0]).args;
+
+ // Sanity checks: name
+ Assert.ok(threadRow.thread.name, "Thread name is not empty");
+ Assert.equal(
+ l10nArgs.name,
+ threadRow.thread.name,
+ "Displayed thread name is correct"
+ );
+
+ // Sanity checks: tid
+ let tidContent = l10nArgs.tid;
+ let tid = Number.parseInt(tidContent);
+ threadIds.push(tid);
+ Assert.notEqual(tid, 0, "The tid should be set");
+ Assert.equal(tid, threadRow.thread.tid, "Displayed tid is correct");
+
+ // Sanity checks: CPU (per thread)
+ await testCpu(
+ cpu,
+ threadRow.thread.totalCpu,
+ threadRow.thread.slopeCpu,
+ HARDCODED_ASSUMPTIONS_THREAD
+ );
+ }
+ // By default, threads are sorted by tid.
+ let threadList = threadIds.join(",");
+ Assert.equal(
+ threadList,
+ threadIds.sort((a, b) => a - b).join(","),
+ "The thread rows are in the default sort order."
+ );
+ }
+ }
+
+ await promiseAboutProcessesUpdated({
+ doc,
+ force: true,
+ tabAboutProcesses,
+ });
+
+ // Testing subframes.
+ info("Testing subframes");
+ let foundAtLeastOneInProcessSubframe = false;
+ for (let row of doc.getElementsByClassName("window")) {
+ let subframe = row.win;
+ if (subframe.tab) {
+ continue;
+ }
+ let url = doc.l10n.getAttributes(row.children[0]).args.url;
+ Assert.equal(url, subframe.documentURI.spec);
+ if (!subframe.isProcessRoot) {
+ foundAtLeastOneInProcessSubframe = true;
+ }
+ }
+ if (showAllFrames) {
+ Assert.ok(
+ foundAtLeastOneInProcessSubframe,
+ "Found at least one about:blank in-process subframe"
+ );
+ } else {
+ Assert.ok(
+ !foundAtLeastOneInProcessSubframe,
+ "We shouldn't have any about:blank in-process subframe"
+ );
+ }
+
+ info("Double-clicking on a tab");
+ let whenTabSwitchedToWeb = BrowserTestUtils.switchTab(gBrowser, () => {
+ // We pass a function to use `BrowserTestUtils.switchTab` not in its
+ // role as a tab switcher but rather in its role as a function that
+ // waits until something else has switched the tab.
+ // We'll actually cause tab switching below, by doucle-clicking
+ // in `about:processes`.
+ });
+ await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => {
+ // Locate and double-click on the representation of `tabHung`.
+ let tbody = content.document.getElementById("process-tbody");
+ for (let row of tbody.getElementsByClassName("tab")) {
+ if (row.parentNode.win.documentURI.spec != "http://example.com/") {
+ continue;
+ }
+ // Simulate double-click.
+ let evt = new content.window.MouseEvent("dblclick", {
+ bubbles: true,
+ cancelable: true,
+ view: content.window,
+ });
+ row.dispatchEvent(evt);
+ return;
+ }
+ Assert.ok(false, "We should have found the hung tab");
+ });
+
+ info("Waiting for tab switch");
+ await whenTabSwitchedToWeb;
+ Assert.equal(
+ gBrowser.selectedTab.linkedBrowser.currentURI.spec,
+ tabHung.linkedBrowser.currentURI.spec,
+ "We should have focused the hung tab"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, tabAboutProcesses);
+
+ info("Double-clicking on the extensions process");
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+ await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => {
+ let extensionsRow =
+ content.document.getElementsByClassName("extensions")[0];
+ Assert.ok(!!extensionsRow, "We should have found the extensions process");
+ let evt = new content.window.MouseEvent("dblclick", {
+ bubbles: true,
+ cancelable: true,
+ view: content.window,
+ });
+ extensionsRow.dispatchEvent(evt);
+ });
+ info("Waiting for about:addons to open");
+ await tabPromise;
+ Assert.equal(
+ gBrowser.selectedTab.linkedBrowser.currentURI.spec,
+ "about:addons",
+ "We should now see the addon tab"
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ info("Testing tab closing");
+
+ // A list of processes we have killed and for which we're waiting
+ // death confirmation. Only used in Fission.
+ let waitForProcessesToDisappear = [];
+ await promiseAboutProcessesUpdated({
+ doc,
+ force: true,
+ tabAboutProcesses,
+ });
+ if (isFission) {
+ // Before closing, all our origins should be present
+ for (let origin of [
+ "http://example.com", // tabHung
+ "http://example.net", // tabCloseProcess*
+ "http://example.org", // tabCloseSeparately*
+ "https://example.org", // tabCloseTogether*
+ ]) {
+ Assert.ok(
+ findProcessRowByOrigin(doc, origin),
+ `There is a process for origin ${origin}`
+ );
+ }
+
+ // Verify that the user context id has been correctly displayed.
+ let userContextProcessRow = findProcessRowByOrigin(
+ doc,
+ "http://example.com^userContextId=1"
+ );
+ Assert.ok(
+ userContextProcessRow,
+ "There is a separate process for the tab with a different user context"
+ );
+ let name = userContextProcessRow.firstChild;
+ if (
+ Services.prefs.getBoolPref("toolkit.aboutProcesses.showProfilerIcons")
+ ) {
+ name = name.firstChild;
+ Assert.ok(
+ name.nextSibling.classList.contains("profiler-icon"),
+ "The profiler icon should be shown"
+ );
+ }
+ Assert.equal(
+ doc.l10n.getAttributes(name).args.origin,
+ "http://example.com — " +
+ ContextualIdentityService.getUserContextLabel(1),
+ "The user context ID should be replaced with the localized container name"
+ );
+
+ // These origins will disappear.
+ for (let origin of [
+ "http://example.net", // tabCloseProcess*
+ "https://example.org", // tabCloseTogether*
+ ]) {
+ let row = findProcessRowByOrigin(doc, origin);
+ let childID = row.process.childID;
+ waitForProcessesToDisappear.push(promiseProcessDied({ childID }));
+ }
+ }
+
+ // Close a few tabs.
+ for (let tab of [tabCloseSeparately1, tabCloseTogether1, tabCloseTogether2]) {
+ info("Closing a tab through about:processes");
+ let found = findTabRowByName(doc, tab.linkedBrowser.contentTitle);
+ Assert.ok(
+ found,
+ `We should have found tab ${tab.linkedBrowser.contentTitle} to close it`
+ );
+ let closeIcons = found.getElementsByClassName("close-icon");
+ Assert.equal(
+ closeIcons.length,
+ 1,
+ "This tab should have exactly one close icon"
+ );
+ closeIcons[0].click();
+ Assert.ok(
+ found.classList.contains("killing"),
+ "We should have marked the row as dying"
+ );
+ }
+
+ //...and a process, if we're in Fission.
+ if (isFission) {
+ info("Closing an entire process through about:processes");
+ let found = findProcessRowByOrigin(doc, "http://example.net");
+ let closeIcons = found.getElementsByClassName("close-icon");
+ Assert.equal(
+ closeIcons.length,
+ 1,
+ "This process should have exactly one close icon"
+ );
+ closeIcons[0].click();
+ Assert.ok(
+ found.classList.contains("killing"),
+ "We should have marked the row as dying"
+ );
+ }
+
+ // Give Firefox a little time to close the tabs and update about:processes.
+ // This might take two updates as we're racing between collecting data and
+ // processes actually being killed.
+ await promiseAboutProcessesUpdated({
+ doc,
+ force: true,
+ tabAboutProcesses,
+ });
+
+ // The tabs we have closed directly or indirectly should now be (closed or crashed) and invisible in about:processes.
+ for (let { origin, tab } of [
+ { origin: "http://example.org", tab: tabCloseSeparately1 },
+ { origin: "https://example.org", tab: tabCloseTogether1 },
+ { origin: "https://example.org", tab: tabCloseTogether2 },
+ ...(isFission
+ ? [
+ { origin: "http://example.net", tab: tabCloseProcess1 },
+ { origin: "http://example.net", tab: tabCloseProcess2 },
+ ]
+ : []),
+ ]) {
+ // Tab shouldn't show up anymore in about:processes
+ Assert.ok(
+ !findTabRowByName(doc, origin),
+ `Tab for ${origin} shouldn't show up anymore in about:processes`
+ );
+ // ...and should be unloaded.
+ Assert.ok(
+ !tab.getAttribute("linkedPanel"),
+ `The tab should now be unloaded (${tab.testOrigin} - ${tab.testTitle})`
+ );
+ }
+
+ // On the other hand, tabs we haven't closed should still be open and visible in about:processes.
+ Assert.ok(
+ tabCloseSeparately2.linkedBrowser,
+ "Killing one tab in the domain should not have closed the other tab"
+ );
+ let foundtabCloseSeparately2 = findTabRowByName(
+ doc,
+ tabCloseSeparately2.linkedBrowser.contentTitle
+ );
+ Assert.ok(
+ foundtabCloseSeparately2,
+ "The second tab is still visible in about:processes"
+ );
+
+ if (isFission) {
+ // After closing, we must have closed some of our origins.
+ for (let origin of [
+ "http://example.com", // tabHung
+ "http://example.org", // tabCloseSeparately*
+ ]) {
+ Assert.ok(
+ findProcessRowByOrigin(doc, origin),
+ `There should still be a process row for origin ${origin}`
+ );
+ }
+
+ info("Waiting for processes to die");
+ await Promise.all(waitForProcessesToDisappear);
+
+ info("Waiting for about:processes to be updated");
+ await promiseAboutProcessesUpdated({
+ doc,
+ force: true,
+ tabAboutProcesses,
+ });
+
+ for (let origin of [
+ "http://example.net", // tabCloseProcess*
+ "https://example.org", // tabCloseTogether*
+ ]) {
+ Assert.ok(
+ !findProcessRowByOrigin(doc, origin),
+ `Process ${origin} should disappear from about:processes`
+ );
+ }
+ }
+
+ info("Additional sanity check for all processes");
+ for (let row of doc.getElementsByClassName("process")) {
+ let { pidContent } = extractProcessDetails(row);
+ Assert.equal(Number.parseInt(pidContent), row.process.pid);
+ }
+ BrowserTestUtils.removeTab(tabAboutProcesses);
+ BrowserTestUtils.removeTab(tabHung);
+ BrowserTestUtils.removeTab(tabUserContext);
+ BrowserTestUtils.removeTab(tabCloseSeparately2);
+
+ // We still need to remove these tabs.
+ // We killed the process, but we don't want to leave zombie tabs lying around.
+ BrowserTestUtils.removeTab(tabCloseProcess1);
+ BrowserTestUtils.removeTab(tabCloseProcess2);
+ BrowserTestUtils.removeTab(audioPlayback);
+
+ await SpecialPowers.popPrefEnv();
+
+ await extension.unload();
+}
diff --git a/toolkit/components/aboutprocesses/tests/browser/small-shot.mp3 b/toolkit/components/aboutprocesses/tests/browser/small-shot.mp3
new file mode 100644
index 0000000000..f9397a5106
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/small-shot.mp3
Binary files differ