feat: add PackedHistogram — memory-optimised sparse variant - #154
Open
fcostaoliveira wants to merge 2 commits into
Open
feat: add PackedHistogram — memory-optimised sparse variant#154fcostaoliveira wants to merge 2 commits into
fcostaoliveira wants to merge 2 commits into
Conversation
A separate opt-in histogram whose backing store grows with the number of POPULATED buckets, not counts_len, so many sparsely-populated histograms cost a fraction of the dense footprint (measured 2355x smaller at ~10 buckets each, 294x at ~100). Reuses the dense bucket geometry via a Histogram<u8> oracle with its counts vector emptied (the geometry lookups never index counts); standard V2 (and V2+DEFLATE) serialization byte-identical to V2Serializer, decodes both directions. - src/packed.rs: record (binary-search + insert, adaptive 1/2/4/8-byte counts), len/min/max/count_at, value_at_percentile/value_at_quantile via a width-specialized blocked prefix-sum mirroring the dense scan, overflow-safe via saturating ops. - V2 serialize/deserialize streamed from the sparse backing (byte-identical). - Minimal pub(crate) hooks in lib.rs / serialization (no public-API growth): index_for/value_for, an oracle counts-clear, and the varint/zig-zag helpers. - Tests: dense-vs-packed parity (300 random trials, index-by-index), width growth, byte-identical V2 interop both directions, two randomized fuzzers (differential vs dense 4000 trials + hostile decode 200k), and targeted coverage tests (98.7% lines / 100% of reachable, 2 documented defensive branches). Green under cargo test, --release, rustfmt, and clippy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files
🚀 New features to boost your workflow:
|
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
Adds
PackedHistogram— a separate, opt-in histogram whose backing store grows with the number of populated buckets rather thancounts_len. It reuses the dense bucket geometry exactly (through aHistogram<u8>oracle whosecountsvector is emptied — the geometry lookups never indexcounts) and speaks the standard V2 / V2+DEFLATE format (byte-identical toV2Serializer), so it interoperates with every existing HdrHistogram reader. The denseHistogram<T>is untouched.Why
Histogram::new_with_boundseagerly allocatesVec<T>of lengthcounts_len— ~184 KB per histogram at the default latency config (1, 3.6e9, 3) — regardless of how many buckets are ever used. For the many sparsely-populated histograms shape (per-endpoint / per-tenant / per-connection latency) that dominates the heap. Measured footprint (sparse backing,memory_size()):Java has
PackedHistogram; Rust had none.Design
idx: Vec<u32>holds the populated flatcountsindices (ascending);cnt: Vec<u8>holds one count per bucket at a uniform adaptive width (1/2/4/8 bytes) that widens on overflow. Record is a binary-search + insert;value_at_quantileis a width-specialized blocked prefix-sum over only the populated buckets, reusing the densevalue_for/lowest_equivalent/highest_equivalent. All query paths are overflow-safe (saturating_*).Only
pub(crate)hooks were added to the dense code (no public-API growth):index_for/value_for, an oracle counts-clear, and re-exports of the existing varint / zig-zag helpers.Correctness & testing
counts, min/max/total, percentile sweeps — bit-for-bit.V2SerializerandV2DeflateSerializer; round-trips both directions; rejects a non-zeronormalizingIndexOffset.cargo-llvm-cov): 98.7% lines / 100% of reachable lines (the 2 uncovered are documented-unreachable defensive branches); every function covered.cargo test,cargo build --release,cargo fmt --check, andcargo clippy(no new warnings onpacked.rs). The existing suite is unaffected.Happy to adjust naming/layout to match how you'd want a new type to land.
Benchmark evidence — vs
iopsystems/histogramv1.5.0 (the closest alternative)Measured on three arches (AWS Intel Granite Rapids / AMD Zen 5 Turin / ARM Neoverse-V2), same bucket geometry both sides (21504 buckets), sparse latency-like workload (1605 populated):
Read (percentile) — ns/query, lower is better:
PackedHistogram(this PR)iopSparseHistogramiopdenseHistogramHistogram<u64>PackedHistogramreads 2.7–3.1× faster than iop'sSparseHistogram, and it records live — iop'sSparseHistogramis a read-only snapshot built from a dense histogram, so it doesn't help the many-sparse-recorders case this PR targets.Histogram(±7%; faster on Zen 5) — the blocked prefix-sum only pays for populated buckets.percentile()is 28–57× slower than HdrHistogram's — it does two full O(total_buckets) rescans plus aVec/BTreeMapallocation per call; not this PR's concern, but it's why the sparse snapshot exists there.)Memory (sparse workload):
PackedHistogram11.1 KB vs iopSparseHistogram18.8 KB (1.7× smaller — adaptive 1–8 B counts vs iop's fixed 8 B) vs dense 168 KB (15× smaller). Results are bit-identical to the dense histogram (parity + differential fuzz enforce it).