summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutperformance
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/aboutperformance
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/aboutperformance')
-rw-r--r--toolkit/components/aboutperformance/content/aboutPerformance.css223
-rw-r--r--toolkit/components/aboutperformance/content/aboutPerformance.html57
-rw-r--r--toolkit/components/aboutperformance/content/aboutPerformance.js1080
-rw-r--r--toolkit/components/aboutperformance/jar.mn8
-rw-r--r--toolkit/components/aboutperformance/moz.build12
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser.ini13
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js425
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments.html28
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html13
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js11
-rw-r--r--toolkit/components/aboutperformance/tests/browser/tab_use_memory.html24
-rw-r--r--toolkit/components/aboutperformance/tests/browser/workers.html41
-rw-r--r--toolkit/components/aboutperformance/tests/browser/workers_memory.html41
-rw-r--r--toolkit/components/aboutperformance/tests/browser/workers_memory_script.js18
-rw-r--r--toolkit/components/aboutperformance/tests/browser/workers_script.js10
15 files changed, 2004 insertions, 0 deletions
diff --git a/toolkit/components/aboutperformance/content/aboutPerformance.css b/toolkit/components/aboutperformance/content/aboutPerformance.css
new file mode 100644
index 0000000000..6b65e13c78
--- /dev/null
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.css
@@ -0,0 +1,223 @@
+/* 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;
+}
+#dispatch-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 */
+#dispatch-tbody {
+ display: block;
+ margin-top: 2em;
+}
+#dispatch-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%;
+}
+td:nth-child(2) {
+ width: 8em;
+}
+td:nth-child(3) {
+ width: 12em;
+}
+td:nth-child(4) {
+ width: 5em;
+}
+#dispatch-tbody td:nth-child(4) {
+ text-align: end;
+}
+td:nth-child(5) {
+ width: 24px;
+ padding: 2px 4px;
+}
+
+/* Show action icons on selected or hovered rows */
+tr:is([selected], :hover) > td > .action-icon {
+ opacity: 1;
+}
+
+.action-icon {
+ opacity: 0;
+ display: flex;
+ align-items: center;
+}
+.action-icon::before {
+ content: "";
+ display: inline-block;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 24px;
+ height: 24px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 16px;
+ border-radius: 4px;
+}
+.action-icon:hover::before {
+ background-color: color-mix(in srgb, currentColor 15%, transparent);
+}
+.action-icon:hover:active::before {
+ background-color: color-mix(in srgb, currentColor 30%, transparent);
+}
+
+/* icons */
+
+.addon-icon::before {
+ background-image: url("chrome://global/skin/icons/shortcut.svg");
+}
+.addon-icon:dir(rtl)::before {
+ transform: scaleX(-1);
+}
+.close-icon::before {
+ background-image: url("chrome://global/skin/icons/close.svg");
+}
+
+#dispatch-thead > tr {
+ height: inherit;
+}
+
+#dispatch-thead > tr > td {
+ border: none;
+}
+#dispatch-thead > tr > td:not(:first-child) {
+ border-inline-start-width: 1px;
+ border-inline-start-style: 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: 2em;
+ max-width: 70vw;
+ overflow: hidden;
+ white-space: nowrap;
+}
+#dispatch-tbody > tr > td:first-child {
+ text-overflow: ellipsis;
+ padding-inline-start: 32px;
+ background-repeat: no-repeat;
+ background-size: 16px 16px;
+ background-position-y: center;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+#dispatch-tbody > tr > td.root {
+ background-position-x: left 36px;
+ padding-inline-start: 62px;
+}
+#dispatch-tbody > tr > td.root:dir(rtl) {
+ background-position-x: right 36px;
+}
+.twisty {
+ margin-inline: -62px 26px;
+ padding-inline: 18px;
+ 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 */
+ width: 100%;
+ inset-inline-start: 0;
+ text-align: center;
+ -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");
+}
+#dispatch-tbody > tr > td.indent {
+ padding-inline-start: 88px;
+ background-position-x: left 62px;
+}
+#dispatch-tbody > tr > td.indent:dir(rtl) {
+ background-position-x: right 62px;
+}
+#dispatch-tbody > tr > td.tracker {
+ background-image: url("chrome://global/skin/icons/trackers.svg");
+ -moz-context-properties: fill;
+ fill: rgb(224, 41, 29);
+}
+#dispatch-tbody > tr > td.worker {
+ background-image: url("chrome://devtools/skin/images/debugging-workers.svg");
+ -moz-context-properties: fill;
+ fill: #808080;
+}
+
+#dispatch-tbody > tr:hover {
+ background-color: var(--in-content-item-hover);
+ color: var(--in-content-item-hover-text);
+}
+#dispatch-tbody > tr[selected] {
+ background-color: var(--in-content-item-selected);
+ color: var(--in-content-item-selected-text);
+}
+
+.clickable {
+ background-repeat: no-repeat;
+ background-position: right 4px center;
+}
+.clickable:dir(rtl) {
+ background-position-x: left 4px;
+}
+.asc {
+ background-image: url(chrome://global/skin/icons/arrow-up-12.svg);
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+.desc {
+ background-image: url(chrome://global/skin/icons/arrow-down-12.svg);
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+#dispatch-thead > tr > td.clickable:hover {
+ background-color: var(--in-content-button-background-hover);
+ color: var(--in-content-button-text-color-hover);
+}
+#dispatch-thead > tr > td.clickable:hover:active {
+ background-color: var(--in-content-button-background-active);
+ color: var(--in-content-button-text-color-active);
+}
+
+.energy-impact {
+ --bar-width: 0;
+ background: linear-gradient(to right, var(--blue-40) calc(var(--bar-width) * 1%), transparent calc(var(--bar-width) * 1%));
+}
+.energy-impact:dir(rtl) {
+ background: linear-gradient(to left, var(--blue-40) calc(var(--bar-width) * 1%), transparent calc(var(--bar-width) * 1%));
+}
diff --git a/toolkit/components/aboutperformance/content/aboutPerformance.html b/toolkit/components/aboutperformance/content/aboutPerformance.html
new file mode 100644
index 0000000000..c0a77c26e2
--- /dev/null
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.html
@@ -0,0 +1,57 @@
+<!-- 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-performance-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/aboutPerformance.ftl" />
+ <script src="chrome://global/content/aboutPerformance.js"></script>
+ <link
+ rel="stylesheet"
+ href="chrome://global/content/aboutPerformance.css"
+ />
+ </head>
+ <body>
+ <table id="dispatch-table">
+ <thead id="dispatch-thead">
+ <tr>
+ <td
+ class="clickable"
+ id="column-name"
+ data-l10n-id="column-name"
+ ></td>
+ <td
+ class="clickable"
+ id="column-type"
+ data-l10n-id="column-type"
+ ></td>
+ <td
+ class="clickable"
+ id="column-energy-impact"
+ data-l10n-id="column-energy-impact"
+ ></td>
+ <td
+ class="clickable"
+ id="column-memory"
+ data-l10n-id="column-memory"
+ ></td>
+ <td></td>
+ <!-- actions -->
+ </tr>
+ </thead>
+ <tbody id="dispatch-tbody"></tbody>
+ </table>
+ </body>
+</html>
diff --git a/toolkit/components/aboutperformance/content/aboutPerformance.js b/toolkit/components/aboutperformance/content/aboutPerformance.js
new file mode 100644
index 0000000000..55d42ed81f
--- /dev/null
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.js
@@ -0,0 +1,1080 @@
+/* -*- 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";
+
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+
+const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
+
+// 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;
+
+// The name of the application
+const BRAND_BUNDLE = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+);
+const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
+
+function extensionCountersEnabled() {
+ return Services.prefs.getBoolPref(
+ "extensions.webextensions.enablePerformanceCounters",
+ false
+ );
+}
+
+// The ids of system add-ons, so that we can hide them when the
+// toolkit.aboutPerformance.showInternals pref is false.
+// The API to access addons is async, so we cache the list during init.
+// The list is unlikely to change while the about:performance
+// tab is open, so not updating seems fine.
+var gSystemAddonIds = new Set();
+
+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) };
+ },
+
+ getAny(ids) {
+ for (let id of ids) {
+ let result = this.get(id);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ },
+};
+
+/**
+ * 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;
+ }
+}
+
+/**
+ * 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 addons = WebExtensionPolicy.getActiveExtensions();
+ let addonHosts = new Map();
+ for (let addon of addons) {
+ addonHosts.set(addon.mozExtensionHostname, addon.id);
+ }
+
+ let counters = await ChromeUtils.requestPerformanceMetrics();
+ let tabs = {};
+ for (let counter of counters) {
+ let {
+ items,
+ host,
+ pid,
+ counterId,
+ windowId,
+ duration,
+ isWorker,
+ memoryInfo,
+ isTopLevel,
+ } = counter;
+ // If a worker has a windowId of 0 or max uint64, attach it to the
+ // browser UI (doc group with id 1).
+ if (isWorker && (windowId == 18446744073709552000 || !windowId)) {
+ windowId = 1;
+ }
+ let dispatchCount = 0;
+ for (let { count } of items) {
+ dispatchCount += count;
+ }
+
+ let memory = 0;
+ for (let field in memoryInfo) {
+ if (field == "media") {
+ for (let mediaField of ["audioSize", "videoSize", "resourcesSize"]) {
+ memory += memoryInfo.media[mediaField];
+ }
+ continue;
+ }
+ memory += memoryInfo[field];
+ }
+
+ let tab;
+ let id = windowId;
+ if (addonHosts.has(host)) {
+ id = addonHosts.get(host);
+ }
+ if (id in tabs) {
+ tab = tabs[id];
+ } else {
+ tab = {
+ windowId,
+ host,
+ dispatchCount: 0,
+ duration: 0,
+ memory: 0,
+ children: [],
+ };
+ tabs[id] = tab;
+ }
+ tab.dispatchCount += dispatchCount;
+ tab.duration += duration;
+ tab.memory += memory;
+ if (!isTopLevel || isWorker) {
+ tab.children.push({
+ host,
+ isWorker,
+ dispatchCount,
+ duration,
+ memory,
+ counterId: pid + ":" + counterId,
+ });
+ }
+ }
+
+ if (extensionCountersEnabled()) {
+ let extCounters =
+ await ExtensionParent.ParentAPIManager.retrievePerformanceCounters();
+ for (let [id, apiMap] of extCounters) {
+ let dispatchCount = 0,
+ duration = 0;
+ for (let [, counter] of apiMap) {
+ dispatchCount += counter.calls;
+ duration += counter.duration;
+ }
+
+ let tab;
+ if (id in tabs) {
+ tab = tabs[id];
+ } else {
+ tab = {
+ windowId: 0,
+ host: id,
+ dispatchCount: 0,
+ duration: 0,
+ memory: 0,
+ children: [],
+ };
+ tabs[id] = tab;
+ }
+ tab.dispatchCount += dispatchCount;
+ tab.duration += duration;
+ }
+ }
+
+ return { tabs, date: Cu.now() };
+ },
+
+ /**
+ * Update the internal state.
+ *
+ * @return {Promise}
+ */
+ async update() {
+ // 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 (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();
+ }
+ },
+
+ // We can only know asynchronously if an origin is matched by the tracking
+ // protection list, so we cache the result for faster future lookups.
+ _trackingState: new Map(),
+ isTracker(host) {
+ if (!this._trackingState.has(host)) {
+ // Temporarily set to false to avoid doing several lookups if a site has
+ // several subframes on the same domain.
+ this._trackingState.set(host, false);
+ if (host.startsWith("about:") || host.startsWith("moz-nullprincipal")) {
+ return false;
+ }
+
+ let uri = Services.io.newURI("http://" + host);
+ let classifier = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
+ Ci.nsIURIClassifier
+ );
+ let feature = classifier.getFeatureByName("tracking-protection");
+ if (!feature) {
+ return false;
+ }
+
+ classifier.asyncClassifyLocalWithFeatures(
+ uri,
+ [feature],
+ Ci.nsIUrlClassifierFeature.blocklist,
+ list => {
+ if (list.length) {
+ this._trackingState.set(host, true);
+ }
+ }
+ );
+ }
+ return this._trackingState.get(host);
+ },
+
+ getCounters() {
+ tabFinder.update();
+ // We rebuild the maps during each iteration to make sure that
+ // we do not maintain references to groups that has been removed
+ // (e.g. pages that have been closed).
+
+ let previous = this._buffer[Math.max(this._buffer.length - 2, 0)].tabs;
+ let current = this._latest.tabs;
+ let counters = [];
+ for (let id of Object.keys(current)) {
+ let tab = current[id];
+ let oldest;
+ for (let index = 0; index <= this._buffer.length - 2; ++index) {
+ if (id in this._buffer[index].tabs) {
+ oldest = this._buffer[index].tabs[id];
+ break;
+ }
+ }
+ let prev = previous[id];
+ let host = tab.host;
+
+ let type = "other";
+ let name = `${host} (${id})`;
+ let image = "chrome://global/skin/icons/defaultFavicon.svg";
+ let found = tabFinder.get(parseInt(id));
+ if (found) {
+ if (found.tabbrowser) {
+ name = found.tab.getAttribute("label");
+ image = found.tab.getAttribute("image");
+ type = "tab";
+ } else {
+ name = {
+ id: "preloaded-tab",
+ title: found.tab.linkedBrowser.contentTitle,
+ };
+ }
+ } else if (id == 1) {
+ name = BRAND_NAME;
+ image = "chrome://branding/content/icon32.png";
+ type = "browser";
+ } else if (/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(host)) {
+ let addon = WebExtensionPolicy.getByHostname(host);
+ if (!addon) {
+ continue;
+ }
+ name = `${addon.name} (${addon.id})`;
+ image =
+ addon.extension.getPreferredIcon?.(32) ||
+ "chrome://mozapps/skin/extensions/extension.svg";
+ type = gSystemAddonIds.has(addon.id) ? "system-addon" : "addon";
+ } else if (id == 0 && !tab.isWorker) {
+ name = { id: "ghost-windows" };
+ }
+
+ if (
+ type != "tab" &&
+ type != "addon" &&
+ !Services.prefs.getBoolPref(
+ "toolkit.aboutPerformance.showInternals",
+ false
+ )
+ ) {
+ continue;
+ }
+
+ // Create a map of all the child items from the previous time we read the
+ // counters, indexed by counterId so that we can quickly find the previous
+ // value for any subitem.
+ let prevChildren = new Map();
+ if (prev) {
+ for (let child of prev.children) {
+ prevChildren.set(child.counterId, child);
+ }
+ }
+ // For each subitem, create a new object including the deltas since the previous time.
+ let children = tab.children.map(child => {
+ let { host, dispatchCount, duration, memory, isWorker, counterId } =
+ child;
+ let dispatchesSincePrevious = dispatchCount;
+ let durationSincePrevious = duration;
+ if (prevChildren.has(counterId)) {
+ let prevCounter = prevChildren.get(counterId);
+ dispatchesSincePrevious -= prevCounter.dispatchCount;
+ durationSincePrevious -= prevCounter.duration;
+ prevChildren.delete(counterId);
+ }
+
+ return {
+ host,
+ dispatchCount,
+ duration,
+ isWorker,
+ memory,
+ dispatchesSincePrevious,
+ durationSincePrevious,
+ };
+ });
+
+ // Any item that remains in prevChildren is a subitem that no longer
+ // exists in the current sample; remember the values of its counters
+ // so that the values don't go down for the parent item.
+ tab.dispatchesFromFormerChildren =
+ (prev && prev.dispatchesFromFormerChildren) || 0;
+ tab.durationFromFormerChildren =
+ (prev && prev.durationFromFormerChildren) || 0;
+ for (let [, counter] of prevChildren) {
+ tab.dispatchesFromFormerChildren += counter.dispatchCount;
+ tab.durationFromFormerChildren += counter.duration;
+ }
+
+ // Create the object representing the counters of the parent item including
+ // the deltas from the previous times.
+ let dispatches = tab.dispatchCount + tab.dispatchesFromFormerChildren;
+ let duration = tab.duration + tab.durationFromFormerChildren;
+ let durationSincePrevious = NaN;
+ let dispatchesSincePrevious = NaN;
+ let dispatchesSinceStartOfBuffer = NaN;
+ let durationSinceStartOfBuffer = NaN;
+ if (prev) {
+ durationSincePrevious =
+ duration - prev.duration - (prev.durationFromFormerChildren || 0);
+ dispatchesSincePrevious =
+ dispatches -
+ prev.dispatchCount -
+ (prev.dispatchesFromFormerChildren || 0);
+ }
+ if (oldest) {
+ dispatchesSinceStartOfBuffer =
+ dispatches -
+ oldest.dispatchCount -
+ (oldest.dispatchesFromFormerChildren || 0);
+ durationSinceStartOfBuffer =
+ duration - oldest.duration - (oldest.durationFromFormerChildren || 0);
+ }
+ counters.push({
+ id,
+ name,
+ image,
+ type,
+ memory: tab.memory,
+ totalDispatches: dispatches,
+ totalDuration: duration,
+ durationSincePrevious,
+ dispatchesSincePrevious,
+ durationSinceStartOfBuffer,
+ dispatchesSinceStartOfBuffer,
+ children,
+ });
+ }
+ return counters;
+ },
+
+ getMaxEnergyImpact(counters) {
+ return Math.max(
+ ...counters.map(c => {
+ return Control._computeEnergyImpact(
+ c.dispatchesSincePrevious,
+ c.durationSincePrevious
+ );
+ })
+ );
+ },
+};
+
+var View = {
+ _fragment: document.createDocumentFragment(),
+ async commit() {
+ let tbody = document.getElementById("dispatch-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);
+
+ // Pause the DOMLocalization mutation observer, or the already translated
+ // content will be translated a second time at the next tick.
+ document.l10n.pauseObserving();
+ while (tbody.firstChild) {
+ tbody.firstChild.remove();
+ }
+ tbody.appendChild(this._fragment);
+ document.l10n.resumeObserving();
+
+ this._fragment = document.createDocumentFragment();
+ },
+ insertAfterRow(row) {
+ row.parentNode.insertBefore(this._fragment, row.nextSibling);
+ this._fragment = document.createDocumentFragment();
+ },
+ displayEnergyImpact(elt, energyImpact, maxEnergyImpact) {
+ if (!energyImpact) {
+ elt.textContent = "–";
+ elt.style.setProperty("--bar-width", 0);
+ } else {
+ let impact;
+ let barWidth;
+ const mediumEnergyImpact = 25;
+ if (energyImpact < 1) {
+ impact = "low";
+ // Width 0-10%.
+ barWidth = 10 * energyImpact;
+ } else if (energyImpact < mediumEnergyImpact) {
+ impact = "medium";
+ // Width 10-50%.
+ barWidth = (10 + 2 * energyImpact) * (5 / 6);
+ } else {
+ impact = "high";
+ // Width 50-100%.
+ let energyImpactFromZero = energyImpact - mediumEnergyImpact;
+ if (maxEnergyImpact > 100) {
+ barWidth =
+ 50 +
+ (energyImpactFromZero / (maxEnergyImpact - mediumEnergyImpact)) *
+ 50;
+ } else {
+ barWidth = 50 + energyImpactFromZero * (2 / 3);
+ }
+ }
+ document.l10n.setAttributes(elt, "energy-impact-" + impact, {
+ value: energyImpact,
+ });
+ if (maxEnergyImpact != -1) {
+ elt.style.setProperty("--bar-width", barWidth);
+ }
+ }
+ },
+ appendRow(
+ name,
+ energyImpact,
+ memory,
+ tooltip,
+ type,
+ maxEnergyImpact = -1,
+ image = ""
+ ) {
+ let row = document.createElement("tr");
+
+ let elt = document.createElement("td");
+ if (typeof name == "string") {
+ elt.textContent = name;
+ } else if (name.title) {
+ document.l10n.setAttributes(elt, name.id, { title: name.title });
+ } else {
+ document.l10n.setAttributes(elt, name.id);
+ }
+ if (image) {
+ elt.style.backgroundImage = `url('${image}')`;
+ }
+
+ if (["subframe", "tracker", "worker"].includes(type)) {
+ elt.classList.add("indent");
+ } else {
+ elt.classList.add("root");
+ }
+ if (["tracker", "worker"].includes(type)) {
+ elt.classList.add(type);
+ }
+ row.appendChild(elt);
+
+ elt = document.createElement("td");
+ let typeLabelType = type == "system-addon" ? "addon" : type;
+ document.l10n.setAttributes(elt, "type-" + typeLabelType);
+ row.appendChild(elt);
+
+ elt = document.createElement("td");
+ elt.classList.add("energy-impact");
+ this.displayEnergyImpact(elt, energyImpact, maxEnergyImpact);
+ row.appendChild(elt);
+
+ elt = document.createElement("td");
+ if (!memory) {
+ elt.textContent = "–";
+ } else {
+ let unit = "KB";
+ memory = Math.ceil(memory / 1024);
+ if (memory > 1024) {
+ memory = Math.ceil((memory / 1024) * 10) / 10;
+ unit = "MB";
+ if (memory > 1024) {
+ memory = Math.ceil((memory / 1024) * 100) / 100;
+ unit = "GB";
+ }
+ }
+ document.l10n.setAttributes(elt, "size-" + unit, { value: memory });
+ }
+ row.appendChild(elt);
+
+ if (tooltip) {
+ for (let key of ["dispatchesSincePrevious", "durationSincePrevious"]) {
+ if (Number.isNaN(tooltip[key]) || tooltip[key] < 0) {
+ tooltip[key] = "–";
+ }
+ }
+ document.l10n.setAttributes(row, "item", tooltip);
+ }
+
+ elt = document.createElement("td");
+ if (type == "tab") {
+ let img = document.createElement("span");
+ img.className = "action-icon close-icon";
+ document.l10n.setAttributes(img, "close-tab");
+ elt.appendChild(img);
+ } else if (type == "addon") {
+ let img = document.createElement("span");
+ img.className = "action-icon addon-icon";
+ document.l10n.setAttributes(img, "show-addon");
+ elt.appendChild(img);
+ }
+ row.appendChild(elt);
+
+ this._fragment.appendChild(row);
+ return row;
+ },
+};
+
+var Control = {
+ _openItems: new Set(),
+ _sortOrder: "",
+ _removeSubtree(row) {
+ while (
+ row.nextSibling &&
+ row.nextSibling.firstChild.classList.contains("indent")
+ ) {
+ row.nextSibling.remove();
+ }
+ },
+ init() {
+ let tbody = document.getElementById("dispatch-tbody");
+ tbody.addEventListener("click", event => {
+ this._updateLastMouseEvent();
+ this._handleActivate(event.target);
+ });
+ tbody.addEventListener("keydown", event => {
+ if (event.key === "Enter" || event.key === " ") {
+ this._handleActivate(event.target);
+ }
+ });
+
+ // Select the tab of double clicked items.
+ tbody.addEventListener("dblclick", event => {
+ let id = parseInt(event.target.parentNode.windowId);
+ if (isNaN(id)) {
+ return;
+ }
+ let found = tabFinder.get(id);
+ if (!found || !found.tabbrowser) {
+ return;
+ }
+ let { tabbrowser, tab } = found;
+ tabbrowser.selectedTab = tab;
+ tabbrowser.ownerGlobal.focus();
+ });
+
+ tbody.addEventListener("mousemove", () => {
+ this._updateLastMouseEvent();
+ });
+
+ window.addEventListener("visibilitychange", event => {
+ if (!document.hidden) {
+ this._updateDisplay(true);
+ }
+ });
+
+ document
+ .getElementById("dispatch-thead")
+ .addEventListener("click", async event => {
+ if (!event.target.classList.contains("clickable")) {
+ return;
+ }
+
+ if (this._sortOrder) {
+ let [column, direction] = this._sortOrder.split("_");
+ const td = document.getElementById(`column-${column}`);
+ td.classList.remove(direction);
+ }
+
+ const columnId = event.target.id;
+ if (columnId == "column-type") {
+ this._sortOrder =
+ this._sortOrder == "type_asc" ? "type_desc" : "type_asc";
+ } else if (columnId == "column-energy-impact") {
+ this._sortOrder =
+ this._sortOrder == "energy-impact_desc"
+ ? "energy-impact_asc"
+ : "energy-impact_desc";
+ } else if (columnId == "column-memory") {
+ this._sortOrder =
+ this._sortOrder == "memory_desc" ? "memory_asc" : "memory_desc";
+ } else if (columnId == "column-name") {
+ this._sortOrder =
+ this._sortOrder == "name_asc" ? "name_desc" : "name_asc";
+ }
+
+ let direction = this._sortOrder.split("_")[1];
+ event.target.classList.add(direction);
+
+ await this._updateDisplay(true);
+ });
+ },
+ _lastMouseEvent: 0,
+ _updateLastMouseEvent() {
+ this._lastMouseEvent = Date.now();
+ },
+ _handleActivate(target) {
+ // Handle showing or hiding subitems of a row.
+ if (target.classList.contains("twisty")) {
+ let row = target.parentNode.parentNode;
+ let id = row.windowId;
+ if (target.classList.toggle("open")) {
+ target.setAttribute("aria-expanded", "true");
+ this._openItems.add(id);
+ this._showChildren(row);
+ View.insertAfterRow(row);
+ } else {
+ target.setAttribute("aria-expanded", "false");
+ this._openItems.delete(id);
+ this._removeSubtree(row);
+ }
+ return;
+ }
+ // Handle closing a tab.
+ if (target.classList.contains("close-icon")) {
+ let row = target.parentNode.parentNode;
+ let id = parseInt(row.windowId);
+ let found = tabFinder.get(id);
+ if (!found || !found.tabbrowser) {
+ return;
+ }
+ let { tabbrowser, tab } = found;
+ tabbrowser.removeTab(tab);
+ this._removeSubtree(row);
+ row.remove();
+ return;
+ }
+ // Handle opening addon details.
+ if (target.classList.contains("addon-icon")) {
+ let row = target.parentNode.parentNode;
+ let id = row.windowId;
+ let parentWin =
+ window.docShell.browsingContext.embedderElement.ownerGlobal;
+ parentWin.BrowserOpenAddonsMgr(
+ "addons://detail/" + encodeURIComponent(id)
+ );
+ 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;
+ }
+ },
+ async update() {
+ await State.update();
+
+ if (document.hidden) {
+ return;
+ }
+
+ await wait(0);
+
+ await this._updateDisplay();
+ },
+ // The force parameter can force a full update even when the mouse has been
+ // moved recently or when an element like a Twisty button is focused.
+ async _updateDisplay(force = false) {
+ let counters = State.getCounters();
+ let maxEnergyImpact = State.getMaxEnergyImpact(counters);
+ let focusedEl;
+
+ // If the mouse has been moved recently, update the data displayed
+ // without moving any item to avoid the risk of users clicking an action
+ // button for the wrong item.
+ // Memory use is unlikely to change dramatically within a few seconds, so
+ // it's probably fine to not update the Memory column in this case.
+ if (
+ !force &&
+ Date.now() - this._lastMouseEvent < TIME_BEFORE_SORTING_AGAIN
+ ) {
+ let energyImpactPerId = new Map();
+ for (let {
+ id,
+ dispatchesSincePrevious,
+ durationSincePrevious,
+ } of counters) {
+ let energyImpact = this._computeEnergyImpact(
+ dispatchesSincePrevious,
+ durationSincePrevious
+ );
+ energyImpactPerId.set(id, energyImpact);
+ }
+
+ let row = document.getElementById("dispatch-tbody").firstChild;
+ while (row) {
+ if (row.windowId && energyImpactPerId.has(row.windowId)) {
+ // We update the value in the Energy Impact column, but don't
+ // update the children, as if the child count changes there's a
+ // risk of making other rows move up or down.
+ const kEnergyImpactColumn = 2;
+ let elt = row.childNodes[kEnergyImpactColumn];
+ View.displayEnergyImpact(
+ elt,
+ energyImpactPerId.get(row.windowId),
+ maxEnergyImpact
+ );
+ }
+ row = row.nextSibling;
+ }
+ return;
+ }
+
+ let selectedId = -1;
+ // Reset the selectedRow field and the _openItems set each time we redraw
+ // to avoid keeping forever references to closed window ids.
+ if (this.selectedRow) {
+ selectedId = this.selectedRow.windowId;
+ this.selectedRow = null;
+ }
+ let openItems = this._openItems;
+ this._openItems = new Set();
+
+ // Preserving a focus target if it is located within the page content
+ let focusedId;
+ const isFocusable = document.activeElement.classList.contains("twisty");
+ if (document.hasFocus() && isFocusable) {
+ focusedId = document.activeElement.parentNode.parentNode.windowId;
+ }
+
+ counters = this._sortCounters(counters);
+ for (let {
+ id,
+ name,
+ image,
+ type,
+ totalDispatches,
+ dispatchesSincePrevious,
+ memory,
+ totalDuration,
+ durationSincePrevious,
+ children,
+ } of counters) {
+ let row = View.appendRow(
+ name,
+ this._computeEnergyImpact(
+ dispatchesSincePrevious,
+ durationSincePrevious
+ ),
+ memory,
+ {
+ totalDispatches,
+ totalDuration: Math.ceil(totalDuration / 1000),
+ dispatchesSincePrevious,
+ durationSincePrevious: Math.ceil(durationSincePrevious / 1000),
+ },
+ type,
+ maxEnergyImpact,
+ image
+ );
+ row.windowId = id;
+ if (id == selectedId) {
+ row.setAttribute("selected", "true");
+ this.selectedRow = row;
+ }
+
+ if (!children.length) {
+ continue;
+ }
+
+ // Show the twisty image as a disclosure button.
+ let elt = row.firstChild;
+ let img = document.createElement("span");
+ img.className = "twisty";
+ img.setAttribute("role", "button");
+ img.setAttribute("tabindex", "0");
+ img.setAttribute("aria-label", name);
+
+ let open = openItems.has(id);
+ if (open) {
+ img.classList.add("open");
+ img.setAttribute("aria-expanded", "true");
+
+ this._openItems.add(id);
+ } else {
+ img.setAttribute("aria-expanded", "false");
+ }
+ if (id === focusedId) {
+ focusedEl = img;
+ }
+
+ // If there's an l10n id on our <td> node, any image we add will be
+ // removed during localization, so move the l10n id to a <span>
+ let l10nAttrs = document.l10n.getAttributes(elt);
+ if (l10nAttrs.id) {
+ let span = document.createElement("span");
+ document.l10n.setAttributes(span, l10nAttrs.id, l10nAttrs.args);
+ elt.removeAttribute("data-l10n-id");
+ elt.removeAttribute("data-l10n-args");
+ elt.insertBefore(span, elt.firstChild);
+ }
+
+ elt.insertBefore(img, elt.firstChild);
+
+ row._children = children;
+ if (open) {
+ this._showChildren(row);
+ }
+ }
+
+ await View.commit();
+
+ if (focusedEl) {
+ focusedEl.focus();
+ }
+ },
+ _showChildren(row) {
+ let children = row._children;
+ children.sort(
+ (a, b) => b.dispatchesSincePrevious - a.dispatchesSincePrevious
+ );
+ for (let row of children) {
+ let host = row.host.replace(/^blob:https?:\/\//, "");
+ let type = "subframe";
+ if (State.isTracker(host)) {
+ type = "tracker";
+ }
+ if (row.isWorker) {
+ type = "worker";
+ }
+ View.appendRow(
+ row.host,
+ this._computeEnergyImpact(
+ row.dispatchesSincePrevious,
+ row.durationSincePrevious
+ ),
+ row.memory,
+ {
+ totalDispatches: row.dispatchCount,
+ totalDuration: Math.ceil(row.duration / 1000),
+ dispatchesSincePrevious: row.dispatchesSincePrevious,
+ durationSincePrevious: Math.ceil(row.durationSincePrevious / 1000),
+ },
+ type
+ );
+ }
+ },
+ _computeEnergyImpact(dispatches, duration) {
+ // 'Dispatches' doesn't make sense to users, and it's difficult to present
+ // two numbers in a meaningful way, so we need to somehow aggregate the
+ // dispatches and duration values we have.
+ // The current formula to aggregate the numbers assumes that the cost of
+ // a dispatch is equivalent to 1ms of CPU time.
+ // Dividing the result by the sampling interval and by 10 gives a number that
+ // looks like a familiar percentage to users, as fullying using one core will
+ // result in a number close to 100.
+ let energyImpact =
+ Math.max(duration || 0, dispatches * 1000) / UPDATE_INTERVAL_MS / 10;
+ // Keep only 2 digits after the decimal point.
+ return Math.ceil(energyImpact * 100) / 100;
+ },
+ _getTypeWeight(type) {
+ let weights = {
+ tab: 3,
+ addon: 2,
+ "system-addon": 1,
+ };
+ return weights[type] || 0;
+ },
+ _sortCounters(counters) {
+ return counters.sort((a, b) => {
+ // Force 'Recently Closed Tabs' to be always at the bottom, because it'll
+ // never be actionable.
+ if (a.name.id && a.name.id == "ghost-windows") {
+ return 1;
+ }
+
+ if (this._sortOrder) {
+ let res;
+ let [column, order] = this._sortOrder.split("_");
+ switch (column) {
+ case "memory":
+ res = a.memory - b.memory;
+ break;
+ case "type":
+ if (a.type != b.type) {
+ res = this._getTypeWeight(b.type) - this._getTypeWeight(a.type);
+ } else {
+ res = String.prototype.localeCompare.call(a.name, b.name);
+ }
+ break;
+ case "name":
+ res = String.prototype.localeCompare.call(a.name, b.name);
+ break;
+ case "energy-impact":
+ res =
+ this._computeEnergyImpact(
+ a.dispatchesSincePrevious,
+ a.durationSincePrevious
+ ) -
+ this._computeEnergyImpact(
+ b.dispatchesSincePrevious,
+ b.durationSincePrevious
+ );
+ break;
+ default:
+ res = String.prototype.localeCompare.call(a.name, b.name);
+ }
+ if (order == "desc") {
+ res = -1 * res;
+ }
+ return res;
+ }
+
+ // Note: _computeEnergyImpact uses UPDATE_INTERVAL_MS which doesn't match
+ // the time between the most recent sample and the start of the buffer,
+ // BUFFER_DURATION_MS would be better, but the values is never displayed
+ // so this is OK.
+ let aEI = this._computeEnergyImpact(
+ a.dispatchesSinceStartOfBuffer,
+ a.durationSinceStartOfBuffer
+ );
+ let bEI = this._computeEnergyImpact(
+ b.dispatchesSinceStartOfBuffer,
+ b.durationSinceStartOfBuffer
+ );
+ if (aEI != bEI) {
+ return bEI - aEI;
+ }
+
+ // a.name is sometimes an object, so we can't use a.name.localeCompare.
+ return String.prototype.localeCompare.call(a.name, b.name);
+ });
+ },
+};
+
+window.onload = async function () {
+ Control.init();
+
+ let addons = await AddonManager.getAddonsByTypes(["extension"]);
+ for (let addon of addons) {
+ if (addon.isSystem) {
+ gSystemAddonIds.add(addon.id);
+ }
+ }
+
+ await Control.update();
+ window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS);
+};
diff --git a/toolkit/components/aboutperformance/jar.mn b/toolkit/components/aboutperformance/jar.mn
new file mode 100644
index 0000000000..6812a70593
--- /dev/null
+++ b/toolkit/components/aboutperformance/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/aboutPerformance.html (content/aboutPerformance.html)
+ content/global/aboutPerformance.js (content/aboutPerformance.js)
+ content/global/aboutPerformance.css (content/aboutPerformance.css)
diff --git a/toolkit/components/aboutperformance/moz.build b/toolkit/components/aboutperformance/moz.build
new file mode 100644
index 0000000000..f397ead22d
--- /dev/null
+++ b/toolkit/components/aboutperformance/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/aboutperformance/tests/browser/browser.ini b/toolkit/components/aboutperformance/tests/browser/browser.ini
new file mode 100644
index 0000000000..3c687bf94b
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files =
+ browser_compartments.html
+ browser_compartments_frame.html
+ browser_compartments_script.js
+ workers.html
+ workers_memory.html
+ workers_script.js
+ workers_memory_script.js
+ tab_use_memory.html
+
+[browser_aboutperformance.js]
+
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js b/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
new file mode 100644
index 0000000000..605f10823e
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
@@ -0,0 +1,425 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function setup_tab(url) {
+ info(`Setting up ${url}`);
+ let tabContent = BrowserTestUtils.addTab(gBrowser, url);
+
+ await BrowserTestUtils.browserLoaded(tabContent.linkedBrowser);
+
+ // For some of these tests we have to wait for the test to consume some
+ // computation or memory.
+ await SpecialPowers.spawn(tabContent.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.waitForTestReady();
+ });
+
+ return tabContent;
+}
+
+async function setup_about_performance() {
+ info("Setting up about:performance");
+ let tabAboutPerformance = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:performance"
+ ));
+
+ await BrowserTestUtils.browserLoaded(tabAboutPerformance.linkedBrowser);
+
+ let doc = tabAboutPerformance.linkedBrowser.contentDocument;
+ let tbody = doc.getElementById("dispatch-tbody");
+
+ // Wait until the table has first been populated.
+ await TestUtils.waitForCondition(() => tbody.childElementCount);
+
+ // And wait for another update using a mutation observer, to give our newly created test tab some time
+ // to burn some CPU.
+ await new Promise(resolve => {
+ let observer = new doc.ownerGlobal.MutationObserver(() => {
+ observer.disconnect();
+ resolve();
+ });
+ observer.observe(tbody, { childList: true });
+ });
+
+ return {
+ tab: tabAboutPerformance,
+ doc,
+ tbody,
+ };
+}
+
+function find_row(tbody, title, tab) {
+ // Find the row for our test tab.
+ let row = tbody.firstChild;
+ while (row && row.firstChild.textContent != title) {
+ row = row.nextSibling;
+ }
+
+ Assert.ok(row, "found a table row for our test tab");
+ Assert.equal(
+ row.windowId,
+ tab.linkedBrowser.outerWindowID,
+ "the correct window id is set"
+ );
+
+ return row;
+}
+
+function checkEnergyMedHigh(row) {
+ let l10nId = row.children[2].getAttribute("data-l10n-id");
+ Assert.ok(
+ ["energy-impact-medium", "energy-impact-high"].includes(l10nId),
+ "our test tab is medium or high energy impact"
+ );
+}
+
+async function checkMemoryAtLeast(bytes, row) {
+ let memCell = row.children[3];
+ ok(memCell, "Found the cell containing the amount of memory");
+
+ if (!memCell.innerText) {
+ info("There's no text yet, wait for an update");
+ await new Promise(resolve => {
+ let observer = new row.ownerDocument.ownerGlobal.MutationObserver(() => {
+ observer.disconnect();
+ resolve();
+ });
+ observer.observe(memCell, { childList: true });
+ });
+ }
+
+ let text = memCell.innerText;
+ ok(text, "Found the text from the memory cell");
+ // We only bother to work in Megabytes, there's currently no reason to
+ // make this more complex.
+ info(`Text is ${text}.`);
+ let mbStr = text.match(/^(\d+(\.\d+)?) MB$/);
+ ok(mbStr && mbStr[1], "Matched a memory size in Megabytes");
+ if (!mbStr) {
+ return;
+ }
+
+ ok(bytes < Number(mbStr[1]) * 1024 * 1024, "Memory usage is high enough");
+}
+
+// Test that we can select the row for a tab and close it using the close
+// button.
+add_task(async function test_tab_operations() {
+ let tabContent = await setup_tab(
+ "http://example.com/browser/toolkit/components/aboutperformance/tests/browser/browser_compartments.html?test=" +
+ Math.random()
+ );
+
+ let aboutPerformance = await setup_about_performance();
+
+ // Find the row corresponding to our tab.
+ let row = find_row(
+ aboutPerformance.tbody,
+ "Main frame for test browser_aboutperformance.js",
+ tabContent
+ );
+
+ // Verify selecting a row works.
+ EventUtils.synthesizeMouseAtCenter(
+ row,
+ {},
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+
+ Assert.equal(
+ row.getAttribute("selected"),
+ "true",
+ "doing a single click selects the row"
+ );
+
+ // Verify selecting a tab with a double click.
+ Assert.equal(
+ gBrowser.selectedTab,
+ aboutPerformance.tab,
+ "the about:performance tab is selected"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ row,
+ { clickCount: 2 },
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+ Assert.equal(
+ gBrowser.selectedTab,
+ tabContent,
+ "after a double click the test tab is selected"
+ );
+
+ info("Verify we can toggle subitems using a twisty image button");
+
+ // Find the row with subtitems for twisty toggle test group.
+ let twistyBtn = aboutPerformance.doc.querySelector("tr > td.root > .twisty");
+
+ // When "toolkit.aboutPerformance.showInternals=false", there is no Twisty.
+ if (
+ Services.prefs.getBoolPref("toolkit.aboutPerformance.showInternals", false)
+ ) {
+ Assert.ok(twistyBtn, "A twisty button was found");
+ let groupRow = twistyBtn.parentNode.parentNode;
+
+ // Verify twisty button is properly set up.
+ Assert.ok(
+ twistyBtn.hasAttribute("aria-label"),
+ "the Twisty image button has an aria-label"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-label"),
+ groupRow.firstChild.textContent,
+ "the Twisty image button's aria-label is the same as 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"
+ );
+
+ // Verify we can toggle/show subitems by clicking the twisty button.
+ EventUtils.synthesizeMouseAtCenter(
+ twistyBtn,
+ {},
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+ Assert.ok(
+ groupRow.nextSibling.children[0].classList.contains("indent"),
+ "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"
+ );
+
+ // Verify the twisty button can be focused with a keyboard.
+ twistyBtn.focus();
+ Assert.equal(
+ twistyBtn,
+ aboutPerformance.doc.activeElement,
+ "the Twisty image button can be focused"
+ );
+ // Verify we can toggle subitems with a keyboard.
+ // Twisty is expanded
+ EventUtils.synthesizeKey(
+ "KEY_Enter",
+ {},
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+ Assert.ok(
+ !groupRow.nextSibling ||
+ !groupRow.nextSibling.children[0].classList.contains("indent"),
+ "pressing Enter on expanded Twisty removes subitems after the row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "false",
+ "the Twisty image button is collapsed after a keypress"
+ );
+ Assert.equal(
+ twistyBtn,
+ aboutPerformance.doc.activeElement,
+ "the Twisty retains focus after the page is updated"
+ );
+ // Twisty is collapsed
+ EventUtils.synthesizeKey(
+ " ",
+ {},
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+ Assert.ok(
+ groupRow.nextSibling.children[0].classList.contains("indent"),
+ "pressing Space on collapsed Twisty adds subitems after the row"
+ );
+ Assert.equal(
+ twistyBtn.getAttribute("aria-expanded"),
+ "true",
+ "the Twisty image button is expanded after a keypress"
+ );
+
+ info("Verify the focus stays on a twisty image button");
+
+ Assert.equal(
+ twistyBtn,
+ aboutPerformance.doc.activeElement,
+ "the Twisty retains focus after the page is updated"
+ );
+ Assert.notEqual(
+ aboutPerformance.doc.activeElement.tagName,
+ "body",
+ "the body does not pull the focus after the page is updated"
+ );
+ EventUtils.synthesizeKey(
+ "KEY_Tab",
+ { shiftKey: true },
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+ Assert.notEqual(
+ twistyBtn,
+ aboutPerformance.doc.activeElement,
+ "the Twisty does not pull the focus after the page is updated"
+ );
+ } else {
+ Assert.ok(
+ !twistyBtn,
+ "No twisty button should exist when the showInternals pref is false"
+ );
+ }
+
+ info("Verify we can close a tab using the X button");
+ // Switch back to about:performance...
+ await BrowserTestUtils.switchTab(gBrowser, aboutPerformance.tab);
+ // ... and click the X button at the end of the row.
+ let tabClosing = BrowserTestUtils.waitForTabClosing(tabContent);
+ EventUtils.synthesizeMouseAtCenter(
+ row.children[4],
+ {},
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+ await tabClosing;
+
+ BrowserTestUtils.removeTab(aboutPerformance.tab);
+});
+
+add_task(async function test_tab_energy() {
+ let tabContent = await setup_tab(
+ "http://example.com/browser/toolkit/components/aboutperformance/tests/browser/browser_compartments.html?test=" +
+ Math.random()
+ );
+
+ let aboutPerformance = await setup_about_performance();
+
+ // Find the row corresponding to our tab.
+ let row = find_row(
+ aboutPerformance.tbody,
+ "Main frame for test browser_aboutperformance.js",
+ tabContent
+ );
+
+ // Ensure it is reported as a medium or high energy impact.
+ checkEnergyMedHigh(row);
+
+ await BrowserTestUtils.removeTab(tabContent);
+ await BrowserTestUtils.removeTab(aboutPerformance.tab);
+});
+
+add_task(async function test_tab_memory() {
+ let tabContent = await setup_tab(
+ "http://example.com/browser/toolkit/components/aboutperformance/tests/browser/tab_use_memory.html"
+ );
+
+ let aboutPerformance = await setup_about_performance();
+
+ // Find the row corresponding to our tab.
+ let row = find_row(
+ aboutPerformance.tbody,
+ "Main frame for test browser_aboutperformance.js",
+ tabContent
+ );
+
+ // The page is using at least 32 MB, due to the big array that it
+ // contains.
+ await checkMemoryAtLeast(32 * 1024 * 1024, row);
+
+ await BrowserTestUtils.removeTab(tabContent);
+ await BrowserTestUtils.removeTab(aboutPerformance.tab);
+});
+
+add_task(async function test_worker_energy() {
+ let tabContent = await setup_tab(
+ "http://example.com/browser/toolkit/components/aboutperformance/tests/browser/workers.html"
+ );
+
+ let aboutPerformance = await setup_about_performance();
+
+ // Find the row corresponding to our tab.
+ let row = find_row(
+ aboutPerformance.tbody,
+ "Main frame for test browser_aboutperformance.js",
+ tabContent
+ );
+
+ // Find the worker under this row.
+ let button = row.firstChild.firstChild;
+ Assert.ok(button && button.classList, "Has a span to create the button");
+ Assert.ok(button.classList.contains("twisty"), "Button is expandable.");
+ Assert.ok(!button.classList.contains("open"), "Not already open");
+
+ // Click the expand button.
+ EventUtils.synthesizeMouseAtCenter(
+ button,
+ {},
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+
+ Assert.ok(button.classList.contains("open"), "It's now open");
+
+ // Move to the next row which is the worker we want to imspect.
+ row = row.nextSibling;
+
+ // Check that it is a worker.
+ Assert.equal(row.children[1].getAttribute("data-l10n-id"), "type-worker");
+
+ // Ensure it is reported as a medium or high energy impact.
+ checkEnergyMedHigh(row);
+
+ await BrowserTestUtils.removeTab(tabContent);
+ await BrowserTestUtils.removeTab(aboutPerformance.tab);
+});
+
+add_task(async function test_worker_memory() {
+ let tabContent = await setup_tab(
+ "http://example.com/browser/toolkit/components/aboutperformance/tests/browser/workers_memory.html"
+ );
+
+ let aboutPerformance = await setup_about_performance();
+
+ // Find the row corresponding to our tab.
+ let row = find_row(
+ aboutPerformance.tbody,
+ "Main frame for test browser_aboutperformance.js",
+ tabContent
+ );
+ Assert.ok(row, "Found the row for our test tab");
+
+ // Find the worker under this row.
+ let button = row.firstChild.firstChild;
+ Assert.ok(button && button.classList, "Has a span to create the button");
+ Assert.ok(button.classList.contains("twisty"), "Button is expandable.");
+ Assert.ok(!button.classList.contains("open"), "Not already open");
+
+ // Click the expand button.
+ EventUtils.synthesizeMouseAtCenter(
+ button,
+ {},
+ aboutPerformance.tab.linkedBrowser.contentWindow
+ );
+
+ Assert.ok(button.classList.contains("open"), "It's now open");
+
+ // Move to the next row which is the worker we want to imspect.
+ row = row.nextSibling;
+
+ // Check that it is a worker.
+ Assert.equal(row.children[1].getAttribute("data-l10n-id"), "type-worker");
+
+ // The page is using at least 32 MB, due to the big array that it
+ // contains.
+ await checkMemoryAtLeast(32 * 1024 * 1024, row);
+
+ await BrowserTestUtils.removeTab(tabContent);
+ await BrowserTestUtils.removeTab(aboutPerformance.tab);
+});
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments.html b/toolkit/components/aboutperformance/tests/browser/browser_compartments.html
new file mode 100644
index 0000000000..53f6cd7d67
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>
+ Main frame for test browser_aboutperformance.js
+ </title>
+</head>
+<body>
+Main frame.
+
+<iframe src="browser_compartments_frame.html?frame=1">
+ Subframe 1
+</iframe>
+
+<iframe src="browser_compartments_frame.html?frame=2">
+ Subframe 2.
+</iframe>
+
+</body>
+
+<script>
+ window.waitForTestReady = async () => {
+ // This test page was born ready.
+ };
+</script>
+
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html b/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html
new file mode 100644
index 0000000000..44a073d3bb
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>
+ Subframe for test browser_compartments.html (do not change this title)
+ </title>
+ <script src="browser_compartments_script.js"></script>
+</head>
+<body>
+Subframe loaded.
+</body>
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js b/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js
new file mode 100644
index 0000000000..2547a1a010
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js
@@ -0,0 +1,11 @@
+// Use some CPU.
+var interval = window.setInterval(() => {
+ // Compute an arbitrary value, print it out to make sure that the JS
+ // engine doesn't discard all our computation.
+ var date = Date.now();
+ var array = [];
+ var i = 0;
+ while (Date.now() - date <= 100) {
+ array[i % 2] = i++;
+ }
+}, 300);
diff --git a/toolkit/components/aboutperformance/tests/browser/tab_use_memory.html b/toolkit/components/aboutperformance/tests/browser/tab_use_memory.html
new file mode 100644
index 0000000000..b72abae3f7
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/tab_use_memory.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>
+ Main frame for test browser_aboutperformance.js
+ </title>
+</head>
+<body>
+<p>
+ Mostly empty document, the script creates a big array.
+</p>
+</body>
+<script>
+ var big_array = [];
+ for (let i = 0; i < 4 * 1024 * 1024; i++) {
+ big_array[i] = i*i;
+ }
+
+ window.waitForTestReady = async () => {
+ // big_array is created so we are ready.
+ };
+</script>
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/workers.html b/toolkit/components/aboutperformance/tests/browser/workers.html
new file mode 100644
index 0000000000..29bb895ae2
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/workers.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>
+ Main frame for test browser_aboutperformance.js
+ </title>
+</head>
+<body>
+
+<p>
+fib(<span id="n">?</span>) = <span id="result">?</span>
+</p>
+
+</body>
+<script>
+ var result = document.querySelector('#result');
+ var span_n = document.querySelector('#n');
+ var worker = new Worker("workers_script.js");
+ var n = 1;
+
+ var wait = new Promise(ready => {
+ worker.onmessage = function(event) {
+ result.textContent = event.data;
+ span_n.textContent = n;
+ console.log(`Got: fib($n) = $result\n`);
+ n++;
+ ready();
+ // Keep looping to keep the worker as "High energy use"
+ worker.postMessage(n);
+ };
+ });
+
+ worker.postMessage(n);
+
+ window.waitForTestReady = async () => {
+ await wait;
+ };
+
+</script>
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/workers_memory.html b/toolkit/components/aboutperformance/tests/browser/workers_memory.html
new file mode 100644
index 0000000000..a39e68723a
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/workers_memory.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>
+ Main frame for test browser_aboutperformance.js
+ </title>
+</head>
+<body>
+
+<p>
+Result: <span id="result">?</span>
+</p>
+
+</body>
+<script>
+ var result = document.querySelector('#result');
+ var worker = new Worker("workers_memory_script.js");
+ var n = 1;
+
+ var waitPromise = new Promise(ready => {
+ worker.onmessage = function(event) {
+ result.textContent = event.data;
+ ready();
+
+ // We seem to need to keep the worker doing something to keep the
+ // memory usage up.
+ setTimeout(() => {
+ n++;
+ worker.postMessage(n);
+ }, 1000);
+ };
+ });
+
+ worker.postMessage(n);
+
+ window.waitForTestReady = async () => {
+ await waitPromise;
+ };
+</script>
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/workers_memory_script.js b/toolkit/components/aboutperformance/tests/browser/workers_memory_script.js
new file mode 100644
index 0000000000..0c0f2e65d7
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/workers_memory_script.js
@@ -0,0 +1,18 @@
+var big_array = [];
+var n = 0;
+
+onmessage = function (e) {
+ var sum = 0;
+ if (n == 0) {
+ for (let i = 0; i < 4 * 1024 * 1024; i++) {
+ big_array[i] = i * i;
+ }
+ } else {
+ for (let i = 0; i < 4 * 1024 * 1024; i++) {
+ sum += big_array[i];
+ big_array[i] += 1;
+ }
+ }
+ self.postMessage(`Iter: ${n}, sum: ${sum}`);
+ n++;
+};
diff --git a/toolkit/components/aboutperformance/tests/browser/workers_script.js b/toolkit/components/aboutperformance/tests/browser/workers_script.js
new file mode 100644
index 0000000000..fae87fa53f
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/workers_script.js
@@ -0,0 +1,10 @@
+function fib(n) {
+ if (n < 2) {
+ return 1;
+ }
+ return fib(n - 1) + fib(n - 2);
+}
+
+onmessage = function (e) {
+ self.postMessage(fib(Number(e.data)));
+};