Problem Statement
SentryId and SpanId store their string value behind a LazyEvaluator<String>. Every instance therefore allocates:
- a
LazyEvaluator object, which in turn allocates
- an
AutoClosableReentrantLock (a ReentrantLock subclass, including its internal Sync object), plus
- a captured lambda
…all to guard a single String.
These two types are allocated at very high frequency — one SentryId per event/transaction and one SpanId per span — so the per-instance lock machinery is far heavier than the value it protects. This is the worst place in the SDK for lock weight relative to the guarded object.
It is most clearly wasteful in the eager constructors, where the string already exists at construction time and there is no laziness benefit at all:
SpanId(String value) → new LazyEvaluator<>(() -> value)
SentryId(String) → new LazyEvaluator<>(() -> normalize(normalized)) / () -> normalized
The no-arg / UUID constructors do benefit from deferring UUID-string generation (added in #3770 / #3818), but a full per-instance ReentrantLock is a heavy way to guard generation of one string.
Solution Brainstorm
Replace the LazyEvaluator<String> in both classes with a lighter volatile-based lazy field:
- Eager (string-arg) constructors set the value directly — no lazy wrapper.
- Lazy constructors keep deferring generation, using a plain
volatile + double-checked synchronized (this) block (no extra lock allocation). Proper synchronization is still required because UUID generation is non-idempotent (two racing threads would otherwise produce different ids).
This drops the LazyEvaluator + ReentrantLock + lambda allocations per id while preserving existing behaviour and laziness. Follow-up to the SDK Overhead Reduction work (#5499).
Problem Statement
SentryIdandSpanIdstore their string value behind aLazyEvaluator<String>. Every instance therefore allocates:LazyEvaluatorobject, which in turn allocatesAutoClosableReentrantLock(aReentrantLocksubclass, including its internalSyncobject), plus…all to guard a single
String.These two types are allocated at very high frequency — one
SentryIdper event/transaction and oneSpanIdper span — so the per-instance lock machinery is far heavier than the value it protects. This is the worst place in the SDK for lock weight relative to the guarded object.It is most clearly wasteful in the eager constructors, where the string already exists at construction time and there is no laziness benefit at all:
SpanId(String value)→new LazyEvaluator<>(() -> value)SentryId(String)→new LazyEvaluator<>(() -> normalize(normalized))/() -> normalizedThe no-arg /
UUIDconstructors do benefit from deferring UUID-string generation (added in #3770 / #3818), but a full per-instanceReentrantLockis a heavy way to guard generation of one string.Solution Brainstorm
Replace the
LazyEvaluator<String>in both classes with a lightervolatile-based lazy field:volatile+ double-checkedsynchronized (this)block (no extra lock allocation). Proper synchronization is still required because UUID generation is non-idempotent (two racing threads would otherwise produce different ids).This drops the
LazyEvaluator+ReentrantLock+ lambda allocations per id while preserving existing behaviour and laziness. Follow-up to the SDK Overhead Reduction work (#5499).