Skip to content

Commit e102e67

Browse files
dougqhclaude
andcommitted
Add id-keyed setTag to TagMap and DDSpan (throughput fast path)
Threads pre-resolved KnownTags.* ids through the span API so callers can skip keyOf name resolution and, for non-intercepted tags, the tag interceptor: - TagMap.set(long, ...) family -> putKnownById -> putKnownValue. - AgentSpan gets default id-keyed setTag methods that resolve the id back to its name and delegate to the String setter (correctness-preserving fallback; only DDSpan overrides for the fast path, so the other 20 implementers are untouched). - DDSpan.setTag(long, ...) routes intercepted ids (span.kind, http.method, http.url, db.statement) back through the String path and sends the rest straight to DDSpanContext.setTag(long, ...) -> unsafeTags.set(long, ...). - TagMapSetByIdForkedTest proves the id path is observationally identical to the name path; SpanCreationByIdBenchmark measures the throughput win. Co-Authored-By: Claude Opus 4.8 <[email protected]>
1 parent b424214 commit e102e67

6 files changed

Lines changed: 478 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package datadog.trace.core;
2+
3+
import static java.util.concurrent.TimeUnit.MICROSECONDS;
4+
5+
import datadog.trace.api.KnownTags;
6+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
7+
import datadog.trace.bootstrap.instrumentation.api.Tags;
8+
import org.openjdk.jmh.annotations.Benchmark;
9+
import org.openjdk.jmh.annotations.BenchmarkMode;
10+
import org.openjdk.jmh.annotations.Fork;
11+
import org.openjdk.jmh.annotations.Measurement;
12+
import org.openjdk.jmh.annotations.Mode;
13+
import org.openjdk.jmh.annotations.OutputTimeUnit;
14+
import org.openjdk.jmh.annotations.Scope;
15+
import org.openjdk.jmh.annotations.Setup;
16+
import org.openjdk.jmh.annotations.State;
17+
import org.openjdk.jmh.annotations.TearDown;
18+
import org.openjdk.jmh.annotations.Threads;
19+
import org.openjdk.jmh.annotations.Warmup;
20+
import org.openjdk.jmh.infra.Blackhole;
21+
22+
/**
23+
* Name-vs-id span-creation benchmark: the same web/jdbc span scenarios as {@link
24+
* SpanCreationBenchmark}, set two ways in the SAME JVM — {@code setTag(String, ...)} (the {@code
25+
* *ByName} arms, which resolve {@code keyOf} on every call) vs {@code setTag(long, ...)} with
26+
* pre-resolved {@link KnownTags} id constants (the {@code *ById} arms, which skip {@code keyOf}
27+
* and, for non-intercepted tags, the tag interceptor). This isolates the THROUGHPUT lever of the id
28+
* API: both arms produce identical span state and allocate identically (the dense store is the
29+
* same), so the delta is the {@code keyOf} name-resolution + megamorphic-dispatch tax the id path
30+
* removes.
31+
*
32+
* <p>Not every tag reaches the fast store path. Ids whose interceptor bit is set (span.kind,
33+
* http.method, http.url, db.statement) route back through the String path inside {@code
34+
* DDSpan.setTag(long, ...)}, so they behave exactly as the name arm — the {@code ById} win comes
35+
* from the non-intercepted majority (component, http.route, peer.port, db.type/instance/user/
36+
* operation, peer.hostname). This is the realistic shape of instrumentation migrated to ids: a
37+
* uniform id call site, fast where the tag allows it. {@code http.status_code} is left on the
38+
* String setter in BOTH arms — its int overload carries a span-field side effect
39+
* (setHttpStatusCode) the id fast path intentionally doesn't, so keeping it name-keyed holds the
40+
* two arms behaviorally equal.
41+
*
42+
* <p>Run with the dense store on ({@code -Ddd.trace.dense.tags.enabled=true}, in the {@code @Fork}
43+
* args). Read {@code gc.alloc.rate.norm} (B/op) to confirm the arms allocate the same; read
44+
* throughput for the id win (directional — per-fork JIT bimodality at @Threads(8)).
45+
*/
46+
@State(Scope.Benchmark)
47+
@Warmup(iterations = 5)
48+
@Measurement(iterations = 5)
49+
@BenchmarkMode(Mode.Throughput)
50+
@Threads(8)
51+
@OutputTimeUnit(MICROSECONDS)
52+
@Fork(
53+
value = 3,
54+
jvmArgsAppend = {
55+
"-DTEST_LOG_LEVEL=warn",
56+
"-Ddd.trace.dense.tags.enabled=true",
57+
"-Ddd.service=petclinic",
58+
"-Ddd.env=staging",
59+
"-Ddd.version=1.2.3",
60+
"-Ddd.tags=team:apm,dc:us1,cluster:prod-1,owner:tracing,tier:backend,region:us-east-1"
61+
})
62+
public class SpanCreationByIdBenchmark {
63+
private static final String INSTRUMENTATION_NAME = "bench";
64+
private static final String SERVER_OPERATION_NAME = "servlet.request";
65+
private static final String JDBC_OPERATION_NAME = "database.query";
66+
67+
private static final String COMPONENT_VALUE = "tomcat-server";
68+
private static final String HTTP_METHOD_VALUE = "GET";
69+
private static final String HTTP_ROUTE_VALUE = "/owners/{ownerId}";
70+
private static final String HTTP_URL_VALUE = "http://localhost:8080/owners/42";
71+
private static final int HTTP_STATUS_VALUE = 100; // in-cache; value itself is immaterial here
72+
private static final int PEER_PORT_VALUE = 80;
73+
74+
private static final String DB_COMPONENT_VALUE = "java-jdbc-statement";
75+
private static final String DB_TYPE_VALUE = "postgresql";
76+
private static final String DB_INSTANCE_VALUE = "petclinic";
77+
private static final String DB_USER_VALUE = "app";
78+
private static final String DB_OPERATION_VALUE = "SELECT";
79+
private static final String DB_STATEMENT_VALUE = "SELECT * FROM owners WHERE id = ?";
80+
private static final String DB_PEER_HOSTNAME_VALUE = "db.internal";
81+
private static final int DB_PEER_PORT_VALUE = 90; // in-cache; value itself is immaterial here
82+
83+
CoreTracer tracer;
84+
85+
@Setup
86+
public void setup(Blackhole blackhole) {
87+
this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build();
88+
}
89+
90+
@TearDown
91+
public void tearDown() {
92+
this.tracer.close();
93+
}
94+
95+
/** Web-server-shaped span, tags set by NAME (keyOf on every call). */
96+
@Benchmark
97+
public void webServerSpanByName() {
98+
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start();
99+
span.setTag(Tags.COMPONENT, COMPONENT_VALUE);
100+
span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
101+
span.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
102+
span.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE);
103+
span.setTag(Tags.HTTP_URL, HTTP_URL_VALUE);
104+
span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);
105+
span.setTag(Tags.PEER_PORT, PEER_PORT_VALUE);
106+
span.finish();
107+
}
108+
109+
/** Web-server-shaped span, tags set by ID (pre-resolved KnownTags constants, no keyOf). */
110+
@Benchmark
111+
public void webServerSpanById() {
112+
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start();
113+
span.setTag(KnownTags.COMPONENT_ID, COMPONENT_VALUE);
114+
span.setTag(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_SERVER);
115+
span.setTag(KnownTags.HTTP_METHOD_ID, HTTP_METHOD_VALUE);
116+
span.setTag(KnownTags.HTTP_ROUTE_ID, HTTP_ROUTE_VALUE);
117+
span.setTag(KnownTags.HTTP_URL_ID, HTTP_URL_VALUE);
118+
span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE); // name-keyed in both arms (see class doc)
119+
span.setTag(KnownTags.PEER_PORT_ID, PEER_PORT_VALUE);
120+
span.finish();
121+
}
122+
123+
/** JDBC/DB-client-shaped span, tags set by NAME. */
124+
@Benchmark
125+
public void jdbcClientSpanByName() {
126+
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, JDBC_OPERATION_NAME).start();
127+
span.setTag(Tags.COMPONENT, DB_COMPONENT_VALUE);
128+
span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT);
129+
span.setTag(Tags.DB_TYPE, DB_TYPE_VALUE);
130+
span.setTag(Tags.DB_INSTANCE, DB_INSTANCE_VALUE);
131+
span.setTag(Tags.DB_USER, DB_USER_VALUE);
132+
span.setTag(Tags.DB_OPERATION, DB_OPERATION_VALUE);
133+
span.setTag(Tags.DB_STATEMENT, DB_STATEMENT_VALUE);
134+
span.setTag(Tags.PEER_HOSTNAME, DB_PEER_HOSTNAME_VALUE);
135+
span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE);
136+
span.finish();
137+
}
138+
139+
/** JDBC/DB-client-shaped span, tags set by ID (7 of 9 reach the fast store path). */
140+
@Benchmark
141+
public void jdbcClientSpanById() {
142+
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, JDBC_OPERATION_NAME).start();
143+
span.setTag(KnownTags.COMPONENT_ID, DB_COMPONENT_VALUE);
144+
span.setTag(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_CLIENT);
145+
span.setTag(KnownTags.DB_TYPE_ID, DB_TYPE_VALUE);
146+
span.setTag(KnownTags.DB_INSTANCE_ID, DB_INSTANCE_VALUE);
147+
span.setTag(KnownTags.DB_USER_ID, DB_USER_VALUE);
148+
span.setTag(KnownTags.DB_OPERATION_ID, DB_OPERATION_VALUE);
149+
span.setTag(KnownTags.DB_STATEMENT_ID, DB_STATEMENT_VALUE);
150+
span.setTag(KnownTags.PEER_HOSTNAME_ID, DB_PEER_HOSTNAME_VALUE);
151+
span.setTag(KnownTags.PEER_PORT_ID, DB_PEER_PORT_VALUE);
152+
span.finish();
153+
}
154+
}

