Skip to content

perf(android): Defer SentryFrameMetricsCollector thread startup#5641

Open
runningcode wants to merge 2 commits into
mainfrom
no/perf-android-defer-framemetrics-thread
Open

perf(android): Defer SentryFrameMetricsCollector thread startup#5641
runningcode wants to merge 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.

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>
@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.45.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 315.04 ms 356.40 ms 41.36 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
18c0bc2 306.73 ms 349.77 ms 43.03 ms
0eaac1e 316.82 ms 357.34 ms 40.52 ms
d15471f 303.49 ms 439.08 ms 135.59 ms
fc5ccaf 276.52 ms 370.46 ms 93.93 ms
e2dce0b 308.96 ms 360.10 ms 51.14 ms
5b1a06b 352.27 ms 413.70 ms 61.43 ms
37ec571 366.04 ms 424.28 ms 58.23 ms
9fbb112 361.43 ms 427.57 ms 66.14 ms
bbc35bb 324.88 ms 425.73 ms 100.85 ms
ff8eea4 313.42 ms 337.08 ms 23.66 ms

App size

Revision Plain With Sentry Diff
18c0bc2 1.58 MiB 2.13 MiB 557.33 KiB
0eaac1e 1.58 MiB 2.19 MiB 619.17 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
fc5ccaf 1.58 MiB 2.13 MiB 557.54 KiB
e2dce0b 0 B 0 B 0 B
5b1a06b 0 B 0 B 0 B
37ec571 0 B 0 B 0 B
9fbb112 1.58 MiB 2.11 MiB 539.18 KiB
bbc35bb 1.58 MiB 2.12 MiB 553.01 KiB
ff8eea4 1.58 MiB 2.28 MiB 718.64 KiB

@runningcode runningcode marked this pull request as ready for review June 25, 2026 13:09
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