Conversation
…f8Cache Adds Utf8CacheSizeBenchmark, which sweeps cache capacity (10/100/1000) across all three UTF8-caching strategies plus a NOCACHE baseline, to compare their throughput and allocation profiles at different operating points. Also updates Utf8Benchmark and Utf8ConcurrentBenchmark with a DDCache arm alongside the existing Simple/Generational arms, and doc comments explaining the single-threaded vs. concurrent recalibrate contract. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Does This Do
Adds a JMH benchmark harness comparing the three UTF-8-encoding cache strategies available for tag-value encoding in trace serialization: the general-purpose
DDCache, and the purpose-builtSimpleUtf8Cache/GenerationalUtf8Cache.Utf8CacheSizeBenchmark(new): sweeps cache capacity (10 / 100 / 1000) acrossNOCACHE/DDCACHE/SIMPLE/GENERATIONALagainst the same synthetic tag/value workload, run with-prof gcto capture allocation alongside throughput.Utf8Benchmark/Utf8ConcurrentBenchmark: added aDDCachearm alongside the existing Simple/Generational arms, plus doc comments clarifying the single-threaded-recalibrate contract these benchmarks assume (matches how the caches are actually driven in the writer today).No production code changes — benchmark-only.
Motivation
We have three different caching implementations doing effectively the same job (string → UTF-8 bytes) with no head-to-head data on how they compare across capacities, or against not caching at all. This gives us that data so future decisions about which cache to use/keep can be evidence-based rather than assumed.
Results (capacity sweep,
-prof gc, normalized)Throughput in ops/s, allocation in bytes/op, GC rate normalized to counts/sec (raw
gc.count/gc.timeare cumulative sums over the run and aren't comparable across different fork/iteration counts without this normalization).GC/s and GC time % are
gc.count/gc.timenormalized by total measurement time (NOCACHE: 1 fork × 5 × 10s = 50s; the three caches: 5 forks × 5 × 10s = 250s) — the raw JMH sums aren't comparable across runs with different fork counts without this.Key findings
String.getBytes(UTF_8)is intrinsified, so a cache lookup is strictly more expensive per-op than just re-encoding — these caches only pay off on allocation, not throughput. This confirms the "bump-pointer bar": any cache meant to reduce app-thread allocation has to beat a TLAB bump-pointer allocation, which is a very cheap operation to beat.tag + intstring concatenation on every lookup (Utf8Workload.nextStandardValue/nextCustomValue), which allocates a freshStringregardless of whether the subsequent UTF-8 encoding is cached. For ASCII-compact strings,getBytes(UTF_8)is itself just a copy of the existing byte array, so a cache hit only saves that one array copy — not the string-construction cost. This structurally caps the max possible allocation win for any UTF8 cache on this workload.Pair.of(key, value)wrapper allocated on every stored miss (FixedSizeCache.produceAndStoreValue). Simple/Generational avoid this on one-off values via a marker/bloom-style gate (Caching.mark) that defers the wrapper allocation (CacheEntry) to a value's second observed touch — same immutable-wrapper-for-lock-freedom pattern as DDCache, but with the wrapper cost paid only for values that actually recur. The marker itself is advisory rather than authoritative: a wrong mark only costs a redundant allocation on the next touch, it can never cause the cache to return incorrect data, which is what keeps the design simple to reason about.Additional Notes
Opened as a standalone side task, decoupled from #12474 (the
@ForegroundSafe/@BackgroundOnlyannotation-checker PR).Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueTest plan
Utf8CacheSizeBenchmarklocally (./gradlew :dd-trace-core:jmh -Pjmh.include=Utf8CacheSizeBenchmark -Pjmh.profilers=gc) across all capacity/kind combinations plus a NOCACHE-only baseline run.🤖 Generated with Claude Code