Skip to content

perf(core): Drop per-instance lock from SentryId and SpanId#5645

Draft
runningcode wants to merge 2 commits into
mainfrom
no/java-589-reduce-id-lock-overhead
Draft

perf(core): Drop per-instance lock from SentryId and SpanId#5645
runningcode wants to merge 2 commits into
mainfrom
no/java-589-reduce-id-lock-overhead

Conversation

@runningcode

@runningcode runningcode commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

📜 Description

SentryId and SpanId stored their string value behind a LazyEvaluator<String>. Each LazyEvaluator allocates an AutoClosableReentrantLock (a ReentrantLock subclass, including its internal Sync object) plus a capturing lambda — on every instance.

This change replaces the LazyEvaluator<String> in both classes with a plain volatile String guarded by a double-checked synchronized (this) block:

  • Eager (string-arg) constructors now assign the value directly — no lazy wrapper, no lock.
  • The no-arg / UUID constructors still defer UUID-string generation; SentryId keeps the UUID reference so toSentryIdString stays lazy.
  • Synchronization is retained (not a lock-free race) because UUID generation is non-idempotent — two racing threads must not produce different ids for the same instance.

No public API change (apiDump produces no diff).

💡 Motivation and Context

One SentryId is created per event/transaction and one SpanId per span, so these are among the most frequently allocated objects in the SDK. Paying for a per-instance ReentrantLock + lambda to guard a single String is pure overhead, and the eager string-arg constructors got no laziness benefit at all. Follow-up to the SDK Overhead Reduction work (#5499).

Resolves JAVA-589.

💚 How did you test it?

Existing SpanIdTest and SentryIdTest cover the lazy-generation and normalization behavior (UUID generated/normalized only once, not on init, etc.). The full :sentry unit test suite passes (3372/3373; the one failing SentryIdTest case is a pre-existing test-isolation artifact that fails identically on main and is unrelated to this change).

Benchmark

Measured against main using the real compiled classes on the same JVM (Temurin 17.0.16). Allocation is measured via ThreadMXBean.getThreadAllocatedBytes (exact TLAB accounting), with objects stored into a live array so the JIT can't scalar-replace them. Throughput is single-threaded, best-of-10 measured rounds after 5 warmup rounds of 1M ops each.

Scenario Bytes/op (before → after) Δ bytes Throughput ops/ms (before → after) Speedup
SpanId(String) eager 104 → 16 −88 (−85%) 93,580 → 458,935 4.9×
SpanId() lazy + toString 232 → 144 −88 (−38%) 41,587 → 65,556 1.6×
SentryId(String) eager 104 → 24 −80 (−77%) 138,296 → 312,029 2.3×
SentryId() lazy + toString 320 → 208 −112 (−35%) 26,472 → 34,084 1.3×
SentryId(UUID) lazy + toString 408 → 304 −104 (−25%) 4,430 → 4,579 1.03×

The ~88-byte reduction on eager SpanId accounts exactly for the removed object graph: LazyEvaluator (24B) + AutoClosableReentrantLock (16B) + its internal NonfairSync/AQS (32B) + the capturing lambda (16B). Eager constructors (hot deserialization/propagation paths) win biggest. The lazy paths still shed the lock machinery; SentryId(UUID) throughput barely moves because that path is dominated by UUID.randomUUID() (a SecureRandom draw), not the lock.

📝 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

Other LazyEvaluator/AutoClosableReentrantLock usages that guard genuinely light objects can be reviewed similarly as part of the SDK overhead reduction effort.

SentryId and SpanId stored their string value behind a
LazyEvaluator<String>, which allocates an AutoClosableReentrantLock
(a ReentrantLock with its internal Sync) plus a capturing lambda on
every instance. Since one SentryId is created per event/transaction
and one SpanId per span, this per-instance lock machinery is far
heavier than the single String it guards, and the eager string-arg
constructors gained no laziness at all.

Replace the LazyEvaluator with a plain volatile String guarded by a
double-checked synchronized(this) block. Eager constructors now assign
the value directly; the no-arg and UUID constructors still defer
UUID-string generation. Synchronization is retained because UUID
generation is non-idempotent and two racing threads must not produce
different ids.

Follow-up to the SDK Overhead Reduction work (#5499).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 25, 2026

Copy link
Copy Markdown

JAVA-589

@sentry

sentry Bot commented Jun 25, 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

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 317.23 ms 362.98 ms 45.75 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
18c0bc2 306.73 ms 349.77 ms 43.03 ms
0eaac1e 316.82 ms 357.34 ms 40.52 ms
d15471f 303.49 ms 439.08 ms 135.59 ms
fc5ccaf 276.52 ms 370.46 ms 93.93 ms
e2dce0b 308.96 ms 360.10 ms 51.14 ms
5b1a06b 352.27 ms 413.70 ms 61.43 ms
37ec571 366.04 ms 424.28 ms 58.23 ms
9fbb112 361.43 ms 427.57 ms 66.14 ms
bbc35bb 324.88 ms 425.73 ms 100.85 ms
ff8eea4 313.42 ms 337.08 ms 23.66 ms

App size

Revision Plain With Sentry Diff
18c0bc2 1.58 MiB 2.13 MiB 557.33 KiB
0eaac1e 1.58 MiB 2.19 MiB 619.17 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
fc5ccaf 1.58 MiB 2.13 MiB 557.54 KiB
e2dce0b 0 B 0 B 0 B
5b1a06b 0 B 0 B 0 B
37ec571 0 B 0 B 0 B
9fbb112 1.58 MiB 2.11 MiB 539.18 KiB
bbc35bb 1.58 MiB 2.12 MiB 553.01 KiB
ff8eea4 1.58 MiB 2.28 MiB 718.64 KiB

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