diff --git a/internal-api/src/jmh/java/datadog/trace/util/ThreadSafeMapCounterBenchmark.java b/internal-api/src/jmh/java/datadog/trace/util/ThreadSafeMapCounterBenchmark.java index bc8bc07b9f5..239dd3890f4 100644 --- a/internal-api/src/jmh/java/datadog/trace/util/ThreadSafeMapCounterBenchmark.java +++ b/internal-api/src/jmh/java/datadog/trace/util/ThreadSafeMapCounterBenchmark.java @@ -23,11 +23,10 @@ * Measures lookup followed by an atomic counter increment in a shared, pre-populated table. Models * per-class or per-method hit counters in the tracer. * - *
The {@link ConcurrentHashtable.D1} case embeds a {@code volatile long} in each entry. {@link - * AtomicLongFieldUpdater} updates that field atomically without allocating an {@link AtomicLong} - * per key. The map baselines store a separate {@link AtomicLong} or {@link LongAdder}; {@code - * LongAdder} spreads contention across internal cells at the cost of more memory and a more - * expensive read. + *
The {@link ConcurrentHashtable.D1} case embeds a {@code volatile long} counter directly in the + * entry and increments it via {@link AtomicLongFieldUpdater}, avoiding a second heap object. The + * map baselines store a separate {@link AtomicLong} or {@link LongAdder}; {@code LongAdder} spreads + * contention across internal cells at the cost of more memory and a more expensive read. * *
Lookups reuse the key instances installed during setup. {@code Objects.equals} therefore * returns on its identity check without dispatching to {@code equals}, so this measures the @@ -50,8 +49,8 @@ *
The self-bound type parameter ({@code TEntry extends Entry Deliberately parameterized on {@code K} alone, not self-bound on the concrete subclass:
+ * {@link D1} stores and links entries internally as {@code Entry Use {@link #drain(Object, BiConsumer)} to avoid a capturing lambda.
*/
- public void drain(@Nonnull Consumer super TEntry> sink) {
- ConcurrentHashtable.drain(state, sink);
+ public void drain(@Strategy @Nonnull Consumer super TEntry> sink) {
+ ConcurrentHashtable.drain(state, castConsumer(sink));
}
/**
@@ -325,8 +395,9 @@ public void drain(@Nonnull Consumer super TEntry> sink) {
* a {@code static final}) plus the accumulator as {@code context} (e.g. the target list or
* event builder) to avoid a capturing-lambda allocation.
*/
- public Deliberately parameterized on {@code K1}/{@code K2} alone, not self-bound on the concrete
+ * subclass -- see {@link D1.Entry} for why.
*
* @param The {@code creator} should build an entry whose {@code keyHash} equals {@link
- * D2.Entry#hash(Object, Object) D2.Entry.hash(key1, key2)}.
*/
@Nonnull
public Maybe Use {@link #drain(Object, BiConsumer)} to avoid a capturing lambda.
*/
- public void drain(@Nonnull Consumer super TEntry> sink) {
- ConcurrentHashtable.drain(state, sink);
+ public void drain(@Strategy @Nonnull Consumer super TEntry> sink) {
+ ConcurrentHashtable.drain(state, castConsumer(sink));
}
/**
@@ -597,8 +711,9 @@ public void drain(@Nonnull Consumer super TEntry> sink) {
* a {@code static final}) plus the accumulator as {@code context} (e.g. the target list or
* event builder) to avoid a capturing-lambda allocation.
*/
- public Build the entry before reserving: there is no cancellation operation, so abandoning a
- * successful reservation permanently consumes capacity.
+ * Prefer {@link #cancelReservation()} plus deferred entry construction (see {@link
+ * ConcurrentHashtable#reserve}) over abandoning a reservation outright: this method by itself
+ * still has no way to give back a slot once claimed.
*/
public boolean tryReserve() {
if (size.incrementAndGet() > capacity) {
@@ -674,6 +791,27 @@ public boolean tryReserve() {
return true;
}
+ /**
+ * Gives back a slot claimed by {@link #tryReserve()} that was never filled — e.g. a concurrent
+ * match was found under the write lock instead of inserting. Lock-free, symmetric with {@link
+ * #decrement()}.
+ *
+ * Caller must call this at most once per successful {@link #tryReserve()}; double-cancelling
+ * corrupts the count the same way double-incrementing would. {@link Reservation#close()}
+ * handles this bookkeeping automatically and should be preferred over calling this directly.
+ *
+ * Under heavy contention on the same logical duplicate, multiple threads can each reserve a
+ * slot for what turns out to be the same entry before any of them cancels, transiently
+ * inflating {@code size} above the table's true occupancy. This can cause an unrelated,
+ * genuinely distinct concurrent insert to see the table as full when it isn't, until the losing
+ * reservations cancel. The effect is self-correcting (bounded by in-flight reservations, not
+ * sustained) and considered an acceptable tradeoff for tables expecting bursts of identical
+ * inserts (e.g. deduplication).
+ */
+ public void cancelReservation() {
+ size.decrementAndGet();
+ }
+
/**
* Reserves one slot, evicting an entry matching {@code evictable} when the table is full.
* Returns {@code false} without changing the table when no entry can be evicted.
@@ -682,9 +820,9 @@ public boolean tryReserve() {
* an abandoned reservation permanently consumes capacity.
*/
@GuardedBy("getTableWriteLock(buckets)")
- public {@code sizeManager} is intentionally package-private: callers outside this class must go
+ * through the {@code State}-accepting static helpers ({@link #estimateSize}, {@link #isFull},
+ * {@link #tryReserve}, {@link #tryReserveOrEvict}, {@link #evictOne}, {@link #evictAll}) rather
+ * than reach into the manager directly.
*/
- public static final class State Complete it with {@link #insertReserved}, or prefer {@link #tryReserve} for a higher-level,
+ * auto-cancelling handle that also defers entry construction until the reservation succeeds.
+ */
+ public static Run a lock-free scan first (see {@link #bucketFor}/{@link #bucketAt}) and only call this
+ * once that scan has missed — a successful reservation isn't required for correctness (the
+ * reservation itself, and the locked comparison inside {@link Reservation#tryGetOrInsertOrNull},
+ * are the source of truth), it just avoids paying for a lock and a factory call when a hit was
+ * already visible lock-free.
+ *
+ * Always returns a non-null handle — even when the table is full — so the caller must check
+ * {@link Reservation#isReserved()} (or simply call {@link Reservation#tryGetOrInsertOrNull},
+ * which returns {@code null} on an absent reservation) rather than assume every reservation is
+ * real.
+ */
+ @Nonnull
+ public static Overloaded up to 4 key components ({@link #tryGetOrInsertOrNull(Object, Function)} through
+ * {@link #tryGetOrInsertOrNull(Object, Object, Object, Object, Function4)}) so a non-capturing
+ * method reference can build the entry directly from its natural constructor arguments, without
+ * an intermediate holder object or a capturing lambda.
+ *
+ * @param Building the entry here, after the reservation already succeeded, keeps the write lock's
+ * critical section limited to the comparison/link/discard decision rather than whatever
+ * construction cost {@code factory} pays. See {@link ConcurrentHashtable.Entry#matches} — the
+ * under-lock comparison is entry-to-entry, so it needs {@code newEntry} already built.
+ */
+ @StrategyConsumer
+ @Nullable
+ public TEntry tryGetOrInsertOrNull(
+ A a, B b, @Strategy @Nonnull BiFunction super A, ? super B, ? extends TEntry> factory) {
+ return state == null ? null : finish(factory.apply(a, b));
+ }
+
+ /**
+ * Three key components; see {@link #tryGetOrInsertOrNull(Object, Object, BiFunction)} for the
+ * general contract.
+ */
+ @StrategyConsumer
+ @Nullable
+ public TEntry tryGetOrInsertOrNull(
+ A a,
+ B b,
+ C c,
+ @Strategy @Nonnull Function3 super A, ? super B, ? super C, ? extends TEntry> factory) {
+ return state == null ? null : finish(factory.apply(a, b, c));
+ }
+
+ /** Four key components; see {@link #tryGetOrInsertOrNull(Object, Object, BiFunction)}. */
+ @StrategyConsumer
+ @Nullable
+ public TEntry tryGetOrInsertOrNull(
+ A a,
+ B b,
+ C c,
+ D d,
+ @Strategy @Nonnull
+ Function4 super A, ? super B, ? super C, ? super D, ? extends TEntry> factory) {
+ return state == null ? null : finish(factory.apply(a, b, c, d));
+ }
+
+ /** {@link Maybe}-wrapping counterpart of {@link #tryGetOrInsertOrNull(Entry)}. */
+ @Nonnull
+ public Maybe The reservation survives drain and clear operations. Complete it with {@link
* #insertReserved}; abandoning it permanently consumes capacity.
*/
- public static Each step follows {@link Entry#next()}, so the iterator reflects entries linked at the time
+ * each step runs rather than a point-in-time snapshot -- entries inserted ahead of the iterator's
+ * current position after iteration starts may or may not be observed, and a concurrently removed
+ * entry remains reachable because {@code unlink()} deliberately retains its {@code next} link for
+ * in-flight readers.
+ */
+ @Nonnull
+ public static {@code
+ * try (Reservation
+ *
+ * {@code
+ * try (Reservation
+ *
+ * See {@link #tryGetOrInsertOrNull(Object, Object, BiFunction)} for the general contract.
+ */
+ @Nullable
+ public TEntry tryGetOrInsertOrNull(@Nonnull TEntry newEntry) {
+ return state == null ? null : finish(newEntry);
+ }
+
+ /**
+ * One key component; see {@link #tryGetOrInsertOrNull(Object, Object, BiFunction)} for the
+ * general contract.
+ */
+ @StrategyConsumer
+ @Nullable
+ public TEntry tryGetOrInsertOrNull(
+ A a, @Strategy @Nonnull Function super A, ? extends TEntry> factory) {
+ return state == null ? null : finish(factory.apply(a));
+ }
+
+ /**
+ * Two key components. Builds {@code factory.apply(...)} (skipped entirely if this reservation
+ * is empty — the table was full) and either links the result as a new entry or discards it in
+ * favor of an existing match found under the write lock. Returns {@code null} only when this
+ * reservation is empty; otherwise always returns a real entry (the newly built one, or the
+ * concurrent match).
+ *
+ *