From 64fb4d889f51add191d080e51b0e2bebbcbdc66e Mon Sep 17 00:00:00 2001 From: James Teh Date: Mon, 28 Apr 2025 22:42:59 +0000 Subject: [PATCH] Bug 1962317: Initial framework for accessibility engine performance testing, plus some initial tests. r=morgan Tests go in accessible/tests/browser/performance. They use the usual addAccessibleTask function to add a test. Inside the task, they can time operations using the timeThis() function, which takes a name and a function to time. As well as logging the time, this also logs all accessibility PerfStats captured during the function. For now, output is simply logged using info(). In future, we may wish to output this data elsewhere; e.g. so it can be picked up by performance monitoring tools. Having this single function should make extending this fairly easy without needing to rewrite tests. Two initial tests have been included: - browser_manyMutations.js: Test for bug 1948375. - browser_spellingErrors.js: Test for bug 1961832. Differential Revision: https://phabricator.services.mozilla.com/D246544 --- accessible/moz.build | 1 + .../tests/browser/performance/browser.toml | 11 +++ .../performance/browser_manyMutations.js | 25 +++++++ .../performance/browser_spellingErrors.js | 32 +++++++++ accessible/tests/browser/performance/head.js | 71 +++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 accessible/tests/browser/performance/browser.toml create mode 100644 accessible/tests/browser/performance/browser_manyMutations.js create mode 100644 accessible/tests/browser/performance/browser_spellingErrors.js create mode 100644 accessible/tests/browser/performance/head.js diff --git a/accessible/moz.build b/accessible/moz.build index 5633abfc051a..d832b79e8c57 100644 --- a/accessible/moz.build +++ b/accessible/moz.build @@ -44,6 +44,7 @@ BROWSER_CHROME_MANIFESTS += [ "tests/browser/general/browser.toml", "tests/browser/hittest/browser.toml", "tests/browser/mac/browser.toml", + "tests/browser/performance/browser.toml", "tests/browser/pivot/browser.toml", "tests/browser/role/browser.toml", "tests/browser/scroll/browser.toml", diff --git a/accessible/tests/browser/performance/browser.toml b/accessible/tests/browser/performance/browser.toml new file mode 100644 index 000000000000..2720a50f9b0b --- /dev/null +++ b/accessible/tests/browser/performance/browser.toml @@ -0,0 +1,11 @@ +[DEFAULT] +subsuite = "a11y" +support-files = [ + "head.js", + "!/accessible/tests/browser/shared-head.js", + "!/accessible/tests/mochitest/*.js", +] + +["browser_manyMutations.js"] + +["browser_spellingErrors.js"] diff --git a/accessible/tests/browser/performance/browser_manyMutations.js b/accessible/tests/browser/performance/browser_manyMutations.js new file mode 100644 index 000000000000..688e2448d792 --- /dev/null +++ b/accessible/tests/browser/performance/browser_manyMutations.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +addAccessibleTask( + `
`, + async function testManyShowEvents(browser) { + await timeThis("manyShowEvents", async () => { + info("Adding many children"); + let lastShown = waitForEvent(EVENT_SHOW, "last"); + await invokeContentTask(browser, [], () => { + const container = content.document.getElementById("container"); + for (let c = 0; c < 100000; ++c) { + const child = content.document.createElement("p"); + child.textContent = c; + container.append(child); + } + container.lastChild.id = "last"; + }); + info("Waiting for last show event"); + await lastShown; + }); + } +); diff --git a/accessible/tests/browser/performance/browser_spellingErrors.js b/accessible/tests/browser/performance/browser_spellingErrors.js new file mode 100644 index 000000000000..a76dcb72b8d7 --- /dev/null +++ b/accessible/tests/browser/performance/browser_spellingErrors.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +addAccessibleTask( + `
`, + async function testRemoveManySpellingErrors(browser) { + let changed = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED); + await invokeContentTask(browser, [], () => { + const editable = content.document.getElementById("editable"); + for (let i = 0; i < 500; ++i) { + const p = content.document.createElement("p"); + p.textContent = "tset tset tset"; + editable.append(p); + } + editable.focus(); + }); + // Spell checking is async, so we need to wait for it to happen. + info("Waiting for spell check"); + await changed; + await timeThis("removeManySpellingErrors", async () => { + info("Removing all content from editable"); + let reorder = waitForEvent(EVENT_REORDER, "editable"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("editable").innerHTML = ""; + }); + info("Waiting for reorder event"); + await reorder; + }); + } +); diff --git a/accessible/tests/browser/performance/head.js b/accessible/tests/browser/performance/head.js new file mode 100644 index 000000000000..bc4ff5ee3db2 --- /dev/null +++ b/accessible/tests/browser/performance/head.js @@ -0,0 +1,71 @@ +/* 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"; + +/* exported timeThis */ + +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Load common.js and promisified-events.js from accessible/tests/mochitest/ for +// all tests. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); + +// All the A11Y metrics in tools/performance/PerfStats.h. +const ALL_A11Y_PERFSTATS = + (1 << 29) | + (1 << 30) | + (1 << 31) | + (1 << 32) | + (1 << 33) | + (1 << 34) | + (1 << 35) | + (1 << 36) | + (1 << 37) | + (1 << 38) | + (1 << 39) | + (1 << 40) | + (1 << 41) | + (1 << 42) | + (1 << 43) | + (1 << 44); + +/** + * Time a function and log how long it took. The given name is included in log + * messages. All accessibility PerfStats metrics are also captured and logged. + */ +async function timeThis(name, func) { + const logPrefix = `Timing: ${name}`; + info(`${logPrefix}: begin`); + const start = performance.now(); + ChromeUtils.setPerfStatsCollectionMask(ALL_A11Y_PERFSTATS); + await func(); + const delta = performance.now() - start; + info(`${logPrefix}: took ${delta} ms`); + const stats = JSON.parse(await ChromeUtils.collectPerfStats()); + ChromeUtils.setPerfStatsCollectionMask(0); + // Filter stuff out of stats that we don't care about. + // Filter out the GPU process, since accessibility doesn't do anything there. + stats.processes = stats.processes.filter(process => process.type != "gpu"); + for (const process of stats.processes) { + // Because of weird JS -> WebIDL 64 bit number issues, we get metrics here + // that aren't for accessibility. For example, 1 << 32 also gets us 1 << 0. + // Filter those out. Also, filter out any metrics with a count of 0. + process.perfstats.metrics = process.perfstats.metrics.filter( + metric => metric.metric.startsWith("A11Y_") && metric.count > 0 + ); + } + // Now that we've filtered metrics, remove any processes that have no metrics left. + stats.processes = stats.processes.filter( + process => !!process.perfstats.metrics.length + ); + info(`${logPrefix}: PerfStats: ${JSON.stringify(stats, null, 2)}`); +}