From 92eb71acf16f406a399273b1094d6be53645dc9b Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 9 Jul 2026 07:49:40 -0400 Subject: [PATCH 1/2] Add id-keyed TagMap.set(long) + insertion-comparison benchmark (Part 2) set(long) is the id-keyed insertion path (skips keyOf). The benchmark compares HashMap / 1.0-bucket / 2.0-name / 2.0-id on alloc + throughput. Header numbers are PRE-bloom-filter; refresh after the bloom lands + this rebases onto it. Follow-ups: id support for TagMap.Ledger + SpanBuilder (deferred). Co-Authored-By: Claude Opus 4.8 --- .../TagMapInsertionComparisonBenchmark.java | 148 ++++++++++++++++++ .../main/java/datadog/trace/api/TagMap.java | 20 +++ 2 files changed, 168 insertions(+) create mode 100644 internal-api/src/jmh/java/datadog/trace/api/TagMapInsertionComparisonBenchmark.java diff --git a/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertionComparisonBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertionComparisonBenchmark.java new file mode 100644 index 00000000000..17ac6afe925 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertionComparisonBenchmark.java @@ -0,0 +1,148 @@ +package datadog.trace.api; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import datadog.trace.bootstrap.instrumentation.api.Tags; +import java.util.HashMap; +import java.util.Map; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +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.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Insertion comparison for the deck's "how do we do vs HashMap / TagMap 1.0" and "id vs name" + * claims (slides 3 / 7 / 8). Same tag count, four ways: + * + * + * + *

Run with {@code -prof gc} for the allocation columns (deterministic). The throughput columns + * are thermal-fragile — quote them only from a quiet machine, and take the id-vs-name and + * vs-HashMap ratios rather than absolute ops/s. True 1.0 (no {@code keyOf}) is not on this branch; + * see {@code tagMapByName} above for the master-run recipe. + * + *

Results — JDK 17 (Zulu 17.0.7, Apple Silicon), 2026-07-09. Alloc via {@code -prof gc} + * is deterministic (quotable); throughput was {@code -f 1 -i 2} on a laptop (DIRECTIONAL only — + * quote throughput from a quiet box, and prefer the ratios below to absolute ops/s). + * + *

{@code
+ *                alloc B/op (7 / 12)    thrpt vs hashMap (7 / 12, directional)
+ * hashMap          352 / 512            1.00x / 1.00x
+ * tagMapById       176 / 400            0.91x / 0.54x
+ * tagMapByName     176 / 400            0.58x / 0.46x
+ * tagMapCustom     408 / 704            0.51x / 0.45x
+ * }
+ * + *

