|
| 1 | +package datadog.trace.api; |
| 2 | + |
| 3 | +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; |
| 4 | + |
| 5 | +/** |
| 6 | + * Registry for generated tag ID ↔ name resolution. The code generator populates this at tracer init |
| 7 | + * via {@link #register(Resolver)}. Once registered, HotSpot CHA devirtualizes and inlines the |
| 8 | + * resolver's switch, making {@link #nameOf}/{@link #keyOf} effectively zero-overhead. |
| 9 | + */ |
| 10 | +public final class KnownTagCodec { |
| 11 | + // Plain (non-volatile) fast-path flag: false until a resolver is ever registered. A plain read is |
| 12 | + // free and hoistable, unlike a volatile read of `resolver` (costly on weak memory models such as |
| 13 | + // ARM). A stale `false` is benign — callers treat the tag as unknown and use the hash buckets, |
| 14 | + // which is correct, just unoptimized; the next read after publication takes the slot path. |
| 15 | + private static boolean active; |
| 16 | + |
| 17 | + private static volatile Resolver resolver; |
| 18 | + |
| 19 | + /** Fast-path gate: true once a resolver has been registered. */ |
| 20 | + public static boolean isActive() { |
| 21 | + return active; |
| 22 | + } |
| 23 | + |
| 24 | + /* |
| 25 | + * tagId bit layout: [63 intercepted] [62-48 globalSerial (15 bits)] [47-32 fieldPos] |
| 26 | + * [31-0 nameHash]. Bit 63 (the sign bit) marks a tag the tag interceptor must see, so the check |
| 27 | + * is a single {@code tagId < 0}. globalSerial is globally unique per known tag; fieldPos is its |
| 28 | + * slot in the global positional layout (TagMap.knownEntries index); nameHash is |
| 29 | + * TagMap.Entry#_hash(name) and is layout-independent. Unknown (string-only) tags have the upper |
| 30 | + * 32 bits zero. NOTE: TagMap.Entry decodes nameHash inline as (int) tagId on its hot path, so the |
| 31 | + * low-32 encoding here must stay in sync with that. |
| 32 | + */ |
| 33 | + public static int globalSerial(long tagId) { |
| 34 | + return (int) ((tagId >>> 48) & 0x7FFF); |
| 35 | + } |
| 36 | + |
| 37 | + /** |
| 38 | + * Flag bit (the sign bit) marking a tag the tag interceptor must process — reserved/"virtual" |
| 39 | + * tags AND intercepted-but-stored tags (e.g. http.method, which the interceptor side-effects and |
| 40 | + * also stores). Encoded in the id so {@code DDSpanContext.setTag(long)} can route with a single |
| 41 | + * sign test ({@link #isIntercepted}) instead of resolving the name. Non-intercepted tags (peer.*, |
| 42 | + * base.service, …) leave it clear and take the fast store path. Must agree with the interceptor's |
| 43 | + * name-based {@code needsIntercept} for every assigned id. |
| 44 | + */ |
| 45 | + public static final long INTERCEPTED = Long.MIN_VALUE; // 1L << 63 |
| 46 | + |
| 47 | + /** True if the tagId is flagged for tag-interceptor processing. */ |
| 48 | + public static boolean isIntercepted(long tagId) { |
| 49 | + return tagId < 0L; |
| 50 | + } |
| 51 | + |
| 52 | + /** Returns the tagId with the {@link #INTERCEPTED} flag set. */ |
| 53 | + public static long intercepted(long tagId) { |
| 54 | + return tagId | INTERCEPTED; |
| 55 | + } |
| 56 | + |
| 57 | + public static int fieldPos(long tagId) { |
| 58 | + return (int) ((tagId >>> 32) & 0xFFFF); |
| 59 | + } |
| 60 | + |
| 61 | + public static int nameHash(long tagId) { |
| 62 | + return (int) tagId; |
| 63 | + } |
| 64 | + |
| 65 | + /** |
| 66 | + * globalSerial partition. {@code [1, FIRST_STORED_SERIAL)} is reserved for "virtual" tags that |
| 67 | + * are specially handled (redirected to span fields or processed by the tag interceptor) and are |
| 68 | + * NOT stored in the TagMap — these are hand-assigned in tracer core. {@code [FIRST_STORED_SERIAL, |
| 69 | + * ..]} is for generated convention tags that ARE stored (slotted/bucketed). {@code globalSerial |
| 70 | + * == 0} means unknown / string-only. Both core and the code generator must agree on this |
| 71 | + * boundary. |
| 72 | + */ |
| 73 | + public static final int FIRST_STORED_SERIAL = 256; |
| 74 | + |
| 75 | + /** True if the tagId names a reserved "virtual"/specially-handled tag (not stored in the map). */ |
| 76 | + public static boolean isReserved(long tagId) { |
| 77 | + int globalSerial = globalSerial(tagId); |
| 78 | + return globalSerial > 0 && globalSerial < FIRST_STORED_SERIAL; |
| 79 | + } |
| 80 | + |
| 81 | + /** True if the tagId names a generated, map-stored (slotted/bucketed) tag. */ |
| 82 | + public static boolean isStored(long tagId) { |
| 83 | + return globalSerial(tagId) >= FIRST_STORED_SERIAL; |
| 84 | + } |
| 85 | + |
| 86 | + /** |
| 87 | + * Sentinel {@code fieldPos} meaning "no positional slot". It is the maximum value the 16-bit |
| 88 | + * fieldPos field can hold, so it always compares {@code >= slotCount()} and routes to the hash |
| 89 | + * buckets rather than the fast positional array. Two kinds of tagId use it: |
| 90 | + * |
| 91 | + * <ul> |
| 92 | + * <li>Reserved/virtual tags ({@code globalSerial < FIRST_STORED_SERIAL}) — not stored at all; |
| 93 | + * the sentinel just guarantees an incidental store never lands in a slot. |
| 94 | + * <li>Unslotted stored tags ({@code globalSerial >= FIRST_STORED_SERIAL}) — "low-priority" tags |
| 95 | + * that get a stable id (and so {@code keyOf}/{@code nameOf} unification with their string |
| 96 | + * form) but are deliberately not given a slot, so they live in the buckets and don't widen |
| 97 | + * {@code knownEntries[]} for every span. {@code getEntry(long)} for these resolves the name |
| 98 | + * and rehashes — the cost of not owning a slot. |
| 99 | + * </ul> |
| 100 | + */ |
| 101 | + public static final int NO_SLOT = 0xFFFF; |
| 102 | + |
| 103 | + /** |
| 104 | + * True if the tagId names a stored tag that deliberately has no positional slot (bucket-only). |
| 105 | + */ |
| 106 | + public static boolean isUnslotted(long tagId) { |
| 107 | + return isStored(tagId) && fieldPos(tagId) == NO_SLOT; |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Builds a tagId from its parts: {@code globalSerial} (globally unique per known tag), {@code |
| 112 | + * fieldPos} (the tag's slot within its span type's positional table), and the tag {@code name} |
| 113 | + * (whose hash is computed via the same function the runtime uses, so the low 32 bits match {@link |
| 114 | + * TagMap.Entry#hash()}). Inverse of {@link #globalSerial}/{@link #fieldPos}/{@link #nameHash}. |
| 115 | + * Intended for the code generator and tests. |
| 116 | + */ |
| 117 | + public static long tagId(int globalSerial, int fieldPos, String name) { |
| 118 | + long nameHash = TagMap.Entry._hash(name) & 0xFFFFFFFFL; |
| 119 | + return ((long) globalSerial << 48) | ((long) (fieldPos & 0xFFFF) << 32) | nameHash; |
| 120 | + } |
| 121 | + |
| 122 | + /** |
| 123 | + * Builds a tagId with no positional slot ({@code fieldPos == }{@link #NO_SLOT}). Use for reserved |
| 124 | + * "virtual" tags and for "low-priority" stored tags that get a stable id but are intentionally |
| 125 | + * kept out of the fast slot array (they route to the hash buckets). See {@link #NO_SLOT}. |
| 126 | + */ |
| 127 | + public static long tagId(int globalSerial, String name) { |
| 128 | + return tagId(globalSerial, NO_SLOT, name); |
| 129 | + } |
| 130 | + |
| 131 | + // Number of positional slots in the global layout = (max stored fieldPos) + 1, declared by the |
| 132 | + // registered provider. Captured once at registration and read as a dynamic constant; TagMap sizes |
| 133 | + // its knownEntries array to exactly this rather than a hardcoded max. 0 when no resolver. |
| 134 | + private static int slotCount; |
| 135 | + |
| 136 | + /** Slot count of the registered provider (max stored fieldPos + 1); 0 if none. */ |
| 137 | + public static int slotCount() { |
| 138 | + return slotCount; |
| 139 | + } |
| 140 | + |
| 141 | + public interface Resolver { |
| 142 | + String nameOf(long tagId); |
| 143 | + |
| 144 | + long keyOf(String name); |
| 145 | + |
| 146 | + /** Number of positional slots this provider uses: (max stored fieldPos) + 1. */ |
| 147 | + int slotCount(); |
| 148 | + } |
| 149 | + |
| 150 | + @SuppressFBWarnings( |
| 151 | + value = "AT_STALE_THREAD_WRITE_OF_PRIMITIVE", |
| 152 | + justification = |
| 153 | + "active/slotCount are plain by design: written once at tracer-init registration (before" |
| 154 | + + " any span processing) and read plain on the hot path. A stale read is benign — the" |
| 155 | + + " tag is treated as unknown and takes the hash-bucket path — so plain reads are" |
| 156 | + + " deliberately preferred over a costly volatile read on weak memory models.") |
| 157 | + public static void register(Resolver resolver) { |
| 158 | + KnownTagCodec.resolver = resolver; // volatile write publishes the resolver |
| 159 | + KnownTagCodec.slotCount = (resolver != null) ? resolver.slotCount() : 0; |
| 160 | + KnownTagCodec.active = |
| 161 | + (resolver != null); // plain write; readers re-read resolver volatile anyway |
| 162 | + } |
| 163 | + |
| 164 | + public static String nameOf(long tagId) { |
| 165 | + if (!active) return null; |
| 166 | + Resolver r = resolver; |
| 167 | + return r != null ? r.nameOf(tagId) : null; |
| 168 | + } |
| 169 | + |
| 170 | + public static long keyOf(String name) { |
| 171 | + if (!active) return 0L; |
| 172 | + Resolver r = resolver; |
| 173 | + return r != null ? r.keyOf(name) : 0L; |
| 174 | + } |
| 175 | + |
| 176 | + private KnownTagCodec() {} |
| 177 | +} |
0 commit comments