perf: Avoid per-transaction Timer thread in SentryTracer (JAVA-596)#5670
Merged
Conversation
📲 Install BuildsAndroid
|
Contributor
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6b019b7 | 403.90 ms | 546.09 ms | 142.19 ms |
| e63ad34 | 310.20 ms | 366.84 ms | 56.63 ms |
| 5865051 | 319.74 ms | 365.60 ms | 45.86 ms |
| a1eadfa | 345.67 ms | 411.26 ms | 65.59 ms |
| b193867 | 319.59 ms | 403.09 ms | 83.50 ms |
| 0eaac1e | 316.82 ms | 357.34 ms | 40.52 ms |
| 22ff2c7 | 306.60 ms | 336.65 ms | 30.05 ms |
| d15471f | 286.65 ms | 314.68 ms | 28.03 ms |
| 05aa61d | 310.43 ms | 372.40 ms | 61.97 ms |
| fcec2f2 | 311.35 ms | 384.94 ms | 73.59 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6b019b7 | 0 B | 0 B | 0 B |
| e63ad34 | 0 B | 0 B | 0 B |
| 5865051 | 0 B | 0 B | 0 B |
| a1eadfa | 0 B | 0 B | 0 B |
| b193867 | 1.58 MiB | 2.19 MiB | 620.00 KiB |
| 0eaac1e | 1.58 MiB | 2.19 MiB | 619.17 KiB |
| 22ff2c7 | 0 B | 0 B | 0 B |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| 05aa61d | 0 B | 0 B | 0 B |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.51 KiB |
1835215 to
ccb508e
Compare
markushi
approved these changes
Jul 6, 2026
markushi
left a comment
Member
There was a problem hiding this comment.
Nice one, looks great to me. Can't wait until we get rid of all timer tasks.
romtsn
reviewed
Jul 6, 2026
romtsn
reviewed
Jul 6, 2026
romtsn
approved these changes
Jul 6, 2026
romtsn
left a comment
Member
There was a problem hiding this comment.
couple of minor things but LGTM otherwise! great improvement
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>
The shared timer executor introduced for transaction idle/deadline timeouts was shut down on every Scopes.close(), including SDK restart. This cancelled the pending idle timeout of any transaction started before the restart (e.g. an in-flight activity transaction), so it never auto-finished and its envelope was never sent. Only close the timer executor on a full close, not on restart, matching the pre-existing per-transaction Timer behaviour. Enable core-thread timeout on the timer executor so the instance abandoned by a restart self-terminates once idle instead of leaking a thread. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ccb508e to
9a94c51
Compare
SentryTracer cached the shared timer executor in a field for the lifetime of the transaction. Replace that field with a boolean flag tracking whether timeouts may still be scheduled, and fetch the executor from the options each time one is scheduled. This ensures the tracer always uses the executor currently held by the options (e.g. the fresh one installed after an SDK restart) rather than a stale reference. Also make the timer executor's keep-alive duration a constructor argument backed by the named TIMER_KEEP_ALIVE_SECONDS constant, and raise it from 10s to 30s so the shared worker thread is less likely to be torn down and respawned between transactions under normal use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Fixes JAVA-596
Closes #5663
📜 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.SentryTracerschedules cancellableFutures (idleTimeoutFuture/deadlineTimeoutFuture) on that executor. It does not cache a reference to the executor: it tracks atimersEnabledflag and looks the executor up from the options each time it schedules, so it always uses the instance currently held by the options (e.g. the fresh one installed after an SDK restart) rather than a stale reference. On finish only the futures are cancelled.Scopes.closewhen not restarting). On restart it is intentionally left running so that pending idle/deadline timeouts of transactions started before the restart still fire and finish those transactions; the abandoned instance self-terminates once idle (allowCoreThreadTimeOut, 30s keep-alive).💡 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.ScopesTestcoverage for the executor lifecycle: a full close shuts the timer executor down, while a restart leaves it running so in-flight timeouts still fire.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.