diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/IsOutboundBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/IsOutboundBenchmark.java new file mode 100644 index 00000000000..1200dea6387 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/IsOutboundBenchmark.java @@ -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); + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 2c2e5d9d1f2..bc8e14ead06 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -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; @@ -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 diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 2b8dbccb738..c01cdaf6b71 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -97,6 +97,32 @@ public class DDSpanContext private final UTF8BytesString threadName; private volatile short httpStatusCode; + + // Cached span.kind ordinal for fast isOutbound() checks. + // 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; @@ -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 setSpanKindOrdinal(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; @@ -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; + } synchronized (unsafeTags) { unsafeTags.remove(tag); } @@ -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); @@ -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); @@ -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) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index e37d89c1fb5..18a88fd7b4a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -123,6 +123,7 @@ public boolean needsIntercept(String tag) { case HTTP_URL: case ORIGIN_KEY: case MEASURED: + case Tags.SPAN_KIND: return true; default: @@ -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.setSpanKindOrdinal(String.valueOf(value)); + return false; default: return intercept(span, tag, value); } @@ -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()); Pair normalized = isClient ? HttpResourceNames.computeForClient(method, path, false) diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanContextTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanContextTest.groovy index af6c813f3c0..fc7d4ce93a4 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanContextTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanContextTest.groovy @@ -6,6 +6,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration import datadog.trace.bootstrap.instrumentation.api.ServiceNameSources +import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.common.writer.ListWriter import datadog.trace.core.propagation.ExtractedContext import datadog.trace.core.test.DDCoreSpecification @@ -352,4 +353,117 @@ class DDSpanContextTest extends DDCoreSpecification { } assert sourceWithoutCommonTags == comparison } + + def "span kind ordinal constants and SPAN_KIND_VALUES array stay in sync"() { + expect: "SPAN_KIND_VALUES array covers all ordinals" + DDSpanContext.SPAN_KIND_VALUES.length == DDSpanContext.SPAN_KIND_CUSTOM + 1 + + and: "each known ordinal maps to the correct Tags constant" + DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_SERVER] == Tags.SPAN_KIND_SERVER + DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_CLIENT] == Tags.SPAN_KIND_CLIENT + DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_PRODUCER] == Tags.SPAN_KIND_PRODUCER + DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_CONSUMER] == Tags.SPAN_KIND_CONSUMER + DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_INTERNAL] == Tags.SPAN_KIND_INTERNAL + DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_BROKER] == Tags.SPAN_KIND_BROKER + + and: "UNSET and CUSTOM map to null" + DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_UNSET] == null + DDSpanContext.SPAN_KIND_VALUES[DDSpanContext.SPAN_KIND_CUSTOM] == null + } + + def "setSpanKindOrdinal round-trips with SPAN_KIND_VALUES for all known kinds"() { + when: + def span = tracer.buildSpan("test", "test").start() + def context = (DDSpanContext) span.context() + context.setSpanKindOrdinal(kindString) + + then: + context.getSpanKindOrdinal() == expectedOrdinal + DDSpanContext.SPAN_KIND_VALUES[expectedOrdinal] == kindString + + cleanup: + span.finish() + + where: + kindString | expectedOrdinal + Tags.SPAN_KIND_SERVER | DDSpanContext.SPAN_KIND_SERVER + Tags.SPAN_KIND_CLIENT | DDSpanContext.SPAN_KIND_CLIENT + Tags.SPAN_KIND_PRODUCER | DDSpanContext.SPAN_KIND_PRODUCER + Tags.SPAN_KIND_CONSUMER | DDSpanContext.SPAN_KIND_CONSUMER + Tags.SPAN_KIND_INTERNAL | DDSpanContext.SPAN_KIND_INTERNAL + Tags.SPAN_KIND_BROKER | DDSpanContext.SPAN_KIND_BROKER + } + + def "setTag and getTag round-trip for span.kind"() { + when: + def span = tracer.buildSpan("test", "test").start() + span.setTag(Tags.SPAN_KIND, kindString) + + then: + span.getTag(Tags.SPAN_KIND) == kindString + + cleanup: + span.finish() + + where: + kindString << [ + Tags.SPAN_KIND_SERVER, + Tags.SPAN_KIND_CLIENT, + Tags.SPAN_KIND_PRODUCER, + Tags.SPAN_KIND_CONSUMER, + Tags.SPAN_KIND_INTERNAL, + Tags.SPAN_KIND_BROKER, + ] + } + + def "getTag returns null when span.kind is not set"() { + when: + def span = tracer.buildSpan("test", "test").start() + + then: + span.getTag(Tags.SPAN_KIND) == null + + cleanup: + span.finish() + } + + def "setTag then removeTag clears span.kind"() { + when: + def span = tracer.buildSpan("test", "test").start() + span.setTag(Tags.SPAN_KIND, kindString) + + then: + span.getTag(Tags.SPAN_KIND) == kindString + + when: + ((DDSpan) span).context().removeTag(Tags.SPAN_KIND) + + then: + span.getTag(Tags.SPAN_KIND) == null + + cleanup: + span.finish() + + where: + kindString << [ + Tags.SPAN_KIND_SERVER, + Tags.SPAN_KIND_CLIENT, + Tags.SPAN_KIND_PRODUCER, + Tags.SPAN_KIND_CONSUMER, + Tags.SPAN_KIND_INTERNAL, + Tags.SPAN_KIND_BROKER, + ] + } + + def "setTag with custom span.kind falls back to tag map"() { + when: + def span = tracer.buildSpan("test", "test").start() + span.setTag(Tags.SPAN_KIND, "custom-kind") + + then: + span.getTag(Tags.SPAN_KIND) == "custom-kind" + + cleanup: + span.finish() + } }