Two takeaways. (1) id and name insertion allocate identically (176/400) — the id-vs-name + * advantage is CPU (skipping {@code keyOf}), not allocation. Dense storage allocs ~50% less than + * HashMap at 7 tags and ~43% less than the bucket/Entry path ({@code tagMapCustom}). (2) id + * insertion recovers most of the {@code keyOf} tax — ~1.6x over name at 7 tags, ~1.2x at 12, + * approaching HashMap throughput — so slide 7's "name-path throughput neutral" is optimistic: the + * name path pays {@code keyOf}, and migrating instrumentation to ids is what buys it back. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(SECONDS) +@Warmup(iterations = 5, time = 2) +@Measurement(iterations = 5, time = 2) +@Fork(3) +@Threads(8) +public class TagMapInsertionComparisonBenchmark { + + // A realistic web/db span's known tag set (same list as DenseStoreAllocBenchmark). + static final String[] KNOWN = + new String[] { + DDTags.BASE_SERVICE, + Tags.VERSION, + Tags.COMPONENT, + Tags.SPAN_KIND, + Tags.HTTP_METHOD, + Tags.HTTP_ROUTE, + Tags.DB_TYPE, + Tags.DB_INSTANCE, + Tags.PEER_HOSTNAME, + Tags.DB_USER, + DDTags.LANGUAGE_TAG_KEY, + Tags.PEER_PORT, + }; + + @Param({"7", "12"}) + int tagCount; + + private String[] knownNames; + private long[] knownIds; + private String[] customNames; + private String[] values; + + @Setup(Level.Trial) + public void setup() { + KnownTags.init(); // register the real (allocation-free) resolver + this.knownNames = new String[tagCount]; + this.knownIds = new long[tagCount]; + this.customNames = new String[tagCount]; + this.values = new String[tagCount]; + for (int i = 0; i < tagCount; i++) { + this.knownNames[i] = KNOWN[i]; + this.knownIds[i] = KnownTagCodec.keyOf(KNOWN[i]); // resolve name -> id once (as codegen would) + this.customNames[i] = "custom.tag." + i; + this.values[i] = "value-" + i; + } + } + + @Benchmark + public Map hashMap() { + final Map m = new HashMap<>(16); + for (int i = 0; i < tagCount; i++) { + m.put(knownNames[i], values[i]); + } + return m; + } + + @Benchmark + public TagMap tagMapByName() { + final TagMap m = TagMap.create(16); + for (int i = 0; i < tagCount; i++) { + m.set(knownNames[i], values[i]); + } + return m; + } + + @Benchmark + public TagMap tagMapById() { + final TagMap m = TagMap.create(16); + for (int i = 0; i < tagCount; i++) { + m.set(knownIds[i], values[i]); + } + return m; + } + + @Benchmark + public TagMap tagMapCustom() { + final TagMap m = TagMap.create(16); + for (int i = 0; i < tagCount; i++) { + m.set(customNames[i], values[i]); + } + return m; + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 1d4327f7d66..4de532367b0 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -172,6 +172,15 @@ static Ledger ledger(int size) { void set(EntryReader newEntry); + /** + * Sets a known tag by its resolved id (a {@code KnownTags.*_ID} constant), storing the value + * densely. This is the id-keyed insertion path: it skips the {@code keyOf} name resolution the + * {@code set(String, ...)} methods pay. The id MUST be a stored known-tag id (see {@link + * datadog.trace.api.KnownTagCodec#isStored}); custom/unknown names have no id and must use the + * name-keyed setters. + */ + void set(long id, Object value); + /** sets the value while returning the prior Entry */ Entry getAndSet(String tag, Object value); @@ -1689,6 +1698,17 @@ public void set(String tag, double value) { this.getAndSet(tag, value); } + @Override + public void set(long id, Object value) { + // id-keyed insertion: the id is already resolved, so skip keyOf and store densely. The name is + // needed only to clear a read-through tombstone (rare), so resolve it lazily in that case. + this.checkWriteAccess(); + if (this.removedFromParent != null) { + this.removedFromParent.remove(KnownTagCodec.nameOf(id)); + } + this.putKnownValue(id, value); + } + @Override public Entry getAndSet(Entry newEntry) { // Entry-based path (set(EntryReader), entry-sharing). The Entry is already constructed by the From f0cb351d37b21d7dfa2ea717bae381353eb90577 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 9 Jul 2026 12:07:48 -0400 Subject: [PATCH 2/2] Refresh insertion-comparison numbers (measured with the bloom fast-path) Idle-box run (-prof gc -f 5 -wi 5 -i 5): dense-id reaches HashMap parity at 7 tags (0.99x, was 0.91x pre-bloom), 0.63x at 12; alloc still ~half HashMap (184/408 vs 352/512), +8 B/op for the bloom's long field. id==name on alloc. Beat at higher counts awaits per-type graph coloring (crude fieldPos&63 collides as tags grow). Co-Authored-By: Claude Opus 4.8 --- .../TagMapInsertionComparisonBenchmark.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertionComparisonBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertionComparisonBenchmark.java index 17ac6afe925..dc1aa6eef9d 100644 --- a/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertionComparisonBenchmark.java +++ b/internal-api/src/jmh/java/datadog/trace/api/TagMapInsertionComparisonBenchmark.java @@ -42,24 +42,29 @@ * vs-HashMap ratios rather than absolute ops/s. True 1.0 (no {@code keyOf}) is not on this branch; * see {@code tagMapByName} above for the master-run recipe. * - *

Results — JDK 17 (Zulu 17.0.7, Apple Silicon), 2026-07-09. Alloc via {@code -prof gc} - * is deterministic (quotable); throughput was {@code -f 1 -i 2} on a laptop (DIRECTIONAL only — - * quote throughput from a quiet box, and prefer the ratios below to absolute ops/s). + *

Results — WITH the bloom-filter fast-path (dense-store → bloom → id stack), JDK 17 (Zulu + * 17.0.7, Apple Silicon, idle box), {@code -prof gc -f 5 -wi 5 -i 5}, 2026-07-09. Alloc is + * deterministic (quotable); throughput was measured on a quiet box (25 iters, tight error bars), + * trustworthy for the ratios — except {@code tagMapById@7} carries ~6% fork-to-fork variance + * (bloom fast-path inlining nondeterminism; check PrintInlining before quoting 0.99x as hard parity). * *

{@code
- *                alloc B/op (7 / 12)    thrpt vs hashMap (7 / 12, directional)
+ *                alloc B/op (7 / 12)    thrpt vs hashMap (7 / 12)
  * hashMap          352 / 512            1.00x / 1.00x
- * tagMapById       176 / 400            0.91x / 0.54x
- * tagMapByName     176 / 400            0.58x / 0.46x
- * tagMapCustom     408 / 704            0.51x / 0.45x
+ * tagMapById       184 / 408            0.99x / 0.63x
+ * tagMapByName     184 / 408            0.66x / 0.50x
+ * tagMapCustom     416 / 712            0.59x / 0.46x
  * }
* - *

Two takeaways. (1) id and name insertion allocate identically (176/400) — the id-vs-name - * advantage is CPU (skipping {@code keyOf}), not allocation. Dense storage allocs ~50% less than - * HashMap at 7 tags and ~43% less than the bucket/Entry path ({@code tagMapCustom}). (2) id - * insertion recovers most of the {@code keyOf} tax — ~1.6x over name at 7 tags, ~1.2x at 12, - * approaching HashMap throughput — so slide 7's "name-path throughput neutral" is optimistic: the - * name path pays {@code keyOf}, and migrating instrumentation to ids is what buys it back. + *

Three takeaways. (1) id and name insertion allocate identically (184/408) — the + * id-vs-name advantage is CPU (skipping {@code keyOf}), not allocation. Dense allocs ~half of + * HashMap at 7 tags (~20% less at 12) and beats the bucket/Entry path ({@code tagMapCustom}) + * everywhere; the bloom cost +8 B/op (one {@code long} field). (2) the bloom brings id insertion + * to HashMap parity at typical counts — 0.99x at 7 tags (was 0.91x pre-bloom), 0.63x at 12 (was + * 0.54x). Not a beat: the crude {@code fieldPos & 63} mapping collides more as tags grow, so some + * appends still scan; per-type graph-coloring is the lever to push 12 toward parity. (3) id + * clearly beats the bucket/1.0 path — 1.4–1.7x ({@code tagMapById} vs {@code tagMapCustom}); + * pin exact 2.0-vs-1.0 with a master run (true 1.0 has no {@code keyOf}). */ @State(Scope.Benchmark) @BenchmarkMode(Mode.Throughput)