Skip to content

feat(core): Replace SentryExecutorService with pre-allocated queue#5717

Draft
romtsn wants to merge 3 commits into
mainfrom
feat/preallocated-executor-queue
Draft

feat(core): Replace SentryExecutorService with pre-allocated queue#5717
romtsn wants to merge 3 commits into
mainfrom
feat/preallocated-executor-queue

Conversation

@romtsn

@romtsn romtsn commented Jul 3, 2026

Copy link
Copy Markdown
Member

Problem

ScheduledThreadPoolExecutor's internal DelayedWorkQueue is a min-heap that grows dynamically (50% per resize, starting from 16). prewarm() was introduced to pre-grow that array during Sentry.init — but init runs on the main thread, which is exactly when ANR risk is highest. The resize cost it avoids is only ~8µs; the prewarm itself adds ~100µs and is the more expensive operation.

More fundamentally, ScheduledThreadPoolExecutor's queue isn't replaceable, so there was no clean way to fix this without a custom implementation.

Solution

Replace SentryExecutorService with a custom implementation backed by:

  • PriorityQueue<ScheduledTask<?>> pre-allocated to INITIAL_QUEUE_CAPACITY = 64 slots at construction — the backing array never resizes during normal SDK operation
  • Single daemon worker thread that uses Object.wait(millis) / notifyAll() — sleeps precisely until the next task is due, wakes immediately when an earlier task is enqueued
  • ScheduledTask<T> extends FutureTask<T> — gets the full Future<T> contract for free

prewarm() is now a documented no-op. The MAX_QUEUE_SIZE = 271 hard limit and purge-on-overflow behaviour are preserved unchanged.

What changed

Before After
Queue impl ScheduledThreadPoolExecutor.DelayedWorkQueue (dynamic, unreplaceable) PriorityQueue pre-allocated to 64 slots
Resize on init Yes (prewarm triggers 3 resizes) Never
prewarm() Submits 40 far-future tasks + purge No-op
@TestOnly constructor Injects ScheduledThreadPoolExecutor Removed (nothing to inject)
Tests Mock-based delegation tests Behaviour tests (execution, scheduling, close, ordering)

Notes

  • ./gradlew spotlessApply apiDump must be run locally before merging — sandbox can't run the JVM
  • prewarm() is kept in the interface as @Deprecated for API compatibility; can be removed from ISentryExecutorService in a follow-up
  • Supersedes / makes unnecessary: perf(core): Remove executor prewarm #5681

cc @romtsn

--

View Junior Session in Sentry

romtsn added 2 commits July 3, 2026 14:16
…plementation

ScheduledThreadPoolExecutor's internal DelayedWorkQueue is a heap that
resizes dynamically (50% growth from initial capacity 16). prewarm() was
introduced to pre-grow that array during init, but doing so on the main
thread is itself the worst possible time to trigger allocations — and the
queue resize cost is only ~8µs anyway.

This replaces the whole approach: a custom executor backed by a
PriorityQueue pre-allocated to INITIAL_QUEUE_CAPACITY=64 at construction
time. The backing array never resizes during normal SDK operation. A single
daemon worker thread uses Object.wait/notifyAll for precise wakeup on
scheduled tasks. prewarm() becomes a documented no-op.

Key properties:
- No array resize at runtime: queue pre-allocated at construction
- Precise scheduling: worker sleeps until next task triggerTime, wakes
  immediately when an earlier task is enqueued
- MAX_QUEUE_SIZE (271) and purge-on-overflow semantics preserved
- ScheduledTask<T> extends FutureTask<T> for free Future<T> contract
- Drops @testonly ScheduledThreadPoolExecutor constructor (nothing to inject)

Refs #5681
Old tests verified delegation to a mocked ScheduledThreadPoolExecutor.
New tests verify actual executor behavior: task execution, scheduling,
close semantics, queue limit enforcement, cancelled-task purging, and
trigger-time ordering.
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
Fails
🚫 Please consider adding a changelog entry for the next release.
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

## Unreleased

### Features

- Replace SentryExecutorService with pre-allocated queue ([#5717](https://github.com/getsentry/sentry-java/pull/5717))

If none of the above apply, you can opt out of this check by adding #skip-changelog to the PR description or adding a skip-changelog label.

Generated by 🚫 dangerJS against d375d9e

@sentry

sentry Bot commented Jul 3, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 300.96 ms 360.30 ms 59.34 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
abfcc92 309.54 ms 380.32 ms 70.78 ms
4e3e79d 328.10 ms 395.64 ms 67.54 ms
8c1fb22 316.62 ms 352.78 ms 36.16 ms
b67bb28 307.59 ms 341.24 ms 33.65 ms
d15471f 361.89 ms 378.07 ms 16.18 ms
22f4345 313.52 ms 364.96 ms 51.44 ms
6727e14 337.22 ms 373.94 ms 36.71 ms
fc5ccaf 322.49 ms 405.25 ms 82.76 ms
22f4345 314.79 ms 375.02 ms 60.23 ms
2195398 319.02 ms 342.38 ms 23.36 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
8c1fb22 0 B 0 B 0 B
b67bb28 0 B 0 B 0 B
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
6727e14 1.58 MiB 2.28 MiB 718.64 KiB
fc5ccaf 1.58 MiB 2.13 MiB 557.54 KiB
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
2195398 0 B 0 B 0 B

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.

2 participants