perf(core): Drop per-instance lock from SentryId and SpanId#5645
Conversation
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 1edbdfa | 364.77 ms | 450.29 ms | 85.52 ms |
| a416a65 | 333.78 ms | 410.37 ms | 76.59 ms |
| d15471f | 302.62 ms | 353.84 ms | 51.22 ms |
| 22f4345 | 314.79 ms | 375.02 ms | 60.23 ms |
| 6b019b7 | 343.31 ms | 417.23 ms | 73.91 ms |
| 22f4345 | 312.78 ms | 347.40 ms | 34.62 ms |
| d217708 | 411.22 ms | 430.86 ms | 19.63 ms |
| 319f256 | 315.96 ms | 372.96 ms | 57.00 ms |
| e2dce0b | 308.96 ms | 360.10 ms | 51.14 ms |
| 8558cac | 306.16 ms | 355.24 ms | 49.09 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 1edbdfa | 1.58 MiB | 2.20 MiB | 635.34 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 |
| 22f4345 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| d217708 | 1.58 MiB | 2.10 MiB | 532.97 KiB |
| 319f256 | 1.58 MiB | 2.19 MiB | 619.79 KiB |
| e2dce0b | 0 B | 0 B | 0 B |
| 8558cac | 0 B | 0 B | 0 B |
8d0622c to
288b97d
Compare
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>
288b97d to
d3f0fe2
Compare
adinauer
left a comment
There was a problem hiding this comment.
The changes do look OK to me but I think your benchmarks are biased towards IDs that are actually being used. The point of the LazyEvaluator was to optimize cases where we don't actually end up using the IDs. I'm curious whether we even need the lazy part + lock anymore since we've moved away from SecureRandom. In case you take the time to benchmark, we might be able to conclude we can reduce the complexity by just eagerly creating the String.
Approving so you can decide on whether to merge as is or invest further time.
|
Thank you. Yeah the point of this PR is that the lock guarding the creation of the string was actually much heavier than the string itself in terms of object allocations. |
📜 Description
SentryIdandSpanIdstored their string value behind aLazyEvaluator<String>. EachLazyEvaluatorallocates anAutoClosableReentrantLock(aReentrantLocksubclass, including its internalSyncobject) plus a capturing lambda — on every instance.This change replaces the
LazyEvaluator<String>in both classes with a plainvolatile Stringguarded by a double-checkedsynchronized (this)block:UUIDconstructors still defer UUID-string generation;SentryIdkeeps theUUIDreference sotoSentryIdStringstays lazy.No public API change (
apiDumpproduces no diff).💡 Motivation and Context
One
SentryIdis created per event/transaction and oneSpanIdper span, so these are among the most frequently allocated objects in the SDK. Paying for a per-instanceReentrantLock+ lambda to guard a singleStringis 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
SpanIdTestandSentryIdTestcover the lazy-generation and normalization behavior (UUID generated/normalized only once, not on init, etc.). The full:sentryunit test suite passes (3372/3373; the one failingSentryIdTestcase is a pre-existing test-isolation artifact that fails identically onmainand is unrelated to this change).Benchmark
Measured against
mainusing the real compiled classes on the same JVM (Temurin 17.0.16). Allocation is measured viaThreadMXBean.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.SpanId(String)eagerSpanId()lazy +toStringSentryId(String)eagerSentryId()lazy +toStringSentryId(UUID)lazy +toStringThe ~88-byte reduction on eager
SpanIdaccounts exactly for the removed object graph:LazyEvaluator(24B) +AutoClosableReentrantLock(16B) + its internalNonfairSync/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 byUUID.randomUUID()(aSecureRandomdraw), not the lock.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Other
LazyEvaluator/AutoClosableReentrantLockusages that guard genuinely light objects can be reviewed similarly as part of the SDK overhead reduction effort.