perf(tooling): measure animations in context and at document scale
Adds fixture variants that keep an identical transform animation and vary only its surroundings — inside a button, under a filtered, clipped, blurred, transformed or faded ancestor — plus the repository's own spinner overrides isolated piece by piece. Adds --filler, which pads the page with static elements, because a variant that costs nothing on a small page is not proven free in a real document. All of them measure zero style recalculations per second, including at 15,000 filler elements, which rules out ancestor context, the custom keyframes, transform-box and document size as explanations for the cost the same spinner shows inside the application.
This commit is contained in:
@@ -41,6 +41,56 @@
|
|||||||
.v-box-shadow .cell { animation: oc-box-shadow 1.5s linear infinite; }
|
.v-box-shadow .cell { animation: oc-box-shadow 1.5s linear infinite; }
|
||||||
.v-filter svg { animation: oc-filter 1.5s linear infinite; }
|
.v-filter svg { animation: oc-filter 1.5s linear infinite; }
|
||||||
.v-width svg { animation: oc-width 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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -49,17 +99,35 @@
|
|||||||
const SPINNER = '<svg viewBox="0 0 24 24" fill="none" stroke="#8ab" stroke-width="2">'
|
const SPINNER = '<svg viewBox="0 0 24 24" fill="none" stroke="#8ab" stroke-width="2">'
|
||||||
+ '<circle cx="12" cy="12" r="9" stroke-opacity=".25"/><path d="M21 12a9 9 0 0 0-9-9"/></svg>';
|
+ '<circle cx="12" cy="12" r="9" stroke-opacity=".25"/><path d="M21 12a9 9 0 0 0-9-9"/></svg>';
|
||||||
const params = new URLSearchParams(location.search);
|
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 variant = params.get('variant') || 'none';
|
||||||
const count = Math.max(1, Number(params.get('count') || 2));
|
const count = Math.max(1, Number(params.get('count') || 2));
|
||||||
const grid = document.getElementById('grid');
|
const grid = document.getElementById('grid');
|
||||||
grid.className = 'grid v-' + variant;
|
grid.className = 'grid v-' + variant;
|
||||||
const usesWrapper = variant.endsWith('-wrapper');
|
const usesWrapper = variant.endsWith('-wrapper');
|
||||||
|
const usesButton = variant === 'ctx-button' || variant === 'ctx-sibling-translatez';
|
||||||
for (let index = 0; index < count; index += 1) {
|
for (let index = 0; index < count; index += 1) {
|
||||||
const cell = document.createElement('div');
|
const cell = document.createElement('div');
|
||||||
cell.className = 'cell';
|
cell.className = 'cell';
|
||||||
cell.innerHTML = usesWrapper ? '<span class="wrap">' + SPINNER + '</span>' : SPINNER;
|
const spinner = SPINNER.replace('<svg', '<svg class="spin"');
|
||||||
|
if (usesWrapper) cell.innerHTML = '<span class="wrap">' + SPINNER + '</span>';
|
||||||
|
else if (usesButton) cell.innerHTML = '<button>' + spinner + '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" fill="#456"/></svg></button>';
|
||||||
|
else cell.innerHTML = SPINNER;
|
||||||
grid.appendChild(cell);
|
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 += '<div class="filler-row"><span>row ' + index + '</span><em>x</em></div>';
|
||||||
|
}
|
||||||
|
container.innerHTML = markup;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ const DEFAULT_VARIANTS = [
|
|||||||
"box-shadow",
|
"box-shadow",
|
||||||
"filter",
|
"filter",
|
||||||
"width",
|
"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]
|
const HELP = `Usage: bun run profile:animation -- [options]
|
||||||
@@ -58,6 +66,8 @@ Options:
|
|||||||
--count <n> Animated elements per variant (default: 2)
|
--count <n> Animated elements per variant (default: 2)
|
||||||
--duration <seconds> Measurement window per variant (default: 10)
|
--duration <seconds> Measurement window per variant (default: 10)
|
||||||
--settle <seconds> Wait before measuring each variant (default: 3)
|
--settle <seconds> Wait before measuring each variant (default: 3)
|
||||||
|
--filler <n> Static elements added to the page, to measure a variant
|
||||||
|
against a realistically sized document (default: 0)
|
||||||
--chrome <path> Chrome/Chromium executable
|
--chrome <path> Chrome/Chromium executable
|
||||||
--profile-dir <path> Reusable isolated Chrome profile
|
--profile-dir <path> Reusable isolated Chrome profile
|
||||||
--headed Show the browser (default: headless)
|
--headed Show the browser (default: headless)
|
||||||
@@ -73,6 +83,7 @@ const parseArgs = (argv) => {
|
|||||||
count: 2,
|
count: 2,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
settle: 3,
|
settle: 3,
|
||||||
|
filler: 0,
|
||||||
chrome: null,
|
chrome: null,
|
||||||
profileDir: join(homedir(), ".openchamber", "browser-profile-google-chrome"),
|
profileDir: join(homedir(), ".openchamber", "browser-profile-google-chrome"),
|
||||||
headless: true,
|
headless: true,
|
||||||
@@ -87,6 +98,7 @@ const parseArgs = (argv) => {
|
|||||||
else if (value === "--count") options.count = Number(argv[++index])
|
else if (value === "--count") options.count = Number(argv[++index])
|
||||||
else if (value === "--duration") options.duration = Number(argv[++index])
|
else if (value === "--duration") options.duration = Number(argv[++index])
|
||||||
else if (value === "--settle") options.settle = 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 === "--chrome") options.chrome = argv[++index]
|
||||||
else if (value === "--profile-dir") options.profileDir = argv[++index]
|
else if (value === "--profile-dir") options.profileDir = argv[++index]
|
||||||
else throw new Error(`Unknown option: ${value}`)
|
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`)
|
console.log(`Measuring ${options.variants.length} variants, ${options.count} element(s) each, ${options.duration}s per variant.\n`)
|
||||||
for (const variant of options.variants) {
|
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)
|
const measured = await measureVariant(client, url, options)
|
||||||
results.push({ variant, ...measured })
|
results.push({ variant, ...measured })
|
||||||
if (!options.json) {
|
if (!options.json) {
|
||||||
|
|||||||
Reference in New Issue
Block a user