Source code

Revision control

Copy as Markdown

Other Tools

/* 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:
for (let i = 0; i < 3; i++) {
// See minimizeMemoryUsage code here to justify the GC+CC+GC:
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,
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(
"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() {
// 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();
}
this.TalosParentProfiler.subtestEnd("DAMP");
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.
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.TalosParentProfiler.subtestStart("Begin DAMP");
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",
},
child: {
esModuleURI:
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();