Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
928babf
Wire BenchmarkUtils.polluteHashDispatch into the remaining map/set be…
dougqh Aug 26, 2026
e605da0
Unify Hashtable static API with ConcurrentHashtable; deprecate Support
dougqh Jul 29, 2026
06ef73e
Migrate HashtableTest to blessed Hashtable static API
dougqh Jul 29, 2026
8442fb0
Hashtable: annotate nullability (@Nonnull/@Nullable)
dougqh Jul 29, 2026
7e70fd9
Rename Hashtable.insertHeadEntry overloads to insertHeadEntryAt/For
dougqh Aug 20, 2026
73b2ebd
Add a strict entry-count cap to Hashtable.D1/D2
dougqh Aug 26, 2026
a78338d
Handle Hashtable.D1's new strict cap in CardinalityLimitReporter
dougqh Aug 26, 2026
67013e2
Add Hashtable.SizeTracker, EvictionCursor, and Table building blocks
dougqh Aug 26, 2026
5526309
Back Hashtable.D1/D2's entry-count cap with SizeTracker
dougqh Aug 26, 2026
40f3185
Port drain from ConcurrentHashtable to Hashtable
dougqh Aug 26, 2026
a1027d3
Expose isFull on D1/D2
dougqh Aug 26, 2026
a82acd1
Mark Hashtable D1/D2 getOrCreate as @Nullable
dougqh Aug 26, 2026
83d3410
Unify the Hashtable factory API on a capped/uncapped vocabulary
dougqh Aug 26, 2026
9901121
Avoid a capturing predicate in Hashtable D1/D2 remove
dougqh Aug 26, 2026
d4c3a8e
Lead the size-tracked Hashtable statics with the SizeTracker
dougqh Aug 26, 2026
d265062
Drop references to the deprecated Support facade from Hashtable javadoc
dougqh Aug 26, 2026
f953eda
Lead getOrCreate's javadoc with the fact that it can refuse
dougqh Aug 26, 2026
3c7d136
Rename getOrCreate to tryGetOrCreate on Hashtable and FlatHashtable
dougqh Aug 26, 2026
ef94afb
Replace Hashtable insertOrReplace with a refusing tryInsertOrReplace
dougqh Aug 26, 2026
26ececc
Clean up Hashtable comments: drop outward references, order by use
dougqh Aug 26, 2026
1f1b5b7
Fold SizeTracker and EvictionCursor into one SizeManager
dougqh Aug 26, 2026
1d187b0
Rename Hashtable.Table to State and make it something you hold
dougqh Aug 26, 2026
dfdc74b
Take State in the size-tracked statics; keep eviction static too
dougqh Aug 26, 2026
f989e26
Round out the State-taking statics: size, isEmpty, bucketFor, forEach
dougqh Aug 26, 2026
91976be
Add size-tracked drain; fix two review nits
dougqh Aug 26, 2026
2f4a18c
Step the eviction cursor on a failed scan; name the count honestly
dougqh Aug 27, 2026
2a3ff3b
Fix two eviction/drain defects found by Codex review
dougqh Aug 27, 2026
9be2923
Add a selection guide to Hashtable and FlatHashtable
dougqh Aug 27, 2026
6725515
Add Hashtable.D1/D2 tryGetOrUpdate to keep the cap refusal off the ca…
dougqh Aug 27, 2026
1168d62
Add a primitive-long context overload of Hashtable.D1.tryGetOrUpdate
dougqh Aug 27, 2026
cdd1ac0
Record the capped-table rerun of HashtableD1Benchmark
dougqh Aug 27, 2026
d05c61f
Assert against double-inserting the same Entry instance
dougqh Aug 28, 2026
c520bb0
Guard MutatingBucketIterator.replace against relinking an already-lin…
dougqh Aug 29, 2026
c9eceab
Add Maybe-returning tryGetOrCreateAsMaybe to Hashtable and FlatHashtable
dougqh Aug 28, 2026
23bd723
Promote tryGetOrCreateAsMaybe to tryGetOrCreate, demote nullable form…
dougqh Aug 28, 2026
3705bab
Rename Hashtable.createCapped to createBounded for family-wide naming…
dougqh Sep 1, 2026
be37ac0
Rename createCapped to createBounded in Hashtable D1/D2 tests
dougqh Sep 1, 2026
36a4f1f
Port removeIf to Hashtable
dougqh Sep 1, 2026
5ff0f1c
Port tryGetOrCreateOrEvict to Hashtable
dougqh Sep 1, 2026
2468c16
Remove Hashtable.tryGetOrUpdate family (D1/D2)
dougqh Sep 1, 2026
f259ef7
Remove duplicate setUp() left by rebase onto #12298's branch
dougqh Sep 23, 2026
5c9a63e
Fix Hashtable.drain leaving size inconsistent when the sink throws
dougqh Sep 24, 2026
7839f2e
Fix quadratic eviction when a bucket has a non-evictable prefix
dougqh Sep 24, 2026
919e01c
Fix capacityFor losing load-factor headroom for small cardinality limits
dougqh Sep 24, 2026
73204b0
Fix Hashtable javadoc inaccuracies flagged in review
dougqh Sep 24, 2026
fa4f452
Trim and complete javadoc on D1/D2 create/getOrCreate methods
dougqh Sep 24, 2026
bccf961
Merge remote-tracking branch 'origin/master' into feat/hashtable-api-…
dougqh Sep 24, 2026
cbc1855
Merge remote-tracking branch 'origin/dougqh/benchmarkutils-map-set-po…
dougqh Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ final class CardinalityLimitReporter {

// Distinct blocked tag names in a window: 9 property fields + the configured peer tags + up to
// AdditionalTagsSchema.MAX_ADDITIONAL_TAG_KEYS + base.service, with headroom for the brief
// overlap
// of old and new peer names across a schema rebuild. Fixed capacity; the table chains on overflow
// rather than dropping, so an underestimate only adds chain depth on this cold path.
// overlap of old and new peer names across a schema rebuild. Fixed, strict-cap capacity: if this
// is ever underestimated, excess distinct tags are silently dropped from the summary rather than
// recorded (see the null-check in record()) -- this is a cold, best-effort logging path, not a
// correctness-sensitive one.
Comment on lines +35 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Keep the capacity consequence without the operational aside.

Suggested change
// overlap of old and new peer names across a schema rebuild. Fixed, strict-cap capacity: if this
// is ever underestimated, excess distinct tags are silently dropped from the summary rather than
// recorded (see the null-check in record()) -- this is a cold, best-effort logging path, not a
// correctness-sensitive one.
// overlap during schema rebuilds. Extra distinct tags are omitted from this best-effort summary
// once the fixed bound is reached.

private static final int TAG_CAPACITY = 64;

// Rough width of one "<tag>=<count>, " entry, used to pre-size the summary builder. Cold path, so
Expand All @@ -43,7 +44,8 @@ final class CardinalityLimitReporter {

private final RatelimitedLogger rlLog;
// Tag name -> blocked count accumulated since the last emitted summary.
private final Hashtable.D1<String, TagBlockEntry> blockedByTag = new Hashtable.D1<>(TAG_CAPACITY);
private final Hashtable.D1<String, TagBlockEntry> blockedByTag =
Hashtable.D1.createBounded(TagBlockEntry.class, TAG_CAPACITY);

CardinalityLimitReporter() {
this(new RatelimitedLogger(log, 5, MINUTES));
Expand All @@ -56,7 +58,10 @@ final class CardinalityLimitReporter {
/** Records {@code count} values blocked for {@code tag} in the current reporting cycle. */
void record(String tag, long count) {
if (count > 0) {
blockedByTag.getOrCreate(tag, TagBlockEntry::new).count += count;
TagBlockEntry entry = blockedByTag.tryGetOrCreateOrNull(tag, TagBlockEntry::new);
if (entry != null) {
entry.count += count;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,17 +278,19 @@ static CIEntry[] _create_flat(float loadFactor) {
}
}
// Mirror the HashMap/TreeMap builds' second loop (UPPER_PREFIXES, suffix 0 & 2): 8 case-
// insensitive collisions. getOrCreate finds the already-present lower-case entry (a hit -> the
// create never fires, nothing allocates) and then the value is overwritten explicitly -- getOr-
// Create itself never updates an existing entry, so without this the FlatHashtable arm would do
// insensitive collisions. tryGetOrCreate finds the already-present lower-case entry (a hit, so
// the create never fires and nothing allocates) and then the value is overwritten explicitly --
// tryGetOrCreate itself never updates an existing entry, so without this the FlatHashtable arm
// would do
// less work (and end up with different final values) than the maps' overwriting put(), a false
// performance advantage. With the overwrite, all three create arms perform the same 24
// operations and end up with the same final values.
for (int suffix = 0; suffix < NUM_SUFFIXES; suffix += 2) {
for (String prefix : UPPER_PREFIXES) {
String key = prefix + "-" + suffix;
CIEntry entry =
FlatHashtable.getOrCreate(table, key, CaseInsensitiveKeyStrategy.INSTANCE, CI_CREATE);
FlatHashtable.tryGetOrCreate(
table, key, CaseInsensitiveKeyStrategy.INSTANCE, CI_CREATE);
entry.value = suffix + 1;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,34 @@
* substitute for {@code HashMap} particularly for simple counter/tally use cases with a primitive
* value, where avoiding the per-update boxing allocation pays off even on a JVM with much better
* allocation handling than JDK 8 had.
*
* <p>Rerun on the capped/{@code State}-backed table (5 forks, 15 datapoints/method, Zulu 17.0.7
* AArch64, 8 threads). <b>Not comparable to the table above:</b> JMH auto-detected the {@code full
* + dont-inline} Blackhole here rather than the cheap {@code compiler} one, on the same JVM build
* and JMH 1.37 -- the mode is auto-detected per run and is not stable across runs, so every
* absolute number in this file is conditional on a mode that JMH does not record beside it. Compare
* within a table, never across. M ops/us:
*
* <pre>{@code
* add_hashMap 1204.8 add_hashtable 974.4
* update_hashMap 577.2 update_hashtable 1862.6
* iterate_hashMap 15.9 iterate_hashtable 21.5
* }</pre>
*
* <p>Within this run: {@code update_hashtable} wins by ~3.2x and {@code iterate_hashtable} by
* ~1.35x, while {@code add_hashtable} now <em>loses</em> by ~19% -- no longer the "roughly
* comparable" of the JDK 8 table, and a wider gap than the slight edge HashMap held in the previous
* Java 17 run. {@code add} is where the capped table's bookkeeping is least amortized: both sides
* allocate one entry per insert, so there is no boxing win to offset it, and the loop does nothing
* else. The counter/tally path -- the case {@code Hashtable} exists for -- is unaffected.
*
* <p>That is the right side of the trade for this family. {@code Hashtable} and {@link
* ConcurrentHashtable} are designed for workloads where <b>updates dominate</b>: the table is
* populated once and then hit repeatedly, so per-insert cost amortizes away and in-place mutation
* of a primitive field is the operation that runs hot. Paying on {@code add} to make {@code update}
* faster is the trade those workloads want. {@code FlatHashtable} and {@code TagMap} sit at the
* other end -- built up and read, not updated in a loop -- so this result does not transfer to
* them, and neither does the reasoning that justifies it.
*/
@Fork(2)
@Warmup(iterations = 2)
Expand Down Expand Up @@ -143,11 +171,14 @@ public static class D1State {
int cursor;
final BhD1Consumer consumer = new BhD1Consumer();

// Level.Iteration, not Trial: this rebuilds the table and the HashMap, so each iteration must
// start from a fresh, identically-sized state rather than inheriting mutated counters. The
// pollution call rides along -- it is idempotent and untimed, so repeating it costs nothing.
@Setup(Level.Iteration)
public void setUp() {
BenchmarkUtils.polluteHashDispatch();

table = new Hashtable.D1<>(CAPACITY);
table = Hashtable.D1.createBounded(D1Counter.class, CAPACITY);
hashMap = new HashMap<>(CAPACITY);
keys = SOURCE_KEYS;
for (int i = 0; i < N_KEYS; ++i) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,14 @@ public static class D2State {
int cursor;
final BhD2Consumer consumer = new BhD2Consumer();

// Level.Iteration, not Trial: this rebuilds the table and the HashMap, so each iteration must
// start from a fresh, identically-sized state rather than inheriting mutated counters. The
// pollution call rides along -- it is idempotent and untimed, so repeating it costs nothing.
@Setup(Level.Iteration)
public void setUp() {
BenchmarkUtils.polluteHashDispatch();

table = new Hashtable.D2<>(CAPACITY);
table = Hashtable.D2.createBounded(D2Counter.class, CAPACITY);
hashMap = new HashMap<>(CAPACITY);
k1s = SOURCE_K1;
k2s = SOURCE_K2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ public void pollute() {

@Setup(Level.Trial)
public void setUp() {
BenchmarkUtils.polluteHashDispatch();

hashMap = new HashMap<>();
fill(hashMap);
synchronizedHashMap = Collections.synchronizedMap(new HashMap<>(hashMap));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ public void pollute() {

@Setup(Level.Trial)
public void setUp() {
BenchmarkUtils.polluteHashDispatch();

hashSet = new HashSet<>(Arrays.asList(ELEMENTS));
synchronizedSet = Collections.synchronizedSet(new HashSet<>(hashSet));
treeSet = new TreeSet<>(Arrays.asList(ELEMENTS));
Expand Down
Loading
Loading