Skip to content

perf: Avoid per-transaction Timer thread in SentryTracer (JAVA-570)#5670

Draft
runningcode wants to merge 2 commits into
mainfrom
no/tracer-shared-timer
Draft

perf: Avoid per-transaction Timer thread in SentryTracer (JAVA-570)#5670
runningcode wants to merge 2 commits into
mainfrom
no/tracer-shared-timer

Conversation

@runningcode

@runningcode runningcode commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

📜 Description

SentryTracer created a java.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 ISentryExecutorService instead:

  • New SentryOptions.getTimerExecutorService() — a single SentryExecutorService created in activate(), kept separate from the main executor so timeout callbacks (which finish transactions) don't contend with cached-event sending.
  • It is not prewarmed: its single worker thread is spawned lazily by the pool on the first scheduled timeout and then reused across all transactions (so no cost when timeouts are never used, and no per-transaction thread creation).
  • SentryTracer now uses cancellable Futures (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 Timer was 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?

  • Updated SentryTracerTest idle/deadline coverage to the new Future-based scheduling; the "timer executor shut down" cases now close the dedicated executor to exercise the immediate-finish fallback.
  • ActivityLifecycleIntegrationTest idle-timeout test now installs a real timerExecutorService.
  • Full :sentry:test and :sentry-android-core:testDebugUnitTest pass.

📝 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

Follow-up audit items can fold the remaining per-Timer sites (DefaultCompositePerformanceCollector, RateLimiter, LifecycleWatcher) onto the same dedicated scheduler in a separate cleanup pass.

@linear-code

linear-code Bot commented Jul 1, 2026

Copy link
Copy Markdown

JAVA-570

@sentry

sentry Bot commented Jul 1, 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 Jul 1, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 364.32 ms 439.22 ms 74.90 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

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

Previous results on branch: no/tracer-shared-timer

Startup times

Revision Plain With Sentry Diff
fde1218 312.75 ms 364.31 ms 51.56 ms

App size

Revision Plain With Sentry Diff
fde1218 0 B 0 B 0 B

@runningcode runningcode force-pushed the no/tracer-shared-timer branch from 83219ca to f823179 Compare July 1, 2026 14:49
runningcode and others added 2 commits July 1, 2026 17:00
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>
@runningcode runningcode force-pushed the no/tracer-shared-timer branch from f823179 to 1835215 Compare July 1, 2026 15:00
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