mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 12:36:15 -08:00
test(perf): end the settle window at its scheduled deadline
A cap or quiet timer delayed by a busy main thread used the moment it ran as the settle point, so shifts after the 3 s cap could enter the settled counter. The settle point is now the deadline the timer was scheduled for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7a1003be55
commit
926d3040cb
3 files changed
+46
-5
No files matched your search
@@ -526,6 +526,37 @@ test('the cap ends the settle window while the workspace content keeps mutating'
|
||||
assert.doesNotThrow(() => assertJourneyRendererProbeState(state));
|
||||
});
|
||||
|
||||
test('a late cap timer ends the window at its scheduled deadline', async () => {
|
||||
const fixture = createFixture({
|
||||
settle: { ...FAST_SETTLE, capMs: 100, quietMs: 60_000 },
|
||||
});
|
||||
const [observer] = fixture.observers as [FakeObserver];
|
||||
const now = () => fixture.window.performance.now();
|
||||
renderFirstCard(fixture);
|
||||
await waitFor(() => fixture.rawState().final);
|
||||
const cutoffMs =
|
||||
(fixture.rawState().firstCardPaintEpochMs ?? 0) -
|
||||
fixture.window.performance.timeOrigin;
|
||||
observer.emit([layoutShift(now(), 0.25)]);
|
||||
// Hold the main thread past the cap so its timer runs late, and shift
|
||||
// the layout after the deadline while it is held.
|
||||
while (now() < cutoffMs + 250) {
|
||||
// busy
|
||||
}
|
||||
observer.emit([layoutShift(now(), 0.5)]);
|
||||
observer.queue.push(layoutShift(now(), 1));
|
||||
await waitFor(() => fixture.rawState().settle.status !== 'pending');
|
||||
const state = fixture.state();
|
||||
assert.equal(state.settle.status, 'cap');
|
||||
const settledAfterCutoff =
|
||||
(state.settle.epochMs ?? 0) - (state.firstCardPaintEpochMs ?? 0);
|
||||
assert.ok(
|
||||
settledAfterCutoff >= 100 && settledAfterCutoff < 150,
|
||||
`settle point at the deadline, not when the timer ran (${settledAfterCutoff} ms)`
|
||||
);
|
||||
assert.equal(state.counters.layoutShiftScoreSettled, 0.25);
|
||||
});
|
||||
|
||||
test('watches the document element when the settle root is missing', async () => {
|
||||
const fixture = createFixture({
|
||||
settle: { ...FAST_SETTLE, rootSelector: 'app-missing-root' },
|
||||
|
||||
@@ -421,12 +421,17 @@ export function journeyRendererProbeScript(
|
||||
};
|
||||
};
|
||||
// Starts at the first-card cutoff. Every mutation record under the root
|
||||
// restarts the quiet timer; the cap timer never moves.
|
||||
// restarts the quiet timer; the cap timer never moves. A timer can run
|
||||
// late on a busy main thread, so the settle point is the deadline it was
|
||||
// scheduled for, never the moment it ran: a late timer must not let
|
||||
// shifts after the deadline into the counter.
|
||||
const startSettle = (settle: JourneyRendererProbeSettleOptions) => {
|
||||
const root = document.querySelector(settle.rootSelector);
|
||||
state.settle.observedTarget =
|
||||
root === null ? 'documentElement' : 'root';
|
||||
let quietTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let quietDeadlineEpochMs = Number.POSITIVE_INFINITY;
|
||||
const capDeadlineEpochMs = epoch() + settle.capMs;
|
||||
const capTimer = setTimeout(() => end('cap'), settle.capMs);
|
||||
const settleObserver = new MutationObserver((records) => {
|
||||
if (state.settle.status !== 'pending') return;
|
||||
@@ -440,10 +445,13 @@ export function journeyRendererProbeScript(
|
||||
clearTimeout(capTimer);
|
||||
state.settle.domMutations += settleObserver.takeRecords().length;
|
||||
settleObserver.disconnect();
|
||||
endSettle(status, epoch());
|
||||
const deadline =
|
||||
status === 'cap' ? capDeadlineEpochMs : quietDeadlineEpochMs;
|
||||
endSettle(status, Math.min(epoch(), deadline));
|
||||
};
|
||||
const armQuiet = (): void => {
|
||||
clearTimeout(quietTimer);
|
||||
quietDeadlineEpochMs = epoch() + settle.quietMs;
|
||||
quietTimer = setTimeout(() => end('quiet'), settle.quietMs);
|
||||
};
|
||||
settleObserver.observe(root ?? document.documentElement ?? document, {
|
||||
|
||||
@@ -129,9 +129,11 @@ The settle window opens at the first-card cutoff. A second
|
||||
content pane (the document element if it is missing, reported in
|
||||
`evidence.settle.observedTarget`). The window closes when nothing in that
|
||||
subtree has mutated for 500 ms, or 3 s after the cutoff, whichever comes
|
||||
first. The settle point is sampled when the timer fires, and every entry
|
||||
that starts before it counts, including entries still queued in the
|
||||
observer. Why this point:
|
||||
first. The settle point is the deadline the firing timer was scheduled for
|
||||
(the last mutation plus 500 ms, or the cutoff plus 3 s), or the moment it
|
||||
ran if that is earlier, so a timer delayed by a busy main thread does not
|
||||
let later shifts in. Every entry that starts at or before the settle point
|
||||
counts, including entries still queued in the observer. Why this point:
|
||||
|
||||
- A DOM change in the content pane is what causes the shifts this counter
|
||||
is after (data resolving, skeletons swapped for content), so quiet in that
|
||||
|
||||
Reference in new issue
Block a user