feat: add hdr_record_value_capped and hdr_total_count (memtier_benchmark compatible api) - #166
Conversation
hdr_record_value_capped clamps a value into [lowest_discernible_value, highest_trackable_value] and records it, for callers that would rather saturate than drop out-of-range samples (see HdrHistogram#126). It delegates to hdr_record_value, so the write path is unchanged. hdr_total_count is a NULL-safe getter for the total recorded count. Both are already carried as local patches in memtier_benchmark's vendored copy; upstreaming them lets it use the library unmodified. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
🤖 Automated first-pass review — a human maintainer's review is still required before merge. Looks good to me. The change only adds API: A few small things:
|
Clamp to [0, highest_trackable_value] instead of raising to lowest_discernible_value. 0 and values below the lowest discernible value are valid and were being moved; Java, Python and Rust all record them as-is. Negatives are clamped to 0 so the call cannot fail on in-type input, matching Rust's saturating_record. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks, agreed on all three.
|
Merge main (HdrHistogram#158 restructured the record functions) and address review: - hdr_record_value_capped_atomic: the atomic twin, a thin wrapper over hdr_record_value_atomic. memtier writes a shared histogram from several threads and carries a local copy of this helper. - hdr_total_count now uses hdr_atomic_load_64, so it can be called while other threads use the *_atomic record functions. With a plain read, ThreadSanitizer reports a race against the atomic increment; with the atomic load it reports none. - Document that hdr_record_value_capped returns true for any value on a valid histogram. - Tests: atomic/non-atomic equivalence, and a two-writer concurrent test that polls hdr_total_count while recording out-of-range and negative values. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve the conflict with HdrHistogram#158's record-path restructuring: keep upstream's record_value_counted helpers and re-add hdr_record_value_capped after hdr_record_value_atomic. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This reverts commit 6ecb9df.
paulorsousa
left a comment
There was a problem hiding this comment.
Looks good to me
Just noticed a possible situation on 32bit Windows (commented on the code), but this issue already exists in the codebase, so it may be worth fixing it on a separate PR
(update_min_max_atomic() and hdr_phaser_flip_phase() via _hdr_phaser_get_epoch())
| int64_t hdr_total_count(const struct hdr_histogram* h) | ||
| { | ||
| /* atomic load: safe to call while other threads use the *_atomic record functions */ | ||
| return h != NULL ? hdr_atomic_load_64((int64_t*) &h->total_count) : 0; |
There was a problem hiding this comment.
Maybe we have a portability issue here for 32-bit Windows
There, this 64-bit read may combine parts of different updates and return an incorrect count.
Microsoft documents this limitation here
There was a problem hiding this comment.
Confirmed, and thanks for catching it. In src/hdr_atomic.h the MSVC hdr_atomic_load_64 is _ReadBarrier(); return *field;, a plain read, and hdr_atomic_store_64 is the matching plain write. On 32-bit x86 a plain int64_t access is two 32-bit accesses, so a concurrent hdr_atomic_add_fetch_64 (the _InterlockedCompareExchange64 loop) can be observed half-applied. The GCC/Clang __atomic_load_n path is not affected.
So hdr_total_count inherits this from the helper rather than introducing it, but it does expose it through a public getter. The other users I can see on main are update_min_max_atomic (lines 143 and 154) and the phaser epoch (_hdr_phaser_get_epoch/_set_epoch), as you listed.
I have not reproduced a torn read and I have no 32-bit Windows machine to test on; the existing Windows x86 CI legs would not detect it either. The likely fix is in the helpers only: on !_WIN64 use _InterlockedCompareExchange64(field, 0, 0) for the load and the existing exchange for the store, leaving _WIN64 as is. I have not written it. Agreed that it belongs in a separate PR rather than here, since #166 is already merged.
What
Three small additive APIs:
hdr_record_value_cappedclamps the value into[0, highest_trackable_value]and records it, so out-of-range samples saturate instead of being rejected. It delegates tohdr_record_value, so the write path itself is unchanged.hdr_record_value_capped_atomicis the same overhdr_record_value_atomic, safe to call from several threads.hdr_total_countreturns the total recorded count, and 0 for a NULL histogram. It uses an atomic load, so it can be called while other threads use the*_atomicrecord functions.Why
All three are already carried as local patches in memtier_benchmark, which records latencies from worker threads into a shared histogram while its main thread reads the total and resets or merges it. Upstreaming them lets that project use the library unmodified, and lets the amalgamated core in #164 match what it vendors.
The clamping is related to #126, which asks for out-of-range values to be capped. This PR does not change
hdr_record_value, which still rejects them, so it does not close #126.Behaviour, checked against the other implementations
memtier's version also raised 0 (and anything below
lowest_discernible_value) up tolowest_discernible_value. I dropped that, because none of the other implementations do it. I ran the same probe (lowest 10, highest 1000, 3 significant figures) against each:recordValueArrayIndexOutOfBoundsExceptionArrayIndexOutOfBoundsExceptionhdrh0.10.3record_valueFalseFalserecordErr(ValueOutOfRangeResizeDisabled)u64)saturating_recordu64)hdr_record_valueSo only the top end is clamped here, like Rust's
saturating_record. Negatives are clamped to 0, so the call cannot fail for any input.One difference to be aware of: Java, Python and Rust enforce the size of the counts array, which is rounded up to a power of two, while C checks
highest_trackable_valueexactly. With the bounds above, 5000 is accepted by the other three but rejected byhdr_record_valuehere. That is existing behaviour and this PR does not change it.The atomic load in
hdr_total_countThe existing getters read
total_countplainly. That is fine single-threaded, but this one is meant to be polled while writers run. With a plain read, ThreadSanitizer reports a data race betweenhdr_total_countand the atomic increment incounts_inc_normalised_atomic; withhdr_atomic_load_64it reports none (the new concurrent test, run both ways). The result is identical single-threaded.Tests / gates
test_record_value_cappedcovers 0, a value below the lowest discernible value, in-range, above-range and negative input, and checks where each lands.test_record_value_capped_atomicchecks the atomic variant produces the same histogram as the plain one, includingINT64_MINandINT64_MAX.test_recording_capped_concurrentlyruns two writers with a mix of in-range, above-range and negative values while the main thread pollshdr_total_count, then compares against a single-threaded reference. It checks the total never goes backwards and no record is lost.test_hdr_total_countcovers empty, weighted records and NULL.ctestgcc and clang 9/9, ASan + UBSan 9/9,HDR_LOG_REQUIRED=DISABLEDbuild 6/6 with no warnings. The concurrent test is also clean under ThreadSanitizer.capped(v)againstrecord(clamp(v))over 112 configurations (including a rotated histogram andINT64_MIN/INT64_MAX) found them byte-identical and never returned false.No SOVERSION bump here, since that looks like a release-time decision (see the discussion on #165).
🤖 Generated with Claude Code