diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java new file mode 100644 index 00000000000..e9e2d73b517 --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java @@ -0,0 +1,232 @@ +package datadog.trace.core; + +import static datadog.context.Context.root; +import static datadog.trace.bootstrap.instrumentation.api.AgentSpan.fromContext; +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.context.Context; +import datadog.context.ContextScope; +import datadog.context.propagation.CarrierSetter; +import datadog.context.propagation.CarrierVisitor; +import datadog.context.propagation.Propagators; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +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.Param; +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; + +/** + * One whole request per op, through the header code paths: extract the inbound headers with the + * default propagator, start the server span from what was extracted, run a few children, make one + * outbound HTTP call that injects its headers, then finish everything. + * + *

The existing propagation benchmarks each measure one step (extracting into a context, + * injecting from a prebuilt one, parsing one header value). They can't see costs that the extracted + * state causes later, on every span of the trace: for example, a {@code tracestate} that leads each + * span to allocate sampling state. Measuring the whole request catches those. + * + *

{@code headers} selects the inbound headers, under the default propagation styles: + * + *

+ */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class HeaderPathBenchmark { + static final int JDBC_SPANS = 3; + + static final String TRACE_ID_HEX = "66a0f6c800000000a3ce929d0e0e4736"; + static final String TRACE_ID_LOW = "11803532876627986230"; // a3ce929d0e0e4736 + static final String PARENT_ID = "67667974448284343"; // 00f067aa0ba902b7 + static final String PARENT_ID_HEX = "00f067aa0ba902b7"; + + static final String SERVER_INSTRUMENTATION = "tomcat-server"; + static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); + static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; + static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); + static final String CLIENT_INSTRUMENTATION = "okhttp"; + static final CharSequence CLIENT_OPERATION = UTF8BytesString.create("okhttp.request"); + + static final CarrierVisitor> GETTER = + new CarrierVisitor>() { + @Override + public void forEachKeyValue( + Map carrier, BiConsumer visitor) { + carrier.forEach(visitor); + } + }; + static final CarrierSetter> SETTER = Map::put; + + @Param({"none", "datadog", "tracecontext", "both", "bothBaggage"}) + String headers; + + CoreTracer tracer; + Map inbound; + SpanPrototype serverProto; + SpanPrototype jdbcProto; + SpanPrototype clientProto; + + /** One outbound carrier per thread, reused across requests. */ + @State(Scope.Thread) + public static class Outbound { + final Map headers = new HashMap<>(); + } + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + this.inbound = inboundHeaders(headers); + this.serverProto = + SpanPrototype.builder() + .initInstrumentationName(SERVER_INSTRUMENTATION) + .initOperationName(SERVER_OPERATION) + .initSpanType("web") + .initComponentAndIntegration("tomcat-server") + .initKind(Tags.SPAN_KIND_SERVER) + .initTag("language", "jvm") + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(JDBC_INSTRUMENTATION) + .initOperationName(JDBC_OPERATION) + .initSpanType("sql") + .initComponentAndIntegration("java-jdbc") + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + this.clientProto = + SpanPrototype.builder() + .initInstrumentationName(CLIENT_INSTRUMENTATION) + .initOperationName(CLIENT_OPERATION) + .initSpanType("http") + .initComponentAndIntegration("okhttp") + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + checkPropagation(); + } + + /** + * Fails fast if the headers aren't actually extracted and injected, or if the outbound call + * doesn't continue the extracted trace, either of which would measure something else. + */ + void checkPropagation() { + Context parent = Propagators.defaultPropagator().extract(root(), inbound, GETTER); + if (!headers.equals("none") && fromContext(parent) == null) { + throw new IllegalStateException("nothing extracted from " + headers + " headers"); + } + Outbound outbound = new Outbound(); + request(outbound); + String injectedTraceId = outbound.headers.get("x-datadog-trace-id"); + if (injectedTraceId == null) { + throw new IllegalStateException("nothing injected: " + outbound.headers); + } + if (!headers.equals("none") && !injectedTraceId.equals(TRACE_ID_LOW)) { + throw new IllegalStateException( + "outbound call didn't continue the extracted trace: " + outbound.headers); + } + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + static Map inboundHeaders(String style) { + Map h = new HashMap<>(); + h.put("host", "localhost:8080"); + h.put("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/120.0"); + h.put("accept", "application/json"); + if (style.equals("datadog") || style.startsWith("both")) { + h.put("x-datadog-trace-id", TRACE_ID_LOW); + h.put("x-datadog-parent-id", PARENT_ID); + h.put("x-datadog-sampling-priority", "1"); + h.put("x-datadog-tags", "_dd.p.dm=-0,_dd.p.tid=66a0f6c800000000"); + } + if (style.equals("tracecontext") || style.startsWith("both")) { + h.put("traceparent", "00-" + TRACE_ID_HEX + "-" + PARENT_ID_HEX + "-01"); + h.put( + "tracestate", + "dd=s:1;p:" + + PARENT_ID_HEX + + ";t.dm:-0;t.tid:66a0f6c800000000," + + "ot=th:8;rv:9b8233f7e3a151,rojo=00f067aa0ba902b7"); + } + if (style.equals("bothBaggage")) { + h.put("baggage", "userId=alice,serverNode=DF%2028,isProduction=false"); + } + return h; + } + + @Benchmark + public void request(Outbound outbound) { + outbound.headers.clear(); + // HttpServerDecorator.extract / startSpan + Context parent = Propagators.defaultPropagator().extract(root(), inbound, GETTER); + AgentSpan extractedSpan = fromContext(parent); + AgentSpanContext extracted = extractedSpan == null ? null : extractedSpan.spanContext(); + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION, extracted); + server.setMeasured(true); + server.applyOverwriting(serverProto); + server.setTag(Tags.HTTP_METHOD, "GET"); + server.setTag(Tags.HTTP_URL, "http://localhost:8080/owners/42"); + server.setTag(Tags.HTTP_ROUTE, "/owners/{ownerId}"); + server.setTag(Tags.PEER_PORT, 54321); + + try (ContextScope serverScope = tracer.attach(parent.with(server))) { + for (int i = 0; i < JDBC_SPANS; ++i) { + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.applyOverwriting(jdbcProto); + jdbc.setTag(Tags.DB_TYPE, "postgresql"); + jdbc.setTag(Tags.DB_INSTANCE, "petclinic"); + jdbc.setTag(Tags.PEER_HOSTNAME, "db.internal"); + jdbc.setResourceName("SELECT * FROM owners WHERE id = ?"); + try (ContextScope s = tracer.activateSpan(jdbc)) { + // the statement executes + } + jdbc.finish(); + } + + // the outbound call: start the client span, inject its headers, finish + AgentSpan client = tracer.startSpan(CLIENT_INSTRUMENTATION, CLIENT_OPERATION); + client.applyOverwriting(clientProto); + client.setTag(Tags.HTTP_METHOD, "GET"); + client.setTag(Tags.HTTP_URL, "http://inventory.internal/items/7"); + client.setTag(Tags.PEER_HOSTNAME, "inventory.internal"); + client.setTag(Tags.PEER_PORT, 8080); + Propagators.defaultPropagator() + .inject(tracer.currentContext().with(client), outbound.headers, SETTER); + client.setHttpStatusCode(200); + client.finish(); + } + server.setHttpStatusCode(200); + server.finish(); + } +} diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java new file mode 100644 index 00000000000..46e2887084e --- /dev/null +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/TraceShapeBenchmark.java @@ -0,0 +1,284 @@ +package datadog.trace.core; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +import datadog.context.ContextScope; +import datadog.trace.api.KnownTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPrototype; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +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.Param; +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; + +/** + * One whole trace per op, shaped like a web request: a server root, {@code children} jdbc + * prepared-statement children and one {@code @Trace} internal span, all finished. A trace has + * {@code children + 2} spans. + * + *

{@code children} varies the trace size, because single-span and small-trace results can hide + * costs that land on every child span: a change can make the root cheaper while charging each + * child, which only the larger traces show. + * + *

For retrospective comparison across tracer versions, each version keeps its own copy of this + * benchmark, so the arms reflect that version's code: + * + *

    + *
  • {@code legacy}: every tag set by name. It runs unchanged on every version, so it isolates + * the tracer's own costs. + *
  • {@code normal}: the calls this version's decorators make (copied from {@code + * HttpServerDecorator}, {@code JDBCDecorator} / {@code DatabaseClientDecorator} and {@code + * BaseDecorator} at this version), with request parsing replaced by fixed values. This is the + * version's real cost. + *
  • {@code newApi}: this version's newest span APIs, where they exist: here, construction from + * a {@link SpanPrototype} and tags set by id. + *
+ * + *

Tag values are realistic on purpose: the client port, the database port and the status fall + * outside {@code Integer}'s box cache, so primitive boxing shows up. + * + *

This copy: 1.68 snapshot (decorators apply a prototype in {@code afterStart}, #12037; peer + * tags by id, #12715). + */ +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@BenchmarkMode(Mode.Throughput) +@Threads(8) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn") +public class TraceShapeBenchmark { + /** The number of jdbc children, so the trace has {@code children + 2} spans. */ + @Param({"1", "5", "20"}) + int children; + + // server: HttpServerDecorator (tomcat) + static final String SERVER_INSTRUMENTATION = "tomcat-server"; + static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request"); + static final CharSequence SERVER_COMPONENT = UTF8BytesString.create("tomcat-server"); + static final CharSequence WEB = UTF8BytesString.create("web"); + static final String USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0"; + static final String METHOD = "GET"; + static final String URL = "http://localhost:8080/owners/42"; + static final String HOSTNAME = "localhost"; + static final String CLIENT_IP = "127.0.0.1"; + static final int CLIENT_PORT = 54321; // ephemeral, outside the box cache + static final String ROUTE = "/owners/{ownerId}"; + static final CharSequence SERVER_RESOURCE = UTF8BytesString.create("GET /owners/{ownerId}"); + static final int STATUS = 200; // outside the box cache + + // jdbc: JDBCDecorator over a postgres prepared statement + static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement"; + static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query"); + static final CharSequence JDBC_RENAMED_OPERATION = UTF8BytesString.create("postgresql.query"); + static final CharSequence JAVA_JDBC = UTF8BytesString.create("java-jdbc"); + static final CharSequence JDBC_PREPARED_STATEMENT = + UTF8BytesString.create("java-jdbc-prepared_statement"); + static final CharSequence SQL = UTF8BytesString.create("sql"); + static final String DB_TYPE = "postgresql"; + static final String DB_SERVICE = "postgresql"; + static final String DB_POOL = "HikariPool-1"; + static final String DB_USER = "app"; + static final String DB_INSTANCE = "petclinic"; + static final String DB_HOST = "db.internal"; + static final String DB_OPERATION = "SELECT"; + static final CharSequence STATEMENT = UTF8BytesString.create("SELECT * FROM owners WHERE id = ?"); + + // internal: TraceDecorator (@Trace) + static final String TRACE_INSTRUMENTATION = "trace"; + static final CharSequence TRACE_OPERATION = UTF8BytesString.create("trace.annotation"); + static final CharSequence TRACE_COMPONENT = UTF8BytesString.create("trace"); + static final CharSequence TRACE_RESOURCE = UTF8BytesString.create("OwnerService.findOwner"); + + CoreTracer tracer; + + SpanPrototype serverProto; + SpanPrototype jdbcProto; + SpanPrototype traceProto; + + @Setup + public void setup(Blackhole blackhole) { + this.tracer = CoreTracer.builder().writer(new DropWriter(blackhole)).build(); + // the decorators' prototype chains: Base (type, component) -> Server / Client (kind, language) + this.serverProto = + SpanPrototype.builder() + .initInstrumentationName(SERVER_INSTRUMENTATION) + .initOperationName(SERVER_OPERATION) + .initSpanType(WEB) + .initComponentAndIntegration(SERVER_COMPONENT) + .initKind(Tags.SPAN_KIND_SERVER) + .initTag("language", "jvm") + .build(); + this.jdbcProto = + SpanPrototype.builder() + .initInstrumentationName(JDBC_INSTRUMENTATION) + .initOperationName(JDBC_OPERATION) + .initSpanType(SQL) + .initComponentAndIntegration(JAVA_JDBC) + .initKind(Tags.SPAN_KIND_CLIENT) + .build(); + this.traceProto = + SpanPrototype.builder() + .initInstrumentationName(TRACE_INSTRUMENTATION) + .initOperationName(TRACE_OPERATION) + .initComponentAndIntegration(TRACE_COMPONENT) + .build(); + } + + @TearDown + public void tearDown() { + this.tracer.close(); + } + + // ---- legacy: every tag by name ---- + + @Benchmark + public void legacy() { + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); + server.setSpanType(WEB); + server.setTag(Tags.COMPONENT, SERVER_COMPONENT); + server.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER); + server.setTag("language", "jvm"); + legacyRequest(server); + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < children; ++i) { + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.setSpanType(SQL); + jdbc.setTag(Tags.COMPONENT, JAVA_JDBC); + jdbc.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT); + legacyJdbc(jdbc); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(TRACE_INSTRUMENTATION, TRACE_OPERATION); + internal.setTag(Tags.COMPONENT, TRACE_COMPONENT); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); + server.finish(); + } + + static void legacyRequest(AgentSpan server) { + server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); + server.setTag(Tags.HTTP_METHOD, METHOD); + server.setTag(Tags.HTTP_URL, URL); + server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); + server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); + server.setTag(Tags.PEER_PORT, CLIENT_PORT); + server.setTag(Tags.HTTP_ROUTE, ROUTE); + } + + static void legacyJdbc(AgentSpan jdbc) { + jdbc.setTag(Tags.DB_TYPE, DB_TYPE); + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(Tags.DB_POOL_NAME, DB_POOL); + jdbc.setTag(Tags.DB_USER, DB_USER); + jdbc.setTag(Tags.DB_INSTANCE, DB_INSTANCE); + jdbc.setTag(Tags.PEER_HOSTNAME, DB_HOST); + jdbc.setResourceName(STATEMENT); + jdbc.setTag(Tags.DB_OPERATION, DB_OPERATION); + jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); + } + + // ---- normal: what this version's decorators do ---- + + @Benchmark + public void normal() { + // HttpServerDecorator: startSpan, afterStart, onRequest, then onResponseStatus at the end + AgentSpan server = tracer.startSpan(SERVER_INSTRUMENTATION, SERVER_OPERATION); + server.applyOverwriting(serverProto); // BaseDecorator.afterStart + server.setTag(Tags.HTTP_USER_AGENT, USER_AGENT); + server.setTag(Tags.HTTP_METHOD, METHOD); + server.setTag(Tags.HTTP_URL, URL); + server.setTag(Tags.HTTP_HOSTNAME, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); // HttpResourceDecorator.withServerPath + server.setTag(Tags.PEER_HOST_IPV4, CLIENT_IP); + server.setTag(Tags.PEER_PORT, CLIENT_PORT); // setPeerPort + server.setTag(Tags.HTTP_ROUTE, ROUTE); // the framework, once routing resolves + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < children; ++i) { + // AbstractPreparedStatementInstrumentation: startSpan, afterStart, onConnection, + // onPreparedStatement + AgentSpan jdbc = tracer.startSpan(JDBC_INSTRUMENTATION, JDBC_OPERATION); + jdbc.applyOverwriting(jdbcProto); + jdbc.setTag(Tags.DB_TYPE, DB_TYPE); // processDatabaseType + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); // postProcessServiceAndOperationName + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(Tags.DB_POOL_NAME, DB_POOL); + jdbc.setTag(Tags.DB_USER, DB_USER); // DatabaseClientDecorator.onConnection + jdbc.setTag(Tags.DB_INSTANCE, DB_INSTANCE); + jdbc.setTag(Tags.PEER_HOSTNAME, DB_HOST); + jdbc.setResourceName(STATEMENT); // withQueryInfo + jdbc.setTag(Tags.DB_OPERATION, DB_OPERATION); + jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); + jdbc.setTag(Tags.COMPONENT, JDBC_PREPARED_STATEMENT); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(TRACE_INSTRUMENTATION, TRACE_OPERATION); + internal.applyOverwriting(traceProto); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); // onResponseStatus + server.finish(); + } + + // ---- newApi: construction from a prototype, tags by id ---- + + @Benchmark + public void newApi() { + AgentSpan server = tracer.startSpan(serverProto, SERVER_OPERATION); + server.setTag(KnownTags.HTTP_USERAGENT_ID, USER_AGENT); + server.setTag(KnownTags.HTTP_METHOD_ID, METHOD); + server.setTag(KnownTags.HTTP_URL_ID, URL); + server.setTag(KnownTags.HTTP_HOSTNAME_ID, HOSTNAME); + server.setResourceName(SERVER_RESOURCE); + server.setTag(KnownTags.PEER_IPV4_ID, CLIENT_IP); + server.setTag(KnownTags.PEER_PORT_ID, CLIENT_PORT); + server.setTag(KnownTags.HTTP_ROUTE_ID, ROUTE); + try (ContextScope scope = tracer.activateSpan(server)) { + for (int i = 0; i < children; ++i) { + AgentSpan jdbc = tracer.startSpan(jdbcProto, JDBC_OPERATION); + jdbc.setTag(KnownTags.DB_TYPE_ID, DB_TYPE); + jdbc.setServiceName(DB_SERVICE, JAVA_JDBC); + jdbc.setOperationName(JDBC_RENAMED_OPERATION); + jdbc.setTag(KnownTags.DB_POOL_NAME_ID, DB_POOL); + jdbc.setTag(KnownTags.DB_USER_ID, DB_USER); + jdbc.setTag(KnownTags.DB_INSTANCE_ID, DB_INSTANCE); + jdbc.setTag(KnownTags.PEER_HOSTNAME_ID, DB_HOST); + jdbc.setResourceName(STATEMENT); + jdbc.setTag(KnownTags.DB_OPERATION_ID, DB_OPERATION); + jdbc.spanContext().setIntegrationName(JDBC_PREPARED_STATEMENT); + jdbc.setTag(KnownTags.COMPONENT_ID, JDBC_PREPARED_STATEMENT); + finishScoped(jdbc); + } + AgentSpan internal = tracer.startSpan(traceProto, TRACE_OPERATION); + internal.setResourceName(TRACE_RESOURCE); + finishScoped(internal); + } + server.setHttpStatusCode(STATUS); + server.finish(); + } + + /** The advice's exit: activate around the call, then close and finish. */ + void finishScoped(AgentSpan span) { + ContextScope scope = tracer.activateSpan(span); + scope.close(); + span.finish(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 0fde50c117f..296d52399a6 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -497,14 +497,27 @@ static long customHash(String tag) { } /** - * Folds a tag hash to the 32-bit bucket hash. A tag id's serial is in bits 63-48, so it is - * folded down from there rather than from bit 32, where a bucket mask would discard it. + * Folds a tag hash to the 32-bit bucket hash, whose low bits pick the bucket. * - *

Never zero: BucketGroup treats a zero hash as a vacant slot. The fold can produce zero - * when a serial equals the id's flag bits (serial 4 with the trace-level bit, 4), so zero maps - * to the same nonzero sentinel {@link #_hash} uses. + *

A known tag with a slot is bucketed by its slot: slot coloring gives tags that appear + * together on a span type distinct slots, so they mostly land in distinct buckets and need no + * BucketGroup. Trace-level tags reuse the span-level slot numbers, so their level bit moves + * them to the other half of the buckets. The serial sits above the slot, so two known tags + * never share a bucket hash. + * + *

A known tag without a slot folds its serial (bits 63-48) down instead, and a custom tag + * uses its name hash. + * + *

Never zero: BucketGroup treats a zero hash as a vacant slot. A slotted known tag's hash + * carries its nonzero serial; the other folds map zero to the same nonzero sentinel {@link + * #_hash} uses. */ static int bucketHash(long tagHash) { + int slot = (int) (tagHash >>> 32) & 0xFFFF; + if ((tagHash >>> 32) != 0 && slot != KnownTagCodec.NO_SLOT) { + int level = (int) (tagHash & KnownTagCodec.LEVEL_TRACE) << 1; // trace-level: flip bit 3 + return ((int) (tagHash >>> 48) << 16) | (slot ^ level); + } int hash = (int) (tagHash >>> 48) ^ (int) tagHash; return hash == 0 ? 0xDD06 : hash; } diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java new file mode 100644 index 00000000000..7a58dbe3534 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapBucketHashTest.java @@ -0,0 +1,67 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +/** How {@link TagMap.Entry#bucketHash} places known and custom tags in the buckets. */ +class TagMapBucketHashTest { + private static final int BUCKET_MASK = 15; // TagMap's 16 buckets + + /** Every known tag id the registry generated. */ + static List knownTagIds() throws IllegalAccessException { + List ids = new ArrayList<>(); + for (Field field : KnownTags.class.getFields()) { + if (Modifier.isStatic(field.getModifiers()) + && field.getType() == long.class + && field.getName().endsWith("_ID")) { + ids.add(field.getLong(null)); + } + } + return ids; + } + + @Test + void knownTagsGetDistinctNonzeroBucketHashes() throws IllegalAccessException { + List ids = knownTagIds(); + assertFalse(ids.isEmpty()); + Set hashes = new HashSet<>(); + for (long id : ids) { + int hash = TagMap.Entry.bucketHash(id); + assertNotEquals(0, hash, "zero marks a vacant BucketGroup slot"); + assertTrue( + hashes.add(hash), "two known tags share a bucket hash: " + KnownTagCodec.nameOf(id)); + } + } + + @Test + void aSlottedKnownTagIsBucketedBySlot() throws IllegalAccessException { + for (long id : knownTagIds()) { + int slot = KnownTagCodec.slotOf(id); + if (slot == KnownTagCodec.NO_SLOT) { + continue; + } + // trace-level tags reuse the span-level slot numbers, so they move to the other half + int expected = (slot ^ (KnownTagCodec.isTraceLevel(id) ? 8 : 0)) & BUCKET_MASK; + assertEquals(expected, TagMap.Entry.bucketHash(id) & BUCKET_MASK, KnownTagCodec.nameOf(id)); + } + } + + @Test + void tagsWithoutASlotAndCustomTagsKeepTheirHashes() { + long noSlot = ((long) 5 << 48) | ((long) KnownTagCodec.NO_SLOT << 32); + assertEquals((int) (noSlot >>> 48) ^ (int) noSlot, TagMap.Entry.bucketHash(noSlot)); + + long custom = TagMap.Entry.customHash("my.custom.tag"); + assertEquals((int) custom == 0 ? 0xDD06 : (int) custom, TagMap.Entry.bucketHash(custom)); + } +}