Skip to content

🐛 fix(frames): exclude background time from frozen_frame / long_task, cap the duration, one span per stall - #11

Merged
nitinstp23 merged 2 commits into
mainfrom
fix/frozen-frame-background
Sep 22, 2026
Merged

nitinstp23 merged 2 commits into
mainfrom
fix/frozen-frame-background

Conversation

@nitinstp23

Copy link
Copy Markdown
Contributor

Summary

Fixes B14-2083 (snabbit app-webview-rum: frozen_frame.duration up to 25 s, p90 ≈ 6 s on one screen). Also answers the client's question: the timer did not pause in the background.

  • Web (long-task.ts). Long tasks are PerformanceObserver entries. Ordinary hidden-tab throttling does not fabricate them, but an Android WebView renderer suspended by its host mid-task reports one task covering the whole suspension when it resumes. The tracker now records hidden / freeze / resume intervals (the anr.ts pattern) and drops any entry overlapping one, ignores buffered entries that ended before install, and caps frozen_frame.duration at the new frozenFrameMaxMs (default 10 s, min 700 ms) with frozen_frame.capped: true. long_task.duration keeps the measured value.
  • One stall, one span. Where PerformanceObserver.supportedEntryTypes includes long-animation-frame (Chrome 123+) the tracker no longer also observes longtask, which described the same stall twice (two long_task, two frozen_frame, two view.frozen_frame.count increments).
  • React Native (frame-metrics.ts). The rAF-gap loop had no AppState guard, so the first frame after resume was a "frozen frame" as long as the background stay. It now follows AppState, resets its baseline across a background stay, and applies the same cap. AppState is injectable, so the loop finally has unit tests.

Tests

  • long-task.test.ts (new): visible task → long_task + frozen_frame; task spanning a hidden interval dropped; task while hidden dropped; task before hidden kept; freeze/resume; pre-install buffered entry ignored; cap + flag; LoAF-only when supported, longtask fallback otherwise.
  • frame-metrics.test.ts (new, first native test): frozen frame while active; background gap not reported and baseline reset; frames while backgrounded ignored; cap + flag; no AppState available.
  • make ci green locally.

Docs

CHANGELOG [Unreleased], docs/configuration.md (frozenFrameMaxMs row, enableLongTaskDetection description), README frozen-frame row.

…ap the duration, one span per stall

B14-2083 (snabbit app-webview). Web long tasks come from
PerformanceObserver entries, and a WebView renderer suspended by its
host mid-task reports one task spanning the whole suspension when it
resumes, so a user switching apps produced 6-25 s frozen_frame spans.
The tracker now records hidden / frozen / suspended intervals and drops
any entry overlapping one, ignores buffered entries that ended before
install, and caps frozen_frame.duration at the new frozenFrameMaxMs
(default 10 s) with frozen_frame.capped=true. Where the browser has
Long Animation Frames it no longer also observes longtask, which
described the same stall a second time.

The React Native rAF loop had the same gap in a purer form: rAF stops
in the background and the first frame after resume arrived as one gap
the length of the whole stay. It now follows AppState, drops the gap
across a background stay, and applies the same cap. AppState is
injectable so the loop is unit-tested.
@nitinstp23

Copy link
Copy Markdown
Contributor Author

Verification against scout-rum-example-apps/webview-react-web

Integration build = #9 + #10 + #11 merged locally (make ci green, 345 tests), packed and installed into the harness.

Headless Chrome 153 via Playwright, 29/29 (sessionSampleRate: 100, OTLP exports intercepted and parsed):

  • Marker: written on load with sampled; visibilitychange:hidden clears it and the heartbeat keeps it cleared for 12.5 s; visible/freeze/resume re-arm; a seeded dead sampled session exports exactly one app_unclean_exit attributed to it (session.id = crash.previous_session_id), never app_crash; marker records reportedSessionId; an already-reported session and an unsampled session export nothing.
  • Android WebView UA: no marker written, a pending marker is not filed, other spans still export.
  • Array.prototype.at deleted before init: no TypeError, no error span, CLS/INP absent while TTFB/FCP/LCP report; an app error carries error.origin=app; an error whose top frame is the SDK module carries error.origin=sdk + sdk_internal, is exported once for three throws, and error.count stays at the one app error.
  • 1 s stall → one long_task (LoAF only) + one frozen_frame (1.0 s); a stall overlapping a hidden interval is dropped; an 11 s stall reports 10 s with frozen_frame.capped=true.

Real Android System WebView (emulator API 34, Chrome 113, webview-bridge-flutter mode=off, DevTools over adb), 7/7: UA has wv; Scout.instance.config.enableUncleanExitDetection === false; no marker key in localStorage; 1 s stall → one long_task + one frozen_frame; HOME → visibilityState=hidden + app_paused; return → app_resumed; no app_unclean_exit/app_crash.

Kill cycle with the marker forced on (VITE_SCOUT_UNCLEAN_EXIT=true), 6/6: first launch reports nothing; HOME → 16 s → am force-stop → relaunch: no app_unclean_exit; foreground force-stop → relaunch: exactly one; a second foreground kill of the same (resumed) session: none; never app_crash.

@nitinstp23
nitinstp23 marked this pull request as ready for review September 22, 2026 09:13
…ground

# Conflicts:
#	CHANGELOG.md
#	src/web/index.ts
@nitinstp23
nitinstp23 merged commit aaf98bf into main Sep 22, 2026
nitinstp23 added a commit that referenced this pull request Sep 22, 2026
The merge of origin/main into fix/frozen-frame-background left conflict
markers in src/web/index.ts and CHANGELOG.md, and #11 was merged with
them. index.ts keeps the guarded mount() installer and passes
frozenFrameMaxMs to the long-task tracker; the CHANGELOG stacks the
#9, #10 and #11 entries under [Unreleased]. make ci green (345 tests).
@nitinstp23
nitinstp23 deleted the fix/frozen-frame-background branch September 22, 2026 10:08
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.

1 participant