diff --git a/scripts/perf/animation-fixture.html b/scripts/perf/animation-fixture.html index 33cccb9c..a4b08ce0 100644 --- a/scripts/perf/animation-fixture.html +++ b/scripts/perf/animation-fixture.html @@ -41,6 +41,56 @@ .v-box-shadow .cell { animation: oc-box-shadow 1.5s linear infinite; } .v-filter svg { animation: oc-filter 1.5s linear infinite; } .v-width svg { animation: oc-width 1.5s linear infinite; } + + /* + Context variants. The same composited animation can stop being composited + because of an ancestor, so these reproduce the surroundings a spinner + actually lives in. Each keeps the identical transform animation and varies + only the context. + */ + .v-ctx-button .cell svg, + .v-ctx-filter .cell svg, + .v-ctx-overflow .cell svg, + .v-ctx-backdrop .cell svg, + .v-ctx-opacity .cell svg, + .v-ctx-transformed-parent .cell svg, + .v-ctx-sibling-translatez .cell svg, + .v-ctx-currentcolor .cell svg { animation: oc-transform-rotate 1s linear infinite; } + + /* Mirrors the repository rule that gives every other button SVG a GPU hint. */ + .v-ctx-sibling-translatez button svg:not(.spin) { transform: translateZ(0); } + + .v-ctx-filter .cell { filter: brightness(1.05); } + .v-ctx-overflow .cell { overflow: hidden; } + .v-ctx-backdrop .cell { backdrop-filter: blur(2px); } + .v-ctx-opacity .cell { opacity: 0.9; } + .v-ctx-transformed-parent .cell { transform: translateY(1px); } + .v-ctx-currentcolor .cell { color: color-mix(in srgb, #8ab 60%, #345); } + .v-ctx-currentcolor .cell svg { stroke: currentColor; } + + /* + The repository's own spinner rules, isolated. The application overrides + Tailwind's animate-spin with these, so each piece is measured separately to + find which one stops the rotation being composited. + */ + @keyframes oc-webkit-spin { + 0% { transform: translateZ(0) rotate(0deg); } + 100% { transform: translateZ(0) rotate(360deg); } + } + .v-app-keyframes svg { animation: oc-webkit-spin 1s linear infinite; } + .v-app-fillbox svg { + animation: oc-transform-rotate 1s linear infinite; + transform-box: fill-box; + transform-origin: 50% 50%; + } + .v-app-full svg { + animation: oc-webkit-spin 1s linear infinite; + will-change: transform; + transform-box: fill-box; + transform-origin: 50% 50%; + overflow: visible; + display: block; + } @@ -49,17 +99,35 @@ const SPINNER = '' + ''; const params = new URLSearchParams(location.search); + // Style recalculation cost depends on how much document there is to walk, so + // a variant that is free on a small page is not proven free in a real one. + const filler = Math.max(0, Number(params.get('filler') || 0)); const variant = params.get('variant') || 'none'; const count = Math.max(1, Number(params.get('count') || 2)); const grid = document.getElementById('grid'); grid.className = 'grid v-' + variant; const usesWrapper = variant.endsWith('-wrapper'); + const usesButton = variant === 'ctx-button' || variant === 'ctx-sibling-translatez'; for (let index = 0; index < count; index += 1) { const cell = document.createElement('div'); cell.className = 'cell'; - cell.innerHTML = usesWrapper ? '' + SPINNER + '' : SPINNER; + const spinner = SPINNER.replace(''; + else if (usesButton) cell.innerHTML = ''; + else cell.innerHTML = SPINNER; grid.appendChild(cell); } + + if (filler > 0) { + const container = document.createElement('div'); + container.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;'; + let markup = ''; + for (let index = 0; index < filler; index += 1) { + markup += '
row ' + index + 'x
'; + } + container.innerHTML = markup; + document.body.appendChild(container); + } diff --git a/scripts/profile-animation.mjs b/scripts/profile-animation.mjs index b0bdc632..ffe24d8d 100644 --- a/scripts/profile-animation.mjs +++ b/scripts/profile-animation.mjs @@ -47,6 +47,14 @@ const DEFAULT_VARIANTS = [ "box-shadow", "filter", "width", + "ctx-button", + "ctx-sibling-translatez", + "ctx-filter", + "ctx-overflow", + "ctx-backdrop", + "ctx-opacity", + "ctx-transformed-parent", + "ctx-currentcolor", ] const HELP = `Usage: bun run profile:animation -- [options] @@ -58,6 +66,8 @@ Options: --count Animated elements per variant (default: 2) --duration Measurement window per variant (default: 10) --settle Wait before measuring each variant (default: 3) + --filler Static elements added to the page, to measure a variant + against a realistically sized document (default: 0) --chrome Chrome/Chromium executable --profile-dir Reusable isolated Chrome profile --headed Show the browser (default: headless) @@ -73,6 +83,7 @@ const parseArgs = (argv) => { count: 2, duration: 10, settle: 3, + filler: 0, chrome: null, profileDir: join(homedir(), ".openchamber", "browser-profile-google-chrome"), headless: true, @@ -87,6 +98,7 @@ const parseArgs = (argv) => { else if (value === "--count") options.count = Number(argv[++index]) else if (value === "--duration") options.duration = Number(argv[++index]) else if (value === "--settle") options.settle = Number(argv[++index]) + else if (value === "--filler") options.filler = Number(argv[++index]) else if (value === "--chrome") options.chrome = argv[++index] else if (value === "--profile-dir") options.profileDir = argv[++index] else throw new Error(`Unknown option: ${value}`) @@ -170,7 +182,7 @@ const main = async () => { console.log(`Measuring ${options.variants.length} variants, ${options.count} element(s) each, ${options.duration}s per variant.\n`) for (const variant of options.variants) { - const url = `http://127.0.0.1:${fixturePort}/?variant=${encodeURIComponent(variant)}&count=${options.count}` + const url = `http://127.0.0.1:${fixturePort}/?variant=${encodeURIComponent(variant)}&count=${options.count}&filler=${options.filler}` const measured = await measureVariant(client, url, options) results.push({ variant, ...measured }) if (!options.json) {