Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package datadog.trace.core;

import static java.util.concurrent.TimeUnit.MICROSECONDS;

import datadog.trace.api.KnownTags;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

/**
* Name-vs-id span-creation benchmark: the same web/jdbc span scenarios as {@link
* SpanCreationBenchmark}, set two ways in the SAME JVM — {@code setTag(String, ...)} (the {@code
* *ByName} arms, which resolve {@code keyOf} on every call) vs {@code setTag(long, ...)} with
* pre-resolved {@link KnownTags} id constants (the {@code *ById} arms, which skip {@code keyOf}
* and, for non-intercepted tags, the tag interceptor). This isolates the THROUGHPUT lever of the id
* API: both arms produce identical span state and allocate identically (the dense store is the
* same), so the delta is the {@code keyOf} name-resolution + megamorphic-dispatch tax the id path
* removes.
*
* <p>Not every tag reaches the fast store path. Ids whose interceptor bit is set (span.kind,
* http.method, http.url, db.statement) route back through the String path inside {@code
* DDSpan.setTag(long, ...)}, so they behave exactly as the name arm — the {@code ById} win comes
* from the non-intercepted majority (component, http.route, peer.port, db.type/instance/user/
* operation, peer.hostname). This is the realistic shape of instrumentation migrated to ids: a
* uniform id call site, fast where the tag allows it. {@code http.status_code} is left on the
* String setter in BOTH arms — its int overload carries a span-field side effect
* (setHttpStatusCode) the id fast path intentionally doesn't, so keeping it name-keyed holds the
* two arms behaviorally equal.
*
* <p>Run with the dense store on ({@code -Ddd.trace.dense.tags.enabled=true}, in the {@code @Fork}
* args). Read {@code gc.alloc.rate.norm} (B/op) to confirm the arms allocate the same; read
* throughput for the id win (directional — per-fork JIT bimodality at @Threads(8)).
*/
@State(Scope.Benchmark)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@BenchmarkMode(Mode.Throughput)
@Threads(8)
@OutputTimeUnit(MICROSECONDS)
@Fork(
value = 3,
jvmArgsAppend = {
"-DTEST_LOG_LEVEL=warn",
"-Ddd.trace.dense.tags.enabled=true",
"-Ddd.service=petclinic",
"-Ddd.env=staging",
"-Ddd.version=1.2.3",
"-Ddd.tags=team:apm,dc:us1,cluster:prod-1,owner:tracing,tier:backend,region:us-east-1"
})
public class SpanCreationByIdBenchmark {
private static final String INSTRUMENTATION_NAME = "bench";
private static final String SERVER_OPERATION_NAME = "servlet.request";
private static final String JDBC_OPERATION_NAME = "database.query";

private static final String COMPONENT_VALUE = "tomcat-server";
private static final String HTTP_METHOD_VALUE = "GET";
private static final String HTTP_ROUTE_VALUE = "/owners/{ownerId}";
private static final String HTTP_URL_VALUE = "http://localhost:8080/owners/42";
private static final int HTTP_STATUS_VALUE = 100; // in-cache; value itself is immaterial here
private static final int PEER_PORT_VALUE = 80;

private static final String DB_COMPONENT_VALUE = "java-jdbc-statement";
private static final String DB_TYPE_VALUE = "postgresql";
private static final String DB_INSTANCE_VALUE = "petclinic";
private static final String DB_USER_VALUE = "app";
private static final String DB_OPERATION_VALUE = "SELECT";
private static final String DB_STATEMENT_VALUE = "SELECT * FROM owners WHERE id = ?";
private static final String DB_PEER_HOSTNAME_VALUE = "db.internal";
private static final int DB_PEER_PORT_VALUE = 90; // in-cache; value itself is immaterial here

CoreTracer tracer;

@Setup
public void setup(Blackhole blackhole) {
this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build();
}

@TearDown
public void tearDown() {
this.tracer.close();
}

/** Web-server-shaped span, tags set by NAME (keyOf on every call). */
@Benchmark
public void webServerSpanByName() {
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start();
span.setTag(Tags.COMPONENT, COMPONENT_VALUE);
span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);
span.setTag(Tags.HTTP_METHOD, HTTP_METHOD_VALUE);
span.setTag(Tags.HTTP_ROUTE, HTTP_ROUTE_VALUE);
span.setTag(Tags.HTTP_URL, HTTP_URL_VALUE);
span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE);
span.setTag(Tags.PEER_PORT, PEER_PORT_VALUE);
span.finish();
}

