Skip to content

[iOS] Don't report app backgrounding as a JS long task - #1530

Draft
cdn34dd wants to merge 1 commit into
developfrom
dd/fix/1523-ios-js-long-task-background-k7q2
Draft

cdn34dd wants to merge 1 commit into
developfrom
dd/fix/1523-ios-js-long-task-background-k7q2

Conversation

@cdn34dd

@cdn34dd cdn34dd commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Workflow Automation • View in Workflow Automation

Fixes #1523. On iOS, the JS refresh rate listener now pauses when the app resigns active and restarts when it becomes active again. The first frame after a restart begins a new measurement instead of being compared with the last frame from before the pause.

  • JSRefreshRateMonitor observes UIApplication.willResignActiveNotification and didBecomeActiveNotification. These now call the existing appWillResignActive and appDidBecomeActive hooks, which nothing called before. The handlers hop onto the shared SDK queue, because startMonitoring replaces the listener on that queue and the notifications arrive on the main thread.
  • JSRefreshRateListener.start() resets lastFrameTimestamp on the JS thread before it creates the new CADisplayLink.
  • The notification center, the queue and the listener factory can be injected, and frame handling is split into onFrame(timestamp:targetTimestamp:), so the behavior can be unit-tested.

Motivation

CADisplayLink stops ticking while the app is in the background, but lastFrameTimestamp was never reset. The first tick after foregrounding was therefore measured against the last tick before backgrounding. That produced one long_task.target: javascript event, plus a matching JS refresh rate sample, lasting the whole time the app was in the background. Android already handles this: FrameRateProvider.start() resets the frame time and is driven by onHostPause/onHostResume. The native iOS SDK's refresh rate reader does the same using the same two notifications.

Trade-off: like the native iOS SDK, JS frames are not measured while the app is inactive but still on screen, for example while Control Center or an incoming-call banner is showing.

Additional Notes

Testing:

  • Added JSRefreshRateMonitorTests:
    • the listener is stopped on willResignActive and restarted on didBecomeActive;
    • notifications that arrive before monitoring starts are no-ops;
    • after a stop and start, a 60 s gap between frames does not produce a frame-time callback.
  • The tests have not been run in Xcode or on a simulator yet; please run the iOS test target in CI or locally. The fix and the tests were compiled and run on Linux with Swift 5.10 against small UIKit/CADisplayLink stubs, where the Objective-C selector observers were replaced by block observers. All three tests passed with the fix. Removing the timestamp reset, or removing the notification wiring, made the matching test fail.
  • Not yet checked manually on a simulator: background the app with a non-zero longTaskThresholdMs, wait, foreground it, and confirm no JS long task appears for the background duration.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)
  • If this PR is auto-generated, please make sure also to manually update the code related to the change

🤖 Generated with Claude Code (link removed)

NOTE: some AI-generated links and images were removed to ensure safety.


PR by Bits - View session in Datadog

Comment @DataDog to request changes

The CADisplayLink used to measure JS frame times stops ticking while the
app is in the background, but lastFrameTimestamp was never reset, so the
first frame after resuming was reported as a JS long task spanning the
whole background duration. JSRefreshRateMonitor already had
appWillResignActive/appDidBecomeActive hooks, but nothing called them.

Observe UIApplication willResignActive/didBecomeActive to stop/restart
the listener, and reset lastFrameTimestamp on start, matching Android's
FrameRateProvider and the native iOS SDK.

Fixes #1523

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: mariedm <29802155+mariedm@users.noreply.github.com>
@datadog-prod-us1-5

Copy link
Copy Markdown

View session in Datadog

Bits Code status: ✅ Done

CI Auto-fix: Disabled | Enable

Comment @DataDog to request changes

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: backgrounding the app is reported as a JS long task lasting the entire background duration

2 participants