Skip to content

perf(android): Move start-reason binder off main thread (JAVA-616) - #5866

Closed
runningcode wants to merge 2 commits into
mainfrom
no/java-616-defer-start-reasons-off-main
Closed

perf(android): Move start-reason binder off main thread (JAVA-616)#5866
runningcode wants to merge 2 commits into
mainfrom
no/java-616-defer-start-reasons-off-main

Conversation

@runningcode

@runningcode runningcode commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📜 Description

On API 35+, AppStartMetrics.registerLifecycleCallbacks performed an ActivityManager.getHistoricalProcessStartReasons(1) binder call synchronously on the main thread. Under auto-init this runs at ContentProvider time (SentryPerformanceProvider.onAppLaunched), blocking the main thread on a system_server round-trip during the coldest part of app start.

This moves the lookup onto a short-lived background daemon thread. The Sentry executor doesn't exist this early (ContentProvider time), so a plain thread is used. cachedStartInfo is now volatile and consumed best-effort:

  • Cold/warm classification moves into onActivityCreated: it prefers the resolved ApplicationStartInfo and falls back to the existing pre-API-35 heuristic if the lookup hasn't resolved yet.
  • The headless-start path consults the resolved start info before defaulting to COLD.
  • getAppStartReason() returns null if the lookup hasn't resolved by the time it is read.

No public API change (apiDump clean).

💡 Motivation and Context

#5702. Fixes #5702.

The binder call stalled the main thread at the most latency-sensitive moment of startup. Measured on a Pixel 10 (API 36) across cold starts, the synchronous call cost ~0.5–2.2 ms of main-thread time (median ~0.7 ms, mean ~0.9 ms). Deferring it reclaims that time from the main thread.

💚 How did you test it?

  • AppStartMetricsTestApi35 unit tests updated to inject a synchronous executor and drive onActivityCreated; all pass.
  • On-device (Pixel 10, API 36, cold starts via force-stop → launch): before ≈ 0.5–2.2 ms on the main thread (median ~0.7 ms); after, the same work runs on the background thread and cachedStartInfo was ready before the first activity in every observed cold start — even with the sample's artificial start-padding removed — so cold/warm and app.start.reason are preserved in practice.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

  • Consider a follow-up to reuse a shared background executor once one is available that early, instead of a one-off thread.

@linear-code

linear-code Bot commented Jul 29, 2026

Copy link
Copy Markdown

JAVA-616

@sentry

sentry Bot commented Jul 29, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.51.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 292.85 ms 370.38 ms 77.53 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
d15471f 369.38 ms 459.08 ms 89.70 ms
bbc35bb 324.88 ms 425.73 ms 100.85 ms
b193867 319.59 ms 403.09 ms 83.50 ms
62b579c 299.75 ms 364.84 ms 65.09 ms
382d6c1 306.85 ms 368.70 ms 61.85 ms
48277cd 320.38 ms 379.90 ms 59.52 ms
5b1a06b 315.40 ms 353.33 ms 37.94 ms
ee747ae 415.92 ms 470.15 ms 54.23 ms
37ec571 366.04 ms 424.28 ms 58.23 ms
462dea2 277.68 ms 359.83 ms 82.15 ms

App size

Revision Plain With Sentry Diff
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
bbc35bb 1.58 MiB 2.12 MiB 553.01 KiB
b193867 1.58 MiB 2.19 MiB 620.00 KiB
62b579c 0 B 0 B 0 B
382d6c1 1.58 MiB 2.29 MiB 719.85 KiB
48277cd 0 B 0 B 0 B
5b1a06b 0 B 0 B 0 B
ee747ae 1.58 MiB 2.10 MiB 530.95 KiB
37ec571 0 B 0 B 0 B
462dea2 0 B 0 B 0 B

Previous results on branch: no/java-616-defer-start-reasons-off-main

Startup times

Revision Plain With Sentry Diff
7a6cf7e 320.28 ms 373.06 ms 52.78 ms

App size

Revision Plain With Sentry Diff
7a6cf7e 0 B 0 B 0 B

private volatile @Nullable ApplicationStartInfo cachedStartInfo;
// Runs the getHistoricalProcessStartReasons binder call off the main thread. The Sentry executor
// does not exist this early (ContentProvider time), so a plain daemon thread is used by default.
private @NotNull Executor startInfoExecutor =

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.

We could have the executor as a local variable and immediately call shut down after enqueuing like here, to avoid any dangling executors.

private @Nullable ApplicationStartInfo cachedStartInfo;
// Volatile: written from the background lookup thread, read from the main thread. Null until the
// deferred getHistoricalProcessStartReasons lookup resolves.
private volatile @Nullable ApplicationStartInfo cachedStartInfo;

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.

How about turning this into a LazyEvaluator<ApplicationStartInfo> cachedStartInfo? Then the executor can simple call get(), to trigger evaluation off the main thread.

getHistoricalProcessStartReasons is an API 35+ binder round-trip to
system_server that ran synchronously on the main thread at ContentProvider
time, stalling the coldest part of app start. Run it on a background thread
instead. The result is consumed best-effort: cold/warm falls back to the
pre-API-35 heuristic and app.start.reason is null if the lookup has not
resolved by the first activity.
@runningcode
runningcode force-pushed the no/java-616-defer-start-reasons-off-main branch from 89ad41e to b988cb3 Compare August 4, 2026 15:58
Comment thread CHANGELOG.md

### Performance

- Defer the API 35+ `getHistoricalProcessStartReasons` binder call to a background thread so it no longer blocks the main thread during app start ([#5866](https://github.com/getsentry/sentry-java/pull/5866))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • 🚫 The changelog entry seems to be part of an already released section ## 8.51.0.
    Consider moving the entry to the ## Unreleased section, please.

@runningcode

Copy link
Copy Markdown
Contributor Author

@markushi thanks for the feedback on this, with the changes to main that created merge conflicts and the fact that this can cause a timing issue on startup I will close this.

@runningcode runningcode closed this Aug 4, 2026
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.

Defer the API 35+ getHistoricalProcessStartReasons binder call off the main thread

2 participants