Use the unified Hashtable API in client-side stats - #12312
Draft
dougqh wants to merge 8 commits into
Draft
Conversation
dougqh
commented
Aug 26, 2026
dougqh
commented
Aug 26, 2026
dougqh
commented
Aug 26, 2026
dougqh
commented
Aug 26, 2026
dougqh
commented
Aug 26, 2026
dougqh
added a commit
that referenced
this pull request
Aug 26, 2026
From reviewing the first real consumer (#12312), where each of these was either reaching into state.buckets or reaching into state.sizeManager to do something the API should have offered directly: size(state) / isEmpty(state) bucketFor(state, keyHash) -- typed, so the chain walk needs no witness forEach(state, consumer) -- and the context-passing overload Also adds insertReserved(state, keyHash, entry), which links an entry without touching the count because the caller already holds a reservation. That is the other half of tryReserveOrEvict, and it is deliberately a different name from insertHeadEntryFor(State, ...) -- that one reserves as it inserts, so using it after a reservation would count the entry twice. Splitting them keeps the refuse-before-you- allocate shape available: reserve, and only build the entry once the slot is yours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dougqh
added a commit
that referenced
this pull request
Aug 26, 2026
Addresses the review comments on #12312, with the API additions made in #12101 and percolated here: state.sizeManager.size() -> Hashtable.size(state) ... == 0 -> Hashtable.isEmpty(state) bucketFor(state.buckets, hash) -> bucketFor(state, hash) insertHeadEntryFor(state.buckets, ...) -> insertReserved(state, ...) forEach(state.buckets, ...) -> forEach(state, ...) No reference to state.buckets or state.sizeManager remains -- what State holds is now its own business. Also drops the field comment that re-documented the eviction cursor living inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dougqh
force-pushed
the
feat/client-side-stats-hashtable-api
branch
from
August 26, 2026 22:57
3d295de to
4790ecf
Compare
This comment has been minimized.
This comment has been minimized.
Contributor
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
dougqh
added a commit
that referenced
this pull request
Aug 27, 2026
Addresses the review comments on #12312, with the API additions made in #12101 and percolated here: state.sizeManager.size() -> Hashtable.size(state) ... == 0 -> Hashtable.isEmpty(state) bucketFor(state.buckets, hash) -> bucketFor(state, hash) insertHeadEntryFor(state.buckets, ...) -> insertReserved(state, ...) forEach(state.buckets, ...) -> forEach(state, ...) No reference to state.buckets or state.sizeManager remains -- what State holds is now its own business. Also drops the field comment that re-documented the eviction cursor living inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dougqh
force-pushed
the
feat/client-side-stats-hashtable-api
branch
from
August 27, 2026 00:07
4790ecf to
c5d4d06
Compare
dougqh
added a commit
that referenced
this pull request
Aug 27, 2026
Addresses the review comments on #12312, with the API additions made in #12101 and percolated here: state.sizeManager.size() -> Hashtable.size(state) ... == 0 -> Hashtable.isEmpty(state) bucketFor(state.buckets, hash) -> bucketFor(state, hash) insertHeadEntryFor(state.buckets, ...) -> insertReserved(state, ...) forEach(state.buckets, ...) -> forEach(state, ...) No reference to state.buckets or state.sizeManager remains -- what State holds is now its own business. Also drops the field comment that re-documented the eviction cursor living inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dougqh
force-pushed
the
feat/client-side-stats-hashtable-api
branch
from
August 27, 2026 00:16
c5d4d06 to
00f8ad1
Compare
Replaces the deprecated Support facade, and the hand-rolled bookkeeping, with Hashtable.State: - four fields (buckets, maxAggregates, size, evictCursor) become one Hashtable.State<AggregateEntry> - evictOneStale's cursor-resumed two-pass scan -- the [cursor, length) then [0, cursor) walk, plus its helper, ~25 lines -- disappears into Hashtable.tryReserveOrEvict, which reserves a slot and only evicts if the table is actually full - expungeStaleAggregates' manual iterator loop becomes evictAll - clear stops pairing three resets by hand - the stale test is a static final Predicate<AggregateEntry>, so eviction allocates no lambda and needs no cast Behaviour is unchanged: same cap, same evict-a-stale-entry-or-drop policy on the miss path, same amortized resumable scan -- that scan just lives in the primitive now instead of here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The eviction predicate spelled the rule out as hitCount == 0, so the table had to know how staleness is defined. Moving it onto the entry leaves the call site reading AggregateEntry::isStale. That is an unbound instance-method reference, so it still coerces to Predicate<AggregateEntry> and is still non-capturing -- LambdaMetafactory links it to one cached instance, same as the lambda it replaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses the review comments on #12312, with the API additions made in #12101 and percolated here: state.sizeManager.size() -> Hashtable.size(state) ... == 0 -> Hashtable.isEmpty(state) bucketFor(state.buckets, hash) -> bucketFor(state, hash) insertHeadEntryFor(state.buckets, ...) -> insertReserved(state, ...) forEach(state.buckets, ...) -> forEach(state, ...) No reference to state.buckets or state.sizeManager remains -- what State holds is now its own business. Also drops the field comment that re-documented the eviction cursor living inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Notes why AggregateTable.size() stays exact despite delegating to an estimate: findOrInsert reserves and links without yielding, so the reservation window is never observable from outside this class. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deleting evictOneStale left its javadoc behind, where it silently attached to forEach -- so forEach claimed to unlink stale entries and linked #evictCursor, a field that no longer exists. The mechanical half of that text (cursor-resumed two-pass scan, its amortization) now belongs to Hashtable.tryReserveOrEvict, so it goes. The domain half is knowledge this class still owns and nothing else records: why a full table drops the new key instead of evicting an established one, and why cardinality limiting reduces but does not eliminate eviction. That moves onto findOrInsert, where the decision is actually made. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing references it any more: this PR moved the last production caller (AggregateTable) onto the blessed statics, and the facade held no logic of its own -- every member was a one-line delegate. Removes 174 lines from Hashtable and the 135-line DeprecatedSupportTests group, most of which asserted only that a one-liner forwards. The two members that did have unique behaviour, create(int, float) and MAX_RATIO, are covered by capacityFor(int, float), which has its own tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dougqh
force-pushed
the
feat/client-side-stats-hashtable-api
branch
from
August 27, 2026 00:47
2f6f714 to
8435b63
Compare
5 tasks
Removes the nullable that only ever appears once the tag table is at capacity -- the shape most likely to ship as a rare production NPE. The primitive-long overload keeps record() allocation-free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Does This Do?
Migrates client-side statistics off the deprecated
Hashtable.Supportfacade and onto the API from #12101, as the first real consumer of it.This is the "use it to review it" half of that PR. The API was shaped in the abstract; this is what it looks like from the outside.
Result
AggregateTablegoes from 164 to 132 lines, and from four pieces of hand-managed state to one:The biggest deletion is
evictOneStaleand its helper — a cursor-resumed two-pass scan over[cursor, length)then[0, cursor), ~25 lines — which collapses into one call that reserves a slot and evicts only if the table is actually full:The caller no longer knows a cursor exists, but still gets its amortization: a sustained eviction stream never re-scans the hot prefix more than twice across N evictions.
Also gone: the manual
size--after every unlink (the manager owns the count now), the three paired resets inclear, and the iterator loop inexpungeStaleAggregates.STALEis astatic final Predicate<AggregateEntry>— non-capturing, so eviction allocates nothing, and typed, so it needs no cast.What this exercised in the API
Two warts found by writing this, both fixed in #12101 rather than worked around here:
removeMatchingneeded an explicitHashtable.<AggregateEntry>type witness, becauseTEntryhad nothing to infer from.Predicate<? super Entry>, forcingentry -> ((AggregateEntry) entry).getHitCount() == 0.Parameterizing
State<TEntry>and having the statics take it fixed both, and made a spine/manager mismatch unconstructible rather than merely discouraged.Behavior
Unchanged. Same cap, same evict-a-stale-entry-or-drop policy on the miss path, same amortized resumable scan — the scan just lives in the primitive now.
Follow-up this unblocks
No production code references
Hashtable.Supportany more. The facade holds no logic and can be deleted outright — deliberately left for its own PR so this one stays a behaviour-neutral migration.Test plan
./gradlew :dd-trace-core:test --tests "datadog.trace.common.metrics.*"— passes unchanged./gradlew :dd-trace-core:compileJava— no deprecation warnings fromAggregateTableany more🤖 Generated with Claude Code