diff --git a/resources/benchmark-runner.mjs b/resources/benchmark-runner.mjs index 6d113c2c8..146ff6698 100644 --- a/resources/benchmark-runner.mjs +++ b/resources/benchmark-runner.mjs @@ -1,5 +1,6 @@ import { Metric } from "./metric.mjs"; import { params } from "./shared/params.mjs"; +import { forceLayout } from "./shared/helpers.mjs"; import { SUITE_RUNNER_LOOKUP } from "./suite-runner.mjs"; const performance = globalThis.performance; @@ -30,8 +31,9 @@ class Page { } layout() { - const body = this._frame.contentDocument.body.getBoundingClientRect(); - this.layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0); + const body = this._frame ? this._frame.contentDocument.body : document.body; + const value = forceLayout(body, params.layoutMode); + body._leakedLayoutValue = value; // Prevent dead code elimination. } async waitForElement(selector) { diff --git a/resources/developer-mode.mjs b/resources/developer-mode.mjs index 2b139f56f..3aa89ce02 100644 --- a/resources/developer-mode.mjs +++ b/resources/developer-mode.mjs @@ -1,5 +1,5 @@ import { Suites, Tags } from "./tests.mjs"; -import { params } from "./shared/params.mjs"; +import { params, LAYOUT_MODES } from "./shared/params.mjs"; export function createDeveloperModeContainer() { const container = document.createElement("div"); @@ -23,6 +23,7 @@ export function createDeveloperModeContainer() { settings.append(createUIForWarmupBeforeSync()); settings.append(createUIForSyncStepDelay()); settings.append(createUIForAsyncSteps()); + settings.append(createUIForLayoutMode()); content.append(document.createElement("hr")); content.append(settings); @@ -114,6 +115,31 @@ function createTimeRangeUI(labelText, paramKey, unit = "ms", min = 0, max = 1000 return label; } +function createUIForLayoutMode() { + return createSelectUI("Force layout mode", params.layoutMode, LAYOUT_MODES, (value) => { + params.layoutMode = value; + }); +} + +function createSelectUI(labelValue, initialValue, choices, paramsUpdateCallback) { + const select = document.createElement("select"); + select.onchange = () => { + paramsUpdateCallback(select.value); + updateURL(); + }; + + choices.forEach((choice) => { + const option = new Option(choice, choice); + select.add(option); + }); + select.value = initialValue; + + const label = document.createElement("label"); + label.append(span(labelValue), select); + + return label; +} + function createUIForSuites() { const control = document.createElement("nav"); control.className = "suites"; diff --git a/resources/shared/helpers.mjs b/resources/shared/helpers.mjs index 729d5f3a4..735430f34 100644 --- a/resources/shared/helpers.mjs +++ b/resources/shared/helpers.mjs @@ -23,8 +23,15 @@ export function getAllElements(selector, path = [], lookupStartNode = document) return elements; } -export function forceLayout() { - const rect = document.body.getBoundingClientRect(); - const e = document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0); - return e; +export function forceLayout(body, layoutMode = "getBoundingRectAndElementFromPoint") { + body ??= document.body; + const rect = body.getBoundingClientRect(); + switch (layoutMode) { + case "getBoundingRectAndElementFromPoint": + return document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0); + case "getBoundingClientRect": + return rect.height; + default: + throw Error(`Invalid layoutMode: ${layoutMode}`); + } } diff --git a/resources/shared/params.mjs b/resources/shared/params.mjs index c01debadb..32fc70d85 100644 --- a/resources/shared/params.mjs +++ b/resources/shared/params.mjs @@ -1,3 +1,5 @@ +export const LAYOUT_MODES = Object.freeze(["getBoundingClientRect", "getBoundingRectAndElementFromPoint"]); + export class Params { viewport = { width: 800, @@ -27,6 +29,8 @@ export class Params { // "generate": generate a random seed // : use the provided integer as a seed shuffleSeed = "off"; + // Choices: "getBoundingClientRect" or "getBoundingRectAndElementFromPoint" + layoutMode = LAYOUT_MODES[0]; // Measure more workload prepare time. measurePrepare = false; @@ -57,8 +61,9 @@ export class Params { this.useAsyncSteps = this._parseBooleanParam(searchParams, "useAsyncSteps"); this.waitBeforeSync = this._parseIntParam(searchParams, "waitBeforeSync", 0); this.warmupBeforeSync = this._parseIntParam(searchParams, "warmupBeforeSync", 0); - this.measurementMethod = this._parseMeasurementMethod(searchParams); + this.measurementMethod = this._parseEnumParam(searchParams, "measurementMethod", ["raf"]); this.shuffleSeed = this._parseShuffleSeed(searchParams); + this.layoutMode = this._parseEnumParam(searchParams, "layoutMode", LAYOUT_MODES); this.measurePrepare = this._parseBooleanParam(searchParams, "measurePrepare"); const unused = Array.from(searchParams.keys()); @@ -124,14 +129,14 @@ export class Params { return tags; } - _parseMeasurementMethod(searchParams) { - if (!searchParams.has("measurementMethod")) - return defaultParams.measurementMethod; - const measurementMethod = searchParams.get("measurementMethod"); - if (measurementMethod !== "raf") - throw new Error(`Invalid measurement method: '${measurementMethod}', must be 'raf'.`); - searchParams.delete("measurementMethod"); - return measurementMethod; + _parseEnumParam(searchParams, paramKey, enumArray) { + if (!searchParams.has(paramKey)) + return defaultParams[paramKey]; + const value = searchParams.get(paramKey); + if (!enumArray.includes(value)) + throw new Error(`Got invalid ${paramKey}: '${value}', choices are ${enumArray}`); + searchParams.delete(paramKey); + return value; } _parseShuffleSeed(searchParams) { diff --git a/resources/shared/test-runner.mjs b/resources/shared/test-runner.mjs index 52defcb36..6c606d5f2 100644 --- a/resources/shared/test-runner.mjs +++ b/resources/shared/test-runner.mjs @@ -67,12 +67,9 @@ export class TestRunner { asyncStartTime = syncEndTime; }; const measureAsync = () => { - const bodyReference = this.#frame ? this.#frame.contentDocument.body : document.body; - const windowReference = this.#frame ? this.#frame.contentWindow : window; // Some browsers don't immediately update the layout for paint. // Force the layout here to ensure we're measuring the layout time. - const height = bodyReference.getBoundingClientRect().height; - windowReference._unusedHeightValue = height; // Prevent dead code elimination. + this.page.layout(); const asyncEndTime = performance.now(); performance.mark(asyncEndLabel);