diff options
Diffstat (limited to 'toolkit/components/processtools/tests/browser')
4 files changed, 598 insertions, 0 deletions
diff --git a/toolkit/components/processtools/tests/browser/browser.ini b/toolkit/components/processtools/tests/browser/browser.ini new file mode 100644 index 0000000000..a7436faaf5 --- /dev/null +++ b/toolkit/components/processtools/tests/browser/browser.ini @@ -0,0 +1,10 @@ +[DEFAULT] +prefs= + media.rdd-process.enabled=true + +support-files = + dummy.html + +[browser_test_powerMetrics.js] +[browser_test_procinfo.js] +skip-if = (ccov && os == "linux") # https://bugzilla.mozilla.org/show_bug.cgi?id=1608080 diff --git a/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js b/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js new file mode 100644 index 0000000000..8dfef0651d --- /dev/null +++ b/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js @@ -0,0 +1,400 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Return a web-based URL for a given file based on the testing directory. + * @param {String} fileName + * file that caller wants its web-based url + */ +function GetTestWebBasedURL(fileName) { + return ( + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.org" + ) + fileName + ); +} + +const kNS_PER_MS = 1000000; + +function printProcInfo(procInfo) { + info( + ` pid: ${procInfo.pid}, type = parent, cpu time = ${procInfo.cpuTime / + kNS_PER_MS}ms` + ); + for (let child of procInfo.children) { + info( + ` pid: ${child.pid}, type = ${child.type}, cpu time = ${child.cpuTime / + kNS_PER_MS}ms` + ); + } +} + +// It would be nice to have an API to list all the statically defined labels of +// a labeled_counter. Hopefully bug 1672273 will help with this. +const kGleanProcessTypeLabels = [ + "parent.active", + "parent.active.playing-audio", + "parent.active.playing-video", + "parent.inactive", + "parent.inactive.playing-audio", + "parent.inactive.playing-video", + "prealloc", + "privilegedabout", + "rdd", + "socket", + "web.background", + "web.background-perceivable", + "web.foreground", + "extension", + "gpu", + "gmplugin", + "utility", + "__other__", +]; + +async function getChildCpuTime(pid) { + return (await ChromeUtils.requestProcInfo()).children.find(p => p.pid == pid) + .cpuTime; +} + +add_task(async () => { + // Temporarily open a new foreground tab to make the current tab a background + // tab, and burn some CPU time in it while it's in the background. + const kBusyWaitForMs = 50; + let cpuTimeSpentOnBackgroundTab; + let firstBrowser = gBrowser.selectedTab.linkedBrowser; + let processPriorityChangedPromise = BrowserTestUtils.contentTopicObserved( + firstBrowser.browsingContext, + "ipc:process-priority-changed" + ); + await BrowserTestUtils.withNewTab( + GetTestWebBasedURL("dummy.html"), + async () => { + await processPriorityChangedPromise; + // We can't be sure that a busy loop lasting for a specific duration + // will use the same amount of CPU time, as that would require a core + // to be fully available for our busy loop, which is unlikely on single + // core hardware. + // To be able to have a predictable amount of CPU time used, we need to + // check using ChromeUtils.requestProcInfo how much CPU time has actually + // been spent. + let pid = firstBrowser.frameLoader.remoteTab.osPid; + let initalCpuTime = await getChildCpuTime(pid); + let afterCpuTime; + do { + await SpecialPowers.spawn( + firstBrowser, + [kBusyWaitForMs], + async kBusyWaitForMs => { + let startTime = Date.now(); + while (Date.now() - startTime < 10) { + // Burn CPU time... + } + } + ); + afterCpuTime = await getChildCpuTime(pid); + } while (afterCpuTime - initalCpuTime < kBusyWaitForMs * kNS_PER_MS); + cpuTimeSpentOnBackgroundTab = Math.floor( + (afterCpuTime - initalCpuTime) / kNS_PER_MS + ); + } + ); + + let beforeProcInfo = await ChromeUtils.requestProcInfo(); + await Services.fog.testFlushAllChildren(); + + let cpuTimeByType = {}, + gpuTimeByType = {}; + for (let label of kGleanProcessTypeLabels) { + cpuTimeByType[label] = Glean.power.cpuTimePerProcessTypeMs[ + label + ].testGetValue(); + gpuTimeByType[label] = Glean.power.gpuTimePerProcessTypeMs[ + label + ].testGetValue(); + } + let totalCpuTime = Glean.power.totalCpuTimeMs.testGetValue(); + let totalGpuTime = Glean.power.totalGpuTimeMs.testGetValue(); + + let afterProcInfo = await ChromeUtils.requestProcInfo(); + + info("CPU time from ProcInfo before calling testFlushAllChildren:"); + printProcInfo(beforeProcInfo); + + info("CPU time for each label:"); + let totalCpuTimeByType = 0; + for (let label of kGleanProcessTypeLabels) { + totalCpuTimeByType += cpuTimeByType[label] ?? 0; + info(` ${label} = ${cpuTimeByType[label]}`); + } + + info("CPU time from ProcInfo after calling testFlushAllChildren:"); + printProcInfo(afterProcInfo); + + Assert.equal( + totalCpuTimeByType, + totalCpuTime, + "The sum of CPU time used by all process types should match totalCpuTimeMs" + ); + + // In infra the parent process time will be reported as parent.inactive, + // but when running the test locally the user might move the mouse a bit. + let parentTime = + (cpuTimeByType["parent.active"] || 0) + + (cpuTimeByType["parent.inactive"] || 0); + Assert.greaterOrEqual( + parentTime, + Math.floor(beforeProcInfo.cpuTime / kNS_PER_MS), + "reported parent cpu time should be at least what the first requestProcInfo returned" + ); + Assert.lessOrEqual( + parentTime, + Math.ceil(afterProcInfo.cpuTime / kNS_PER_MS), + "reported parent cpu time should be at most what the second requestProcInfo returned" + ); + + kGleanProcessTypeLabels + .filter(label => label.startsWith("parent.") && label.includes(".playing-")) + .forEach(label => { + Assert.strictEqual( + cpuTimeByType[label], + undefined, + `no media was played so the CPU time for ${label} should be undefined` + ); + }); + + if (beforeProcInfo.children.some(p => p.type == "preallocated")) { + Assert.greaterOrEqual( + cpuTimeByType.prealloc, + beforeProcInfo.children.reduce( + (time, p) => + time + + (p.type == "preallocated" ? Math.floor(p.cpuTime / kNS_PER_MS) : 0), + 0 + ), + "reported cpu time for preallocated content processes should be at least the sum of what the first requestProcInfo returned." + ); + // We can't compare with the values returned by the second requestProcInfo + // call because one preallocated content processes has been turned into + // a normal content process when we opened a tab. + } else { + info( + "no preallocated content process existed when we started our test, but some might have existed before" + ); + } + + if (beforeProcInfo.children.some(p => p.type == "privilegedabout")) { + Assert.greaterOrEqual( + cpuTimeByType.privilegedabout, + 1, + "we used some CPU time in a foreground tab, but don't know how much as the process might have started as preallocated" + ); + } + + for (let label of [ + "rdd", + "socket", + "extension", + "gpu", + "gmplugin", + "utility", + ]) { + if (!kGleanProcessTypeLabels.includes(label)) { + Assert.ok( + false, + `coding error in the test, ${label} isn't part of ${kGleanProcessTypeLabels.join( + ", " + )}` + ); + } + if (beforeProcInfo.children.some(p => p.type == label)) { + Assert.greaterOrEqual( + cpuTimeByType[label], + Math.floor( + beforeProcInfo.children.find(p => p.type == label).cpuTime / + kNS_PER_MS + ), + "reported cpu time for " + + label + + " process should be at least what the first requestProcInfo returned." + ); + Assert.lessOrEqual( + cpuTimeByType[label], + Math.ceil( + afterProcInfo.children.find(p => p.type == label).cpuTime / kNS_PER_MS + ), + "reported cpu time for " + + label + + " process should be at most what the second requestProcInfo returned." + ); + } else { + info( + "no " + + label + + " process existed when we started our test, but some might have existed before" + ); + } + } + + Assert.greaterOrEqual( + cpuTimeByType["web.background"], + cpuTimeSpentOnBackgroundTab, + "web.background should be at least the time we spent." + ); + + Assert.greaterOrEqual( + cpuTimeByType["web.foreground"], + 1, + "we used some CPU time in a foreground tab, but don't know how much" + ); + + // We only have web.background-perceivable CPU time if a muted video was + // played in a background tab. + Assert.strictEqual( + cpuTimeByType["web.background-perceivable"], + undefined, + "CPU time should only be recorded in the web.background-perceivable label" + ); + + // __other__ should be undefined, if it is not, we have a missing label in the metrics.yaml file. + Assert.strictEqual( + cpuTimeByType.__other__, + undefined, + "no CPU time should be recorded in the __other__ label" + ); + + info("GPU time for each label:"); + let totalGpuTimeByType = undefined; + for (let label of kGleanProcessTypeLabels) { + if (gpuTimeByType[label] !== undefined) { + totalGpuTimeByType = (totalGpuTimeByType || 0) + gpuTimeByType[label]; + } + info(` ${label} = ${gpuTimeByType[label]}`); + } + + Assert.equal( + totalGpuTimeByType, + totalGpuTime, + "The sum of GPU time used by all process types should match totalGpuTimeMs" + ); + + // __other__ should be undefined, if it is not, we have a missing label in the metrics.yaml file. + Assert.strictEqual( + gpuTimeByType.__other__, + undefined, + "no GPU time should be recorded in the __other__ label" + ); + + // Now test per-thread CPU time. + // We don't test parentActive as the user is not marked active on infra. + let processTypes = [ + "parentInactive", + "contentBackground", + "contentForeground", + ]; + if (beforeProcInfo.children.some(p => p.type == "gpu")) { + processTypes.push("gpuProcess"); + } + // The list of accepted labels is not accessible to the JS code, so test only the main thread. + const kThreadName = "geckomain"; + if (AppConstants.NIGHTLY_BUILD) { + for (let processType of processTypes) { + Assert.greater( + Glean.powerCpuMsPerThread[processType][kThreadName].testGetValue(), + 0, + `some CPU time should have been recorded for the ${processType} main thread` + ); + Assert.greater( + Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(), + 0, + `some thread wake ups should have been recorded for the ${processType} main thread` + ); + } + } else { + // We are not recording per thread CPU use outside of the Nightly channel. + for (let processType of processTypes) { + Assert.equal( + Glean.powerCpuMsPerThread[processType][kThreadName].testGetValue(), + undefined, + `no CPU time should have been recorded for the ${processType} main thread` + ); + Assert.equal( + Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(), + undefined, + `no thread wake ups should have been recorded for the ${processType} main thread` + ); + } + } +}); + +add_task(async function test_tracker_power() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + let initialValues = []; + for (let trackerType of [ + "ad", + "analytics", + "cryptomining", + "fingerprinting", + "social", + "unknown", + ]) { + initialValues[trackerType] = + Glean.power.cpuTimePerTrackerTypeMs[trackerType].testGetValue() || 0; + } + + await BrowserTestUtils.withNewTab( + GetTestWebBasedURL("dummy.html"), + async () => { + // Load a tracker in a subframe, as we only record CPU time used by third party trackers. + await SpecialPowers.spawn( + gBrowser.selectedTab.linkedBrowser, + [ + GetTestWebBasedURL("dummy.html").replace( + "example.org", + "trackertest.org" + ), + ], + async frameUrl => { + let iframe = content.document.createElement("iframe"); + iframe.setAttribute("src", frameUrl); + await new content.Promise(resolve => { + iframe.onload = resolve; + content.document.body.appendChild(iframe); + }); + } + ); + } + ); + + await Services.fog.testFlushAllChildren(); + + let unknownTrackerCPUTime = + Glean.power.cpuTimePerTrackerTypeMs.unknown.testGetValue() || 0; + Assert.greater( + unknownTrackerCPUTime, + initialValues.unknown, + "The CPU time of unknown trackers should have increased" + ); + + for (let trackerType of [ + "ad", + "analytics", + "cryptomining", + "fingerprinting", + "social", + ]) { + Assert.equal( + Glean.power.cpuTimePerTrackerTypeMs[trackerType].testGetValue() || 0, + initialValues[trackerType], + `no new CPU time should have been recorded for ${trackerType} trackers` + ); + } +}); diff --git a/toolkit/components/processtools/tests/browser/browser_test_procinfo.js b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js new file mode 100644 index 0000000000..673864bdd0 --- /dev/null +++ b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js @@ -0,0 +1,168 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const DUMMY_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" + ) + "/dummy.html"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const HAS_THREAD_NAMES = + AppConstants.platform != "win" || + AppConstants.isPlatformAndVersionAtLeast("win", 10); +const isFissionEnabled = SpecialPowers.useRemoteSubframes; + +const SAMPLE_SIZE = 10; +const NS_PER_MS = 1000000; + +function checkProcessCpuTime(proc) { + Assert.greater(proc.cpuTime, 0, "Got some cpu time"); + + let cpuThreads = 0; + for (let thread of proc.threads) { + cpuThreads += Math.floor(thread.cpuTime / NS_PER_MS); + } + Assert.greater(cpuThreads, 0, "Got some cpu time in the threads"); + let processCpuTime = Math.ceil(proc.cpuTime / NS_PER_MS); + if (AppConstants.platform == "win" && processCpuTime < cpuThreads) { + // On Windows, our test jobs likely run in VMs without constant TSC, + // so we might have low precision CPU time measurements. + const MAX_DISCREPENCY = 100; + Assert.ok( + cpuThreads - processCpuTime < MAX_DISCREPENCY, + `on Windows, we accept a discrepency of up to ${MAX_DISCREPENCY}ms between the process CPU time and the sum of its threads' CPU time, process CPU time: ${processCpuTime}, sum of thread CPU time: ${cpuThreads}` + ); + } else { + Assert.greaterOrEqual( + processCpuTime, + cpuThreads, + "The total CPU time of the process should be at least the sum of the CPU time spent by the still alive threads" + ); + } +} + +add_task(async function test_proc_info() { + // Open a few `about:home` tabs, they'll end up in `privilegedabout`. + let tabsAboutHome = []; + for (let i = 0; i < 5; ++i) { + let tab = BrowserTestUtils.addTab(gBrowser, "about:home"); + tabsAboutHome.push(tab); + gBrowser.selectedTab = tab; + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + } + + await BrowserTestUtils.withNewTab( + { gBrowser, url: DUMMY_URL }, + async function(browser) { + // We test `SAMPLE_SIZE` times to increase a tad the chance of encountering race conditions. + for (let z = 0; z < SAMPLE_SIZE; z++) { + let parentProc = await ChromeUtils.requestProcInfo(); + + Assert.equal( + parentProc.type, + "browser", + "Parent proc type should be browser" + ); + + checkProcessCpuTime(parentProc); + + // Under Windows, thread names appeared with Windows 10. + if (HAS_THREAD_NAMES) { + Assert.ok( + parentProc.threads.some(thread => thread.name), + "At least one of the threads of the parent process is named" + ); + } + + Assert.ok(parentProc.memory > 0, "Memory was set"); + + // While it's very unlikely that the parent will disappear while we're running + // tests, some children can easily vanish. So we go twice through the list of + // children. Once to test stuff that all process data respects the invariants + // that don't care whether we have a race condition and once to test that at + // least one well-known process that should not be able to vanish during + // the test respects all the invariants. + for (let childProc of parentProc.children) { + Assert.notEqual( + childProc.type, + "browser", + "Child proc type should not be browser" + ); + + // We set the `childID` for child processes that have a `ContentParent`/`ContentChild` + // actor hierarchy. + if (childProc.type.startsWith("web")) { + Assert.notEqual( + childProc.childID, + 0, + "Child proc should have been set" + ); + } + Assert.notEqual( + childProc.type, + "unknown", + "Child proc type should be known" + ); + if (childProc.type == "webIsolated") { + Assert.notEqual( + childProc.origin || "", + "", + "Child process should have an origin" + ); + } + + checkProcessCpuTime(childProc); + } + + // We only check other properties on the `privilegedabout` subprocess, which + // as of this writing is always active and available. + var hasPrivilegedAbout = false; + var numberOfAboutTabs = 0; + for (let childProc of parentProc.children) { + if (childProc.type != "privilegedabout") { + continue; + } + hasPrivilegedAbout = true; + Assert.ok(childProc.memory > 0, "Memory was set"); + + for (var win of childProc.windows) { + if (win.documentURI.spec != "about:home") { + // We're only interested in about:home for this test. + continue; + } + numberOfAboutTabs++; + Assert.ok( + win.outerWindowId > 0, + `ContentParentID should be > 0 ${win.outerWindowId}` + ); + if (win.documentTitle) { + // Unfortunately, we sometimes reach this point before the document is fully loaded, so + // `win.documentTitle` may still be empty. + Assert.equal(win.documentTitle, "New Tab"); + } + } + Assert.ok( + numberOfAboutTabs >= tabsAboutHome.length, + "We have found at least as many about:home tabs as we opened" + ); + + // Once we have verified the privileged about process, bailout. + break; + } + + Assert.ok( + hasPrivilegedAbout, + "We have found the privileged about process" + ); + } + + for (let tab of tabsAboutHome) { + BrowserTestUtils.removeTab(tab); + } + } + ); +}); diff --git a/toolkit/components/processtools/tests/browser/dummy.html b/toolkit/components/processtools/tests/browser/dummy.html new file mode 100644 index 0000000000..e69dad24d4 --- /dev/null +++ b/toolkit/components/processtools/tests/browser/dummy.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +<div id="holder" class="">Holder</div> +<script> + let text = ""; + for (let i = 0; i < 1000; i++) { + text += "more"; + // eslint-disable-next-line no-unsanitized/property + document.getElementById("holder").innerHTML = text; + } + document.getElementById("holder").classList.add("loaded"); +</script> +</body> +</html> |