Recalibrate the UTF8 caches every N spans instead of per span#11945
Recalibrate the UTF8 caches every N spans instead of per span#11945dougqh wants to merge 6 commits into
Conversation
TraceMapperV0_4 called TAG_CACHE.recalibrate()/VALUE_CACHE.recalibrate() on every span. recalibrate() is O(cacheSize) (decay every entry, purge cold ones, retune the promotion threshold), so per-span it dominated serializer-thread CPU — a PetClinic multi-endpoint JFR put GenerationalUtf8Cache.recalibrate at ~17% of the dd-trace-processor thread's samples, its #1 hotspot. Per-span was never the intended cadence: the caches adapt to shifts in tag-value cardinality on a timescale far longer than one span, so recalibrating once every RECALIBRATE_SPAN_INTERVAL (512) spans preserves their effectiveness while cutting recalibrate calls ~500x. Gated via a span counter and a pure, unit-tested shouldRecalibrate() predicate. Correctness of the caches is unaffected (recalibrate only decays/retunes; it never changes returned bytes). The win is CPU on the background serializer thread, which converts to application-thread throughput/latency under core contention (busy boxes, CPU-limited containers) and protects GC/JIT headroom elsewhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
🎯 Code Coverage (details) 🔗 Commit SHA: 996c42e | Docs | Datadog PR Page | Give us feedback! |
🟢 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. |
…che guard - Trim the RECALIBRATE_SPAN_INTERVAL comment to explain intent (control recalibration frequency) without the profiling numbers (those live in the PR). - Move the span-counter increment inside shouldRecalibrate() so the gate is self-contained. - Only consult shouldRecalibrate() when a cache is actually present, so the cache-off config pays nothing (and the static-final guard folds away when caches are enabled). - Test now asserts the window-invariant (any 2*interval window crosses exactly two boundaries), since shouldRecalibrate() is now stateful. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allocate SPAN_COUNTER only when a cache exists, and use its presence as the "recalibration enabled" gate inside shouldRecalibrate(). This drops the explicit (TAG_CACHE != null || VALUE_CACHE != null) guard from the call site — accept() now just checks shouldRecalibrate() — while keeping the cache-off config free (static-final null folds the whole block away). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
More details
Cache recalibration interval logic is sound. The PR gates recalibrate() calls to once per 512 spans via an atomic counter and power-of-two bitmask, reducing serializer-thread CPU from 18.3% to 0.2% without affecting cache correctness—recalibrate() only tunes efficiency, never changes the bytes returned. Thread-safe atomic increment, correct bitmask arithmetic, proper null-guard when caches are disabled, and comprehensive unit tests all validate the change.
📊 Validated against 5 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit ea5f842 · What is Autotest? · Any feedback? Reach out in #autotest
…n firstSpanWritten firstSpanWritten is only touched on the single serializer thread. The new static AtomicLong recalibration counter makes SpotBugs treat this class as multithreaded, which retroactively flags the pre-existing non-volatile boolean. Suppress it, matching the same pattern already used in TracerHealthMetrics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review: the span counter drives a recalibration cadence, not an exact count, and the serializer is single-threaded, so AtomicLong is unnecessary -- a plain long suffices (a benign race would at most nudge when recalibrate fires). A static-final boolean gates it so the JIT still folds the whole block away when both caches are off. Also drops the now-useless firstSpanWritten SpotBugs suppression: with the AtomicLong gone the class is no longer flagged as multithreaded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What Does This Do
Reduces the CPU cost of the UTF8 caches
The caches were designed to be recalibrated periodically, but they don't need to recalibrate on every span
Motivation
Reduces the biggest CPU cost in the background threads
GenerationalUtf8Cache.recalibrate was 18.3% of background thread CPU activity
Additional Notes
From Claude...
TraceMapperV0_4recalibrated the UTF8 caches (TAG_CACHE,VALUE_CACHE) on every span.recalibrate()is O(cacheSize) — it decays every entry's score, purges cold ones, and retunes the promotion threshold — so driven per-span it became the largest single consumer of serializer-thread CPU.This gates it to once every
RECALIBRATE_SPAN_INTERVAL(512) spans, via a span counter and a pure, unit-testedshouldRecalibrate()predicate.Evidence
Baseline profile — Spring PetClinic multi-endpoint under JMeter, JFR execution sampling, background (
dd-trace-processor) thread:Measured A/B — same-base build (the only diff is this commit), same workload, JFR:
recalibratedrops out of the serializer's hot methods entirely.-Xmx1gbox, which is the regime where freed background CPU is least likely to convert. On a core-bound or CPU-limited-container host, more of it should land as throughput.Why it's safe
recalibrateonly decays/retunes; it never changes the bytesgetUtf8returns.Impact
A background serializer-thread CPU reduction, alloc-neutral. It converts to application-thread throughput/latency under core contention (busy hosts, CPU-limited containers where the cgroup quota throttles the whole process) and protects GC/JIT headroom where cores are spare.
🤖 Generated with Claude Code