summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutprocesses
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutprocesses')
-rw-r--r--toolkit/components/aboutprocesses/content/aboutProcesses.css237
-rw-r--r--toolkit/components/aboutprocesses/content/aboutProcesses.html29
-rw-r--r--toolkit/components/aboutprocesses/content/aboutProcesses.js1434
-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.ini8
-rw-r--r--toolkit/components/aboutprocesses/tests/browser/browser_aboutprocesses_default_options.js7
-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/head.js912
11 files changed, 2666 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..4cc1083857
--- /dev/null
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.css
@@ -0,0 +1,237 @@
+/* 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%;
+ min-width: 40em;
+}
+
+/* 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);
+ min-width: 40em;
+ background-color: var(--in-content-box-background);
+}
+tr {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+}
+
+/* At least one column needs to have a flexible width,
+ so no width specified for td:nth-child(1) aka column-name*/
+
+/* column-memory-resident */
+td:nth-child(2) {
+ width: 15%;
+}
+#process-tbody td:nth-child(2) {
+ text-align: end;
+}
+
+/* column-cpu-total */
+td:nth-child(3) {
+ width: 15%;
+}
+#process-tbody td:nth-child(3) {
+ text-align: end;
+}
+
+/* column-action-icon */
+td:nth-child(4) {
+ width: 16px;
+ text-align: center;
+}
+
+#process-thead > tr {
+ height: inherit;
+}
+
+#process-thead > tr > td {
+ border: none;
+ background-color: var(--in-content-button-background);
+}
+#process-thead > tr > td: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;
+ border-bottom: 1px solid var(--in-content-border-color);
+}
+td {
+ padding: 5px 10px;
+ min-height: 16px;
+ max-height: 16px;
+ color: var(--in-content-text-color);
+ max-width: 70vw;
+ 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;
+}
+#process-tbody > tr > td:first-child {
+ text-overflow: ellipsis;
+}
+.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/twisty-collapsed.svg");
+ position: absolute;
+ display: block;
+ line-height: 50%;
+ top: 4px; /* Half the image's height */
+ inset-inline-start: -16px;
+ width: 100%;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+.twisty:dir(rtl)::before {
+ content: url("chrome://global/skin/icons/twisty-collapsed-rtl.svg");
+}
+.twisty.open::before {
+ content: url("chrome://global/skin/icons/twisty-expanded.svg");
+}
+#process-tbody > tr > td.indent {
+ padding-inline: 48px 0;
+}
+#process-tbody > tr > td.double_indent {
+ padding-inline: 58px 0;
+}
+
+#process-tbody > tr[selected] > td {
+ background-color: var(--in-content-item-selected);
+ color: var(--in-content-selected-text);
+}
+#process-tbody > tr:hover {
+ background-color: var(--in-content-item-hover);
+}
+
+.clickable {
+ background-repeat: no-repeat;
+ background-position: right 4px center;
+}
+.clickable:dir(rtl) {
+ background-position-x: left 4px;
+}
+.asc,
+.desc {
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+/*
+ Linux has conventions opposite to Windows, macOS on the direction of arrows
+ when sorting.
+*/
+%ifdef XP_LINUX
+.asc {
+ background-image: url(chrome://global/skin/icons/arrow-up-12.svg);
+}
+.desc {
+ background-image: url(chrome://global/skin/icons/arrow-dropdown-12.svg);
+}
+%else
+.asc {
+ background-image: url(chrome://global/skin/icons/arrow-dropdown-12.svg);
+}
+.desc {
+ background-image: url(chrome://global/skin/icons/arrow-up-12.svg);
+}
+%endif
+
+#process-thead > tr > td.clickable:hover {
+ background-color: var(--in-content-button-background-hover);
+}
+#process-thead > tr > td.clickable:hover:active {
+ background-color: var(--in-content-button-background-active);
+}
+
+#process-tbody > tr.process > td.type {
+ font-weight: bold;
+}
+#process-tbody > 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 */
+ fill-opacity: 0; /* Make SVG background transparent */
+ -moz-context-properties: fill, fill-opacity;
+ fill: currentColor;
+}
+
+tr:is([selected], :hover):not(.killing) > .close-icon {
+ opacity: 1;
+}
+
+.close-icon:hover {
+ background-color: var(--in-content-button-background-hover);
+}
+
+.close-icon:hover:active {
+ background-color: var(--in-content-button-background-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.
+ */
+
+#process-tbody > tr.separate-from-previous-process-group {
+ border-top: dotted 1px var(--in-content-box-border-color);
+ margin-top: -1px;
+}
diff --git a/toolkit/components/aboutprocesses/content/aboutProcesses.html b/toolkit/components/aboutprocesses/content/aboutProcesses.html
new file mode 100644
index 0000000000..a95fa83393
--- /dev/null
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.html
@@ -0,0 +1,29 @@
+<!-- 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'">
+ <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>
+ <td class="clickable" id="column-name" data-l10n-id="about-processes-column-name"></td>
+ <td class="clickable" id="column-memory-resident" data-l10n-id="about-processes-column-memory-resident"></td> <!-- Memory usage. -->
+ <td class="clickable" id="column-cpu-total" data-l10n-id="about-processes-column-cpu-total"></td><!--CPU (User and Kernel)-->
+ <td id="column-kill" data-l10n-id="about-processes-column-action">⚙</td><!-- 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..709e76db2b
--- /dev/null
+++ b/toolkit/components/aboutprocesses/content/aboutProcesses.js
@@ -0,0 +1,1434 @@
+/* -*- 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 often we should add a sample to our buffer.
+const BUFFER_SAMPLING_RATE_MS = 1000;
+
+// The age of the oldest sample to keep.
+const BUFFER_DURATION_MS = 10000;
+
+// 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
+
+const SHOW_THREADS = Services.prefs.getBoolPref(
+ "toolkit.aboutProcesses.showThreads"
+);
+const SHOW_ALL_SUBFRAMES = Services.prefs.getBoolPref(
+ "toolkit.aboutProcesses.showAllSubframes"
+);
+
+/**
+ * Returns a Promise that's resolved after the next turn of the event loop.
+ *
+ * Just returning a resolved Promise would mean that any `then` callbacks
+ * would be called right after the end of the current turn, so `setTimeout`
+ * is used to delay Promise resolution until the next turn.
+ *
+ * In mochi tests, it's possible for this to be called after the
+ * about:performance window has been torn down, which causes `setTimeout` to
+ * throw an NS_ERROR_NOT_INITIALIZED exception. In that case, returning
+ * `undefined` is fine.
+ */
+function wait(ms = 0) {
+ try {
+ let resolve;
+ let p = new Promise(resolve_ => {
+ resolve = resolve_;
+ });
+ setTimeout(resolve, ms);
+ return p;
+ } catch (e) {
+ dump(
+ "WARNING: wait aborted because of an invalid Window state in aboutPerformance.js.\n"
+ );
+ return undefined;
+ }
+}
+
+/**
+ * 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.
+ *
+ * @type Promise<{
+ * 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 gPromisePrefetchedUnits;
+
+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 = {
+ /**
+ * Indexed by the number of minutes since the snapshot was taken.
+ *
+ * @type {Array<ApplicationSnapshot>}
+ */
+ _buffer: [],
+ /**
+ * The latest snapshot.
+ *
+ * @type ApplicationSnapshot
+ */
+ _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 the buffer is empty, add one value for bootstraping purposes.
+ if (!this._buffer.length) {
+ this._latest = await this._promiseSnapshot();
+ this._buffer.push(this._latest);
+ await wait(BUFFER_SAMPLING_RATE_MS * 1.1);
+ }
+
+ let now = Cu.now();
+
+ // If we haven't sampled in a while, add a sample to the buffer.
+ let latestInBuffer = this._buffer[this._buffer.length - 1];
+ let deltaT = now - latestInBuffer.date;
+ if (force || deltaT > BUFFER_SAMPLING_RATE_MS) {
+ this._latest = await this._promiseSnapshot();
+ this._buffer.push(this._latest);
+ }
+
+ // If we have too many samples, remove the oldest sample.
+ let oldestInBuffer = this._buffer[0];
+ if (oldestInBuffer.date + BUFFER_DURATION_MS < this._latest.date) {
+ this._buffer.shift();
+ }
+ },
+
+ _getThreadDelta(cur, prev, deltaT) {
+ let name = cur.name || "???";
+ let result = {
+ tid: cur.tid,
+ name,
+ // Total amount of CPU used, in ns (user).
+ totalCpuUser: cur.cpuUser,
+ slopeCpuUser: null,
+ // Total amount of CPU used, in ns (kernel).
+ totalCpuKernel: cur.cpuKernel,
+ slopeCpuKernel: null,
+ // Total amount of CPU used, in ns (user + kernel).
+ totalCpu: cur.cpuUser + cur.cpuKernel,
+ slopeCpu: null,
+ };
+ if (!prev) {
+ return result;
+ }
+ if (prev.tid != cur.tid) {
+ throw new Error("Assertion failed: A thread cannot change tid.");
+ }
+ result.slopeCpuUser = (cur.cpuUser - prev.cpuUser) / deltaT;
+ result.slopeCpuKernel = (cur.cpuKernel - prev.cpuKernel) / deltaT;
+ result.slopeCpu = result.slopeCpuKernel + result.slopeCpuUser;
+ 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);
+ // Resident set size is the total memory used by the process, including shared memory.
+ // Resident unique size is the memory used by the process, without shared memory.
+ // Since all processes share memory with the parent process, we count the shared memory
+ // as part of the parent process (`"browser"`) rather than as part of the individual
+ // processes.
+ let totalRamSize =
+ cur.type == "browser" ? cur.residentSetSize : cur.residentUniqueSize;
+ let result = {
+ pid: cur.pid,
+ childID: cur.childID,
+ filename: cur.filename,
+ totalRamSize,
+ deltaRamSize: null,
+ totalCpuUser: cur.cpuUser,
+ slopeCpuUser: null,
+ totalCpuKernel: cur.cpuKernel,
+ slopeCpuKernel: null,
+ totalCpu: cur.cpuUser + cur.cpuKernel,
+ slopeCpu: null,
+ type: cur.type,
+ origin: cur.origin || "",
+ threads: null,
+ displayRank: Control._getDisplayGroupRank(cur, windows),
+ windows,
+ // If this process has an unambiguous title, store it here.
+ title: null,
+ };
+ // 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, null, null)
+ );
+ }
+ 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 => {
+ let prevThread = prevThreads.get(curThread.tid);
+ if (!prevThread) {
+ return this._getThreadDelta(curThread);
+ }
+ return this._getThreadDelta(curThread, prevThread, deltaT);
+ });
+ }
+ result.deltaRamSize =
+ cur.type == "browser"
+ ? cur.residentSetSize - prev.residentSetSize
+ : cur.residentUniqueSize - prev.residentUniqueSize;
+ result.slopeCpuUser = (cur.cpuUser - prev.cpuUser) / deltaT;
+ result.slopeCpuKernel = (cur.cpuKernel - prev.cpuKernel) / deltaT;
+ result.slopeCpu = result.slopeCpuUser + result.slopeCpuKernel;
+ result.threads = threads;
+ return result;
+ },
+
+ getCounters() {
+ tabFinder.update();
+
+ // We rebuild the maps during each iteration to make sure that
+ // we do not maintain references to processes that have been
+ // shutdown.
+
+ let current = this._latest;
+ let counters = [];
+
+ for (let cur of current.processes.values()) {
+ // Look for the oldest point of comparison
+ let oldest = null;
+ let delta;
+ for (let index = 0; index <= this._buffer.length - 2; ++index) {
+ oldest = this._buffer[index].processes.get(cur.pid);
+ if (oldest) {
+ // Found it!
+ break;
+ }
+ }
+ if (oldest) {
+ // Existing process. Let's display slopes info.
+ delta = this._getProcessDelta(cur, oldest);
+ } else {
+ // New process. Let's display basic info.
+ delta = this._getProcessDelta(cur, null);
+ }
+ counters.push(delta);
+ }
+
+ return counters;
+ },
+};
+
+var View = {
+ _fragment: document.createDocumentFragment(),
+ // Processes, tabs and subframes that we killed during the previous iteration.
+ // Array<{pid:Number} | {windowId:Number}>
+ _killedRecently: [],
+ async commit() {
+ this._killedRecently.length = 0;
+ let tbody = document.getElementById("process-tbody");
+
+ // Force translation to happen before we insert the new content in the DOM
+ // to avoid flicker when resizing.
+ await document.l10n.translateFragment(this._fragment);
+
+ while (tbody.firstChild) {
+ tbody.firstChild.remove();
+ }
+ tbody.appendChild(this._fragment);
+ this._fragment = document.createDocumentFragment();
+ },
+ insertAfterRow(row) {
+ row.parentNode.insertBefore(this._fragment, row.nextSibling);
+ this._fragment = document.createDocumentFragment();
+ },
+
+ /**
+ * Append a row showing a single process (without its threads).
+ *
+ * @param {ProcessDelta} data The data to display.
+ * @return {DOMElement} The row displaying the process.
+ */
+ appendProcessRow(data, units) {
+ let row = document.createElement("tr");
+ row.classList.add("process");
+
+ if (data.isHung) {
+ row.classList.add("hung");
+ }
+
+ // Column: Name
+ {
+ let fluentName;
+ let classNames = [];
+ switch (data.type) {
+ case "web":
+ fluentName = "about-processes-web-process-name";
+ break;
+ case "webIsolated":
+ fluentName = "about-processes-web-isolated-process-name";
+ break;
+ case "webLargeAllocation":
+ fluentName = "about-processes-web-large-allocation-process-name";
+ break;
+ case "file":
+ fluentName = "about-processes-file-process-name";
+ break;
+ case "extension":
+ fluentName = "about-processes-extension-process-name";
+ classNames = ["extensions"];
+ break;
+ case "privilegedabout":
+ fluentName = "about-processes-privilegedabout-process-name";
+ break;
+ case "withCoopCoep":
+ fluentName = "about-processes-with-coop-coep-process-name";
+ break;
+ case "browser":
+ fluentName = "about-processes-browser-process-name";
+ break;
+ case "plugin":
+ fluentName = "about-processes-plugin-process-name";
+ break;
+ case "gmpPlugin":
+ fluentName = "about-processes-gmp-plugin-process-name";
+ break;
+ case "gpu":
+ fluentName = "about-processes-gpu-process-name";
+ break;
+ case "vr":
+ fluentName = "about-processes-vr-process-name";
+ break;
+ case "rdd":
+ fluentName = "about-processes-rdd-process-name";
+ break;
+ case "socket":
+ fluentName = "about-processes-socket-process-name";
+ break;
+ case "remoteSandboxBroker":
+ fluentName = "about-processes-remote-sandbox-broker-process-name";
+ break;
+ case "forkServer":
+ fluentName = "about-processes-fork-server-process-name";
+ break;
+ case "preallocated":
+ fluentName = "about-processes-preallocated-process-name";
+ 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-name";
+ break;
+ }
+ let elt = this._addCell(row, {
+ fluentName,
+ fluentArgs: {
+ pid: "" + data.pid, // Make sure that this number is not localized
+ origin: data.origin,
+ type: data.type,
+ },
+ classes: ["type", "favicon", ...classNames],
+ });
+
+ 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://browser/skin/link.svg";
+ }
+ }
+ elt.style.backgroundImage = `url('${image}')`;
+ }
+
+ // Column: Resident size
+ {
+ let formattedTotal = this._formatMemory(data.totalRamSize);
+ if (data.deltaRamSize) {
+ let formattedDelta = this._formatMemory(data.deltaRamSize);
+ this._addCell(row, {
+ fluentName: "about-processes-total-memory-size",
+ fluentArgs: {
+ total: formattedTotal.amount,
+ totalUnit: units.memory[formattedTotal.unit],
+ delta: Math.abs(formattedDelta.amount),
+ deltaUnit: units.memory[formattedDelta.unit],
+ deltaSign: data.deltaRamSize > 0 ? "+" : "-",
+ },
+ classes: ["totalMemorySize"],
+ });
+ } else {
+ this._addCell(row, {
+ fluentName: "about-processes-total-memory-size-no-change",
+ fluentArgs: {
+ total: formattedTotal.amount,
+ totalUnit: units.memory[formattedTotal.unit],
+ },
+ classes: ["totalMemorySize"],
+ });
+ }
+ }
+
+ // Column: CPU: User and Kernel
+ if (data.slopeCpu == null) {
+ this._addCell(row, {
+ fluentName: "about-processes-cpu-user-and-kernel-not-ready",
+ classes: ["cpu"],
+ });
+ } else {
+ let { duration, unit } = this._getDuration(data.totalCpu);
+ let localizedUnit = units.duration[unit];
+ if (data.slopeCpu == 0) {
+ this._addCell(row, {
+ fluentName: "about-processes-cpu-user-and-kernel-idle",
+ fluentArgs: {
+ total: duration,
+ unit: localizedUnit,
+ },
+ classes: ["cpu"],
+ });
+ } else {
+ this._addCell(row, {
+ fluentName: "about-processes-cpu-user-and-kernel",
+ fluentArgs: {
+ percent: data.slopeCpu,
+ total: duration,
+ unit: localizedUnit,
+ },
+ classes: ["cpu"],
+ });
+ }
+ }
+
+ // Column: Kill button – but not for all processes.
+ let killButton = this._addCell(row, {
+ content: "",
+ classes: ["action-icon"],
+ });
+
+ if (["web", "webIsolated", "webLargeAllocation"].includes(data.type)) {
+ // This type of process can be killed.
+ if (this._killedRecently.some(kill => kill.pid && kill.pid == data.pid)) {
+ // We're racing between the "kill" action and the visual refresh.
+ // In a few cases, we could end up with the visual refresh showing
+ // a process as un-killed while we actually just killed it.
+ //
+ // 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"
+ );
+ }
+ }
+
+ this._fragment.appendChild(row);
+ return row;
+ },
+
+ appendThreadSummaryRow(data, isOpen) {
+ let row = document.createElement("tr");
+ row.classList.add("thread-summary");
+
+ // Column: Name
+ let elt = this._addCell(row, {
+ fluentName: "about-processes-thread-summary",
+ fluentArgs: { number: data.threads.length },
+ classes: ["name", "indent"],
+ });
+ if (data.threads.length) {
+ let img = document.createElement("span");
+ img.classList.add("twisty");
+ if (data.isOpen) {
+ img.classList.add("open");
+ }
+ elt.insertBefore(img, elt.firstChild);
+ }
+
+ // Column: Resident size
+ this._addCell(row, {
+ content: "",
+ classes: ["totalRamSize"],
+ });
+
+ // Column: CPU: User and Kernel
+ this._addCell(row, {
+ content: "",
+ classes: ["cpu"],
+ });
+
+ // Column: action
+ this._addCell(row, {
+ content: "",
+ classes: ["action-icon"],
+ });
+
+ this._fragment.appendChild(row);
+ return row;
+ },
+
+ appendDOMWindowRow(data, parent) {
+ let row = document.createElement("tr");
+ row.classList.add("window");
+
+ // Column: filename
+ let tab = tabFinder.get(data.outerWindowId);
+ let fluentName;
+ let name;
+ let className;
+ if (parent.type == "extension") {
+ fluentName = "about-processes-extension-name";
+ if (data.addon) {
+ name = data.addon.name;
+ } else if (data.documentURI.scheme == "about") {
+ // about: URLs don't have an host.
+ name = data.documentURI.spec;
+ } else {
+ name = data.documentURI.host;
+ }
+ } else if (tab && tab.tabbrowser) {
+ fluentName = "about-processes-tab-name";
+ name = data.documentTitle;
+ className = "tab";
+ } else if (tab) {
+ fluentName = "about-processes-preloaded-tab";
+ name = null;
+ className = "preloaded-tab";
+ } else if (data.count == 1) {
+ fluentName = "about-processes-frame-name-one";
+ name = data.prePath;
+ className = "frame-one";
+ } else {
+ fluentName = "about-processes-frame-name-many";
+ name = data.prePath;
+ className = "frame-many";
+ }
+ let elt = this._addCell(row, {
+ fluentName,
+ fluentArgs: {
+ name,
+ url: data.documentURI.spec,
+ number: data.count,
+ shortUrl:
+ data.documentURI.scheme == "about"
+ ? data.documentURI.spec
+ : data.documentURI.prePath,
+ },
+ classes: ["name", "indent", "favicon", className],
+ });
+ let image = tab?.tab.getAttribute("image");
+ if (image) {
+ elt.style.backgroundImage = `url('${image}')`;
+ }
+
+ // Column: Resident size (empty)
+ this._addCell(row, {
+ content: "",
+ classes: ["totalRamSize"],
+ });
+
+ // Column: CPU (empty)
+ this._addCell(row, {
+ content: "",
+ classes: ["cpu"],
+ });
+
+ // Column: action
+ let killButton = this._addCell(row, {
+ content: "",
+ classes: ["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");
+ }
+ }
+ this._fragment.appendChild(row);
+ return row;
+ },
+
+ /**
+ * Append a row showing a single thread.
+ *
+ * @param {ThreadDelta} data The data to display.
+ * @return {DOMElement} The row displaying the thread.
+ */
+ appendThreadRow(data, units) {
+ let row = document.createElement("tr");
+ row.classList.add("thread");
+
+ // Column: filename
+ this._addCell(row, {
+ fluentName: "about-processes-thread-name",
+ fluentArgs: {
+ name: data.name,
+ tid: "" + data.tid /* Make sure that this number is not localized */,
+ },
+ classes: ["name", "double_indent"],
+ });
+
+ // Column: Resident size (empty)
+ this._addCell(row, {
+ content: "",
+ classes: ["totalRamSize"],
+ });
+
+ // Column: CPU: User and Kernel
+ if (data.slopeCpu == null) {
+ this._addCell(row, {
+ fluentName: "about-processes-cpu-user-and-kernel-not-ready",
+ classes: ["cpu"],
+ });
+ } else {
+ let { duration, unit } = this._getDuration(data.totalCpu);
+ let localizedUnit = units.duration[unit];
+ if (data.slopeCpu == 0) {
+ this._addCell(row, {
+ fluentName: "about-processes-cpu-user-and-kernel-idle",
+ fluentArgs: {
+ total: duration,
+ unit: localizedUnit,
+ },
+ classes: ["cpu"],
+ });
+ } else {
+ this._addCell(row, {
+ fluentName: "about-processes-cpu-user-and-kernel",
+ fluentArgs: {
+ percent: data.slopeCpu,
+ total: duration,
+ unit: localizedUnit,
+ },
+ classes: ["cpu"],
+ });
+ }
+ }
+
+ // Column: Buttons (empty)
+ this._addCell(row, {
+ content: "",
+ classes: [],
+ });
+
+ this._fragment.appendChild(row);
+ return row;
+ },
+
+ _addCell(row, { content, classes, fluentName, fluentArgs }) {
+ let elt = document.createElement("td");
+ if (fluentName) {
+ let span = document.createElement("span");
+ document.l10n.setAttributes(span, fluentName, fluentArgs);
+ elt.appendChild(span);
+ } else {
+ elt.textContent = content;
+ elt.setAttribute("title", content);
+ }
+ elt.classList.add(...classes);
+ row.appendChild(elt);
+ return elt;
+ },
+
+ _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 = {
+ _openItems: new Set(),
+ // The set of all processes reported as "hung" by the process hang monitor.
+ //
+ // type: Set<ChildID>
+ _hungItems: new Set(),
+ _sortColumn: null,
+ _sortAscendent: true,
+ _removeSubtree(row) {
+ let sibling = row.nextSibling;
+ while (sibling && !sibling.classList.contains("process")) {
+ let next = sibling.nextSibling;
+ if (sibling.classList.contains("thread")) {
+ sibling.remove();
+ }
+ sibling = next;
+ }
+ },
+ init() {
+ this._initHangReports();
+
+ // Start prefetching units.
+ gPromisePrefetchedUnits = (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;
+ // - change selection.
+ tbody.addEventListener("click", event => {
+ this._updateLastMouseEvent();
+
+ // Handle showing or hiding subitems of a row.
+ let target = event.target;
+ if (target.classList.contains("twisty")) {
+ this._handleTwisty(target);
+ return;
+ }
+ if (target.classList.contains("close-icon")) {
+ this._handleKill(target);
+ return;
+ }
+
+ // Handle selection changes
+ let row = target.parentNode;
+ if (this.selectedRow) {
+ this.selectedRow.removeAttribute("selected");
+ }
+ if (row.windowId) {
+ row.setAttribute("selected", "true");
+ this.selectedRow = row;
+ } else if (this.selectedRow) {
+ this.selectedRow = null;
+ }
+ });
+
+ // 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;
+ }
+
+ if (this._sortColumn) {
+ const td = document.getElementById(this._sortColumn);
+ td.classList.remove("asc");
+ td.classList.remove("desc");
+ }
+
+ const columnId = event.target.id;
+ if (columnId == this._sortColumn) {
+ // Reverse sorting order.
+ this._sortAscendent = !this._sortAscendent;
+ } else {
+ this._sortColumn = columnId;
+ this._sortAscendent = true;
+ }
+
+ if (this._sortAscendent) {
+ event.target.classList.remove("desc");
+ event.target.classList.add("asc");
+ } else {
+ event.target.classList.remove("asc");
+ event.target.classList.add("desc");
+ }
+
+ 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 wait(0);
+
+ await this._updateDisplay(force);
+ },
+
+ // The force parameter can force a full update even when the mouse has been
+ // moved recently.
+ async _updateDisplay(force = false) {
+ if (
+ !force &&
+ Date.now() - this._lastMouseEvent < TIME_BEFORE_SORTING_AGAIN
+ ) {
+ return;
+ }
+
+ let counters = State.getCounters();
+ let units = await gPromisePrefetchedUnits;
+
+ // Reset the selectedRow field and the _openItems set each time we redraw
+ // to avoid keeping forever references to dead processes.
+ let openItems = this._openItems;
+ this._openItems = new Set();
+
+ // Similarly, we reset `_hungItems`, based on the assumption that the process hang
+ // monitor will inform us again before the next update. Since the process hang monitor
+ // pings its clients about once per second and we update about once per 2 seconds
+ // (or more if the mouse moves), we should be ok.
+ let hungItems = this._hungItems;
+ this._hungItems = new Set();
+
+ counters = this._sortProcesses(counters);
+ let previousProcess = null;
+ for (let process of counters) {
+ this._sortDOMWindows(process.windows);
+
+ let isOpen = openItems.has(process.pid);
+ process.isOpen = isOpen;
+
+ let isHung = process.childID && hungItems.has(process.childID);
+ process.isHung = isHung;
+
+ let processRow = View.appendProcessRow(process, units);
+ processRow.process = process;
+
+ if (process.type != "extension") {
+ // We do not want to display extensions.
+ let winRow;
+ for (let win of process.windows) {
+ if (SHOW_ALL_SUBFRAMES || win.tab || win.isProcessRoot) {
+ winRow = View.appendDOMWindowRow(win, process);
+ winRow.win = win;
+ }
+ }
+ }
+
+ if (SHOW_THREADS) {
+ let threadSummaryRow = View.appendThreadSummaryRow(process, isOpen);
+ threadSummaryRow.process = process;
+
+ if (isOpen) {
+ this._openItems.add(process.pid);
+ this._showThreads(processRow, units);
+ }
+ }
+ if (
+ this._sortColumn == null &&
+ previousProcess &&
+ previousProcess.displayRank != process.displayRank
+ ) {
+ // Add a separation between successive categories of processes.
+ processRow.classList.add("separate-from-previous-process-group");
+ }
+ previousProcess = process;
+ }
+
+ await View.commit();
+ },
+ _showThreads(row, units) {
+ let process = row.process;
+ this._sortThreads(process.threads);
+ let elt = row;
+ for (let thread of process.threads) {
+ // Enrich `elt` with a property `thread`, used for testing.
+ elt = View.appendThreadRow(thread, units);
+ elt.thread = thread;
+ }
+ return elt;
+ },
+ _sortThreads(threads) {
+ return threads.sort((a, b) => {
+ let order;
+ switch (this._sortColumn) {
+ case "column-name":
+ order = a.name.localeCompare(b.name) || a.pid - b.pid;
+ break;
+ case "column-cpu-total":
+ order = b.slopeCpu - a.slopeCpu;
+ break;
+
+ case "column-memory-resident":
+ case "column-pid":
+ case null:
+ order = b.tid - a.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-pid":
+ order = b.pid - a.pid;
+ break;
+ case "column-name":
+ order =
+ String(a.origin).localeCompare(b.origin) ||
+ String(a.type).localeCompare(b.type) ||
+ a.pid - b.pid;
+ break;
+ case "column-cpu-total":
+ order = b.slopeCpu - a.slopeCpu;
+ 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 "webLargeAllocation":
+ 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;
+ }
+ },
+
+ // Open/close list of threads.
+ async _handleTwisty(target) {
+ // We await immediately, to ensure that all DOM changes are made in the same tick.
+ // Otherwise, it's both wasteful and harder to test.
+ let units = await gPromisePrefetchedUnits;
+ let row = target.parentNode.parentNode;
+ let id = row.process.pid;
+ if (target.classList.toggle("open")) {
+ this._openItems.add(id);
+ this._showThreads(row, units);
+ View.insertAfterRow(row);
+ } else {
+ this._openItems.delete(id);
+ 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");
+ }
+ }
+ }
+ },
+};
+
+window.onload = async function() {
+ Control.init();
+ await Control.update();
+ 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..58d1b5693e
--- /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..f397ead22d
--- /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.ini"]
diff --git a/toolkit/components/aboutprocesses/tests/browser/browser.ini b/toolkit/components/aboutprocesses/tests/browser/browser.ini
new file mode 100644
index 0000000000..d66c196532
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/browser.ini
@@ -0,0 +1,8 @@
+[default]
+head = head.js
+skip-if = asan || tsan # With sanitizers, we regularly hit internal timeouts.
+
+[browser_aboutprocesses_default_options.js]
+[browser_aboutprocesses_show_all_frames.js]
+[browser_aboutprocesses_show_threads.js]
+[browser_aboutprocesses_show_frames_without_threads.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_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/head.js b/toolkit/components/aboutprocesses/tests/browser/head.js
new file mode 100644
index 0000000000..dc677b4798
--- /dev/null
+++ b/toolkit/components/aboutprocesses/tests/browser/head.js
@@ -0,0 +1,912 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+// A bunch of assumptions we make about the behavior of the parent process,
+// and which we use as sanity checks. If Firefox evolves, we will need to
+// update these values.
+// Note that Test Verify can really stress the cpu durations.
+const HARDCODED_ASSUMPTIONS_PROCESS = {
+ minimalNumberOfThreads: 10,
+ 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;
+const MEMORY_REGEXP = /([0-9.,]+)(TB|GB|MB|KB|B)( \(([-+]?)([0-9.,]+)(GB|MB|KB|B)\))?/;
+//Example: "383.55MB (-12.5MB)"
+const CPU_REGEXP = /(\~0%|idle|[0-9.,]+%|[?]) \(([0-9.,]+) ?(ns|µs|ms|s|m|h|d)\)/;
+//Example: "13% (4,470ms)"
+
+// Wait for `about:processes` to be updated.
+function promiseAboutProcessesUpdated({
+ doc,
+ tbody,
+ force,
+ tabAboutProcesses,
+}) {
+ let result = new Promise(resolve => {
+ let observer = new doc.ownerGlobal.MutationObserver(() => {
+ info("Observed about:processes refresh");
+ observer.disconnect();
+ resolve();
+ });
+ observer.observe(tbody, { childList: true });
+ });
+ if (force) {
+ SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => {
+ info("Forcing about:processes refresh");
+ content.Control.update(/* force = */ true);
+ });
+ }
+ return result;
+}
+
+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);
+}
+function testCpu(string, total, slope, assumptions) {
+ info(`Testing CPU display ${string} vs total ${total}, slope ${slope}`);
+ if (string == "(measuring)") {
+ info("Still measuring");
+ return;
+ }
+ let [, extractedPercentage, extractedTotal, extractedUnit] = CPU_REGEXP.exec(
+ string
+ );
+
+ 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;
+ }
+ }
+
+ 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}]`
+ );
+}
+
+function testMemory(string, total, delta, assumptions) {
+ Assert.ok(
+ true,
+ `Testing memory display ${string} vs total ${total}, delta ${delta}`
+ );
+ let extracted = MEMORY_REGEXP.exec(string);
+ Assert.notEqual(
+ extracted,
+ null,
+ `Can we parse ${string} with ${MEMORY_REGEXP}?`
+ );
+ let [
+ ,
+ extractedTotal,
+ extractedUnit,
+ ,
+ extractedDeltaSign,
+ extractedDeltaTotal,
+ extractedDeltaUnit,
+ ] = 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}]`
+ );
+ }
+
+ if (extractedDeltaSign == null) {
+ Assert.equal(delta || 0, 0);
+ return;
+ }
+ let deltaTotalNumber = Number.parseFloat(
+ // Remove the thousands separator that breaks parseFloat.
+ extractedDeltaTotal.replace(/,/g, "")
+ );
+ 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 memoryResidentContent = children[1].textContent;
+ let cpuContent = children[2].textContent;
+ let fluentArgs = document.l10n.getAttributes(children[0].children[0]).args;
+ let process = {
+ memoryResidentContent,
+ cpuContent,
+ pidContent: fluentArgs.pid,
+ typeContent: fluentArgs.type,
+ threads: null,
+ };
+ let threadDetailsRow = row.nextSibling;
+ while (threadDetailsRow) {
+ if (threadDetailsRow.classList.contains("thread-summary")) {
+ break;
+ }
+ threadDetailsRow = threadDetailsRow.nextSibling;
+ }
+ if (!threadDetailsRow) {
+ return process;
+ }
+ process.threads = {
+ row: threadDetailsRow,
+ numberContent: document.l10n.getAttributes(
+ threadDetailsRow.children[0].children[1]
+ ).args.number,
+ };
+ return process;
+}
+
+function findTabRowByName(doc, name) {
+ for (let row of doc.getElementsByClassName("name")) {
+ if (!row.parentNode.classList.contains("window")) {
+ continue;
+ }
+ let foundName = document.l10n.getAttributes(row.children[0]).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 testAboutProcessesWithConfig({ showAllFrames, showThreads }) {
+ const isFission = Services.prefs.getBoolPref("fission.autostart");
+ Services.prefs.setBoolPref(
+ "toolkit.aboutProcesses.showAllSubframes",
+ showAllFrames
+ );
+ Services.prefs.setBoolPref("toolkit.aboutProcesses.showThreads", showThreads);
+
+ // Install a test extension to also cover processes and sub-frames related to the
+ // extension process.
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { 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);
+ 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);
+ });
+ 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 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(doc);
+ Assert.ok(tbody);
+
+ 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;
+ }
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ Services.obs.notifyObservers(
+ {
+ childID: hungChildID,
+ hangType: Ci.nsIHangReport.PLUGIN_HANG,
+ pluginName: "Fake plug-in",
+ QueryInterface: ChromeUtils.generateQI(["nsIHangReport"]),
+ },
+ "process-hang-report"
+ );
+ }
+ };
+ fakeProcessHangMonitor();
+
+ // Give about:processes a little time to appear and be populated.
+ await TestUtils.waitForCondition(
+ () => tbody.childElementCount,
+ "The table should be populated",
+ /* interval = */ 300
+ );
+ await TestUtils.waitForCondition(
+ () => !!tbody.getElementsByClassName("hung").length,
+ "The hung process should appear",
+ /* interval = */ 300
+ );
+
+ info("Looking at the contents of about:processes");
+ let processesToBeFound = [
+ // The browser process.
+ {
+ name: "browser",
+ type: ["browser"],
+ predicate: row => row.process.type == "browser",
+ },
+ // The hung process.
+ {
+ name: "hung",
+ type: ["web", "webIsolated"],
+ predicate: row =>
+ row.classList.contains("hung") && row.classList.contains("process"),
+ },
+ // Any non-hung process
+ {
+ name: "non-hung",
+ type: ["web", "webIsolated"],
+ predicate: row =>
+ !row.classList.contains("hung") &&
+ row.classList.contains("process") &&
+ row.process.type == "web",
+ },
+ ];
+ 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 {
+ memoryResidentContent,
+ cpuContent,
+ pidContent,
+ typeContent,
+ threads,
+ } = extractProcessDetails(row);
+
+ info("Sanity checks: type");
+ Assert.ok(
+ finder.type.includes(typeContent),
+ `Type ${typeContent} should be one of ${finder.type}`
+ );
+
+ 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");
+ testMemory(
+ memoryResidentContent,
+ row.process.totalRamSize,
+ row.process.deltaRamSize,
+ HARDCODED_ASSUMPTIONS_PROCESS
+ );
+
+ info("Sanity checks: CPU (Total)");
+ testCpu(
+ cpuContent,
+ 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 {
+ info("Sanity checks: number of threads");
+ let numberOfThreads = Number.parseInt(threads.numberContent);
+ Assert.ok(
+ numberOfThreads >= HARDCODED_ASSUMPTIONS_PROCESS.minimalNumberOfThreads
+ );
+ Assert.ok(
+ numberOfThreads <= HARDCODED_ASSUMPTIONS_PROCESS.maximalNumberOfThreads
+ );
+ Assert.equal(
+ numberOfThreads,
+ row.process.threads.length,
+ "The number we display should be the number of threads"
+ );
+
+ info("Testing that we can open the list of threads");
+ let twisty = threads.row.getElementsByClassName("twisty")[0];
+ twisty.click();
+
+ // Since `twisty.click()` is partially async, we need to wait
+ // until all the threads are properly displayed.
+ await promiseAboutProcessesUpdated({ doc, tbody, tabAboutProcesses });
+ let numberOfThreadsFound = 0;
+ await TestUtils.waitForCondition(
+ () => {
+ numberOfThreadsFound = 0;
+ for (
+ let threadRow = threads.row.nextSibling;
+ threadRow && threadRow.classList.contains("thread");
+ threadRow = threadRow.nextSibling
+ ) {
+ numberOfThreadsFound++;
+ }
+ return numberOfThreadsFound == numberOfThreads;
+ },
+ `We should see ${numberOfThreads} threads, found ${numberOfThreadsFound}`,
+ /* interval = */ 300
+ );
+ for (
+ let threadRow = threads.row.nextSibling;
+ threadRow && threadRow.classList.contains("thread");
+ threadRow = threadRow.nextSibling
+ ) {
+ await TestUtils.waitForCondition(
+ () =>
+ threadRow.children.length >= 3 && threadRow.children[2].textContent,
+ "The thread row should be populated"
+ );
+ let children = threadRow.children;
+ let cpuContent = children[2].textContent;
+ let tidContent = document.l10n.getAttributes(children[0].children[0])
+ .args.tid;
+
+ info("Sanity checks: tid");
+ let tid = Number.parseInt(tidContent);
+ Assert.notEqual(tid, 0, "The tid should be set");
+ Assert.equal(tid, threadRow.thread.tid, "Displayed tid is correct");
+
+ info("Sanity checks: CPU (User and Kernel)");
+ testCpu(
+ cpuContent,
+ threadRow.thread.totalCpu,
+ threadRow.thread.slopeCpu,
+ HARDCODED_ASSUMPTIONS_THREAD
+ );
+ }
+ }
+
+ // 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 = document.l10n.getAttributes(row.children[0].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"
+ );
+ }
+ }
+
+ await promiseAboutProcessesUpdated({
+ doc,
+ tbody,
+ force: true,
+ tabAboutProcesses,
+ });
+
+ 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;
+ await TestUtils.waitForCondition(
+ () =>
+ gBrowser.selectedTab.linkedBrowser.currentURI.spec ==
+ tabHung.linkedBrowser.currentURI.spec,
+ "We should have focused the hung tab"
+ );
+ gBrowser.selectedTab = tabAboutProcesses;
+
+ info("Double-clicking on the extensions process");
+ let whenTabSwitchedToAddons = 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 () => {
+ 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 tab switch");
+ await whenTabSwitchedToAddons;
+ await TestUtils.waitForCondition(
+ () => 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,
+ tbody,
+ 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}`
+ );
+ }
+
+ // 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,
+ tbody,
+ 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
+ await TestUtils.waitForCondition(
+ () => !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,
+ tbody,
+ force: true,
+ tabAboutProcesses,
+ });
+
+ for (let origin of [
+ "http://example.net", // tabCloseProcess*
+ "https://example.org", // tabCloseTogether*
+ ]) {
+ await TestUtils.waitForCondition(
+ () => !findProcessRowByOrigin(doc, origin),
+ `Process ${origin} should disappear from about:processes`
+ );
+ }
+ }
+
+ info("Additional sanity check for all processes");
+ for (let row of document.getElementsByClassName("process")) {
+ let { pidContent, typeContent } = extractProcessDetails(row);
+ Assert.equal(typeContent, row.process.type);
+ Assert.equal(Number.parseInt(pidContent), row.process.pid);
+ }
+ BrowserTestUtils.removeTab(tabAboutProcesses);
+ BrowserTestUtils.removeTab(tabHung);
+ 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);
+
+ Services.prefs.clearUserPref("toolkit.aboutProcesses.showAllSubframes");
+ Services.prefs.clearUserPref("toolkit.aboutProcesses.showThreads");
+
+ await extension.unload();
+}