Skip to content

RUM-16123: Move broadcast receiver dispatch off the main thread#3524

Merged
hamorillo merged 3 commits into
developfrom
hector.morilloprieto/RUM-16123
Jun 11, 2026
Merged

RUM-16123: Move broadcast receiver dispatch off the main thread#3524
hamorillo merged 3 commits into
developfrom
hector.morilloprieto/RUM-16123

Conversation

@hamorillo

Copy link
Copy Markdown
Contributor

What does this PR do?

Introduces BroadcastReceiverThread, a dedicated HandlerThread that serves as the dispatch thread for the SDK's BroadcastReceivers. The handler is passed to Context.registerReceiver via ThreadSafeReceiver, so CONNECTIVITY_ACTION, ACTION_BATTERY_CHANGED, and ACTION_POWER_SAVE_MODE_CHANGED callbacks are delivered on the background thread instead of the main thread.

Motivation

Fixes ANRs reported in #3480. Root cause: Context.registerReceiver(...) without a Handler argument dispatches onReceive on the main thread by default. On devices protected by PairIP (Google Play Integrity Code Transparency), each onReceive call incurs non-deterministic overhead from com.pairip.VMRunner.executeVM on the main thread looper, causing ANRs. PR #3420 attempted to fix this by trampolining onReceive body through an executor, but PairIP wraps onReceive at entry on the main thread — before the trampoline could offload work — so the ANRs persisted. This PR reverts that executor-trampoline approach and takes a different one instead: passing a background Handler to registerReceiver so the framework never dispatches on the main thread at all. The @Volatile fields on networkInfo and systemInfo introduced in #3420 are retained for cross-thread visibility.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • 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)

The trampoline introduced in #3420 didn't fix the ANRs (see #3480) — PairIP wraps onReceive() at entry on the main thread, before the trampoline can offload its body. Remove the executorService parameters and wiring on both BroadcastReceiver*InfoProviders ahead of a follow-up commit that registers the receivers with a background Handler. Keep @volatile on systemInfo, networkInfo, and lastNetworkInfo since they're independently useful for cross-thread visibility once dispatch moves off-main.
Introduce BroadcastReceiverThread, a dedicated HandlerThread used to deliver CONNECTIVITY_ACTION, ACTION_BATTERY_CHANGED, and ACTION_POWER_SAVE_MODE_CHANGED broadcasts to BroadcastReceiverNetworkInfoProvider and BroadcastReceiverSystemInfoProvider on a background thread instead of the main thread. The handler is passed to registerReceiver via ThreadSafeReceiver, and the thread is created in setupExecutors() and shut down in shutDownExecutors(). The @volatile fields on networkInfo and systemInfo continue to guarantee visibility for any reader thread.
@codecov-commenter

codecov-commenter commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.24%. Comparing base (0b3c64a) to head (1f1103a).
⚠️ Report is 12 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3524      +/-   ##
===========================================
+ Coverage    72.21%   72.24%   +0.03%     
===========================================
  Files          965      966       +1     
  Lines        35643    35652       +9     
  Branches      5948     5948              
===========================================
+ Hits         25739    25756      +17     
+ Misses        8278     8275       -3     
+ Partials      1626     1621       -5     
Files with missing lines Coverage Δ
...n/com/datadog/android/core/internal/CoreFeature.kt 87.26% <100.00%> (+0.17%) ⬆️
...l/net/info/BroadcastReceiverNetworkInfoProvider.kt 97.03% <100.00%> (+0.15%) ⬆️
...droid/core/internal/receiver/ThreadSafeReceiver.kt 100.00% <100.00%> (ø)
...rnal/system/BroadcastReceiverSystemInfoProvider.kt 96.43% <100.00%> (-0.12%) ⬇️
...id/core/internal/thread/BroadcastReceiverThread.kt 100.00% <100.00%> (ø)

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hamorillo hamorillo marked this pull request as ready for review June 11, 2026 07:06
@hamorillo hamorillo requested review from a team as code owners June 11, 2026 07:06
* Lifecycle: the underlying thread is started eagerly at construction time.
* Call [shutdown] when the SDK is torn down to release the thread.
*/
internal class BroadcastReceiverThread {

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.

This is a bit confusing, because this class is not a thread, despite having Thread in the name.

Maybe we can make it extend HandlerThread? internal class BroadcastReceiverThread: HandlerThread("datadog-broadcast-receiver-thread") without any properties and simply do Handler creation and wiring in CoreFeature? WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, make sense. 🙇‍♂️

1f1103a

Make BroadcastReceiverThread extend HandlerThread directly instead of wrapping one, removing the handler property and shutdown() wrapper. Create a single Handler from the thread's looper in setupInfoProviders() and share it across BroadcastReceiverSystemInfoProvider and BroadcastReceiverNetworkInfoProvider.
// region BroadcastReceiver

override fun onReceive(context: Context, intent: Intent?) {
executorService.executeSafe(HANDLE_INTENT_OPERATION_NAME, internalLogger) {

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.

Question about how ANR can happen with previous approach, since we offload immediately the onReceive from main thread to executorService, the blocking task which causes ANR must happen before onReceive is called?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct. My guess is that PairIP instruments onReceive at the method entry point — before any code in the body runs, including the executorService.executeSafe(...) call. The overhead is in the PairIP VM's instrumentation of the method itself on the main thread looper, not in anything the body does. So by the time the executor would offload the work, the ANR-causing delay has already occurred on the main thread. Passing a background Handler to registerReceiver moves the entire dispatch — including the PairIP entry instrumentation — off the main thread, which is why I hope this approach will work.

@hamorillo hamorillo merged commit edd7172 into develop Jun 11, 2026
27 checks passed
@hamorillo hamorillo deleted the hector.morilloprieto/RUM-16123 branch June 11, 2026 08:58
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.

5 participants