diff --git a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8Benchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8Benchmark.java index 69631eba694..89e6c075a5a 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8Benchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8Benchmark.java @@ -6,6 +6,8 @@ import datadog.communication.serialization.GenerationalUtf8Cache; import datadog.communication.serialization.SimpleUtf8Cache; +import datadog.trace.api.cache.DDCache; +import datadog.trace.api.cache.DDCaches; import java.nio.charset.StandardCharsets; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -23,6 +25,11 @@ * gc" to check bytes / op. Since {@link String#getBytes(java.nio.charset.Charset)} is intrinsified * the caches typically perform worse throughput wise; the benefit of the caches is to reduce * allocation. + * + *

{@link DDCache} (general-purpose, no eviction quality control, no recalibration) is included + * as a baseline "naive cache" arm alongside the purpose-built {@link SimpleUtf8Cache} / {@link + * GenerationalUtf8Cache}: it has no seen-once filtering, so every miss -- including one-time + * "custom" values -- immediately allocates and stores an entry, unlike the other two. */ @BenchmarkMode(Mode.Throughput) public class Utf8Benchmark { @@ -49,6 +56,14 @@ public static final byte[] tagUtf8_w_cache() { return tag.getBytes(StandardCharsets.UTF_8); } + static final DDCache DDCACHE_TAG_CACHE = DDCaches.newFixedSizeCache(128); + + @Benchmark + public static final byte[] tagUtf8_w_ddcache() { + String tag = nextTag(); + return DDCACHE_TAG_CACHE.computeIfAbsent(tag, t -> t.getBytes(StandardCharsets.UTF_8)); + } + @Benchmark public static final void valueUtf8_baseline(Blackhole bh) { for (int i = 0; i < NUM_LOOKUPS; ++i) { @@ -92,6 +107,23 @@ public static final void valueUtf8_cache_simple(Blackhole bh) { } } + // Matches GENERATIONAL_VALUE_CACHE's combined eden+tenured capacity (64 + 128). + static final DDCache DDCACHE_VALUE_CACHE = DDCaches.newFixedSizeCache(192); + + @Benchmark + public static final void valueUtf8_cache_ddcache(Blackhole bh) { + DDCache valueCache = DDCACHE_VALUE_CACHE; + // No recalibrate() call: DDCache has no decay/eviction-quality bookkeeping to drive. + + for (int i = 0; i < NUM_LOOKUPS; ++i) { + String tag = nextTag(); + String value = nextValue(tag); + + byte[] lookup = valueCache.computeIfAbsent(value, v -> v.getBytes(StandardCharsets.UTF_8)); + bh.consume(lookup); + } + } + @Benchmark public static final void valueUtf8_nocache(Blackhole bh) { for (int i = 0; i < NUM_LOOKUPS; ++i) { diff --git a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8CacheSizeBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8CacheSizeBenchmark.java new file mode 100644 index 00000000000..8ffd37a6159 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8CacheSizeBenchmark.java @@ -0,0 +1,153 @@ +package datadog.trace.common.writer.ddagent; + +import static datadog.trace.common.writer.ddagent.Utf8Workload.NUM_LOOKUPS; +import static datadog.trace.common.writer.ddagent.Utf8Workload.nextTag; +import static datadog.trace.common.writer.ddagent.Utf8Workload.nextValue; +import static java.nio.charset.StandardCharsets.UTF_8; + +import datadog.communication.serialization.EncodingCache; +import datadog.communication.serialization.GenerationalUtf8Cache; +import datadog.communication.serialization.SimpleUtf8Cache; +import datadog.trace.api.cache.DDCache; +import datadog.trace.api.cache.DDCaches; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Sweeps cache capacity for all three caches against the same {@link Utf8Workload}, since the fixed + * sizes in {@link Utf8Benchmark} (128, 192) only show each cache at one operating point. The + * capacities picked -- 10, 100, 1000 -- roughly bracket the range actually used in production (a + * handful of near-static tag names up to {@link SimpleUtf8Cache#MAX_CAPACITY}), so this shows how + * each design degrades as the workload's ~180-entry stable set plus unbounded "custom" churn + * outgrows or comfortably fits the budget. + * + *

Best paired with {@code -prof gc}: the interesting signal is allocation (entries created, + * bytes/op), not throughput -- see {@link Utf8Benchmark}'s note on why raw ops/s undersells the + * caches relative to the intrinsified {@code getBytes} baseline. + * + *

{@code NOCACHE} is the uncached baseline (always calls {@code getBytes} directly) -- it + * ignores {@code capacity}, so it's redundant across the three capacity values, but keeping it in + * the same parameter sweep makes it trivial to eyeball against every row of the cached variants. + * + *

Results (capacity sweep, {@code -prof gc}, normalized)

+ * + *

Throughput in ops/s, allocation in bytes/op, GC rate normalized to counts/sec ({@code + * gc.count}/{@code gc.time} are cumulative sums over the whole run, so raw values aren't comparable + * across runs with different fork counts without dividing by total measurement time). + * + *

+ * capacity  kind          ops/s     alloc B/op  alloc vs. NOCACHE  alloc MB/s  GC/s   GC time %
+ * 10        NOCACHE       2007.9    1,247,228   --                 2388.3     3.98   0.22%
+ * 10        DDCACHE       1315.7    1,447,111   +16.0% (worse)      1815.7    3.03   0.17%
+ * 10        SIMPLE        1401.6    1,305,537   +4.7%  (worse)      1745.1    2.91   0.16%
+ * 10        GENERATIONAL  1205.6    1,318,938   +5.7%  (worse)      1516.4    2.53   0.15%
+ * 100       NOCACHE       2003.6    1,247,238   --                 2383.1     3.96   0.22%
+ * 100       DDCACHE       1397.4    1,248,407   +0.1%  (breakeven)  1663.7    2.77   0.16%
+ * 100       SIMPLE        1451.3    1,161,541   -6.9%               1607.6    2.68   0.15%
+ * 100       GENERATIONAL  1422.4    1,046,133   -16.1%              1419.0    2.36   0.13%
+ * 1000      NOCACHE       1966.6    1,247,231   --                 2339.1     3.88   0.22%
+ * 1000      DDCACHE       1648.9    1,034,356   -17.1%              1626.6    2.72   0.20%
+ * 1000      SIMPLE        1641.3    974,316     -21.9%              1525.1    2.54   0.14%
+ * 1000      GENERATIONAL  1709.5    961,062     -23.0%              1566.8    2.61   0.15%
+ * 
+ * + *

Key findings

+ * + * + */ +@BenchmarkMode(Mode.Throughput) +@State(Scope.Thread) +public class Utf8CacheSizeBenchmark { + + public enum CacheKind { + NOCACHE, + DDCACHE, + SIMPLE, + GENERATIONAL + } + + @Param({"10", "100", "1000"}) + int capacity; + + @Param({"NOCACHE", "DDCACHE", "SIMPLE", "GENERATIONAL"}) + CacheKind cacheKind; + + EncodingCache cache; + + // Only Simple/Generational have anything to recalibrate; DDCache is a no-op here. + Runnable recalibrate; + + @Setup(Level.Trial) + public void setup() { + switch (cacheKind) { + case NOCACHE: + cache = value -> value.toString().getBytes(UTF_8); + recalibrate = () -> {}; + break; + case DDCACHE: + DDCache ddCache = DDCaches.newFixedSizeCache(capacity); + cache = value -> ddCache.computeIfAbsent(value.toString(), v -> v.getBytes(UTF_8)); + recalibrate = () -> {}; + break; + case SIMPLE: + SimpleUtf8Cache simpleCache = new SimpleUtf8Cache(capacity); + cache = simpleCache; + recalibrate = simpleCache::recalibrate; + break; + case GENERATIONAL: + GenerationalUtf8Cache generationalCache = new GenerationalUtf8Cache(capacity); + cache = generationalCache; + recalibrate = generationalCache::recalibrate; + break; + } + } + + @Benchmark + public void lookup(Blackhole bh) { + // Single thread drives recalibrate inline, at a transaction boundary -- see Utf8Benchmark. + recalibrate.run(); + + for (int i = 0; i < NUM_LOOKUPS; ++i) { + String tag = nextTag(); + String value = nextValue(tag); + + bh.consume(cache.encode(value)); + } + } +} diff --git a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8ConcurrentBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8ConcurrentBenchmark.java index 51080906bec..3135909a96d 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8ConcurrentBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/Utf8ConcurrentBenchmark.java @@ -6,6 +6,9 @@ import datadog.communication.serialization.GenerationalUtf8Cache; import datadog.communication.serialization.SimpleUtf8Cache; +import datadog.trace.api.cache.DDCache; +import datadog.trace.api.cache.DDCaches; +import java.nio.charset.StandardCharsets; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Group; @@ -29,12 +32,17 @@ * *

The recalibrate thread runs continuously, which is deliberately more aggressive than a real * periodic cadence — it maximizes contention so this doubles as a concurrency guardrail. + * + *

{@link DDCache} has no recalibrate step, so its group uses 8 lookup threads (no dedicated + * recalibrate thread) against the same shared cache -- a pure contention/throughput comparison + * rather than a recalibrate-race guardrail. */ @BenchmarkMode(Mode.Throughput) @State(Scope.Group) public class Utf8ConcurrentBenchmark { static final GenerationalUtf8Cache VALUE_CACHE = new GenerationalUtf8Cache(64, 128); static final SimpleUtf8Cache SIMPLE_VALUE_CACHE = new SimpleUtf8Cache(128); + static final DDCache DDCACHE_VALUE_CACHE = DDCaches.newFixedSizeCache(192); @Benchmark @Group("generational") @@ -69,4 +77,16 @@ public void simple_lookup(Blackhole bh) { public void simple_recalibrate() { SIMPLE_VALUE_CACHE.recalibrate(); } + + @Benchmark + @Group("ddcache") + @GroupThreads(8) + public void ddcache_lookup(Blackhole bh) { + for (int i = 0; i < NUM_LOOKUPS; ++i) { + String tag = nextTag(); + String value = nextValue(tag); + bh.consume( + DDCACHE_VALUE_CACHE.computeIfAbsent(value, v -> v.getBytes(StandardCharsets.UTF_8))); + } + } }