‎dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import datadog.trace.api.DDTags;
1616
import datadog.trace.api.DDTraceId;
1717
import datadog.trace.api.EndpointTracker;
18+
import datadog.trace.api.KnownTagCodec;
1819
import datadog.trace.api.TagMap;
1920
import datadog.trace.api.TraceConfig;
2021
import datadog.trace.api.debugger.DebuggerConfigBridge;
@@ -513,6 +514,91 @@ public DDSpan setTag(final String tag, final Object value) {
513514
return this;
514515
}
515516

517+
// Id-keyed setTag overrides: the throughput fast path. A non-intercepted id goes straight to the
518+
// context's dense store with no keyOf and no interceptor. An intercepted id (span.kind,
519+
// http.url, ...) is resolved back to its name and routed through the String setter, which owns
520+
// the interceptor round trip and the http.status quirk -- so behavior is identical to a
521+
// name-keyed set, just without paying keyOf when it isn't needed.
522+
@Override
523+
public DDSpan setTag(final long id, final boolean value) {
524+
if (KnownTagCodec.isIntercepted(id)) {
525+
return setTag(KnownTagCodec.nameOf(id), value);
526+
}
527+
context.setTag(id, value);
528+
return this;
529+
}
530+
531+
@Override
532+
public DDSpan setTag(final long id, final int value) {
533+
if (KnownTagCodec.isIntercepted(id)) {
534+
return setTag(KnownTagCodec.nameOf(id), value);
535+
}
536+
context.setTag(id, value);
537+
return this;
538+
}
539+
540+
@Override
541+
public DDSpan setTag(final long id, final long value) {
542+
if (KnownTagCodec.isIntercepted(id)) {
543+
return setTag(KnownTagCodec.nameOf(id), value);
544+
}
545+
context.setTag(id, value);
546+
return this;
547+
}
548+
549+
@Override
550+
public DDSpan setTag(final long id, final float value) {
551+
if (KnownTagCodec.isIntercepted(id)) {
552+
return setTag(KnownTagCodec.nameOf(id), value);
553+
}
554+
context.setTag(id, value);
555+
return this;
556+
}
557+
558+
@Override
559+
public DDSpan setTag(final long id, final double value) {
560+
if (KnownTagCodec.isIntercepted(id)) {
561+
return setTag(KnownTagCodec.nameOf(id), value);
562+
}
563+
context.setTag(id, value);
564+
return this;
565+
}
566+
567+
@Override
568+
public DDSpan setTag(final long id, final String value) {
569+
if (KnownTagCodec.isIntercepted(id)) {
570+
return setTag(KnownTagCodec.nameOf(id), value);
571+
}
572+
if (value == null) {
573+
context.setTag(id, (Object) null);
574+
} else {
575+
context.setTag(id, value);
576+
}
577+
return this;
578+
}
579+
580+
@Override
581+
public DDSpan setTag(final long id, final CharSequence value) {
582+
if (KnownTagCodec.isIntercepted(id)) {
583+
return setTag(KnownTagCodec.nameOf(id), value);
584+
}
585+
if (value == null || value.length() == 0) {
586+
context.setTag(id, (Object) null);
587+
} else {
588+
context.setTag(id, value);
589+
}
590+
return this;
591+
}
592+
593+
@Override
594+
public DDSpan setTag(final long id, final Object value) {
595+
if (KnownTagCodec.isIntercepted(id)) {
596+
return setTag(KnownTagCodec.nameOf(id), value);
597+
}
598+
context.setTag(id, value);
599+
return this;
600+
}
601+
516602
@Override
517603
public AgentSpan setAllTags(Map<String, ?> map) {
518604
context.setAllTags(map);

‎dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import datadog.trace.api.DDTags;
1212
import datadog.trace.api.DDTraceId;
1313
import datadog.trace.api.Functions;
14+
import datadog.trace.api.KnownTagCodec;
1415
import datadog.trace.api.ProcessTags;
1516
import datadog.trace.api.SizingHint;
1617
import datadog.trace.api.TagMap;
@@ -1109,6 +1110,63 @@ public void setTag(final String tag, final double value) {
11091110
}
11101111
}
11111112

