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));
+ }
+}