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

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

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.Warmup;

/**
* Measures the cost of DDSpan.isOutbound(), which is called on every root span start and finish.
*/
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(NANOSECONDS)
@Fork(value = 1)
public class IsOutboundBenchmark {

static final CoreTracer TRACER = CoreTracer.builder().build();

private DDSpan clientSpan;
private DDSpan serverSpan;
private DDSpan unsetSpan;

@Setup
public void setup() {
clientSpan = (DDSpan) TRACER.startSpan("benchmark", "client.op");
clientSpan.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT);

serverSpan = (DDSpan) TRACER.startSpan("benchmark", "server.op");
serverSpan.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER);

unsetSpan = (DDSpan) TRACER.startSpan("benchmark", "unset.op");
}

@Benchmark
public boolean isOutbound_client() {
return clientSpan.isOutbound();
}

@Benchmark
public boolean isOutbound_server() {
return serverSpan.isOutbound();
}

@Benchmark
public boolean isOutbound_unset() {
return unsetSpan.isOutbound();
}

@Benchmark
public Object getTag_spanKind_client() {
return clientSpan.getTag(Tags.SPAN_KIND);
}

@Benchmark
public Object getTag_spanKind_unset() {
return unsetSpan.getTag(Tags.SPAN_KIND);
}
}
5 changes: 2 additions & 3 deletions dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities;
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities;
import datadog.trace.bootstrap.instrumentation.api.SpanWrapper;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.core.util.StackTraces;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
Expand Down Expand Up @@ -943,8 +942,8 @@ public DDSpan setMetaStruct(final String field, final Object value) {

@Override
public boolean isOutbound() {
Object spanKind = context.getTag(Tags.SPAN_KIND);
return Tags.SPAN_KIND_CLIENT.equals(spanKind) || Tags.SPAN_KIND_PRODUCER.equals(spanKind);
byte ordinal = context.getSpanKindOrdinal();
return ordinal == DDSpanContext.SPAN_KIND_CLIENT || ordinal == DDSpanContext.SPAN_KIND_PRODUCER;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,32 @@ public class DDSpanContext
private final UTF8BytesString threadName;

private volatile short httpStatusCode;
Comment thread
dougqh marked this conversation as resolved.

// Cached span.kind ordinal for fast isOutbound() checks.
Comment thread
dougqh marked this conversation as resolved.
// Ordinal constants -- keep in sync with SPAN_KIND_VALUES array.
static final byte SPAN_KIND_UNSET = 0;
static final byte SPAN_KIND_SERVER = 1;
static final byte SPAN_KIND_CLIENT = 2;
static final byte SPAN_KIND_PRODUCER = 3;
static final byte SPAN_KIND_CONSUMER = 4;
static final byte SPAN_KIND_INTERNAL = 5;
static final byte SPAN_KIND_BROKER = 6;
static final byte SPAN_KIND_CUSTOM = 7;

/** Maps ordinal to canonical string constant. Index 0 (UNSET) and 7 (CUSTOM) are null. */
static final String[] SPAN_KIND_VALUES = {
null, // UNSET
Tags.SPAN_KIND_SERVER,
Tags.SPAN_KIND_CLIENT,
Tags.SPAN_KIND_PRODUCER,
Tags.SPAN_KIND_CONSUMER,
Tags.SPAN_KIND_INTERNAL,
Tags.SPAN_KIND_BROKER,
null // CUSTOM
};

private volatile byte spanKindOrdinal = SPAN_KIND_UNSET;

private CharSequence integrationName;
private CharSequence serviceNameSource;

Expand Down Expand Up @@ -716,6 +742,51 @@ public short getHttpStatusCode() {
return httpStatusCode;
}

/** Identity-first string comparison: checks reference equality, then falls back to equals. */
static boolean tagEquals(String tagValue, String tagLiteral) {
return (tagValue == tagLiteral) || tagLiteral.equals(tagValue);
}

/**
* Cache the span.kind ordinal for fast isOutbound() checks. Called from TagInterceptor when
* span.kind is set.
*/
public void setSpanKind(String kind) {
if (kind == null) {
spanKindOrdinal = SPAN_KIND_UNSET;
} else if (tagEquals(kind, Tags.SPAN_KIND_SERVER)) {
spanKindOrdinal = SPAN_KIND_SERVER;
} else if (tagEquals(kind, Tags.SPAN_KIND_CLIENT)) {
spanKindOrdinal = SPAN_KIND_CLIENT;
} else if (tagEquals(kind, Tags.SPAN_KIND_PRODUCER)) {
spanKindOrdinal = SPAN_KIND_PRODUCER;
} else if (tagEquals(kind, Tags.SPAN_KIND_CONSUMER)) {
spanKindOrdinal = SPAN_KIND_CONSUMER;
} else if (tagEquals(kind, Tags.SPAN_KIND_INTERNAL)) {
spanKindOrdinal = SPAN_KIND_INTERNAL;
} else if (tagEquals(kind, Tags.SPAN_KIND_BROKER)) {
spanKindOrdinal = SPAN_KIND_BROKER;
} else {
spanKindOrdinal = SPAN_KIND_CUSTOM;
}
}

byte getSpanKindOrdinal() {
return spanKindOrdinal;
}

/** Returns the span.kind string from the cached ordinal, or falls back to the tag map. */
public String getSpanKindString() {
byte ordinal = spanKindOrdinal;
if (ordinal > SPAN_KIND_UNSET && ordinal < SPAN_KIND_CUSTOM) {
return SPAN_KIND_VALUES[ordinal];
}
// UNSET or CUSTOM -- fall through to tag map
synchronized (unsafeTags) {
return unsafeTags.getString(Tags.SPAN_KIND);
}
}

public void setOrigin(final CharSequence origin) {
DDSpanContext context = getRootSpanContextOrThis();
context.origin = origin;
Expand Down Expand Up @@ -763,6 +834,10 @@ public void setMetric(final TagMap.EntryReader entry) {
}

public void removeTag(String tag) {
if (tagEquals(tag, Tags.SPAN_KIND)) {
// Clear the cached ordinal; unsafeTags still needs to be updated below.
spanKindOrdinal = SPAN_KIND_UNSET;
Comment thread
dougqh marked this conversation as resolved.
}
synchronized (unsafeTags) {
unsafeTags.remove(tag);
}
Expand All @@ -782,9 +857,7 @@ public void setTag(final String tag, final Object value) {
return;
}
if (null == value) {
synchronized (unsafeTags) {
unsafeTags.remove(tag);
}
removeTag(tag);
} else if (!tagInterceptor.interceptTag(this, tag, value)) {
synchronized (unsafeTags) {
unsafeTags.set(tag, value);
Expand All @@ -797,9 +870,7 @@ public void setTag(final String tag, final String value) {
return;
}
if (null == value) {
synchronized (unsafeTags) {
unsafeTags.remove(tag);
}
removeTag(tag);
} else if (!tagInterceptor.interceptTag(this, tag, value)) {
synchronized (unsafeTags) {
unsafeTags.set(tag, value);
Expand Down Expand Up @@ -1015,6 +1086,8 @@ Object getTag(final String key) {
return threadName.toString();
case Tags.HTTP_STATUS:
return 0 == httpStatusCode ? null : (int) httpStatusCode;
case Tags.SPAN_KIND:
return getSpanKindString();
default:
Object value;
synchronized (unsafeTags) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public boolean needsIntercept(String tag) {
case HTTP_URL:
case ORIGIN_KEY:
case MEASURED:
case Tags.SPAN_KIND:
return true;

default:
Expand Down Expand Up @@ -193,6 +194,11 @@ public boolean interceptTag(DDSpanContext span, String tag, Object value) {
return interceptOrigin(span, value);
case MEASURED:
return interceptMeasured(span, value);
case Tags.SPAN_KIND:
// Cache the ordinal for fast isOutbound() checks.
// Return false so the value is still stored in unsafeTags for serialization.
span.setSpanKind(String.valueOf(value));
return false;
default:
return intercept(span, tag, value);
}
Expand Down Expand Up @@ -223,7 +229,7 @@ private static void setResourceFromUrl(
path = uri == null ? null : uri.getPath();
}
if (path != null) {
final boolean isClient = Tags.SPAN_KIND_CLIENT.equals(span.unsafeGetTag(Tags.SPAN_KIND));
final boolean isClient = Tags.SPAN_KIND_CLIENT.equals(span.getSpanKindString());
Copy link
Copy Markdown
Contributor Author

@dougqh dougqh Apr 15, 2026

Choose a reason for hiding this comment

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

Because I decided to keep the "enum" package visible for now, we cannot do a faster ordinal comparison here. I think that's okay.

Pair<CharSequence, Byte> normalized =
isClient
? HttpResourceNames.computeForClient(method, path, false)
Expand Down
Loading