/** Web-server-shaped span, tags set by ID (pre-resolved KnownTags constants, no keyOf). */
@Benchmark
public void webServerSpanById() {
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, SERVER_OPERATION_NAME).start();
span.setTag(KnownTags.COMPONENT_ID, COMPONENT_VALUE);
span.setTag(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_SERVER);
span.setTag(KnownTags.HTTP_METHOD_ID, HTTP_METHOD_VALUE);
span.setTag(KnownTags.HTTP_ROUTE_ID, HTTP_ROUTE_VALUE);
span.setTag(KnownTags.HTTP_URL_ID, HTTP_URL_VALUE);
span.setTag(Tags.HTTP_STATUS, HTTP_STATUS_VALUE); // name-keyed in both arms (see class doc)
span.setTag(KnownTags.PEER_PORT_ID, PEER_PORT_VALUE);
span.finish();
}

/** JDBC/DB-client-shaped span, tags set by NAME. */
@Benchmark
public void jdbcClientSpanByName() {
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, JDBC_OPERATION_NAME).start();
span.setTag(Tags.COMPONENT, DB_COMPONENT_VALUE);
span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT);
span.setTag(Tags.DB_TYPE, DB_TYPE_VALUE);
span.setTag(Tags.DB_INSTANCE, DB_INSTANCE_VALUE);
span.setTag(Tags.DB_USER, DB_USER_VALUE);
span.setTag(Tags.DB_OPERATION, DB_OPERATION_VALUE);
span.setTag(Tags.DB_STATEMENT, DB_STATEMENT_VALUE);
span.setTag(Tags.PEER_HOSTNAME, DB_PEER_HOSTNAME_VALUE);
span.setTag(Tags.PEER_PORT, DB_PEER_PORT_VALUE);
span.finish();
}

/** JDBC/DB-client-shaped span, tags set by ID (7 of 9 reach the fast store path). */
@Benchmark
public void jdbcClientSpanById() {
AgentSpan span = tracer.buildSpan(INSTRUMENTATION_NAME, JDBC_OPERATION_NAME).start();
span.setTag(KnownTags.COMPONENT_ID, DB_COMPONENT_VALUE);
span.setTag(KnownTags.SPAN_KIND_ID, Tags.SPAN_KIND_CLIENT);
span.setTag(KnownTags.DB_TYPE_ID, DB_TYPE_VALUE);
span.setTag(KnownTags.DB_INSTANCE_ID, DB_INSTANCE_VALUE);
span.setTag(KnownTags.DB_USER_ID, DB_USER_VALUE);
span.setTag(KnownTags.DB_OPERATION_ID, DB_OPERATION_VALUE);
span.setTag(KnownTags.DB_STATEMENT_ID, DB_STATEMENT_VALUE);
span.setTag(KnownTags.PEER_HOSTNAME_ID, DB_PEER_HOSTNAME_VALUE);
span.setTag(KnownTags.PEER_PORT_ID, DB_PEER_PORT_VALUE);
span.finish();
}
}
86 changes: 86 additions & 0 deletions dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import datadog.trace.api.DDTags;
import datadog.trace.api.DDTraceId;
import datadog.trace.api.EndpointTracker;
import datadog.trace.api.KnownTagCodec;
import datadog.trace.api.TagMap;
import datadog.trace.api.TraceConfig;
import datadog.trace.api.debugger.DebuggerConfigBridge;
Expand Down Expand Up @@ -513,6 +514,91 @@ public DDSpan setTag(final String tag, final Object value) {
return this;
}

