Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package datadog.trace.core;

import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.common.writer.Writer;
import java.util.List;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
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;

/**
* Benchmark for {@code DDSpanContext.setTag} on a <b>primitive</b> value -- the path the caller
* collapse touched. Before: {@code precheckIntercept} (an {@code isOptimized} call + a {@code
* needsIntercept} resolve) and then, when intercepted, a second resolve inside {@code
* interceptTag}. After: one {@code handlerId} resolve, then {@code handleIntercept(handlerId)}
* directly.
*
* <p>Two paths: <b>intercepted</b> (e.g. {@code http.status_code} -- exercises the
* double-&gt;single resolve), and <b>notIntercepted</b> (an app metric -- exercises the dropped
* {@code isOptimized} call; stores the primitive without boxing). Per-thread span so the (mutating)
* {@code setTag} has no cross-thread contention under {@code @Threads(8)}.
*
* <p>Run before (pre-collapse DDSpanContext) vs after by toggling DDSpanContext.java.
*
* <pre>
* ./gradlew :dd-trace-core:jmh # add -prof gc
* </pre>
*/
@Fork(2)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
@Threads(8)
public class DDSpanContextSetTagBenchmark {

private static final CoreTracer TRACER =
CoreTracer.builder().writer(new NoopWriter()).strictTraceWrites(false).build();

@State(Scope.Thread)
public static class SpanState {
DDSpan span;

@Setup
public void setup() {
this.span = (DDSpan) TRACER.startSpan("benchmark", "op");
}
}

@Benchmark
public Object intercepted(SpanState s) {
// DDSpan.setTag delegates to the refactored DDSpanContext.setTag(String, int)
return s.span.setTag(Tags.HTTP_STATUS, 200);
}

@Benchmark
public Object notIntercepted(SpanState s) {
return s.span.setTag("app.queue.depth", 200);
}

static final class NoopWriter implements Writer {
@Override
public void write(List<DDSpan> trace) {}

@Override
public void start() {}

@Override
public boolean flush() {
return true;
}

@Override
public void close() {}

@Override
public void incrementDropCounts(int spanCount) {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package datadog.trace.core.taginterceptor;

import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

/**
* Benchmark for {@link TagInterceptor#needsIntercept(String)} -- the per-tag gate {@code
* DDSpanContext} runs on every {@code setTag}. This is where the change lives: the old code
* switched over ~22 {@code String} case labels (a {@code hashCode} switch + {@code equals}); the
* new code does one open-addressed {@link datadog.trace.util.StringIndex} probe ({@code ==} fast
* path), falling to the (usually empty) split-tags set only on a miss.
*
* <p>Two paths, both exercised: <b>miss</b> (the common case -- most tags aren't intercepted, so
* the old switch fell all the way to {@code default}), and <b>hit</b> (an intercepted tag).
* Read-only, so {@code @Threads(8)} is clean (no span, no store to dilute the signal). Tag keys are
* interned literals -- the realistic case.
*
* <p>Run before (switch) vs after (StringIndex) by toggling TagInterceptor; {@code -prof gc} should
* show ~0 B/op both ways (this proves <i>throughput</i>, not allocation).
*
* <pre>
* ./gradlew :dd-trace-core:jmh # add -prof gc
* </pre>
*/
@Fork(2)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
@Threads(8)
@State(Scope.Benchmark)
public class TagInterceptorBenchmark {

private final TagInterceptor interceptor = new TagInterceptor(new RuleFlags());

// Intercepted (fixed) tags -- handlerId hits the FIXED index.
static final String[] INTERCEPTED = {
DDTags.RESOURCE_NAME,
DDTags.SERVICE_NAME,
Tags.DB_STATEMENT,
Tags.HTTP_STATUS,
Tags.ERROR,
Tags.SPAN_KIND,
Tags.HTTP_URL,
Tags.SAMPLING_PRIORITY,
};

// Ordinary integration/app tags that are NOT intercepted -- the common miss path.
static final String[] NOT_INTERCEPTED = {
"http.useragent",
"db.instance",
"messaging.system",
"component",
"thread.name",
"network.peer.address",
"http.route",
"rpc.method",
};

/** Per-thread cursors so threads don't contend on a shared index under {@code @Threads(8)}. */
@State(Scope.Thread)
public static class Cursor {
int hit;
int miss;
}

@Benchmark
public boolean intercepted(Cursor cursor) {
int i = cursor.hit;
cursor.hit = (i + 1) % INTERCEPTED.length;
return interceptor.needsIntercept(INTERCEPTED[i]);
}

@Benchmark
public boolean notIntercepted(Cursor cursor) {
int i = cursor.miss;
cursor.miss = (i + 1) % NOT_INTERCEPTED.length;
return interceptor.needsIntercept(NOT_INTERCEPTED[i]);
}
}
64 changes: 25 additions & 39 deletions dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -906,10 +906,11 @@ public void setTag(TagMap.EntryReader entry) {
return;
}

// pre-check to avoid boxing
// resolve the handler once; id 0 short-circuits without a second lookup
int handlerId = tagInterceptor.handlerId(entry.tag());
boolean intercepted =
precheckIntercept(entry.tag())
&& tagInterceptor.interceptTag(this, entry.tag(), entry.objectValue());
handlerId != 0
&& tagInterceptor.handleIntercept(this, handlerId, entry.tag(), entry.objectValue());
if (!intercepted) {
synchronized (unsafeTags) {
unsafeTags.set(entry);
Expand All @@ -918,31 +919,11 @@ public void setTag(TagMap.EntryReader entry) {
}

/*
* Uses to determine if there's an opportunity to avoid primitve boxing.
* If the underlying map doesn't support efficient primitives, then boxing is used.
* If the tag may be intercepted, then boxing is also used.
* Used when handlerId determined the tag is intercepted (id != 0): the primitive must be boxed to
* pass to the interceptor. The box is reused -- the optimized TagMap caches it on store.
*/
private boolean precheckIntercept(String tag) {
// Usually only a single instanceof TagMap will be loaded,
// so isOptimized is turned into a direct call and then inlines to a constant
// Since isOptimized just returns a constant - doesn't require synchronization
return !unsafeTags.isOptimized() || tagInterceptor.needsIntercept(tag);
}

/*
* Used when precheckIntercept determines that boxing is unavoidable
*
* Either because the tagInterceptor needs to be fully checked (which requires boxing)
* In that case, a box has already been created so it makes sense to pass the box
* onto TagMap, since optimized TagMap will cache the box
*
* -- OR --
*
* The TagMap isn't optimized and will need to box the primitive regardless of
* tag interception
*/
private void setBox(String tag, Object box) {
if (!tagInterceptor.interceptTag(this, tag, box)) {
private void setBox(int handlerId, String tag, Object box) {
if (!tagInterceptor.handleIntercept(this, handlerId, tag, box)) {
synchronized (unsafeTags) {
unsafeTags.set(tag, box);
}
Expand All @@ -953,8 +934,9 @@ public void setTag(final String tag, final boolean value) {
if (null == tag) {
return;
}
if (precheckIntercept(tag)) {
this.setBox(tag, value);
int handlerId = tagInterceptor.handlerId(tag);
if (handlerId != 0) {
this.setBox(handlerId, tag, value);
} else {
synchronized (unsafeTags) {
unsafeTags.set(tag, value);
Expand All @@ -966,8 +948,9 @@ public void setTag(final String tag, final int value) {
if (null == tag) {
return;
}
if (precheckIntercept(tag)) {
this.setBox(tag, value);
int handlerId = tagInterceptor.handlerId(tag);
if (handlerId != 0) {
this.setBox(handlerId, tag, value);
} else {
synchronized (unsafeTags) {
unsafeTags.set(tag, value);
Expand All @@ -979,10 +962,11 @@ public void setTag(final String tag, final long value) {
if (null == tag) {
return;
}
// check needsIntercept first to avoid unnecessary boxing
boolean intercepted =
tagInterceptor.needsIntercept(tag) && tagInterceptor.interceptTag(this, tag, value);
if (!intercepted) {
// resolve once; handlerId 0 (not intercepted) stores the primitive without boxing
int handlerId = tagInterceptor.handlerId(tag);
if (handlerId != 0) {
this.setBox(handlerId, tag, value);
} else {
synchronized (unsafeTags) {
unsafeTags.set(tag, value);
}
Expand All @@ -993,8 +977,9 @@ public void setTag(final String tag, final float value) {
if (null == tag) {
return;
}
if (precheckIntercept(tag)) {
this.setBox(tag, value);
int handlerId = tagInterceptor.handlerId(tag);
if (handlerId != 0) {
this.setBox(handlerId, tag, value);
} else {
synchronized (unsafeTags) {
unsafeTags.set(tag, value);
Expand All @@ -1006,8 +991,9 @@ public void setTag(final String tag, final double value) {
if (null == tag) {
return;
}
if (precheckIntercept(tag)) {
this.setBox(tag, value);
int handlerId = tagInterceptor.handlerId(tag);
if (handlerId != 0) {
this.setBox(handlerId, tag, value);
} else {
synchronized (unsafeTags) {
unsafeTags.set(tag, value);
Expand Down
Loading