diff options
Diffstat (limited to 'testing/talos/talos/tests/devtools/addon/content/damp.js')
-rw-r--r-- | testing/talos/talos/tests/devtools/addon/content/damp.js | 557 |
1 files changed, 557 insertions, 0 deletions
diff --git a/testing/talos/talos/tests/devtools/addon/content/damp.js b/testing/talos/talos/tests/devtools/addon/content/damp.js new file mode 100644 index 0000000000..412eaced01 --- /dev/null +++ b/testing/talos/talos/tests/devtools/addon/content/damp.js @@ -0,0 +1,557 @@ +/* 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"; + +/* globals dampWindow */ + +const { gBrowser, MozillaFileLogger, requestIdleCallback } = dampWindow; + +const { AddonManager } = require("resource://gre/modules/AddonManager.jsm"); + +const DampLoadParentModule = require("damp-test/actors/DampLoadParent.jsm"); +const DAMP_TESTS = require("damp-test/damp-tests.js"); + +// Record allocation count in new subtests if DEBUG_DEVTOOLS_ALLOCATIONS is set to +// "normal". Print allocation sites to stdout if DEBUG_DEVTOOLS_ALLOCATIONS is set to +// "verbose". +const DEBUG_ALLOCATIONS = Services.env.get("DEBUG_DEVTOOLS_ALLOCATIONS"); + +const DEBUG_SCREENSHOTS = Services.env.get("DEBUG_DEVTOOLS_SCREENSHOTS"); + +// Maximum time spent in one test, in milliseconds +const TEST_TIMEOUT = 5 * 60000; + +function getMostRecentBrowserWindow() { + return Services.wm.getMostRecentWindow("navigator:browser"); +} + +function Damp() {} + +Damp.prototype = { + async garbageCollect() { + dump("Garbage collect\n"); + let startTime = Cu.now(); + + // Minimize memory usage + // mimic miminizeMemoryUsage, by only flushing JS objects via GC. + // We don't want to flush all the cache like minimizeMemoryUsage, + // as it slow down next executions almost like a cold start. + + // See minimizeMemoryUsage code to justify the 3 iterations and the setTimeout: + // https://searchfox.org/mozilla-central/rev/33c21c060b7f3a52477a73d06ebcb2bf313c4431/xpcom/base/nsMemoryReporterManager.cpp#2574-2585,2591-2594 + for (let i = 0; i < 3; i++) { + // See minimizeMemoryUsage code here to justify the GC+CC+GC: + // https://searchfox.org/mozilla-central/rev/be78e6ea9b10b1f5b2b3b013f01d86e1062abb2b/dom/base/nsJSEnvironment.cpp#341-349 + Cu.forceGC(); + Cu.forceCC(); + Cu.forceGC(); + await new Promise(done => setTimeout(done, 0)); + } + ChromeUtils.addProfilerMarker( + "DAMP", + { startTime, category: "Test" }, + "GC" + ); + }, + + async ensureTalosParentProfiler() { + // TalosParentProfiler is part of TalosPowers, which is a separate WebExtension + // that may or may not already have finished loading at this point (unlike most + // Pageloader tests, Damp doesn't wait for Pageloader to find TalosPowers before + // running). getTalosParentProfiler is used to wait for TalosPowers to be around + // before continuing. + async function getTalosParentProfiler() { + try { + const { + TalosParentProfiler, + } = require("resource://talos-powers/TalosParentProfiler.jsm"); + return TalosParentProfiler; + } catch (err) { + await new Promise(resolve => setTimeout(resolve, 500)); + return getTalosParentProfiler(); + } + } + + this.TalosParentProfiler = await getTalosParentProfiler(); + }, + + // Take a screenshot of the whole browser window and open it in a background tab + async screenshot(label) { + const win = this._win; + const canvas = win.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "html:canvas" + ); + const context = canvas.getContext("2d"); + canvas.width = win.innerWidth; + canvas.height = win.innerHeight; + context.drawWindow(win, 0, 0, canvas.width, canvas.height, "white"); + const imgURL = canvas.toDataURL(); + const url = `data:text/html,<title>${label}</title> + <h1>${label}</h1> + <img width="100%" height="100%" src="${imgURL}"/>`; + this._win.gBrowser.addTab(url, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + }, + + /** + * Helper to tell when a test start and when it is finished. + * It helps recording its duration, but also put markers for profiler.firefox.com + * when profiling DAMP. + * + * When this method is called, the test is considered to be starting immediately + * When the test is over, the returned object's `done` method should be called. + * + * @param label String + * Test title, displayed everywhere in PerfHerder, DevTools Perf Dashboard, ... + * @param record Boolean + * Optional, if passed false, the test won't be recorded. It won't appear in + * PerfHerder. Instead we will record profiler.firefox.com markers and only + * print the timings on stdout. + * + * @return object + * With a `done` method, to be called whenever the test is finished running + * and we should record its duration. + */ + runTest(label, record = true) { + if (DEBUG_ALLOCATIONS) { + if (!this.allocationTracker) { + this.allocationTracker = this.startAllocationTracker(); + } + // Flush the current allocations before running the test + this.allocationTracker.flushAllocations(); + } + + let start = Cu.now(); + + return { + done: () => { + let end = Cu.now(); + let duration = end - start; + ChromeUtils.addProfilerMarker( + "DAMP", + { startTime: start, category: "Test" }, + label + ); + if (record) { + this._results.push({ + name: label, + value: duration, + }); + } else { + dump(`'${label}' took ${duration}ms.\n`); + } + + if (DEBUG_ALLOCATIONS == "normal" && record) { + this._results.push({ + name: label + ".allocations", + value: this.allocationTracker.countAllocations(), + }); + } else if (DEBUG_ALLOCATIONS == "verbose") { + this.allocationTracker.logAllocationSites(); + } + if (DEBUG_SCREENSHOTS) { + this.screenshot(label); + } + }, + }; + }, + + async addTab(url) { + // Disable opening animation to avoid intermittents and prevent having to wait for + // animation's end. (See bug 1480953) + let tab = (this._win.gBrowser.selectedTab = + this._win.gBrowser.addTrustedTab(url, { skipAnimation: true })); + let browser = tab.linkedBrowser; + await this._awaitBrowserLoaded(browser); + return tab; + }, + + async waitForPendingPaints(window) { + let utils = window.windowUtils; + let startTime = Cu.now(); + while (utils.isMozAfterPaintPending) { + await new Promise(done => { + window.addEventListener( + "MozAfterPaint", + function listener() { + ChromeUtils.addProfilerMarker( + "DAMP", + { category: "Test" }, + "pending paint" + ); + done(); + }, + { once: true } + ); + }); + } + ChromeUtils.addProfilerMarker( + "DAMP", + { startTime, category: "Test" }, + "pending paints" + ); + }, + + reloadPage(onReload) { + return new Promise(resolve => { + let browser = gBrowser.selectedBrowser; + if (typeof onReload == "function") { + onReload().then(resolve); + } else { + resolve(this._awaitBrowserLoaded(browser)); + } + browser.reload(); + }); + }, + + async testSetup(url) { + let tab = await this.addTab(url); + await new Promise(resolve => { + setTimeout(resolve, 100); + }); + return tab; + }, + + async testTeardown(url) { + // Disable closing animation to avoid intermittents and prevent having to wait for + // animation's end. (See bug 1480953) + this._win.gBrowser.removeCurrentTab({ animate: false }); + + // Force freeing memory now so that it doesn't happen during the next test + await this.garbageCollect(); + + let duration = Math.round(Cu.now() - this._startTime); + dump(`${this._currentTest} took ${duration}ms.\n`); + + this._runNextTest(); + }, + + _win: undefined, + _dampTab: undefined, + _results: [], + _nextTestIndex: 0, + _tests: [], + _onSequenceComplete: 0, + + // Timeout ID to guard against current test never finishing + _timeout: null, + + // The unix time at which the current test started (ms) + _startTime: null, + + // Name of the test currently executed (i.e. path from /tests folder) + _currentTest: null, + + _runNextTest() { + clearTimeout(this._timeout); + + if (this._nextTestIndex >= this._tests.length) { + this._onSequenceComplete(); + return; + } + + let test = this._tests[this._nextTestIndex++]; + this._startTime = Cu.now(); + this._currentTest = test; + + dump(`Loading test '${test}'\n`); + let testMethod = require(`damp-test/tests/${test}`); + + this._timeout = setTimeout(() => { + this.error("Test timed out"); + }, TEST_TIMEOUT); + + dump(`Executing test '${test}'\n`); + let promise = testMethod(); + + // If test method is an async function, ensure catching its exceptions + if (promise && typeof promise.catch == "function") { + promise.catch(e => { + this.exception(e); + }); + } + }, + // Each command at the array a function which must call nextCommand once it's done + _doSequence(tests, onComplete) { + this._tests = tests; + this._onSequenceComplete = onComplete; + this._results = []; + this._nextTestIndex = 0; + + this._runNextTest(); + }, + + _log(str) { + if (MozillaFileLogger && MozillaFileLogger.log) { + MozillaFileLogger.log(str); + } + + dump(str); + }, + + _logLine(str) { + return this._log(str + "\n"); + }, + + _reportAllResults() { + const testNames = []; + const testResults = []; + + let out = ""; + for (const i in this._results) { + const res = this._results[i]; + const disp = [] + .concat(res.value) + .map(function (a) { + return isNaN(a) ? -1 : a.toFixed(1); + }) + .join(" "); + out += res.name + ": " + disp + "\n"; + + if (!Array.isArray(res.value)) { + // Waw intervals array is not reported to talos + testNames.push(res.name); + testResults.push(res.value); + } + } + this._log("\n" + out); + + if (DEBUG_SCREENSHOTS) { + // When we are printing screenshots, we don't want to want to exit firefox + // so that we have time to view them. + dump( + "All tests are finished, please review the screenshots and close the browser manually.\n" + ); + return; + } + + if (this.testDone) { + this.testDone({ testResults, testNames }); + } else { + // alert(out); + } + }, + + _doneInternal() { + // Ignore any duplicated call to this method + // Call startTest() again in order to reset this flag. + if (this._done) { + return; + } + this._done = true; + + if (this.allocationTracker) { + this.allocationTracker.stop(); + this.allocationTracker = null; + } + this._win.gBrowser.selectedTab = this._dampTab; + + if (this._results) { + this._logLine("DAMP_RESULTS_JSON=" + JSON.stringify(this._results)); + this._reportAllResults(); + } + + ChromeUtils.addProfilerMarker("DAMP", { + startTime: this._startTimestamp, + category: "Test", + }); + this.TalosParentProfiler.pause(); + + this._unregisterDampLoadActors(); + }, + + startAllocationTracker() { + const { + allocationTracker, + } = require("devtools/shared/test-helpers/allocation-tracker"); + return allocationTracker(); + }, + + error(message) { + // Log a unique prefix in order to be interpreted as an error and stop DAMP from + // testing/talos/talos/talos_process.py + dump("TEST-UNEXPECTED-FAIL | damp | "); + + // Print the currently executed test, if we already started executing one + if (this._currentTest) { + dump(this._currentTest + ": "); + } + + dump(message + "\n"); + + // Stop further test execution and immediatly close DAMP + this._tests = []; + this._results = null; + this._doneInternal(); + }, + + exception(e) { + const str = + "Exception: " + (e?.message || e) + "\n" + (e?.stack || "No stack"); + this.error(str); + }, + + // Waits for any pending operations that may execute on Firefox startup and that + // can still be pending when we start running DAMP tests. + async waitBeforeRunningTests() { + // Addons may still be being loaded, so wait for them to be fully set up. + if (!AddonManager.isReady) { + let onAddonManagerReady = new Promise(resolve => { + let listener = { + onStartup() { + AddonManager.removeManagerListener(listener); + resolve(); + }, + onShutdown() {}, + }; + AddonManager.addManagerListener(listener); + }); + await onAddonManagerReady; + } + + // SessionRestore triggers some saving sequence on idle, + // so wait for that to be processed before starting tests. + // https://searchfox.org/mozilla-central/rev/83a923ef7a3b95a516f240a6810c20664b1e0ac9/browser/components/sessionstore/content/content-sessionStore.js#828-830 + // https://searchfox.org/mozilla-central/rev/83a923ef7a3b95a516f240a6810c20664b1e0ac9/browser/components/sessionstore/content/content-sessionStore.js#858 + await new Promise(resolve => { + setTimeout(resolve, 1500); + }); + await new Promise(resolve => { + requestIdleCallback(resolve, { timeout: 15000 }); + }); + + await this.ensureTalosParentProfiler(); + + // Free memory before running the first test, otherwise we may have a GC + // related to Firefox startup or DAMP setup during the first test. + await this.garbageCollect(); + }, + + /** + * This is the main entry point for DAMP, called from + * testing/talos/talos/tests/devtools/addon/api + */ + startTest() { + let promise = new Promise(resolve => { + this.testDone = resolve; + }); + + try { + // Is DAMP finished executing? Help preventing async execution when DAMP had an error + this._done = false; + + this._registerDampLoadActors(); + + this._win = Services.wm.getMostRecentWindow("navigator:browser"); + this._dampTab = this._win.gBrowser.selectedTab; + this._win.gBrowser.selectedBrowser.focus(); // Unfocus the URL bar to avoid caret blink + + // Filter tests via `./mach --subtests filter` command line argument + let filter = Services.prefs.getCharPref("talos.subtests", ""); + + const suite = Services.prefs.getCharPref("talos.damp.suite", ""); + let testSuite; + if (suite === "all") { + testSuite = Object.values(DAMP_TESTS).flat(); + } else { + testSuite = DAMP_TESTS[suite]; + if (!testSuite) { + this.error(`Unable to find any test suite matching '${suite}'`); + } + } + + let tests = testSuite + .filter(test => !test.disabled) + .filter(test => test.name.includes(filter)); + + if (tests.length === 0) { + this.error(`Unable to find any test matching '${filter}'`); + } + + // Run cold test only once + let topWindow = getMostRecentBrowserWindow(); + if (topWindow.coldRunDAMPDone) { + tests = tests.filter(test => !test.cold); + } else { + topWindow.coldRunDAMPDone = true; + } + + // Construct the sequence array while filtering tests + let sequenceArray = []; + for (let test of tests) { + sequenceArray.push(test.path); + } + + this.waitBeforeRunningTests() + .then(() => { + this._startTimestamp = Cu.now(); + this.TalosParentProfiler.resume(); + this._doSequence(sequenceArray, this._doneInternal); + }) + .catch(e => { + this.exception(e); + }); + } catch (e) { + this.exception(e); + } + + return promise; + }, + + /** + * Wait for a page-show/load event on the provided browser element, using the + * JSWindowActor pair at content/actors/DampLoad. + */ + _awaitBrowserLoaded(browser) { + dump( + `Wait for a pageshow event for browsing context ${browser.browsingContext.id}\n` + ); + return new Promise(resolve => { + const eventDispatcher = DampLoadParentModule.EventDispatcher; + const onPageShow = (eventName, data) => { + dump(`Received pageshow event for ${data.browsingContext.id}\n`); + if (data.browsingContext !== browser.browsingContext) { + return; + } + + eventDispatcher.off("DampLoadParent:PageShow", onPageShow); + resolve(); + }; + + eventDispatcher.on("DampLoadParent:PageShow", onPageShow); + }); + }, + + _registerDampLoadActors() { + dump(`[DampLoad helper] Register DampLoad actors\n`); + ChromeUtils.registerWindowActor("DampLoad", { + kind: "JSWindowActor", + parent: { + esModuleURI: + "resource://damp-test/content/actors/DampLoadParent.sys.mjs", + }, + child: { + esModuleURI: + "resource://damp-test/content/actors/DampLoadChild.sys.mjs", + events: { + pageshow: { mozSystemGroup: true }, + }, + }, + + // Only listen to top level content frame load. + allFrames: false, + includeChrome: false, + }); + }, + + _unregisterDampLoadActors() { + dump(`[DampLoad helper] Unregister DampLoad actors\n`); + ChromeUtils.unregisterWindowActor("DampLoad"); + }, +}; + +exports.damp = new Damp(); |