// Id-keyed setTag overrides: the throughput fast path. A non-intercepted id goes straight to the
// context's dense store with no keyOf and no interceptor. An intercepted id (span.kind,
// http.url, ...) is resolved back to its name and routed through the String setter, which owns
// the interceptor round trip and the http.status quirk -- so behavior is identical to a
// name-keyed set, just without paying keyOf when it isn't needed.
@Override
public DDSpan setTag(final long id, final boolean value) {
if (KnownTagCodec.isIntercepted(id)) {
return setTag(KnownTagCodec.nameOf(id), value);
}
context.setTag(id, value);
Comment on lines +524 to +527

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep ID setters functional when dense tags are disabled

With the normal configuration where dd.trace.dense.tags.enabled is unset, CoreTracer never calls KnownTags.init(), so KnownTagCodec.nameOf(id) returns null. Once an instrumentation uses these new overloads, intercepted IDs such as SPAN_KIND_ID therefore delegate to a null-key String setter and silently lose the tag; non-intercepted IDs are stored but later materialize with null names. Initialize the resolver independently of the experimental dense-store flag, or otherwise make the ID path handle an inactive codec.

Useful? React with 👍 / 👎.

Comment on lines +524 to +527

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor split-by-tags for ID-keyed tags

With dd.trace.split-by-tags configured to a known but normally non-intercepted key such as component, TagInterceptor.needsIntercept(tag) dynamically returns true and the name-keyed setter derives the service name. The ID path consults only the static intercepted bit, bypasses the interceptor, and stores the tag without applying the configured service split. Ensure the fast path also accounts for dynamically configured interceptor keys before migrating such call sites.

Useful? React with 👍 / 👎.

return this;
}

@Override
public DDSpan setTag(final long id, final int value) {
if (KnownTagCodec.isIntercepted(id)) {
return setTag(KnownTagCodec.nameOf(id), value);
}
context.setTag(id, value);
Comment on lines +533 to +536

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve HTTP status handling for its known ID

When a caller uses the permitted stored ID KnownTags.HTTP_STATUS_CODE_ID, that ID lacks the intercepted flag, so the int overload takes this direct context path. Unlike setTag(Tags.HTTP_STATUS, int), it never updates DDSpanContext.httpStatusCode or runs interceptHttpStatusCode; consequently getHttpStatusCode(), 404 resource naming, and status serialization from the dedicated field remain incorrect. Route this ID through the existing String/status path or mark it intercepted.

Useful? React with 👍 / 👎.

return this;
}

@Override
public DDSpan setTag(final long id, final long value) {
if (KnownTagCodec.isIntercepted(id)) {
return setTag(KnownTagCodec.nameOf(id), value);
}
context.setTag(id, value);
return this;
}

@Override
public DDSpan setTag(final long id, final float value) {
if (KnownTagCodec.isIntercepted(id)) {
return setTag(KnownTagCodec.nameOf(id), value);
}
context.setTag(id, value);
return this;
}

@Override
public DDSpan setTag(final long id, final double value) {
if (KnownTagCodec.isIntercepted(id)) {
return setTag(KnownTagCodec.nameOf(id), value);
}
context.setTag(id, value);
return this;
}

@Override
public DDSpan setTag(final long id, final String value) {
if (KnownTagCodec.isIntercepted(id)) {
return setTag(KnownTagCodec.nameOf(id), value);
}
if (value == null) {
context.setTag(id, (Object) null);
} else {
context.setTag(id, value);
Comment on lines +572 to +575

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove empty String values on the ID path

For a non-intercepted known ID, calling setTag(id, "") stores the empty value, whereas the existing setTag(String, String) contract removes null or empty strings and the adjacent CharSequence ID overload does the same. This makes behavior depend on whether the compile-time value type is String or CharSequence; include value.isEmpty() in this removal branch.

Useful? React with 👍 / 👎.

}
return this;
}

@Override
public DDSpan setTag(final long id, final CharSequence value) {
if (KnownTagCodec.isIntercepted(id)) {
return setTag(KnownTagCodec.nameOf(id), value);
}
if (value == null || value.length() == 0) {
context.setTag(id, (Object) null);
} else {
context.setTag(id, value);
}
return this;
}

@Override
public DDSpan setTag(final long id, final Object value) {
if (KnownTagCodec.isIntercepted(id)) {
return setTag(KnownTagCodec.nameOf(id), value);
}
context.setTag(id, value);
return this;
}