1113+
// Id-keyed setTag fast path. Precondition (enforced by the DDSpan caller): the id names a stored,
1114+
// NON-intercepted known tag -- so there is no keyOf resolution and no tag-interceptor round trip,
1115+
// just the dense store write. Intercepted ids are routed back through the String path by DDSpan
1116+
// (which also owns the http.status quirk), so they never reach here.
1117+
public void setTag(final long id, final Object value) {
1118+
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
1119+
if (null == value) {
1120+
removeTag(KnownTagCodec.nameOf(id));
1121+
return;
1122+
}
1123+
synchronized (unsafeTags) {
1124+
unsafeTags.set(id, value);
1125+
}
1126+
}
1127+
1128+
public void setTag(final long id, final CharSequence value) {
1129+
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
1130+
synchronized (unsafeTags) {
1131+
unsafeTags.set(id, value);
1132+
}
1133+
}
1134+
1135+
public void setTag(final long id, final boolean value) {
1136+
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
1137+
synchronized (unsafeTags) {
1138+
unsafeTags.set(id, value);
1139+
}
1140+
}
1141+
1142+
public void setTag(final long id, final int value) {
1143+
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
1144+
synchronized (unsafeTags) {
1145+
unsafeTags.set(id, value);
1146+
}
1147+
}
1148+
1149+
public void setTag(final long id, final long value) {
1150+
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
1151+
synchronized (unsafeTags) {
1152+
unsafeTags.set(id, value);
1153+
}
1154+
}
1155+
1156+
public void setTag(final long id, final float value) {
1157+
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
1158+
synchronized (unsafeTags) {
1159+
unsafeTags.set(id, value);
1160+
}
1161+
}
1162+
1163+
public void setTag(final long id, final double value) {
1164+
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
1165+
synchronized (unsafeTags) {
1166+
unsafeTags.set(id, value);
1167+
}
1168+
}
1169+
11121170
void setAllTags(final TagMap map) {
11131171
setAllTags(map, true);
11141172
}

