Skip to content

Fix negative inputDelay in INP attribution - #789

Merged
tunetheweb merged 1 commit into
GoogleChrome:mainfrom
suhailopensource:fix-inp-negative-input-delay
Aug 25, 2026
Merged

tunetheweb merged 1 commit into
GoogleChrome:mainfrom
suhailopensource:fix-inp-negative-input-delay

Conversation

@suhailopensource

Copy link
Copy Markdown
Contributor

Fixes #788.

Clamps group.processingStart to the interaction's own startTime in attributeINP(), as discussed in the issue:

const processingStart = Math.max(group.processingStart, firstEntry.startTime);

This keeps events with interactionId === 0 counted in processingDuration, per the subpart definitions you set out — input delay is non-handler code, processing is handler code including zero-interactionId events. I verified that directly: with an interaction starting at 1000 and an interactionId: 0 handler running 1050–1090, processingStart still resolves to 1050, so that handler's time lands in processingDuration and the entry stays in processedEventEntries. The clamp only discards zero-interactionId time that predates the interaction's own startTime, which was never part of its latency.

In the issue's repro this gives inputDelay: 0, processingDuration: 75, presentationDelay: 29 (was -98 / 173 / 29).

Worth noting the clamp can only raise processingStart as far as firstEntry.startTime, which is <= startTime + duration, so nextPaintTime cannot move. metric.value and presentationDelay are therefore unchanged, and only inputDelay and processingDuration shift by equal and opposite amounts — the three subparts still sum to metric.value.

first-input cleanup

Also drops the now-redundant || entry.entryType === 'first-input' clause. I confirmed in Chrome 151 that first-input exposes a non-zero interactionId (measured 7282 and 4643 in separate runs), and that it is still delivered when durationThreshold would filter the corresponding event entry — so the fallback for fast-only interactions is preserved.

Two notes on this part:

  • The same pattern appears in two places, lib/InteractionManager.ts:118 and attribution/onINP.ts:213. I simplified both for consistency; happy to drop the second if you'd rather keep this focused.
  • I could only verify the non-zero interactionId in Chrome. If Firefox or Safari still report first-input with interactionId: 0, the simplification would stop INP being reported at all on pages whose only interactions fall under durationThreshold. CI covers both browsers, so it should surface here, but flagging it since the failure mode is a missing metric rather than a wrong value.

Tests

test/unit/attribution-onINP-test.js (new) covers the subparts:

  • the negative-inputDelay scenario — fails on main with expected: 0, actual: -98, passes with the clamp
  • the same-interaction out-of-order case from Fix flakey tests #748, unchanged at 30 / 55 / 25

test/unit/InteractionManager-test.js gains two cases pinning the guard, since there was no existing coverage of the first-input path (grep first-input test/e2e/onINP-test.js returns nothing): a first-input entry with an interactionId is still treated as an INP candidate, and entries without one are skipped. Both pass under the old and new guard by design — they pin behaviour that must hold either way.

These are unit tests rather than e2e deliberately. The bug needs pointerdown.startTime to fall after another event's processingStart, and WebDriver dispatches a whole click with a single hardware timestamp — I tried, and the synthesized entries all share startTime, so an e2e test can't express it.

One thing I have not measured

The longestScript.subpart boundaries reduce to processingStart and processingEnd, so the clamp shifts them: a script starting before the interaction now falls in the input-delay bucket rather than processing-duration, in a bucket whose reported width is 0. I derived that from reading lines 351-358 rather than measuring it, since it needs LoAF entries. Flagging rather than asserting, in case you want it handled differently.

Verification

build, lint and format:check clean. test:unit 26/26. Full --browsers=chrome e2e: 5/5 spec files, 153 passing, 4 skipped. Reverting just the clamp leaves the new test failing and everything else green.

Clamp the frame-wide group.processingStart to the interaction's own startTime
so inputDelay can never be reported as negative. group.processingStart is the
minimum across every event presented in the frame, which can predate the
interaction when a non-interaction handler was still running.

Zero-interactionId events still contribute to processingDuration, and the
three subparts still sum to metric.value.

Also drops the now-redundant first-input checks, since first-input has
exposed a non-zero interactionId for some years, and adds unit coverage for
both the subparts and the first-input candidate path.

Fixes GoogleChrome#788

@tunetheweb tunetheweb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - thanks!

@tunetheweb
tunetheweb merged commit 99f128c into GoogleChrome:main Aug 25, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

INP attribution can report a negative inputDelay

2 participants