Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions long-animation-frame/loaf-forced-style-duration.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<!DOCTYPE HTML>
<meta charset=utf-8>
<title>Long Animation Frame Timing: forcedStyleDuration</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: forcedStyleDuration</h1>
<div id="log"></div>
<script>

promise_test(async t => {
const [entry, script] = await expect_long_frame_with_script((t, busy_wait) => {
const element = document.createElement("div");
element.style.width = "100px";
document.body.appendChild(element);
t.add_cleanup(() => element.remove());

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");
assert_true("forcedStyleDuration" in script,
"forcedStyleDuration should be present on PerformanceScriptTiming");
assert_greater_than_equal(script.forcedStyleDuration, 0,
"forcedStyleDuration should be non-negative");
assert_less_than_equal(script.forcedStyleDuration,
script.forcedStyleAndLayoutDuration,
"forcedStyleDuration should be <= forcedStyleAndLayoutDuration");
}, "forcedStyleDuration attribute exists and is <= forcedStyleAndLayoutDuration");

promise_test(async t => {
const [entry, script] = await expect_long_frame_with_script((t, busy_wait) => {
t.step_timeout(() => {
// Just do a busy wait with no style/layout work
busy_wait(very_long_frame_duration);
}, 0);
}, script => script.invoker === "TimerHandler:setTimeout", t);

assert_true(!!entry, "Entry detected");
assert_true(!!script, "Script detected");
assert_equals(script.forcedStyleDuration, 0,
"forcedStyleDuration should be 0 when no forced style occurred");
assert_equals(script.forcedStyleAndLayoutDuration, 0,
"forcedStyleAndLayoutDuration should also be 0");
}, "forcedStyleDuration is 0 when no forced style recalculation 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, script] = await expect_long_frame_with_script((t, busy_wait) => {
t.step_timeout(() => {
busy_wait(very_long_frame_duration / 3);
// Force multiple style and layout recalculations by changing dimensions
for (let i = 0; i < 10; i++) {
element.style.width = (100 + i * 10) + "px";
void element.offsetHeight;
}
busy_wait(very_long_frame_duration / 3);
}, 0);
}, script => script.invoker === "TimerHandler:setTimeout", t);

assert_true(!!entry, "Entry detected");
assert_true(!!script, "Script detected");
// 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,
"forcedStyleAndLayoutDuration should be >= 0 after forced recalculations");
assert_greater_than_equal(script.forcedStyleDuration, 0,
"forcedStyleDuration should be >= 0 after forced style recalculations");
assert_less_than_equal(script.forcedStyleDuration,
script.forcedStyleAndLayoutDuration,
"forcedStyleDuration should be <= forcedStyleAndLayoutDuration");
}, "forcedStyleDuration accumulates multiple forced style recalculations");

promise_test(async t => {
const [entry, script] = await expect_long_frame_with_script((t, busy_wait) => {
const element = document.createElement("div");
element.style.width = "100px";
document.body.appendChild(element);
t.add_cleanup(() => element.remove());

t.step_timeout(() => {
busy_wait(very_long_frame_duration / 2);
// Force layout by changing dimensions and reading
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");
// When we force layout, we also force style
assert_greater_than_equal(script.forcedStyleAndLayoutDuration,
script.forcedStyleDuration,
"forcedStyleAndLayoutDuration >= forcedStyleDuration when layout is forced");
}, "forcedStyleDuration is part of forcedStyleAndLayoutDuration when layout is also forced");

</script>
</body>

272 changes: 272 additions & 0 deletions long-animation-frame/loaf-style-duration.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
<!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>

3 changes: 2 additions & 1 deletion long-animation-frame/loaf-toJSON.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
'renderStart',
'styleAndLayoutStart',
'blockingTime',
'firstUIEventTimestamp'
'firstUIEventTimestamp',
'styleDuration'
];
for (const key of performanceEntryKeys) {
assert_equals(entryJSON[key], entry[key],
Expand Down