Skip to content

perf(core): Detect Sentry executor thread without a name scan#5691

Open
runningcode wants to merge 2 commits into
mainfrom
no/java-607-executor-thread-marker
Open

perf(core): Detect Sentry executor thread without a name scan#5691
runningcode wants to merge 2 commits into
mainfrom
no/java-607-executor-thread-marker

Conversation

@runningcode

@runningcode runningcode commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Resolves JAVA-607.

Description

PersistingScopeObserver.serializeToDisk decided whether it was already on the Sentry executor thread by scanning the thread name:

if (Thread.currentThread().getName().contains("SentryExecutor")) { ... }

This runs on every scope mutation (breadcrumb, tag, trace, user, …), including on the main thread as the synchronous part of addBreadcrumb. The Thread.getName() + String.contains name scan is pure CPU overhead on that hot path.

This tags the executor threads with a package-private marker Thread subclass (thread name unchanged) and replaces the scan with a cheap SentryExecutorService.isSentryExecutorThread() identity check.

Note: the original rationale claimed this removed a String allocation. On ART, Thread.getName() returns the cached name field without allocating, so the measured win is CPU only (avoiding the scan), not an allocation reduction — see the Benchmark section.

API

Adds one method to the already @ApiStatus.Internal SentryExecutorService; apiDump regenerated (single-line .api addition).

Source

Found via on-device Perfetto method-trace analysis of the Android SDK scope-persistence path (~3,130 scope stores go through this gate). Part of Reduce SDK init time [Android].

🤖 Generated with Claude Code

Benchmark

Measured on-device with androidx Microbenchmark 1.4.1 (benchmark-junit4) on a Samsung Galaxy A55 (SM-A556B), Android API 36 (release build, non-minified).

The benchmark exercises the caller-side cost of a scope mutation with scope persistence enabled (enableScopePersistence = true, cacheDirPath set, no beforeBreadcrumb). The async serialize + disk write is dispatched to a no-op executor so it never runs on the measured thread — mirroring production, where it runs off-thread. Each branch is compared against its base commit (aab952b82e); the per-branch delta isolates this change. allocationCount is deterministic; timeNs has ~±10 ns run-to-run noise on a non-rooted device (unlocked clocks), so it is reported as a median and reproduced across 2 runs.

How it was done

A local, uncommitted androidx microbenchmark module (sentry-android-integration-tests/sentry-uitest-android-microbenchmark) depending on :sentry, with a BenchmarkRule looping the scope mutation. Run with:

./gradlew :sentry-android-integration-tests:sentry-uitest-android-microbenchmark:connectedReleaseAndroidTest \
  -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.suppressErrors=UNLOCKED

Result

Benchmark Metric main This PR Delta
setTag timeNs (median) ~127 ns ~61 ns ~-52%
addBreadcrumb timeNs (median) ~242 ns ~192 ns ~-20%
both allocationCount 7 / 3 7 / 3 none

Correction to the original rationale: on ART, Thread.getName() returns the cached name field without allocating a String, so this change does not reduce allocationCount. The win is pure CPU — avoiding the String.contains scan of the thread name on every scope mutation — which is substantial for cheap mutations like setTag (roughly halved). Reproduced across 2 runs.

@linear-code

linear-code Bot commented Jul 2, 2026

Copy link
Copy Markdown

JAVA-607

@runningcode runningcode force-pushed the no/java-607-executor-thread-marker branch from 9140184 to 2c5fa85 Compare July 2, 2026 16:53
@sentry

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

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 317.56 ms 361.66 ms 44.10 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
abfcc92 304.04 ms 370.33 ms 66.29 ms
4e3e79d 365.83 ms 477.62 ms 111.79 ms
d8912da 329.94 ms 389.68 ms 59.74 ms
d15471f 294.13 ms 399.49 ms 105.36 ms
abf451a 332.82 ms 403.67 ms 70.85 ms
604a261 380.65 ms 451.27 ms 70.62 ms
806307f 357.85 ms 424.64 ms 66.79 ms
22f4345 307.87 ms 354.51 ms 46.64 ms
d217708 375.27 ms 415.68 ms 40.41 ms
c3ee041 310.64 ms 361.90 ms 51.26 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
d8912da 0 B 0 B 0 B
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
abf451a 1.58 MiB 2.20 MiB 635.29 KiB
604a261 1.58 MiB 2.10 MiB 533.42 KiB
806307f 1.58 MiB 2.10 MiB 533.42 KiB
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
d217708 1.58 MiB 2.10 MiB 532.97 KiB
c3ee041 0 B 0 B 0 B

@runningcode runningcode marked this pull request as ready for review July 3, 2026 09:33
PersistingScopeObserver.serializeToDisk gated the on-executor fast path by
scanning Thread.currentThread().getName(), which allocates a String and
scans it on every scope mutation (breadcrumb, tag, trace, ...). Tag the
executor threads with a marker Thread subclass and expose an allocation-free
SentryExecutorService.isSentryExecutorThread() check instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@runningcode runningcode force-pushed the no/java-607-executor-thread-marker branch from 2c5fa85 to 57dc743 Compare July 3, 2026 09:34
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.

3 participants