diff --git a/internal-api/src/main/java/datadog/trace/util/Hashtable.java b/internal-api/src/main/java/datadog/trace/util/Hashtable.java index cc7bc4eddf5..32351cbf208 100644 --- a/internal-api/src/main/java/datadog/trace/util/Hashtable.java +++ b/internal-api/src/main/java/datadog/trace/util/Hashtable.java @@ -8,6 +8,8 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Light weight simple Hashtable system that can be useful when HashMap would be unnecessarily @@ -23,10 +25,14 @@ * Convenience classes are provided for lower key dimensions. * *

For higher key dimensions, client code must implement its own class, but can still use the - * support class to ease the implementation complexity. + * static building blocks on this class to ease the implementation complexity. * *

This outer class is a pure namespace -- it can't be instantiated. The actual table types are - * {@link D1}, {@link D2}, and (for higher-arity callers) {@link Support}-driven custom tables. + * {@link D1}, {@link D2}, and (for higher-arity callers) custom tables driven by the static + * building blocks on this class (see {@link #createFixedBuckets(Class, int)}, {@link + * #bucket(Hashtable.Entry[], long)}, {@link #insertHeadEntry(Hashtable.Entry[], int, + * Hashtable.Entry)}, and friends). The deprecated {@link Support} class is a thin facade over those + * same statics, retained for source compatibility. */ public final class Hashtable { private Hashtable() {} @@ -37,7 +43,8 @@ private Hashtable() {} * *

