Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE HTML>
<meta charset=utf-8>
<title>Long Animation Frame Timing: styleDuration</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<body>
<h1>Long Animation Frame: styleDuration</h1>
<div id="log"></div>
<script>
promise_test(async t => {
const entry = await expect_long_frame((t, busy_wait) => {
busy_wait(very_long_frame_duration);
}, t);
assert_not_equals(entry, "timeout", "Entry should be detected");
assert_true("styleDuration" in entry,
"styleDuration should be present on PerformanceLongAnimationFrameTiming");
assert_greater_than_equal(entry.styleDuration, 0,
"styleDuration should be non-negative");
}, "styleDuration attribute exists on PerformanceLongAnimationFrameTiming");
promise_test(async t => {
const entry = await expect_long_frame((t, busy_wait) => {
t.step_timeout(() => {
// Just do a busy wait with no rendering
busy_wait(very_long_frame_duration);
}, 0);
}, t);
assert_not_equals(entry, "timeout", "Entry should be detected");
// If no rendering occurred, styleDuration should be 0
if (entry.renderStart === 0) {
assert_equals(entry.styleDuration, 0,
"styleDuration should be 0 when no rendering occurred");
}
}, "styleDuration is 0 when no rendering phase occurs");
promise_test(async t => {
const element = document.createElement("div");
element.id = "test-element";
element.style.width = "100px";
document.body.appendChild(element);
t.add_cleanup(() => element.remove());
const entry = await expect_long_frame(async (t, busy_wait) => {
// Use requestAnimationFrame to trigger render-phase style
await new Promise(resolve => {
requestAnimationFrame(() => {
// Modify styles that will need recalculation during render
element.style.backgroundColor = "green";
element.style.height = "50px";
busy_wait(very_long_frame_duration);
resolve();
});
});
}, t);
assert_not_equals(entry, "timeout", "Entry should be detected");
assert_greater_than_equal(entry.styleDuration, 0,
"styleDuration should be non-negative");
// If there's a styleAndLayoutStart, rendering occurred
if (entry.styleAndLayoutStart > 0) {
// styleDuration captures render-phase style work
assert_greater_than_equal(entry.styleDuration, 0,
"styleDuration should capture render-phase style work");
}
}, "styleDuration captures render-phase style recalculation");
promise_test(async t => {
const entry = await expect_long_frame((t, busy_wait) => {
busy_wait(very_long_frame_duration);
}, t);
assert_not_equals(entry, "timeout", "Entry should be detected");
assert_less_than_equal(entry.styleDuration, entry.duration,
"styleDuration should not exceed total frame duration");
}, "styleDuration does not exceed frame duration");
promise_test(async t => {
const element = document.createElement("div");
element.style.width = "100px";
document.body.appendChild(element);
t.add_cleanup(() => element.remove());
const [entry, script] = await expect_long_frame_with_script((t, busy_wait) => {
t.step_timeout(() => {
busy_wait(very_long_frame_duration / 2);
// Force style and layout recalculation by changing dimensions
// and reading a layout-dependent property
element.style.width = "200px";
void element.offsetHeight;
busy_wait(very_long_frame_duration / 2);
}, 0);
}, script => script.invoker === "TimerHandler:setTimeout", t);
assert_true(!!entry, "Entry detected");
assert_true(!!script, "Script detected");
// styleDuration is render-phase only, so it should NOT include forced style
// The forced style is tracked in script.forcedStyleDuration instead
// On very fast machines, style/layout may complete in < 1ms and report as 0
// due to millisecond truncation, so we only assert >= 0
assert_greater_than_equal(script.forcedStyleAndLayoutDuration, 0,
"Script's forcedStyleAndLayoutDuration should be >= 0");
assert_greater_than_equal(script.forcedStyleDuration, 0,
"Script's forcedStyleDuration should be >= 0");
assert_less_than_equal(script.forcedStyleDuration,
script.forcedStyleAndLayoutDuration,
"forcedStyleDuration should be <= forcedStyleAndLayoutDuration");
// entry.styleDuration only includes render-phase style, not forced style
}, "styleDuration does not include forced style from scripts (tracked separately)");
promise_test(async t => {
const element = document.createElement("div");
element.id = "render-test";
element.style.width = "100px";
document.body.appendChild(element);
t.add_cleanup(() => element.remove());
const entry = await expect_long_frame(async (t, busy_wait) => {
// Trigger a render with style changes
element.className = "new-class";
await new Promise(resolve => {
requestAnimationFrame(() => {
busy_wait(very_long_frame_duration);
resolve();
});
});
}, t);
assert_not_equals(entry, "timeout", "Entry should be detected");
// When rendering occurs, styleDuration captures the style recalc time
if (entry.renderStart > 0 && entry.styleAndLayoutStart > 0) {
assert_greater_than_equal(entry.styleDuration, 0,
"styleDuration should be >= 0 during render phase");
}
}, "styleDuration is measured during the render phase");
promise_test(async t => {
const container = document.createElement("div");
container.style.width = "100px";
document.body.appendChild(container);
t.add_cleanup(() => container.remove());
const entry = await expect_long_frame(async (t, busy_wait) => {
await new Promise(resolve => {
const observer = new ResizeObserver(entries => {
// Do work in the resize observer callback but don't trigger
// additional resizes to avoid the "undelivered notifications" error
busy_wait(very_long_frame_duration / 2);
});
observer.observe(container);
// Trigger resize
requestAnimationFrame(() => {
container.style.width = "200px";
requestAnimationFrame(() => {
observer.disconnect();
resolve();
});
});
});
}, t);
assert_not_equals(entry, "timeout", "Entry should be detected");
// styleDuration should capture style recalculations from ResizeObserver
assert_greater_than_equal(entry.styleDuration, 0,
"styleDuration should be >= 0 during ResizeObserver callback");
// The styleAndLayoutStart should be set since rendering occurred
assert_greater_than(entry.styleAndLayoutStart, 0,
"styleAndLayoutStart should be > 0 when rendering occurred");
}, "styleDuration captures style recalculations during ResizeObserver callbacks");
promise_test(async t => {
// Create a container query scenario
const style = document.createElement("style");
style.textContent = `
.cq-container {
container-type: inline-size;
width: 200px;
}
@container (min-width: 150px) {
.cq-child {
background-color: red;
padding: 10px;
}
}
@container (min-width: 250px) {
.cq-child {
background-color: blue;
padding: 20px;
}
}
`;
document.head.appendChild(style);
t.add_cleanup(() => style.remove());
const container = document.createElement("div");
container.className = "cq-container";
const child = document.createElement("div");
child.className = "cq-child";
child.textContent = "Container query test";
container.appendChild(child);
document.body.appendChild(container);
t.add_cleanup(() => container.remove());
const entry = await expect_long_frame(async (t, busy_wait) => {
await new Promise(resolve => {
requestAnimationFrame(() => {
// Change container size to trigger container query recalculation
container.style.width = "300px";
busy_wait(very_long_frame_duration);
resolve();
});
});
}, t);
assert_not_equals(entry, "timeout", "Entry should be detected");
// styleDuration should capture container query style recalculations
assert_greater_than_equal(entry.styleDuration, 0,
"styleDuration should be >= 0 during container query recalculation");
}, "styleDuration captures style recalculations from container queries");
promise_test(async t => {
// Create multiple elements with ResizeObservers
const elements = [];
for (let i = 0; i < 3; i++) {
const el = document.createElement("div");
el.style.width = "100px";
el.style.height = "100px";
el.style.display = "inline-block";
document.body.appendChild(el);
elements.push(el);
}
t.add_cleanup(() => elements.forEach(el => el.remove()));
const entry = await expect_long_frame(async (t, busy_wait) => {
await new Promise(resolve => {
const observers = elements.map((el, idx) => {
const observer = new ResizeObserver(entries => {
// Just do work, don't trigger more resizes
busy_wait(very_long_frame_duration / 4);
});
observer.observe(el);
return observer;
});
requestAnimationFrame(() => {
// Resize all elements at once
elements.forEach((el, i) => {
el.style.width = (150 + i * 10) + "px";
});
requestAnimationFrame(() => {
observers.forEach(o => o.disconnect());
resolve();
});
});
});
}, t);
assert_not_equals(entry, "timeout", "Entry should be detected");
// styleDuration should capture style work from multiple ResizeObservers
assert_greater_than_equal(entry.styleDuration, 0,
"styleDuration should capture style work from multiple ResizeObservers");
}, "styleDuration captures style recalculations from multiple ResizeObservers");
</script>
</body>