diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/aboutperformance | |
parent | Initial commit. (diff) | |
download | firefox-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')
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))); +}; |