perf: Avoid per-transaction Timer thread in SentryTracer (JAVA-570)#5670
Draft
runningcode wants to merge 2 commits into
Draft
perf: Avoid per-transaction Timer thread in SentryTracer (JAVA-570)#5670runningcode wants to merge 2 commits into
runningcode wants to merge 2 commits into
Conversation
📲 Install BuildsAndroid
|
Contributor
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| abfcc92 | 337.38 ms | 427.39 ms | 90.00 ms |
| 4e3e79d | 369.55 ms | 418.39 ms | 48.83 ms |
| 9054d65 | 330.94 ms | 403.24 ms | 72.30 ms |
| a416a65 | 316.52 ms | 359.67 ms | 43.15 ms |
| d15471f | 315.61 ms | 360.22 ms | 44.61 ms |
| 22f4345 | 325.23 ms | 454.66 ms | 129.43 ms |
| 6b019b7 | 403.90 ms | 546.09 ms | 142.19 ms |
| d217708 | 409.83 ms | 474.72 ms | 64.89 ms |
| 52feca7 | 314.77 ms | 378.67 ms | 63.90 ms |
| f634d01 | 375.06 ms | 420.04 ms | 44.98 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| abfcc92 | 1.58 MiB | 2.13 MiB | 557.31 KiB |
| 4e3e79d | 0 B | 0 B | 0 B |
| 9054d65 | 1.58 MiB | 2.29 MiB | 723.38 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 |
| d217708 | 1.58 MiB | 2.10 MiB | 532.97 KiB |
| 52feca7 | 0 B | 0 B | 0 B |
| f634d01 | 1.58 MiB | 2.10 MiB | 533.40 KiB |
83219ca to
f823179
Compare
Transactions with an idle or deadline timeout each created a java.util.Timer, which spawns a thread synchronously on the calling thread (often the main thread on Android). At scale (screen loads, HTTP spans) this was the dominant source of SDK thread churn. Schedule the idle/deadline timeouts on a dedicated, shared ISentryExecutorService held by SentryOptions instead, so no thread is created per transaction. It is kept separate from the main executor so timeout callbacks (which finish transactions) don't contend with cached event sending, and it is not prewarmed: its single worker thread is spawned lazily on the first scheduled timeout and reused thereafter. The dedicated executor uses removeOnCancelPolicy so cancelled timeouts (idle timers are rescheduled per child span) don't accumulate in its queue. On finish only the scheduled futures are cancelled; the executor is closed with the SDK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f823179 to
1835215
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📜 Description
SentryTracercreated ajava.util.Timer(new Timer(true)) for every transaction configured with an idle or deadline timeout.new Timer(...)spawns a dedicated thread synchronously on the calling thread — often the main thread on Android — and each such transaction got its own throwaway thread.This change schedules the idle/deadline timeouts on a dedicated, shared
ISentryExecutorServiceinstead:SentryOptions.getTimerExecutorService()— a singleSentryExecutorServicecreated inactivate(), kept separate from the main executor so timeout callbacks (which finish transactions) don't contend with cached-event sending.SentryTracernow uses cancellableFutures (idleTimeoutFuture/deadlineTimeoutFuture) scheduled on that executor. On finish only the futures are cancelled; the executor is closed with the SDK (Scopes.close) and recreated on re-init if it was closed.💡 Motivation and Context
At scale (screen loads, HTTP spans, user-interaction transactions) the per-transaction
Timerwas the dominant source of SDK thread churn, and the thread creation happened on the caller's thread. This is item #4 ("Note B") of the thread/executor audit (JAVA-570 / SDK-1347) and is the biggest thread-count win in that effort. Using a dedicated executor (rather than the main one) keeps transaction-finishing work off the executor that sends cached events.💚 How did you test it?
SentryTracerTestidle/deadline coverage to the newFuture-based scheduling; the "timer executor shut down" cases now close the dedicated executor to exercise the immediate-finish fallback.ActivityLifecycleIntegrationTestidle-timeout test now installs a realtimerExecutorService.:sentry:testand:sentry-android-core:testDebugUnitTestpass.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Follow-up audit items can fold the remaining per-
Timersites (DefaultCompositePerformanceCollector,RateLimiter,LifecycleWatcher) onto the same dedicated scheduler in a separate cleanup pass.