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