mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
fix(desktop): anchor the switch trace at the input event, not handler dispatch
Blind-review round 17 (Fable high) finding: startedAt sampled performance.now() at click-handler dispatch, silently excluding input delay — a click queued behind a long task under-reported by the whole queueing time, unbounded, in exactly the contention regime the tracer exists to expose. begin() now anchors at the dispatching event's timeStamp when one is present (window.event is set only during synchronous dispatch, so stale timestamps cannot leak in from async continuations; min() guards skewed clocks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Max Lampert <maxwell@squareup.com>
This commit is contained in:
@@ -458,6 +458,32 @@ test("a scheduled route-exit abandon canceled in the same task keeps the trace",
|
||||
);
|
||||
});
|
||||
|
||||
test("the trace anchors at the input event, not handler dispatch", async () => {
|
||||
await withSettleHarness(async ({ begin, settle, flush }) => {
|
||||
// Real-clock gap: earlier tests fired visibilitychange listeners, and a
|
||||
// back-dated anchor overlapping those timestamps is (correctly) dropped
|
||||
// by the hidden-window guard. Let them age out first.
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
// A click can sit queued behind a long task before its handler runs;
|
||||
// that input delay is felt switch latency and must be inside totalMs.
|
||||
// window.event is set only during synchronous dispatch, so this anchor
|
||||
// can never leak in from async continuations.
|
||||
globalThis.window.event = { timeStamp: performance.now() - 550 };
|
||||
begin("aaaa1111aaaa1111");
|
||||
delete globalThis.window.event;
|
||||
settle("aaaa1111aaaa1111");
|
||||
flush();
|
||||
const measure = performance
|
||||
.getEntriesByName("buzz:channel-switch:click-to-settled")
|
||||
.at(-1);
|
||||
assert.ok(measure, "measure recorded");
|
||||
assert.ok(
|
||||
measure.duration >= 550,
|
||||
`input delay must be inside the measure (got ${measure.duration})`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("beginning a switch revokes a pending route-exit abandon for that channel", async () => {
|
||||
await withSettleHarness(
|
||||
async ({ begin, scheduleAbandon, settle, flush, measures }) => {
|
||||
|
||||
@@ -252,9 +252,21 @@ export function beginChannelSwitchTrace(channelId: string): void {
|
||||
// cleanup's scheduled abandon pending; it must not kill the fresh trace
|
||||
// when its microtask drains.
|
||||
cancelRouteExitAbandon(channelId);
|
||||
// Anchor at the triggering input event when one is dispatching: a click
|
||||
// can sit queued behind a long task before its handler runs, and that
|
||||
// input delay is felt switch latency. window.event is set only during
|
||||
// synchronous dispatch, so a stale timestamp can never leak in from async
|
||||
// continuations; min() guards against skewed event clocks.
|
||||
const now = performance.now();
|
||||
const dispatchingEvent =
|
||||
typeof window === "undefined" ? undefined : window.event;
|
||||
const startedAt =
|
||||
dispatchingEvent && typeof dispatchingEvent.timeStamp === "number"
|
||||
? Math.min(dispatchingEvent.timeStamp, now)
|
||||
: now;
|
||||
activeTrace = {
|
||||
channelId,
|
||||
startedAt: performance.now(),
|
||||
startedAt,
|
||||
routeCommitAt: null,
|
||||
windowFetch: null,
|
||||
membersFetch: null,
|
||||
|
||||
Reference in New Issue
Block a user