Skip to content

perf(android): Defer SentryFrameMetricsCollector thread startup#5641

Merged
runningcode merged 2 commits into
mainfrom
no/perf-android-defer-framemetrics-thread
Jun 30, 2026
Merged

perf(android): Defer SentryFrameMetricsCollector thread startup#5641
runningcode merged 2 commits into
mainfrom
no/perf-android-defer-framemetrics-thread

Conversation

@runningcode

@runningcode runningcode commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Resolves JAVA-535

📜 Description

SentryFrameMetricsCollector created and started its HandlerThread in the constructor and then called new Handler(handlerThread.getLooper()). getLooper() blocks the caller until the new thread's looper is ready — and the collector is constructed on the main thread during SentryAndroid.init (loadDefaultAndMetadataOptions).

The handler is only ever used by trackCurrentWindow(), which no-ops until a listener is registered via startCollection(). So this starts the HandlerThread lazily on the first startCollection() (profiling / SpanFrameMetricsCollector). The constructor still registers the activity-lifecycle callback (current-window tracking is unchanged) but no longer blocks. Apps that never collect frame metrics never start the thread at all.

Reusing the SDK's shared executor isn't viable here: addOnFrameMetricsAvailableListener requires a dedicated non-main-thread Handler (Looper), which the shared ScheduledExecutorService doesn't provide — so the right move is to defer the dedicated thread, not share one.

💡 Motivation and Context

From the customer-provided Perfetto trace (sentryinvest): the main thread blocked ~5.75 ms on HandlerThread.getLooper() during init via SentryFrameMetricsCollector.<init>.

💚 How did you test it?

SentryFrameMetricsCollectorTest (incl. a new test asserting handler is null after construction and non-null after startCollection); full sentry-android-core suite passes.

Pixel 3 (Android 12) benchmark — ART method trace → Perfetto trace_processor:

construction HandlerThread.getLooper() blocking Object.wait()
old (pre-change) 1 1
new (deferred) 0 0

The synchronous main-thread wait for the frame-metrics looper is entirely removed from construction.

Cold-start A/B — sentry-samples-android (release, Pixel 3, 15 cold starts each via am start -W -S, TotalTime ms, first cold-disk run dropped):

median mean min stdev
with change 434 434 416 9
without change 441 459 421 49

Directionally faster on every statistic, but within end-to-end cold-start noise. Note this sample auto-inits Sentry and calls Sentry.startProfiler() in onCreate, so frame collection (the lazy-thread trigger) fires during startup either way — the change just moves when within startup. So this isn't a strong end-to-end showcase; the deterministic proof is the ART trace above. The real win is for apps that don't start frame collection at startup (the thread is then never created at init).

⚠️ Caveat: these runs were captured in a very warm room (~34 °C), so thermal throttling may have affected the absolute timings and likely contributed to the elevated variance / spikes in the "without change" run (stdev 49 vs 9).

📝 Checklist

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • No breaking change or entry added to the changelog.

@linear-code

linear-code Bot commented Jun 25, 2026

Copy link
Copy Markdown

JAVA-535

@sentry

sentry Bot commented Jun 25, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 376.94 ms 446.28 ms 69.34 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
1edbdfa 364.77 ms 450.29 ms 85.52 ms
a416a65 333.78 ms 410.37 ms 76.59 ms
d15471f 302.62 ms 353.84 ms 51.22 ms
22f4345 314.79 ms 375.02 ms 60.23 ms
6b019b7 343.31 ms 417.23 ms 73.91 ms
22f4345 312.78 ms 347.40 ms 34.62 ms
d217708 411.22 ms 430.86 ms 19.63 ms
319f256 315.96 ms 372.96 ms 57.00 ms
e2dce0b 308.96 ms 360.10 ms 51.14 ms
8558cac 306.16 ms 355.24 ms 49.09 ms

App size

Revision Plain With Sentry Diff
1edbdfa 1.58 MiB 2.20 MiB 635.34 KiB
a416a65 1.58 MiB 2.12 MiB 555.26 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
6b019b7 0 B 0 B 0 B
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
d217708 1.58 MiB 2.10 MiB 532.97 KiB
319f256 1.58 MiB 2.19 MiB 619.79 KiB
e2dce0b 0 B 0 B 0 B
8558cac 0 B 0 B 0 B

@runningcode runningcode marked this pull request as ready for review June 25, 2026 13:09

@0xadam-brown 0xadam-brown 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.

Nice! That's a real win 🧵 ⚡ 💯

One question for your consideration; otherwise lgtm.

@romtsn romtsn 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.

love how easy and impactful these changes are 🚀

@runningcode runningcode force-pushed the no/perf-android-defer-framemetrics-thread branch from b557893 to fd5a77c Compare June 30, 2026 09:40
@runningcode runningcode enabled auto-merge (squash) June 30, 2026 09:41
runningcode and others added 2 commits June 30, 2026 11:47
SentryFrameMetricsCollector created and started its HandlerThread in the
constructor, blocking the calling thread (the main thread during SDK
init) on HandlerThread.getLooper(). The handler is only needed once
startCollection() registers a listener, so start the thread lazily there
instead. Apps that never collect frame metrics no longer start the
thread at all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@runningcode runningcode force-pushed the no/perf-android-defer-framemetrics-thread branch from fd5a77c to 647105c Compare June 30, 2026 09:48
@runningcode runningcode merged commit 3859a2c into main Jun 30, 2026
70 checks passed
@runningcode runningcode deleted the no/perf-android-defer-framemetrics-thread branch June 30, 2026 09: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.

3 participants