Subclasses add the actual key field(s) and a {@code matches(...)} method tailored to their * key arity. See {@link D1.Entry} and {@link D2.Entry}; for higher arities, client code can - * subclass this directly and use {@link Support} to drive the table mechanics. + * subclass this directly and drive the table with the static building blocks on {@link + * Hashtable}. */ public abstract static class Entry { public final long keyHash; @@ -47,11 +54,12 @@ protected Entry(long keyHash) { this.keyHash = keyHash; } - public final void setNext(TEntry next) { + public final void setNext(@Nullable TEntry next) { this.next = next; } @SuppressWarnings("unchecked") + @Nullable public final TEntry next() { return (TEntry) this.next; } @@ -94,17 +102,18 @@ public static final class D1> { public abstract static class Entry extends Hashtable.Entry { final K key; - protected Entry(K key) { + protected Entry(@Nullable K key) { super(hash(key)); this.key = key; } /** The key this entry was created with. */ + @Nullable public K key() { return this.key; } - public boolean matches(Object key) { + public boolean matches(@Nullable Object key) { return Objects.equals(this.key, key); } @@ -116,40 +125,57 @@ public boolean matches(Object key) { * [Integer.MIN_VALUE, Integer.MAX_VALUE]}; real-key collisions in chains are resolved by * {@link #matches(Object)}. */ - public static long hash(Object key) { + public static long hash(@Nullable Object key) { return (key == null) ? Long.MIN_VALUE : key.hashCode(); } } - // Package-private so iterator tests in the same package can drive Support.bucketIterator and - // friends directly against the table's bucket array. + // Package-private so iterator tests in the same package can drive the Hashtable static + // building blocks directly against the table's bucket array. final Hashtable.Entry[] buckets; private int size; public D1(int capacity) { - this.buckets = Support.create(capacity); + this.buckets = new Hashtable.Entry[sizeFor(capacity)]; this.size = 0; } + /** + * Creates a single-key table with a fixed bucket count sized for {@code capacity} entries. The + * {@code entryClass} pins the concrete entry type so the compiler infers both {@code K} and + * {@code TEntry} at the call site -- e.g. {@code D1.createFixedBuckets(MyEntry.class, 64)} -- + * keeping the factory symmetric with the rest of the flat-collections family (see {@link + * Hashtable#createFixedBuckets(Class, int)} for why the class isn't otherwise consumed). + * Capacity is fixed; the table does not resize. + */ + @Nonnull + public static > D1 createFixedBuckets( + @Nonnull Class entryClass, int capacity) { + return new D1<>(capacity); + } + public int size() { return this.size; } - public TEntry get(K key) { + @Nullable + public TEntry get(@Nullable K key) { long keyHash = D1.Entry.hash(key); - for (TEntry te = Support.bucket(this.buckets, keyHash); te != null; te = te.next()) { - if (te.keyHash == keyHash && te.matches(key)) { - return te; + for (TEntry curEntry = bucket(this.buckets, keyHash); + curEntry != null; + curEntry = curEntry.next()) { + if (curEntry.keyHash == keyHash && curEntry.matches(key)) { + return curEntry; } } return null; } - public TEntry remove(K key) { + @Nullable + public TEntry remove(@Nullable K key) { long keyHash = D1.Entry.hash(key); - for (MutatingBucketIterator iter = - Support.mutatingBucketIterator(this.buckets, keyHash); + for (MutatingBucketIterator iter = mutatingBucketIterator(this.buckets, keyHash); iter.hasNext(); ) { TEntry curEntry = iter.next(); @@ -163,14 +189,15 @@ public TEntry remove(K key) { return null; } - public void insert(TEntry newEntry) { - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + public void insert(@Nonnull TEntry newEntry) { + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; } - public TEntry insertOrReplace(TEntry newEntry) { + @Nullable + public TEntry insertOrReplace(@Nonnull TEntry newEntry) { for (MutatingBucketIterator iter = - Support.mutatingBucketIterator(this.buckets, newEntry.keyHash); + mutatingBucketIterator(this.buckets, newEntry.keyHash); iter.hasNext(); ) { TEntry curEntry = iter.next(); @@ -180,7 +207,7 @@ public TEntry insertOrReplace(TEntry newEntry) { } } - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; return null; } @@ -195,26 +222,30 @@ public TEntry insertOrReplace(TEntry newEntry) { * that calls {@code super(key)}. A mismatched hash will leave the new entry inserted at a * bucket that future {@link #get} calls won't probe. */ - public TEntry getOrCreate(K key, Function creator) { + @Nonnull + public TEntry getOrCreate( + @Nullable K key, @Nonnull Function creator) { long keyHash = D1.Entry.hash(key); - for (TEntry te = Support.bucket(this.buckets, keyHash); te != null; te = te.next()) { - if (te.keyHash == keyHash && te.matches(key)) { - return te; + for (TEntry curEntry = bucket(this.buckets, keyHash); + curEntry != null; + curEntry = curEntry.next()) { + if (curEntry.keyHash == keyHash && curEntry.matches(key)) { + return curEntry; } } TEntry newEntry = creator.apply(key); - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; return newEntry; } public void clear() { - Support.clear(this.buckets); + Hashtable.clear(this.buckets); this.size = 0; } - public void forEach(Consumer consumer) { - Support.forEach(this.buckets, consumer); + public void forEach(@Nonnull Consumer consumer) { + Hashtable.forEach(this.buckets, consumer); } /** @@ -222,8 +253,8 @@ public void forEach(Consumer consumer) { * -- pass a non-capturing {@link BiConsumer} (typically a {@code static final}) plus whatever * side-band state it needs as {@code context}. */ - public void forEach(T context, BiConsumer consumer) { - Support.forEach(this.buckets, context, consumer); + public void forEach(C context, @Nonnull BiConsumer consumer) { + Hashtable.forEach(this.buckets, context, consumer); } } @@ -264,13 +295,25 @@ public abstract static class Entry extends Hashtable.Entry { final K1 key1; final K2 key2; - protected Entry(K1 key1, K2 key2) { + protected Entry(@Nullable K1 key1, @Nullable K2 key2) { super(hash(key1, key2)); this.key1 = key1; this.key2 = key2; } - public boolean matches(K1 key1, K2 key2) { + /** The first key part this entry was created with. */ + @Nullable + public K1 key1() { + return this.key1; + } + + /** The second key part this entry was created with. */ + @Nullable + public K2 key2() { + return this.key2; + } + + public boolean matches(@Nullable K1 key1, @Nullable K2 key2) { return Objects.equals(this.key1, key1) && Objects.equals(this.key2, key2); } @@ -281,7 +324,7 @@ public boolean matches(K1 key1, K2 key2) { * combinations whose chained hash equals {@code hash(0, 0) = 0} or similar values. {@link * #matches(Object, Object)} resolves any such collision. */ - public static long hash(Object key1, Object key2) { + public static long hash(@Nullable Object key1, @Nullable Object key2) { return LongHashingUtils.hash(key1, key2); } } @@ -291,29 +334,46 @@ public static long hash(Object key1, Object key2) { private int size; public D2(int capacity) { - this.buckets = Support.create(capacity); + this.buckets = new Hashtable.Entry[sizeFor(capacity)]; this.size = 0; } + /** + * Creates a composite-key table with a fixed bucket count sized for {@code capacity} entries. + * The {@code entryClass} pins the concrete entry type so the compiler infers {@code K1}, {@code + * K2}, and {@code TEntry} at the call site -- e.g. {@code D2.createFixedBuckets(MyEntry.class, + * 64)} -- keeping the factory symmetric with the rest of the flat-collections family (see + * {@link Hashtable#createFixedBuckets(Class, int)} for why the class isn't otherwise consumed). + * Capacity is fixed; the table does not resize. + */ + @Nonnull + public static > D2 createFixedBuckets( + @Nonnull Class entryClass, int capacity) { + return new D2<>(capacity); + } + public int size() { return this.size; } - public TEntry get(K1 key1, K2 key2) { + @Nullable + public TEntry get(@Nullable K1 key1, @Nullable K2 key2) { long keyHash = D2.Entry.hash(key1, key2); - for (TEntry te = Support.bucket(this.buckets, keyHash); te != null; te = te.next()) { - if (te.keyHash == keyHash && te.matches(key1, key2)) { - return te; + for (TEntry curEntry = bucket(this.buckets, keyHash); + curEntry != null; + curEntry = curEntry.next()) { + if (curEntry.keyHash == keyHash && curEntry.matches(key1, key2)) { + return curEntry; } } return null; } - public TEntry remove(K1 key1, K2 key2) { + @Nullable + public TEntry remove(@Nullable K1 key1, @Nullable K2 key2) { long keyHash = D2.Entry.hash(key1, key2); - for (MutatingBucketIterator iter = - Support.mutatingBucketIterator(this.buckets, keyHash); + for (MutatingBucketIterator iter = mutatingBucketIterator(this.buckets, keyHash); iter.hasNext(); ) { TEntry curEntry = iter.next(); @@ -327,14 +387,15 @@ public TEntry remove(K1 key1, K2 key2) { return null; } - public void insert(TEntry newEntry) { - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + public void insert(@Nonnull TEntry newEntry) { + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; } - public TEntry insertOrReplace(TEntry newEntry) { + @Nullable + public TEntry insertOrReplace(@Nonnull TEntry newEntry) { for (MutatingBucketIterator iter = - Support.mutatingBucketIterator(this.buckets, newEntry.keyHash); + mutatingBucketIterator(this.buckets, newEntry.keyHash); iter.hasNext(); ) { TEntry curEntry = iter.next(); @@ -344,7 +405,7 @@ public TEntry insertOrReplace(TEntry newEntry) { } } - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; return null; } @@ -354,27 +415,32 @@ public TEntry insertOrReplace(TEntry newEntry) { * both lookup and (on miss) insert. The {@code creator} is expected to build an entry whose * {@code keyHash} equals {@link Entry#hash(Object, Object) D2.Entry.hash(key1, key2)}. */ + @Nonnull public TEntry getOrCreate( - K1 key1, K2 key2, BiFunction creator) { + @Nullable K1 key1, + @Nullable K2 key2, + @Nonnull BiFunction creator) { long keyHash = D2.Entry.hash(key1, key2); - for (TEntry te = Support.bucket(this.buckets, keyHash); te != null; te = te.next()) { - if (te.keyHash == keyHash && te.matches(key1, key2)) { - return te; + for (TEntry curEntry = bucket(this.buckets, keyHash); + curEntry != null; + curEntry = curEntry.next()) { + if (curEntry.keyHash == keyHash && curEntry.matches(key1, key2)) { + return curEntry; } } TEntry newEntry = creator.apply(key1, key2); - Support.insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); + insertHeadEntry(this.buckets, newEntry.keyHash, newEntry); this.size += 1; return newEntry; } public void clear() { - Support.clear(this.buckets); + Hashtable.clear(this.buckets); this.size = 0; } - public void forEach(Consumer consumer) { - Support.forEach(this.buckets, consumer); + public void forEach(@Nonnull Consumer consumer) { + Hashtable.forEach(this.buckets, consumer); } /** @@ -382,195 +448,352 @@ public void forEach(Consumer consumer) { * -- pass a non-capturing {@link BiConsumer} (typically a {@code static final}) plus whatever * side-band state it needs as {@code context}. */ - public void forEach(T context, BiConsumer consumer) { - Support.forEach(this.buckets, context, consumer); + public void forEach(C context, @Nonnull BiConsumer consumer) { + Hashtable.forEach(this.buckets, context, consumer); } } + // ============================================================================================ + // Static building blocks over a caller-owned bucket array. + // + // Use these to assemble a custom table (higher arity, primitive keys, extra value fields) when + // D1/D2 don't fit; D1/D2 delegate to them internally. This is the same "static functions over a + // caller-owned array" shape as the concurrent variant (ConcurrentHashtable); see how + // AggregateTable drives a Hashtable.Entry[] with these. The calling class owns the array and + // exposes whatever operations it needs. + // + // Not thread-safe: there is no locking here. Concurrent access, including mixing reads with + // writes, requires external synchronization. + // + // These were previously nested under the Support class; that class is now a deprecated facade + // delegating here (retained for source compatibility with existing callers such as client-side + // statistics). + // ============================================================================================ + + /** Upper bound on the bucket-array length returned by {@link #sizeFor(int)}. */ + static final int MAX_BUCKETS = 1 << 30; + /** - * Building blocks for hash-table operations. + * Allocates a fixed-size bucket array sized to hold {@code capacity} entries: {@code capacity} + * rounded up to the next power of two. * - *

Used by {@link D1} and {@link D2}, and available to callers that want to assemble their own - * higher-arity table (3+ key parts) without re-implementing the bucket-array mechanics. The - * typical recipe: + *

Returns a concrete {@code Hashtable.Entry[]} (chain heads are stored at the base type), so + * the array assigns directly to a caller's {@code Hashtable.Entry[]} field. As with the + * concurrent variant's {@code createFixedBuckets}, {@code entryClass} is not consumed to + * allocate -- the array is a heterogeneous {@code Entry[]}, not a reflectively-allocated {@code + * TEntry[]}. It is accepted only to keep the factory call-shape symmetric across the + * flat-collections family ({@code createFixedBuckets(MyEntry.class, n)}). Capacity is fixed; the + * table does not resize. * - *

+ *

For load-factor headroom over a target working-set size, size {@code capacity} yourself + * (e.g. {@code createFixedBuckets(MyEntry.class, (int) (n * 4 / 3f))}); the deprecated {@link + * Support#create(int, float)} bundled that scaling but has no blessed equivalent. + */ + @Nonnull + public static Hashtable.Entry[] createFixedBuckets( + @Nonnull Class entryClass, int capacity) { + return new Entry[sizeFor(capacity)]; + } + + /** + * Rounds {@code requestedSize} up to the next power of two, capped at {@link #MAX_BUCKETS}, and + * returns the bucket-array length to allocate. Throws {@link IllegalArgumentException} for + * negative inputs or inputs above the cap. The concurrent variant shares this so the two families + * round identically. + */ + public static int sizeFor(int requestedSize) { + if (requestedSize < 0) { + throw new IllegalArgumentException("requestedSize must be non-negative: " + requestedSize); + } + if (requestedSize > MAX_BUCKETS) { + throw new IllegalArgumentException( + "requestedSize exceeds maximum bucket count (" + MAX_BUCKETS + "): " + requestedSize); + } + if (requestedSize <= 1) { + return 1; + } + return Integer.highestOneBit(requestedSize - 1) << 1; + } + + public static int bucketIndex(@Nonnull Object[] buckets, long keyHash) { + return (int) (keyHash & buckets.length - 1); + } + + /** + * Returns the head entry of the bucket that {@code keyHash} maps to, cast to the caller's + * concrete entry type. The unchecked cast lives here so the chain-walk loop at the call site + * doesn't need to thread a raw {@link Entry} variable through. + */ + @SuppressWarnings("unchecked") + @Nullable + public static TEntry bucket( + @Nonnull Hashtable.Entry[] buckets, long keyHash) { + return (TEntry) buckets[bucketIndex(buckets, keyHash)]; + } + + /** + * Splices {@code entry} in as the new head of the chain at {@code bucketIndex}. Caller is + * responsible for size accounting -- this method only touches the chain pointers. + */ + public static void insertHeadEntry( + @Nonnull Hashtable.Entry[] buckets, int bucketIndex, @Nonnull Hashtable.Entry entry) { + entry.setNext(buckets[bucketIndex]); + buckets[bucketIndex] = entry; + } + + /** + * Convenience overload of {@link #insertHeadEntry(Hashtable.Entry[], int, Hashtable.Entry)} that + * derives the bucket index from {@code keyHash}. Use this when the caller has the hash but not + * the index; if the index has already been computed for another reason, prefer the int-taking + * overload to avoid the redundant mask. + */ + public static void insertHeadEntry( + @Nonnull Hashtable.Entry[] buckets, long keyHash, @Nonnull Hashtable.Entry entry) { + insertHeadEntry(buckets, bucketIndex(buckets, keyHash), entry); + } + + public static void clear(@Nonnull Hashtable.Entry[] buckets) { + Arrays.fill(buckets, null); + } + + /** + * Walks every entry in {@code buckets} and invokes {@code consumer} on it. The unchecked cast to + * {@code TEntry} lives here (mirroring {@link Entry#next()}) so callers don't have to sprinkle it + * across their own forEach loops. + */ + @SuppressWarnings("unchecked") + public static void forEach( + @Nonnull Hashtable.Entry[] buckets, @Nonnull Consumer consumer) { + for (int i = 0; i < buckets.length; i++) { + for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { + consumer.accept((TEntry) e); + } + } + } + + /** + * Context-passing variant of {@link #forEach(Hashtable.Entry[], Consumer)}. Pair a non-capturing + * {@link BiConsumer} (typically a {@code static final}) with side-band state passed as {@code + * context} to avoid a fresh-Consumer allocation each call. + */ + @SuppressWarnings("unchecked") + public static void forEach( + @Nonnull Hashtable.Entry[] buckets, + C context, + @Nonnull BiConsumer consumer) { + for (int i = 0; i < buckets.length; i++) { + for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { + consumer.accept(context, (TEntry) e); + } + } + } + + @Nonnull + public static BucketIterator bucketIterator( + @Nonnull Hashtable.Entry[] buckets, long keyHash) { + return new BucketIterator(buckets, keyHash); + } + + @Nonnull + public static + MutatingBucketIterator mutatingBucketIterator( + @Nonnull Hashtable.Entry[] buckets, long keyHash) { + return new MutatingBucketIterator(buckets, keyHash); + } + + /** + * Returns a {@link MutatingTableIterator} over every entry in {@code buckets}. Useful for sweeps + * -- eviction, expunge -- that aren't keyed to a specific hash. + */ + @Nonnull + public static + MutatingTableIterator mutatingTableIterator(@Nonnull Hashtable.Entry[] buckets) { + return new MutatingTableIterator(buckets, 0, buckets.length); + } + + /** + * Variant of {@link #mutatingTableIterator(Hashtable.Entry[])} that walks only the half-open + * bucket range {@code [startBucket, endBucket)}. Useful for resumable sweeps -- e.g. cursor-based + * eviction in {@code AggregateTable} -- where one call drives {@code [cursor, length)} and a + * wrap-around call drives {@code [0, cursor)}. The iterator does not wrap around within a + * single instance; callers compose two iterators when wrap-around is desired. An empty range + * ({@code startBucket == endBucket}) produces an immediately exhausted iterator. + * + * @param startBucket inclusive lower bound; must be in {@code [0, buckets.length]}. + * @param endBucket exclusive upper bound; must be in {@code [startBucket, buckets.length]}. + */ + @Nonnull + public static + MutatingTableIterator mutatingTableIterator( + @Nonnull Hashtable.Entry[] buckets, int startBucket, int endBucket) { + return new MutatingTableIterator(buckets, startBucket, endBucket); + } + + /** + * Deprecated facade over the static building blocks that are now methods on {@link Hashtable} + * itself (mirroring the concurrent variant). Each method here delegates to its {@code + * Hashtable.*} counterpart; the two sizing helpers with no blessed equivalent -- {@link + * #create(int, float)} and {@link #MAX_RATIO} -- keep their real bodies here. * - *

All bucket arrays produced by {@code create} have a power-of-two length, so {@link - * #bucketIndex(Object[], long)} can use a bit mask. + *

Retained only for source compatibility with existing callers (e.g. client-side statistics). + * New code should call the {@code Hashtable.*} statics directly. + * + * @deprecated use the static building blocks on {@link Hashtable} directly. */ + @Deprecated public static final class Support { + private Support() {} + /** - * Allocates a bucket array sized to hold {@code requestedSize} entries. Returned length is - * {@code requestedSize} rounded up to the next power of two (capped at {@link #MAX_BUCKETS}). + * @deprecated use {@link Hashtable#createFixedBuckets(Class, int)}. */ - public static final Hashtable.Entry[] create(int requestedSize) { + @Deprecated + @Nonnull + public static Hashtable.Entry[] create(int requestedSize) { return new Entry[sizeFor(requestedSize)]; } /** - * Variant of {@link #create(int)} that scales the requested working-set size before sizing the - * bucket array. Pair with {@link #MAX_RATIO} to leave headroom over the working set for a - * desired load factor; the canonical call is {@code create(n, MAX_RATIO)}. + * Scales the requested working-set size before sizing the bucket array. Pair with {@link + * #MAX_RATIO} to leave headroom over the working set for a desired load factor; the canonical + * call is {@code create(n, MAX_RATIO)}. + * + *

The scaled size is truncated to {@code int} before going through {@link + * Hashtable#sizeFor(int)}. Truncation rather than {@code ceil} is intentional: {@code sizeFor} + * rounds up to the next power of two anyway, so the fractional part would only matter when + * float fuzz pushes the result across a power-of-two boundary -- {@code ceil} would then double + * the array size for no reason (e.g. {@code 12 * 4/3 = 16.0...0005f -> ceil 17 -> sizeFor 32}). * - *

The scaled size is truncated to {@code int} before going through {@link #sizeFor(int)}. - * Truncation rather than {@code ceil} is intentional: {@code sizeFor} rounds up to the next - * power of two anyway, so the fractional part would only matter when float fuzz pushes the - * result across a power-of-two boundary -- {@code ceil} would then double the array size for no - * reason (e.g. {@code 12 * 4/3 = 16.0...0005f -> ceil 17 -> sizeFor 32}). + *

No blessed equivalent: callers wanting load-factor headroom size the capacity themselves + * and call {@link Hashtable#createFixedBuckets(Class, int)}. + * + * @deprecated size the capacity yourself and use {@link Hashtable#createFixedBuckets(Class, + * int)}. */ - public static final Hashtable.Entry[] create(int requestedSize, float scale) { + @Deprecated + @Nonnull + public static Hashtable.Entry[] create(int requestedSize, float scale) { return new Entry[sizeFor((int) (requestedSize * scale))]; } - /** Upper bound on the bucket array length returned by {@link #sizeFor(int)}. */ - static final int MAX_BUCKETS = 1 << 30; - /** * Inverse of a 75% load factor. Callers that size their bucket array from a target working-set * size {@code n} should pass {@code create(n, MAX_RATIO)} to leave ~25% headroom in the array. */ - public static final float MAX_RATIO = 4.0f / 3.0f; + @Deprecated public static final float MAX_RATIO = 4.0f / 3.0f; /** - * Rounds {@code requestedSize} up to the next power of two, capped at {@link #MAX_BUCKETS}. - * Throws {@link IllegalArgumentException} for negative inputs or inputs above the cap. Returns - * the bucket-array length to allocate. + * @deprecated use {@link Hashtable#sizeFor(int)}. */ - static final int sizeFor(int requestedSize) { - if (requestedSize < 0) { - throw new IllegalArgumentException("requestedSize must be non-negative: " + requestedSize); - } - if (requestedSize > MAX_BUCKETS) { - throw new IllegalArgumentException( - "requestedSize exceeds maximum bucket count (" + MAX_BUCKETS + "): " + requestedSize); - } - if (requestedSize <= 1) { - return 1; - } - return Integer.highestOneBit(requestedSize - 1) << 1; + @Deprecated + static int sizeFor(int requestedSize) { + return Hashtable.sizeFor(requestedSize); } - public static final void clear(Hashtable.Entry[] buckets) { - Arrays.fill(buckets, null); + /** + * @deprecated use {@link Hashtable#clear(Hashtable.Entry[])}. + */ + @Deprecated + public static void clear(@Nonnull Hashtable.Entry[] buckets) { + Hashtable.clear(buckets); } - public static final BucketIterator bucketIterator( - Hashtable.Entry[] buckets, long keyHash) { - return new BucketIterator(buckets, keyHash); + /** + * @deprecated use {@link Hashtable#bucketIterator(Hashtable.Entry[], long)}. + */ + @Deprecated + @Nonnull + public static BucketIterator bucketIterator( + @Nonnull Hashtable.Entry[] buckets, long keyHash) { + return Hashtable.bucketIterator(buckets, keyHash); } - public static final + /** + * @deprecated use {@link Hashtable#mutatingBucketIterator(Hashtable.Entry[], long)}. + */ + @Deprecated + @Nonnull + public static MutatingBucketIterator mutatingBucketIterator( - Hashtable.Entry[] buckets, long keyHash) { - return new MutatingBucketIterator(buckets, keyHash); + @Nonnull Hashtable.Entry[] buckets, long keyHash) { + return Hashtable.mutatingBucketIterator(buckets, keyHash); } /** - * Returns a {@link MutatingTableIterator} over every entry in {@code buckets}. Useful for - * sweeps -- eviction, expunge -- that aren't keyed to a specific hash. + * @deprecated use {@link Hashtable#mutatingTableIterator(Hashtable.Entry[])}. */ - public static final - MutatingTableIterator mutatingTableIterator(Hashtable.Entry[] buckets) { - return new MutatingTableIterator(buckets, 0, buckets.length); + @Deprecated + @Nonnull + public static + MutatingTableIterator mutatingTableIterator(@Nonnull Hashtable.Entry[] buckets) { + return Hashtable.mutatingTableIterator(buckets); } /** - * Variant of {@link #mutatingTableIterator(Hashtable.Entry[])} that walks only the half-open - * bucket range {@code [startBucket, endBucket)}. Useful for resumable sweeps -- e.g. cursor- - * based eviction in {@code AggregateTable} -- where one call drives {@code [cursor, length)} - * and a wrap-around call drives {@code [0, cursor)}. The iterator does not wrap around - * within a single instance; callers compose two iterators when wrap-around is desired. An empty - * range ({@code startBucket == endBucket}) produces an immediately exhausted iterator. - * - * @param startBucket inclusive lower bound; must be in {@code [0, buckets.length]}. - * @param endBucket exclusive upper bound; must be in {@code [startBucket, buckets.length]}. + * @deprecated use {@link Hashtable#mutatingTableIterator(Hashtable.Entry[], int, int)}. */ - public static final + @Deprecated + @Nonnull + public static MutatingTableIterator mutatingTableIterator( - Hashtable.Entry[] buckets, int startBucket, int endBucket) { - return new MutatingTableIterator(buckets, startBucket, endBucket); + @Nonnull Hashtable.Entry[] buckets, int startBucket, int endBucket) { + return Hashtable.mutatingTableIterator(buckets, startBucket, endBucket); } - public static final int bucketIndex(Object[] buckets, long keyHash) { - return (int) (keyHash & buckets.length - 1); + /** + * @deprecated use {@link Hashtable#bucketIndex(Object[], long)}. + */ + @Deprecated + public static int bucketIndex(@Nonnull Object[] buckets, long keyHash) { + return Hashtable.bucketIndex(buckets, keyHash); } /** - * Splices {@code entry} in as the new head of the chain at {@code bucketIndex}. Caller is - * responsible for size accounting -- this method only touches the chain pointers. + * @deprecated use {@link Hashtable#insertHeadEntry(Hashtable.Entry[], int, Hashtable.Entry)}. */ - public static final void insertHeadEntry( - Hashtable.Entry[] buckets, int bucketIndex, Hashtable.Entry entry) { - entry.setNext(buckets[bucketIndex]); - buckets[bucketIndex] = entry; + @Deprecated + public static void insertHeadEntry( + @Nonnull Hashtable.Entry[] buckets, int bucketIndex, @Nonnull Hashtable.Entry entry) { + Hashtable.insertHeadEntry(buckets, bucketIndex, entry); } /** - * Convenience overload of {@link #insertHeadEntry(Hashtable.Entry[], int, Hashtable.Entry)} - * that derives the bucket index from {@code keyHash}. Use this when the caller has the hash but - * not the index; if the index has already been computed for another reason, prefer the - * int-taking overload to avoid the redundant mask. + * @deprecated use {@link Hashtable#insertHeadEntry(Hashtable.Entry[], long, Hashtable.Entry)}. */ - public static final void insertHeadEntry( - Hashtable.Entry[] buckets, long keyHash, Hashtable.Entry entry) { - insertHeadEntry(buckets, bucketIndex(buckets, keyHash), entry); + @Deprecated + public static void insertHeadEntry( + @Nonnull Hashtable.Entry[] buckets, long keyHash, @Nonnull Hashtable.Entry entry) { + Hashtable.insertHeadEntry(buckets, keyHash, entry); } /** - * Returns the head entry of the bucket that {@code keyHash} maps to, cast to the caller's - * concrete entry type. The unchecked cast lives here so the chain-walk loop at the call site - * doesn't need to thread a raw {@link Entry} variable through. + * @deprecated use {@link Hashtable#bucket(Hashtable.Entry[], long)}. */ - @SuppressWarnings("unchecked") - public static final TEntry bucket( - Hashtable.Entry[] buckets, long keyHash) { - return (TEntry) buckets[bucketIndex(buckets, keyHash)]; + @Deprecated + @Nullable + public static TEntry bucket( + @Nonnull Hashtable.Entry[] buckets, long keyHash) { + return Hashtable.bucket(buckets, keyHash); } /** - * Walks every entry in {@code buckets} and invokes {@code consumer} on it. The unchecked cast - * to {@code TEntry} lives here (mirroring {@link Entry#next()}) so callers don't have to - * sprinkle it across their own forEach loops. + * @deprecated use {@link Hashtable#forEach(Hashtable.Entry[], Consumer)}. */ - @SuppressWarnings("unchecked") - public static final void forEach( - Hashtable.Entry[] buckets, Consumer consumer) { - for (int i = 0; i < buckets.length; i++) { - for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { - consumer.accept((TEntry) e); - } - } + @Deprecated + public static void forEach( + @Nonnull Hashtable.Entry[] buckets, @Nonnull Consumer consumer) { + Hashtable.forEach(buckets, consumer); } /** - * Context-passing variant of {@link #forEach(Hashtable.Entry[], Consumer)}. Pair a - * non-capturing {@link BiConsumer} (typically a {@code static final}) with side-band state - * passed as {@code context} to avoid a fresh-Consumer allocation each call. + * @deprecated use {@link Hashtable#forEach(Hashtable.Entry[], Object, BiConsumer)}. */ - @SuppressWarnings("unchecked") - public static final void forEach( - Hashtable.Entry[] buckets, T context, BiConsumer consumer) { - for (int i = 0; i < buckets.length; i++) { - for (Hashtable.Entry e = buckets[i]; e != null; e = e.next()) { - consumer.accept(context, (TEntry) e); - } - } + @Deprecated + public static void forEach( + @Nonnull Hashtable.Entry[] buckets, + C context, + @Nonnull BiConsumer consumer) { + Hashtable.forEach(buckets, context, consumer); } } @@ -589,7 +812,7 @@ public static final class BucketIterator implements Iterat private final long keyHash; private Hashtable.Entry nextEntry; - BucketIterator(Hashtable.Entry[] buckets, long keyHash) { + BucketIterator(@Nonnull Hashtable.Entry[] buckets, long keyHash) { this.keyHash = keyHash; Hashtable.Entry cur = buckets[Support.bucketIndex(buckets, keyHash)]; while (cur != null && cur.keyHash != keyHash) { @@ -605,6 +828,7 @@ public boolean hasNext() { @Override @SuppressWarnings("unchecked") + @Nonnull public TEntry next() { Hashtable.Entry cur = this.nextEntry; if (cur == null) { @@ -651,7 +875,7 @@ public static final class MutatingBucketIterator /** The next entry to be returned by next */ private Hashtable.Entry nextEntry; - MutatingBucketIterator(Hashtable.Entry[] buckets, long keyHash) { + MutatingBucketIterator(@Nonnull Hashtable.Entry[] buckets, long keyHash) { this.buckets = buckets; this.keyHash = keyHash; @@ -685,6 +909,7 @@ public boolean hasNext() { @Override @SuppressWarnings("unchecked") + @Nonnull public TEntry next() { Hashtable.Entry curEntry = this.nextEntry; if (curEntry == null) { @@ -729,7 +954,7 @@ public void remove() { this.curEntry = null; } - public void replace(TEntry replacementEntry) { + public void replace(@Nonnull TEntry replacementEntry) { Hashtable.Entry oldCurEntry = this.curEntry; if (oldCurEntry == null) { throw new IllegalStateException(); @@ -749,7 +974,7 @@ public void replace(TEntry replacementEntry) { this.curEntry = replacementEntry; } - void setPrevNext(Hashtable.Entry nextEntry) { + void setPrevNext(@Nullable Hashtable.Entry nextEntry) { if (this.curPrevEntry == null) { Hashtable.Entry[] buckets = this.buckets; buckets[Support.bucketIndex(buckets, this.keyHash)] = nextEntry; @@ -806,7 +1031,7 @@ public static final class MutatingTableIterator */ private Hashtable.Entry curEntry; - MutatingTableIterator(Hashtable.Entry[] buckets, int startBucket, int endBucket) { + MutatingTableIterator(@Nonnull Hashtable.Entry[] buckets, int startBucket, int endBucket) { this.buckets = buckets; if (startBucket < 0 || startBucket > buckets.length) { throw new IndexOutOfBoundsException( @@ -843,6 +1068,7 @@ public boolean hasNext() { @Override @SuppressWarnings("unchecked") + @Nonnull public TEntry next() { Hashtable.Entry e = this.nextEntry; if (e == null) { diff --git a/internal-api/src/test/java/datadog/trace/util/HashtableTest.java b/internal-api/src/test/java/datadog/trace/util/HashtableTest.java index 953453ca3aa..f566d04ee0d 100644 --- a/internal-api/src/test/java/datadog/trace/util/HashtableTest.java +++ b/internal-api/src/test/java/datadog/trace/util/HashtableTest.java @@ -23,16 +23,16 @@ class HashtableTest { - // ============ Support ============ + // ============ Static building blocks ============ @Nested - class SupportTests { + class StaticBuildingBlockTests { @Test void createRoundsCapacityUpToPowerOfTwo() { // The Hashtable.D1 / D2 size() reflects entries, but the bucket array length is // a power of two >= requestedCapacity. We can verify indirectly via bucketIndex masking. - Hashtable.Entry[] buckets = Support.create(5); + Hashtable.Entry[] buckets = Hashtable.createFixedBuckets(StringIntEntry.class, 5); // Length must be a power of two >= 5 int len = buckets.length; assertTrue(len >= 5); @@ -41,51 +41,78 @@ void createRoundsCapacityUpToPowerOfTwo() { @Test void sizeForReturnsAtLeastOne() { - assertEquals(1, Support.sizeFor(0)); - assertEquals(1, Support.sizeFor(1)); + assertEquals(1, Hashtable.sizeFor(0)); + assertEquals(1, Hashtable.sizeFor(1)); } @Test void sizeForRoundsUpToPowerOfTwo() { - assertEquals(2, Support.sizeFor(2)); - assertEquals(4, Support.sizeFor(3)); - assertEquals(4, Support.sizeFor(4)); - assertEquals(8, Support.sizeFor(5)); - assertEquals(1 << 30, Support.sizeFor(1 << 30)); + assertEquals(2, Hashtable.sizeFor(2)); + assertEquals(4, Hashtable.sizeFor(3)); + assertEquals(4, Hashtable.sizeFor(4)); + assertEquals(8, Hashtable.sizeFor(5)); + assertEquals(1 << 30, Hashtable.sizeFor(1 << 30)); } @Test void sizeForRejectsCapacityAboveMax() { - assertThrows(IllegalArgumentException.class, () -> Support.sizeFor((1 << 30) + 1)); - assertThrows(IllegalArgumentException.class, () -> Support.sizeFor(Integer.MAX_VALUE)); + assertThrows(IllegalArgumentException.class, () -> Hashtable.sizeFor((1 << 30) + 1)); + assertThrows(IllegalArgumentException.class, () -> Hashtable.sizeFor(Integer.MAX_VALUE)); } @Test void sizeForRejectsNegativeCapacity() { - assertThrows(IllegalArgumentException.class, () -> Support.sizeFor(-1)); - assertThrows(IllegalArgumentException.class, () -> Support.sizeFor(Integer.MIN_VALUE)); + assertThrows(IllegalArgumentException.class, () -> Hashtable.sizeFor(-1)); + assertThrows(IllegalArgumentException.class, () -> Hashtable.sizeFor(Integer.MIN_VALUE)); } @Test void bucketIndexIsBoundedByArrayLength() { - Hashtable.Entry[] buckets = Support.create(16); + Hashtable.Entry[] buckets = Hashtable.createFixedBuckets(StringIntEntry.class, 16); for (long h : new long[] {0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, 12345L}) { - int idx = Support.bucketIndex(buckets, h); + int idx = Hashtable.bucketIndex(buckets, h); assertTrue(idx >= 0 && idx < buckets.length, "bucketIndex out of range for hash " + h); } } @Test void clearNullsAllBuckets() { - Hashtable.Entry[] buckets = Support.create(4); + Hashtable.Entry[] buckets = Hashtable.createFixedBuckets(StringIntEntry.class, 4); buckets[0] = new StringIntEntry("x", 1); buckets[1] = new StringIntEntry("y", 2); - Support.clear(buckets); + Hashtable.clear(buckets); for (Hashtable.Entry b : buckets) { assertNull(b); } } + @Test + void insertHeadEntrySplicesAsNewHead() { + Hashtable.Entry[] buckets = Hashtable.createFixedBuckets(StringIntEntry.class, 4); + StringIntEntry a = new StringIntEntry("a", 1); + StringIntEntry b = new StringIntEntry("b", 2); + Hashtable.insertHeadEntry(buckets, 0, a); + assertSame(a, buckets[0]); + assertNull(a.next()); + + Hashtable.insertHeadEntry(buckets, 0, b); + assertSame(b, buckets[0]); + assertSame(a, b.next()); + assertNull(a.next()); + } + } + + // ============ Deprecated Support facade ============ + + /** + * The scaled {@code create(int, float)} factory and {@code MAX_RATIO} are deprecated-only: they + * have no blessed equivalent on {@link Hashtable} but remain in use by client-side statistics, so + * they keep dedicated coverage here. + */ + @Nested + @SuppressWarnings("deprecation") + class DeprecatedSupportTests { + @Test void maxRatioScalesTargetForLoadFactor() { // 75% load factor => bucket array sized at requestedSize * 4/3, rounded up to power of 2. @@ -101,21 +128,6 @@ void createWithScaleRoundsUpToPowerOfTwo() { Hashtable.Entry[] buckets = Support.create(7, 1.5f); assertEquals(16, buckets.length); } - - @Test - void insertHeadEntrySplicesAsNewHead() { - Hashtable.Entry[] buckets = Support.create(4); - StringIntEntry a = new StringIntEntry("a", 1); - StringIntEntry b = new StringIntEntry("b", 2); - Support.insertHeadEntry(buckets, 0, a); - assertSame(a, buckets[0]); - assertNull(a.next()); - - Support.insertHeadEntry(buckets, 0, b); - assertSame(b, buckets[0]); - assertSame(a, b.next()); - assertNull(a.next()); - } } // ============ BucketIterator ============ @@ -126,7 +138,7 @@ class BucketIteratorTests { @Test void walksOnlyMatchingHash() { // Build a bucket array with two entries that share a bucket but have different hashes. - // Use Hashtable.D1 to seed; then call Support.bucketIterator directly with the matching + // Use Hashtable.D1 to seed; then call Hashtable.bucketIterator directly with the matching // hash and verify it only returns the matching entry. Hashtable.D1 table = new Hashtable.D1<>(4); CollidingKey k1 = new CollidingKey("first", 17); @@ -136,7 +148,7 @@ void walksOnlyMatchingHash() { table.insert(new CollidingKeyEntry(k2, 2)); table.insert(new CollidingKeyEntry(k3, 3)); // All three share the same hash (17), so a bucket iterator over hash=17 yields all three. - BucketIterator it = Support.bucketIterator(table.buckets, 17L); + BucketIterator it = Hashtable.bucketIterator(table.buckets, 17L); int count = 0; while (it.hasNext()) { assertNotNull(it.next()); @@ -150,7 +162,7 @@ void exhaustedIteratorThrowsNoSuchElement() { Hashtable.D1 table = new Hashtable.D1<>(4); table.insert(new StringIntEntry("only", 1)); long h = Hashtable.D1.Entry.hash("only"); - BucketIterator it = Support.bucketIterator(table.buckets, h); + BucketIterator it = Hashtable.bucketIterator(table.buckets, h); it.next(); assertFalse(it.hasNext()); assertThrows(NoSuchElementException.class, it::next); @@ -174,7 +186,7 @@ void removeFromHeadOfChainUnlinks() { table.insert(new CollidingKeyEntry(k3, 3)); MutatingBucketIterator it = - Support.mutatingBucketIterator(table.buckets, 17L); + Hashtable.mutatingBucketIterator(table.buckets, 17L); it.next(); // first match (head of chain in insertion-reverse order) it.remove(); // Two should remain @@ -207,7 +219,7 @@ void replaceSwapsEntryAndPreservesChain() { table.insert(e2); MutatingBucketIterator it = - Support.mutatingBucketIterator(table.buckets, 17L); + Hashtable.mutatingBucketIterator(table.buckets, 17L); CollidingKeyEntry first = it.next(); CollidingKeyEntry replacement = new CollidingKeyEntry(first.key, 999); it.replace(replacement); @@ -223,7 +235,7 @@ void removeWithoutNextThrows() { Hashtable.D1 table = new Hashtable.D1<>(4); table.insert(new StringIntEntry("a", 1)); MutatingBucketIterator it = - Support.mutatingBucketIterator(table.buckets, Hashtable.D1.Entry.hash("a")); + Hashtable.mutatingBucketIterator(table.buckets, Hashtable.D1.Entry.hash("a")); assertThrows(IllegalStateException.class, it::remove); } } @@ -241,7 +253,8 @@ void walksEveryEntryAcrossBuckets() { table.insert(new StringIntEntry("c", 3)); Set seen = new HashSet<>(); - for (MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + for (MutatingTableIterator it = + Hashtable.mutatingTableIterator(table.buckets); it.hasNext(); ) { seen.add(it.next().key); } @@ -254,7 +267,7 @@ void walksEveryEntryAcrossBuckets() { @Test void emptyTableIteratorIsExhausted() { Hashtable.D1 table = new Hashtable.D1<>(8); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); assertFalse(it.hasNext()); assertThrows(NoSuchElementException.class, it::next); } @@ -268,7 +281,7 @@ void removeUnlinksBucketHead() { table.insert(new CollidingKeyEntry(k2, 2)); // The head of the chain is whichever was inserted last (insert prepends). - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); CollidingKeyEntry head = it.next(); it.remove(); @@ -289,7 +302,7 @@ void removeUnlinksMidChainEntry() { table.insert(new CollidingKeyEntry(k3, 3)); // Walk to the second entry, remove it. - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); it.next(); CollidingKeyEntry victim = it.next(); it.remove(); @@ -320,7 +333,7 @@ void removeSkipsOverEmptyBuckets() { table.insert(new StringIntEntry("beta", 2)); table.insert(new StringIntEntry("gamma", 3)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); it.next(); it.remove(); int remaining = 0; @@ -335,7 +348,7 @@ void removeSkipsOverEmptyBuckets() { void removeWithoutNextThrows() { Hashtable.D1 table = new Hashtable.D1<>(4); table.insert(new StringIntEntry("a", 1)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); assertThrows(IllegalStateException.class, it::remove); } @@ -344,7 +357,7 @@ void removeTwiceWithoutInterveningNextThrows() { Hashtable.D1 table = new Hashtable.D1<>(4); table.insert(new StringIntEntry("a", 1)); table.insert(new StringIntEntry("b", 2)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); it.next(); it.remove(); assertThrows(IllegalStateException.class, it::remove); @@ -362,7 +375,7 @@ void halfOpenRangeOmitsBucketsOutsideTheRange() { Set seen = new HashSet<>(); for (MutatingTableIterator it = - Support.mutatingTableIterator(table.buckets, 5, 10); + Hashtable.mutatingTableIterator(table.buckets, 5, 10); it.hasNext(); ) { seen.add(it.next().key.label); } @@ -376,7 +389,8 @@ void emptyHalfOpenRangeIsExhausted() { // pass [0, cursor) when cursor == 0 in resumable sweeps. Hashtable.D1 table = new Hashtable.D1<>(8); table.insert(new StringIntEntry("a", 1)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets, 0, 0); + MutatingTableIterator it = + Hashtable.mutatingTableIterator(table.buckets, 0, 0); assertFalse(it.hasNext()); } @@ -385,14 +399,14 @@ void rangeBoundsOutOfOrderThrows() { Hashtable.D1 table = new Hashtable.D1<>(8); assertThrows( IndexOutOfBoundsException.class, - () -> Support.mutatingTableIterator(table.buckets, -1, 4)); + () -> Hashtable.mutatingTableIterator(table.buckets, -1, 4)); assertThrows( IndexOutOfBoundsException.class, - () -> Support.mutatingTableIterator(table.buckets, 4, 2)); // end < start + () -> Hashtable.mutatingTableIterator(table.buckets, 4, 2)); // end < start assertThrows( IndexOutOfBoundsException.class, () -> - Support.mutatingTableIterator( + Hashtable.mutatingTableIterator( table.buckets, 0, table.buckets.length + 1)); // end > len } @@ -403,7 +417,7 @@ void currentBucketReportsLandingIndex() { Hashtable.D1 table = new Hashtable.D1<>(16); table.insert(new CollidingKeyEntry(new CollidingKey("b3", 3), 1)); - MutatingTableIterator it = Support.mutatingTableIterator(table.buckets); + MutatingTableIterator it = Hashtable.mutatingTableIterator(table.buckets); assertEquals(-1, it.currentBucket(), "before any next() currentBucket should be -1"); it.next(); assertEquals(3, it.currentBucket(), "currentBucket should report the entry's bucket");