@Override
public AgentSpan setAllTags(Map<String, ?> map) {
context.setAllTags(map);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import datadog.trace.api.DDTags;
import datadog.trace.api.DDTraceId;
import datadog.trace.api.Functions;
import datadog.trace.api.KnownTagCodec;
import datadog.trace.api.ProcessTags;
import datadog.trace.api.SizingHint;
import datadog.trace.api.TagMap;
Expand Down Expand Up @@ -1109,6 +1110,63 @@ public void setTag(final String tag, final double value) {
}
}

// Id-keyed setTag fast path. Precondition (enforced by the DDSpan caller): the id names a stored,
// NON-intercepted known tag -- so there is no keyOf resolution and no tag-interceptor round trip,
// just the dense store write. Intercepted ids are routed back through the String path by DDSpan
// (which also owns the http.status quirk), so they never reach here.
public void setTag(final long id, final Object value) {
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
if (null == value) {
removeTag(KnownTagCodec.nameOf(id));
return;
}
synchronized (unsafeTags) {
unsafeTags.set(id, value);
}
}

public void setTag(final long id, final CharSequence value) {
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
synchronized (unsafeTags) {
unsafeTags.set(id, value);
}
}

public void setTag(final long id, final boolean value) {
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
synchronized (unsafeTags) {
unsafeTags.set(id, value);
}
}

public void setTag(final long id, final int value) {
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
synchronized (unsafeTags) {
unsafeTags.set(id, value);
}
}

public void setTag(final long id, final long value) {
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
synchronized (unsafeTags) {
unsafeTags.set(id, value);
}
}

public void setTag(final long id, final float value) {
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
synchronized (unsafeTags) {
unsafeTags.set(id, value);
}
}

public void setTag(final long id, final double value) {
assert !KnownTagCodec.isIntercepted(id) : "intercepted id must route through the String path";
synchronized (unsafeTags) {
unsafeTags.set(id, value);
}
}

void setAllTags(final TagMap map) {
setAllTags(map, true);
}
Expand Down
49 changes: 49 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/TagMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -1689,6 +1689,39 @@ public void set(@Nonnull String tag, double value) {
}
}

// The set(long id, ...) family is the id-keyed counterpart of set(String, ...): the caller passes
// an already-resolved KnownTags.* id, so these skip the keyOf name resolution the String setters
// pay on every call. The id MUST name a stored known tag (see KnownTagCodec#isStored) -- custom /
// unknown names have no id and must use the name-keyed setters. Primitives box on the store
// branch, exactly as the String family does.
public void set(long id, @Nonnull Object value) {
this.putKnownById(id, value);
}

public void set(long id, @Nonnull CharSequence value) {
this.putKnownById(id, value);
}

public void set(long id, boolean value) {
this.putKnownById(id, Boolean.valueOf(value));
}

public void set(long id, int value) {
this.putKnownById(id, Integer.valueOf(value));
}

public void set(long id, long value) {
this.putKnownById(id, Long.valueOf(value));
}

public void set(long id, float value) {
this.putKnownById(id, Float.valueOf(value));
}

public void set(long id, double value) {
this.putKnownById(id, Double.valueOf(value));
}

/**
* Places an Entry directly into the map, avoiding a new Entry allocation. Null-tolerant: a null
* {@code newEntry} is a no-op returning null, so an Entry producer (e.g. {@link
Expand Down Expand Up @@ -1754,6 +1787,22 @@ private Entry putKnownLocal(long id, String tag, Object value) {
return this.putKnownValue(id, value);
}

/**
* Id-keyed counterpart of {@link #putKnownLocal}: stores a known tag densely from its resolved id
* with NO {@code keyOf} name resolution. The name is needed only to clear a read-through
* tombstone (rare), so it's resolved lazily via {@link KnownTagCodec#nameOf} on that branch only.
* The id must name a stored known tag (asserted); the value is pre-boxed by the {@code set(long,
* ...)} overloads.
*/
private Entry putKnownById(long id, Object value) {
assert KnownTagCodec.isStored(id) : "set(long) requires a stored known-tag id";
this.checkWriteAccess();
if (this.removedFromParent != null) {
this.removedFromParent.remove(KnownTagCodec.nameOf(id));
}
return this.putKnownValue(id, value);
}

/** Copy-on-write the shared empty buckets to a private array on the first bucket write. */
private Object[] materializeBuckets() {
Object[] b = this.buckets;
Expand Down
Loading
Loading