‎internal-api/src/main/java/datadog/trace/api/TagMap.java‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,39 @@ public void set(@Nonnull String tag, double value) {
16891689
}
16901690
}
16911691

1692+
// The set(long id, ...) family is the id-keyed counterpart of set(String, ...): the caller passes
1693+
// an already-resolved KnownTags.* id, so these skip the keyOf name resolution the String setters
1694+
// pay on every call. The id MUST name a stored known tag (see KnownTagCodec#isStored) -- custom /
1695+
// unknown names have no id and must use the name-keyed setters. Primitives box on the store
1696+
// branch, exactly as the String family does.
1697+
public void set(long id, @Nonnull Object value) {
1698+
this.putKnownById(id, value);
1699+
}
1700+
1701+
public void set(long id, @Nonnull CharSequence value) {
1702+
this.putKnownById(id, value);
1703+
}
1704+
1705+
public void set(long id, boolean value) {
1706+
this.putKnownById(id, Boolean.valueOf(value));
1707+
}
1708+
1709+
public void set(long id, int value) {
1710+
this.putKnownById(id, Integer.valueOf(value));
1711+
}
1712+
1713+
public void set(long id, long value) {
1714+
this.putKnownById(id, Long.valueOf(value));
1715+
}
1716+
1717+
public void set(long id, float value) {
1718+
this.putKnownById(id, Float.valueOf(value));
1719+
}
1720+
1721+
public void set(long id, double value) {
1722+
this.putKnownById(id, Double.valueOf(value));
1723+
}
1724+
16921725
/**
16931726
* Places an Entry directly into the map, avoiding a new Entry allocation. Null-tolerant: a null
16941727
* {@code newEntry} is a no-op returning null, so an Entry producer (e.g. {@link
@@ -1754,6 +1787,22 @@ private Entry putKnownLocal(long id, String tag, Object value) {
17541787
return this.putKnownValue(id, value);
17551788
}
17561789

1790+
/**
1791+
* Id-keyed counterpart of {@link #putKnownLocal}: stores a known tag densely from its resolved id
1792+
* with NO {@code keyOf} name resolution. The name is needed only to clear a read-through
1793+
* tombstone (rare), so it's resolved lazily via {@link KnownTagCodec#nameOf} on that branch only.
1794+
* The id must name a stored known tag (asserted); the value is pre-boxed by the {@code set(long,
1795+
* ...)} overloads.
1796+
*/
1797+
private Entry putKnownById(long id, Object value) {
1798+
assert KnownTagCodec.isStored(id) : "set(long) requires a stored known-tag id";
1799+
this.checkWriteAccess();
1800+
if (this.removedFromParent != null) {
1801+
this.removedFromParent.remove(KnownTagCodec.nameOf(id));
1802+
}
1803+
return this.putKnownValue(id, value);
1804+
}
1805+
17571806
/** Copy-on-write the shared empty buckets to a private array on the first bucket write. */
17581807
private Object[] materializeBuckets() {
17591808
Object[] b = this.buckets;

0 commit comments

Comments
 (0)