From 179825b7dab2386d63bf19cc635349202ccfad7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=8B=E5=B9=B2?= Date: Sun, 2 Aug 2026 08:17:23 +0800 Subject: [PATCH 1/2] feat: expose estimated_size for stateful set operations Cover the accumulating set-operation operators that hold real heap state: HllUnion, CpcUnion, ThetaUnion, ThetaIntersection, TupleUnion, and TupleIntersection. The a-not-b operators are stateless and thus exempt. UnionState and IntersectionState in thetacommon gain heap-only helpers that delegate to SketchHashTable, mirroring the sketch-level pattern from #136. For HllUnion and CpcUnion, whose state embeds a full sketch inline, the sketch's inline size is subtracted to avoid double counting against size_of::(). Addresses #137 --- CHANGELOG.md | 1 + datasketches/src/cpc/union.rs | 10 ++++++++++ datasketches/src/hll/union.rs | 6 ++++++ datasketches/src/theta/intersection.rs | 5 +++++ datasketches/src/theta/union.rs | 5 +++++ datasketches/src/thetacommon/intersection.rs | 5 +++++ datasketches/src/thetacommon/union.rs | 5 +++++ datasketches/src/tuple/intersection.rs | 5 +++++ datasketches/src/tuple/union.rs | 5 +++++ datasketches/tests/cpc_test/union.rs | 14 ++++++++++++++ datasketches/tests/hll_test/union.rs | 14 ++++++++++++++ datasketches/tests/theta_test/intersection.rs | 10 ++++++++++ datasketches/tests/theta_test/union.rs | 10 ++++++++++ datasketches/tests/tuple_test/intersection.rs | 10 ++++++++++ datasketches/tests/tuple_test/union.rs | 10 ++++++++++ 15 files changed, 115 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b618efe..54d665bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All significant changes to this project will be documented in this file. * `FrequentItemsSketch` now supports borrowed-key updates via `update_ref` and `update_with_count_ref`, allowing sketches such as `FrequentItemsSketch` to update from `&str` without allocating on existing-key hits. Frequency queries also accept borrowed key forms matching `Borrow`. * `FrequentItemsSketch` no longer requires item types to implement `Clone` for core updates, queries, and serialization. Custom `FrequentItemValue` implementations can now be non-`Clone`; APIs that return or merge owned items still require `Clone`. * `CountMinSketch` and `FrequentItemsSketch` now expose `estimated_size()`, reporting the in-memory footprint of the sketch in bytes, following the other sketches. +* The stateful set operations `HllUnion`, `CpcUnion`, `ThetaUnion`, `ThetaIntersection`, `TupleUnion`, and `TupleIntersection` now expose `estimated_size()`, reporting the in-memory footprint of the operator's internal state in bytes. ### Bug fixes diff --git a/datasketches/src/cpc/union.rs b/datasketches/src/cpc/union.rs index a9836303..97fa46e0 100644 --- a/datasketches/src/cpc/union.rs +++ b/datasketches/src/cpc/union.rs @@ -335,6 +335,16 @@ impl CpcUnion { } } } + + /// Returns the estimated size of the union in bytes. + pub fn estimated_size(&self) -> usize { + // The state's inline size is already covered by size_of::(). + let heap_size = match &self.state { + UnionState::Accumulator(sketch) => sketch.estimated_size() - size_of::(), + UnionState::BitMatrix(matrix) => matrix.capacity() * size_of::(), + }; + size_of::() + heap_size + } } // testing methods diff --git a/datasketches/src/hll/union.rs b/datasketches/src/hll/union.rs index df995f6c..0fd13d75 100644 --- a/datasketches/src/hll/union.rs +++ b/datasketches/src/hll/union.rs @@ -332,6 +332,12 @@ impl HllUnion { pub fn lower_bound(&self, num_std_dev: NumStdDev) -> f64 { self.gadget.lower_bound(num_std_dev) } + + /// Returns the estimated size of the union in bytes. + pub fn estimated_size(&self) -> usize { + // The gadget's inline size is already covered by size_of::(). + size_of::() - size_of::() + self.gadget.estimated_size() + } } /// Convert a coupon mode (List or Set) to Hll8 target type diff --git a/datasketches/src/theta/intersection.rs b/datasketches/src/theta/intersection.rs index e518bc71..9afd6ba6 100644 --- a/datasketches/src/theta/intersection.rs +++ b/datasketches/src/theta/intersection.rs @@ -67,6 +67,11 @@ impl ThetaIntersection { self.state.has_result() } + /// Returns the estimated size of the intersection in bytes. + pub fn estimated_size(&self) -> usize { + size_of::() + self.state.estimated_size() + } + /// Returns the intersection result as a compact theta sketch. /// /// # Panics diff --git a/datasketches/src/theta/union.rs b/datasketches/src/theta/union.rs index 94a974f2..4f0121a7 100644 --- a/datasketches/src/theta/union.rs +++ b/datasketches/src/theta/union.rs @@ -66,6 +66,11 @@ impl ThetaUnion { pub fn reset(&mut self) { self.state.reset(); } + + /// Returns the estimated size of the union in bytes. + pub fn estimated_size(&self) -> usize { + size_of::() + self.state.estimated_size() + } } /// Builder for [`ThetaUnion`]. diff --git a/datasketches/src/thetacommon/intersection.rs b/datasketches/src/thetacommon/intersection.rs index 77ae3983..9421e050 100644 --- a/datasketches/src/thetacommon/intersection.rs +++ b/datasketches/src/thetacommon/intersection.rs @@ -229,6 +229,11 @@ where self.has_result } + /// Returns the estimated size of the heap allocations in bytes. + pub fn estimated_size(&self) -> usize { + self.table.estimated_size() + } + /// Return the current intersection state as compact-sketch parts. pub fn result(&self, ordered: bool) -> CompactSketchParts where diff --git a/datasketches/src/thetacommon/union.rs b/datasketches/src/thetacommon/union.rs index 3e89189e..fa9585b1 100644 --- a/datasketches/src/thetacommon/union.rs +++ b/datasketches/src/thetacommon/union.rs @@ -152,6 +152,11 @@ where self.table.reset(); self.union_theta = self.table.theta(); } + + /// Returns the estimated size of the heap allocations in bytes. + pub fn estimated_size(&self) -> usize { + self.table.estimated_size() + } } #[cfg(test)] diff --git a/datasketches/src/tuple/intersection.rs b/datasketches/src/tuple/intersection.rs index 58524d4f..5dfaaf79 100644 --- a/datasketches/src/tuple/intersection.rs +++ b/datasketches/src/tuple/intersection.rs @@ -130,6 +130,11 @@ where self.state.has_result() } + /// Returns the estimated size of the intersection in bytes. + pub fn estimated_size(&self) -> usize { + size_of::() + self.state.estimated_size() + } + /// Returns the intersection result as a compact Tuple sketch. /// /// If `ordered` is true, retained entries are sorted ascending by hash. diff --git a/datasketches/src/tuple/union.rs b/datasketches/src/tuple/union.rs index 4fdf5e4b..55e21f4c 100644 --- a/datasketches/src/tuple/union.rs +++ b/datasketches/src/tuple/union.rs @@ -110,6 +110,11 @@ where pub fn reset(&mut self) { self.state.reset(); } + + /// Returns the estimated size of the union in bytes. + pub fn estimated_size(&self) -> usize { + size_of::() + self.state.estimated_size() + } } /// Builder for [`TupleUnion`]. diff --git a/datasketches/tests/cpc_test/union.rs b/datasketches/tests/cpc_test/union.rs index aaa5fa38..b161a7d4 100644 --- a/datasketches/tests/cpc_test/union.rs +++ b/datasketches/tests/cpc_test/union.rs @@ -179,3 +179,17 @@ fn test_lg_k_too_small() { fn test_lg_k_too_large() { CpcSketch::new(27); } + +#[test] +fn test_union_estimated_size() { + let mut union = CpcUnion::new(11); + let empty_size = union.estimated_size(); + assert!(empty_size > 0); + + let mut sketch = CpcSketch::new(11); + for i in 0..1000 { + sketch.update(i); + } + union.update(&sketch); + assert!(union.estimated_size() > empty_size); +} diff --git a/datasketches/tests/hll_test/union.rs b/datasketches/tests/hll_test/union.rs index 6d6d5d34..cfc5c680 100644 --- a/datasketches/tests/hll_test/union.rs +++ b/datasketches/tests/hll_test/union.rs @@ -617,3 +617,17 @@ fn test_union_validation() { union.reset(); assert_eq!(union.lg_max_k(), 15, "lg_max_k should persist after reset"); } + +#[test] +fn test_union_estimated_size() { + let mut union = HllUnion::new(10); + let empty_size = union.estimated_size(); + assert!(empty_size > 0); + + let mut sketch = HllSketch::new(10, HllType::Hll8); + for i in 0..1000 { + sketch.update(i); + } + union.update(&sketch); + assert!(union.estimated_size() > empty_size); +} diff --git a/datasketches/tests/theta_test/intersection.rs b/datasketches/tests/theta_test/intersection.rs index 21d32c59..5a456d5c 100644 --- a/datasketches/tests/theta_test/intersection.rs +++ b/datasketches/tests/theta_test/intersection.rs @@ -317,3 +317,13 @@ fn test_seed_mismatch_non_empty_returns_error() { let mut i = ThetaIntersection::with_seed(123); assert!(i.update(&s).is_err()); } + +#[test] +fn test_intersection_estimated_size() { + let mut intersection = ThetaIntersection::default(); + let empty_size = intersection.estimated_size(); + + let sketch = sketch_with_range(0, 1000); + intersection.update(&sketch).unwrap(); + assert!(intersection.estimated_size() > empty_size); +} diff --git a/datasketches/tests/theta_test/union.rs b/datasketches/tests/theta_test/union.rs index 56fc64d4..24720814 100644 --- a/datasketches/tests/theta_test/union.rs +++ b/datasketches/tests/theta_test/union.rs @@ -687,3 +687,13 @@ fn test_corner_case_union_states() { assert_eq!(compact_result.is_empty(), expected_empty); } } + +#[test] +fn test_union_estimated_size() { + let mut union = ThetaUnionBuilder::default().build(); + let empty_size = union.estimated_size(); + + let sketch = sketch_with_range(12, 0, 1000); + union.update(&sketch).unwrap(); + assert!(union.estimated_size() > empty_size); +} diff --git a/datasketches/tests/tuple_test/intersection.rs b/datasketches/tests/tuple_test/intersection.rs index e339055a..b3a143a0 100644 --- a/datasketches/tests/tuple_test/intersection.rs +++ b/datasketches/tests/tuple_test/intersection.rs @@ -179,3 +179,13 @@ fn estimation_bounds_cover_the_true_intersection() { "expected 25000 in [{lower}, {upper}]" ); } + +#[test] +fn intersection_estimated_size_grows_with_updates() { + let mut intersection = TupleIntersection::new(SumPolicy); + let empty_size = intersection.estimated_size(); + + let sketch = tuple_sketch_with_range(0, 1000); + intersection.update(&sketch).unwrap(); + assert!(intersection.estimated_size() > empty_size); +} diff --git a/datasketches/tests/tuple_test/union.rs b/datasketches/tests/tuple_test/union.rs index bb4af8b0..33717415 100644 --- a/datasketches/tests/tuple_test/union.rs +++ b/datasketches/tests/tuple_test/union.rs @@ -151,3 +151,13 @@ fn estimation_bounds_cover_the_true_union() { "expected 75000 in [{lower}, {upper}]" ); } + +#[test] +fn union_estimated_size_grows_with_updates() { + let mut union = default_union_builder().build(); + let empty_size = union.estimated_size(); + + let sketch = tuple_sketch_with_range(0, 1000); + union.update(&sketch).unwrap(); + assert!(union.estimated_size() > empty_size); +} From 65132efe2838101938b9a5dc19cd72d5c6b4d77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=8B=E5=B9=B2?= Date: Tue, 4 Aug 2026 21:45:20 +0800 Subject: [PATCH 2/2] test: assert exact estimated_size values for set operations Signed-off-by: Renkai Ge --- datasketches/tests/cpc_test/union.rs | 5 ++--- datasketches/tests/hll_test/union.rs | 5 ++--- datasketches/tests/theta_test/intersection.rs | 4 ++-- datasketches/tests/theta_test/union.rs | 4 ++-- datasketches/tests/tuple_test/intersection.rs | 4 ++-- datasketches/tests/tuple_test/union.rs | 4 ++-- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/datasketches/tests/cpc_test/union.rs b/datasketches/tests/cpc_test/union.rs index b161a7d4..1d83da99 100644 --- a/datasketches/tests/cpc_test/union.rs +++ b/datasketches/tests/cpc_test/union.rs @@ -183,13 +183,12 @@ fn test_lg_k_too_large() { #[test] fn test_union_estimated_size() { let mut union = CpcUnion::new(11); - let empty_size = union.estimated_size(); - assert!(empty_size > 0); + assert_eq!(union.estimated_size(), 112); let mut sketch = CpcSketch::new(11); for i in 0..1000 { sketch.update(i); } union.update(&sketch); - assert!(union.estimated_size() > empty_size); + assert_eq!(union.estimated_size(), 16496); } diff --git a/datasketches/tests/hll_test/union.rs b/datasketches/tests/hll_test/union.rs index cfc5c680..ef67b394 100644 --- a/datasketches/tests/hll_test/union.rs +++ b/datasketches/tests/hll_test/union.rs @@ -621,13 +621,12 @@ fn test_union_validation() { #[test] fn test_union_estimated_size() { let mut union = HllUnion::new(10); - let empty_size = union.estimated_size(); - assert!(empty_size > 0); + assert_eq!(union.estimated_size(), 128); let mut sketch = HllSketch::new(10, HllType::Hll8); for i in 0..1000 { sketch.update(i); } union.update(&sketch); - assert!(union.estimated_size() > empty_size); + assert_eq!(union.estimated_size(), 1120); } diff --git a/datasketches/tests/theta_test/intersection.rs b/datasketches/tests/theta_test/intersection.rs index 5a456d5c..65f9d705 100644 --- a/datasketches/tests/theta_test/intersection.rs +++ b/datasketches/tests/theta_test/intersection.rs @@ -321,9 +321,9 @@ fn test_seed_mismatch_non_empty_returns_error() { #[test] fn test_intersection_estimated_size() { let mut intersection = ThetaIntersection::default(); - let empty_size = intersection.estimated_size(); + assert_eq!(intersection.estimated_size(), 72); let sketch = sketch_with_range(0, 1000); intersection.update(&sketch).unwrap(); - assert!(intersection.estimated_size() > empty_size); + assert_eq!(intersection.estimated_size(), 16456); } diff --git a/datasketches/tests/theta_test/union.rs b/datasketches/tests/theta_test/union.rs index 24720814..2a6f1cc0 100644 --- a/datasketches/tests/theta_test/union.rs +++ b/datasketches/tests/theta_test/union.rs @@ -691,9 +691,9 @@ fn test_corner_case_union_states() { #[test] fn test_union_estimated_size() { let mut union = ThetaUnionBuilder::default().build(); - let empty_size = union.estimated_size(); + assert_eq!(union.estimated_size(), 1096); let sketch = sketch_with_range(12, 0, 1000); union.update(&sketch).unwrap(); - assert!(union.estimated_size() > empty_size); + assert_eq!(union.estimated_size(), 65608); } diff --git a/datasketches/tests/tuple_test/intersection.rs b/datasketches/tests/tuple_test/intersection.rs index b3a143a0..ea6e5af9 100644 --- a/datasketches/tests/tuple_test/intersection.rs +++ b/datasketches/tests/tuple_test/intersection.rs @@ -183,9 +183,9 @@ fn estimation_bounds_cover_the_true_intersection() { #[test] fn intersection_estimated_size_grows_with_updates() { let mut intersection = TupleIntersection::new(SumPolicy); - let empty_size = intersection.estimated_size(); + assert_eq!(intersection.estimated_size(), 72); let sketch = tuple_sketch_with_range(0, 1000); intersection.update(&sketch).unwrap(); - assert!(intersection.estimated_size() > empty_size); + assert_eq!(intersection.estimated_size(), 32840); } diff --git a/datasketches/tests/tuple_test/union.rs b/datasketches/tests/tuple_test/union.rs index 33717415..23e75aac 100644 --- a/datasketches/tests/tuple_test/union.rs +++ b/datasketches/tests/tuple_test/union.rs @@ -155,9 +155,9 @@ fn estimation_bounds_cover_the_true_union() { #[test] fn union_estimated_size_grows_with_updates() { let mut union = default_union_builder().build(); - let empty_size = union.estimated_size(); + assert_eq!(union.estimated_size(), 2120); let sketch = tuple_sketch_with_range(0, 1000); union.update(&sketch).unwrap(); - assert!(union.estimated_size() > empty_size); + assert_eq!(union.estimated_size(), 131144); }