From f565aded8dbeee5c26fb90cdbd4dff36db9ac9eb Mon Sep 17 00:00:00 2001 From: zenotme Date: Sat, 11 Apr 2026 12:52:45 +0800 Subject: [PATCH] refactor: introduce SketchHashable trait for consistent hashing across sketches - Updated various sketch implementations (CpcSketch, HllSketch, ThetaSketch) to use the new SketchHashable trait for hashing values, ensuring consistent behavior across different data types. - Simplified update methods to accept types implementing SketchHashable instead of Hash. - Added tests to verify that integer, string, and float inputs are handled consistently across sketches. --- Cargo.lock | 7 - datasketches/src/cpc/sketch.rs | 22 +-- datasketches/src/cpc/union.rs | 6 +- datasketches/src/hash/mod.rs | 4 + datasketches/src/hash/sketch_hashable.rs | 170 +++++++++++++++++++++++ datasketches/src/hll/mod.rs | 5 +- datasketches/src/hll/sketch.rs | 10 +- datasketches/src/hll/union.rs | 10 +- datasketches/src/lib.rs | 3 +- datasketches/src/theta/sketch.rs | 42 +----- datasketches/tests/cpc_update_test.rs | 53 +++++++ datasketches/tests/hll_update_test.rs | 53 +++++++ datasketches/tests/theta_sketch_test.rs | 90 +++++++++++- 13 files changed, 389 insertions(+), 86 deletions(-) create mode 100644 datasketches/src/hash/sketch_hashable.rs diff --git a/Cargo.lock b/Cargo.lock index 104e98f..e857def 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,13 +167,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "examples" -version = "0.0.0" -dependencies = [ - "datasketches", -] - [[package]] name = "fastrand" version = "2.3.0" diff --git a/datasketches/src/cpc/sketch.rs b/datasketches/src/cpc/sketch.rs index 534f0a0..295a9c7 100644 --- a/datasketches/src/cpc/sketch.rs +++ b/datasketches/src/cpc/sketch.rs @@ -24,7 +24,6 @@ use crate::codec::assert::ensure_serial_version_is; use crate::codec::assert::insufficient_data; use crate::codec::family::Family; use crate::common::NumStdDev; -use crate::common::canonical_double; use crate::common::inv_pow2_table::INVERSE_POWERS_OF_2; use crate::cpc::DEFAULT_LG_K; use crate::cpc::Flavor; @@ -49,6 +48,7 @@ use crate::error::Error; use crate::error::ErrorKind; use crate::hash::DEFAULT_UPDATE_SEED; use crate::hash::MurmurHash3X64128; +use crate::hash::SketchHashable; use crate::hash::compute_seed_hash; /// A Compressed Probabilistic Counting sketch. @@ -170,12 +170,10 @@ impl CpcSketch { self.num_coupons == 0 } - /// Update the sketch with a hashable value. - /// - /// For `f32`/`f64` values, use `update_f32`/`update_f64` instead. - pub fn update(&mut self, value: T) { + /// Update the sketch with a value that implements [`SketchHashable`]. + pub fn update(&mut self, value: T) { let mut hasher = MurmurHash3X64128::with_seed(self.seed); - value.hash(&mut hasher); + value.to_hashable().hash(&mut hasher); let (h1, h2) = hasher.finish128(); let k = 1 << self.lg_k; @@ -191,18 +189,6 @@ impl CpcSketch { self.row_col_update(row_col); } - /// Update the sketch with a f64 value. - pub fn update_f64(&mut self, value: f64) { - // Canonicalize double for compatibility with Java - let canonical = canonical_double(value); - self.update(canonical); - } - - /// Update the sketch with a f32 value. - pub fn update_f32(&mut self, value: f32) { - self.update_f64(value as f64); - } - pub(super) fn flavor(&self) -> Flavor { determine_flavor(self.lg_k, self.num_coupons) } diff --git a/datasketches/src/cpc/union.rs b/datasketches/src/cpc/union.rs index a983630..5d300cf 100644 --- a/datasketches/src/cpc/union.rs +++ b/datasketches/src/cpc/union.rs @@ -125,11 +125,11 @@ impl CpcUnion { /// # use datasketches::cpc::CpcSketch; /// /// let mut s1 = CpcSketch::new(12); - /// s1.update(&"apple"); + /// s1.update("apple"); /// /// let mut s2 = CpcSketch::new(12); - /// s2.update(&"apple"); - /// s2.update(&"banana"); + /// s2.update("apple"); + /// s2.update("banana"); /// /// let mut union = CpcUnion::new(12); /// union.update(&s1); diff --git a/datasketches/src/hash/mod.rs b/datasketches/src/hash/mod.rs index 99d2cca..e0e5e89 100644 --- a/datasketches/src/hash/mod.rs +++ b/datasketches/src/hash/mod.rs @@ -15,10 +15,14 @@ // specific language governing permissions and limitations // under the License. +//! Shared hashing utilities. + mod murmurhash; +mod sketch_hashable; mod xxhash; pub(crate) use self::murmurhash::MurmurHash3X64128; +pub use self::sketch_hashable::SketchHashable; pub(crate) use self::xxhash::XxHash64; /// The seed 9001 used in the sketch update methods is a prime number that was chosen very early diff --git a/datasketches/src/hash/sketch_hashable.rs b/datasketches/src/hash/sketch_hashable.rs new file mode 100644 index 0000000..04e9090 --- /dev/null +++ b/datasketches/src/hash/sketch_hashable.rs @@ -0,0 +1,170 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::hash::Hash; + +use crate::common::canonical_double; + +mod private { + pub trait Sealed {} +} + +/// A trait for customizing sketch update hash behavior. +pub trait SketchHashable: private::Sealed { + /// Returns a canonical hashable view for use by sketch update operations. + fn to_hashable(&self) -> impl Hash; +} + +/// A wrapper for byte-oriented inputs that hashes only the payload bytes. +/// +/// Rust's `Hash` implementations for byte-like types such as `&str`, `String`, `&[u8]`, and +/// `Vec` are not raw-byte writes. They delegate through `Hasher::write_*` helpers that also +/// mix structural information, notably the slice length, into the hash stream. That behavior is +/// correct for Rust collections in general, but it does not match DataSketches update hashing. +/// +/// The Java and C++ DataSketches implementations hash string and byte inputs by feeding only the +/// UTF-8 / byte payload into the sketch hash function. They do not append an extra Rust-specific +/// length marker. For cross-language compatibility we need to reproduce that "raw bytes only" +/// contract here. +struct RawBytes<'a>(&'a [u8]); + +impl Hash for RawBytes<'_> { + fn hash(&self, state: &mut H) { + state.write(self.0); + } +} + +macro_rules! impl_sketch_hashable_via_i64 { + ($($src:ty => $mid:ty),* $(,)?) => { + $( + impl private::Sealed for $src {} + + impl SketchHashable for $src { + fn to_hashable(&self) -> impl Hash { + (*self as $mid) as i64 + } + } + )* + }; +} + +macro_rules! impl_sketch_hashable_passthrough { + ($($src:ty),* $(,)?) => { + $( + impl private::Sealed for $src {} + + impl SketchHashable for $src { + fn to_hashable(&self) -> impl Hash { + *self + } + } + )* + }; +} + +impl_sketch_hashable_via_i64!( + i8 => i64, + i16 => i64, + i32 => i64, + i64 => i64, + isize => i64, + u8 => i8, + u16 => i16, + u32 => i32, +); + +impl_sketch_hashable_passthrough!(bool, char, i128, u64, u128, usize); + +impl private::Sealed for f64 {} + +impl SketchHashable for f64 { + fn to_hashable(&self) -> impl Hash { + canonical_double(*self) + } +} + +impl private::Sealed for f32 {} + +impl SketchHashable for f32 { + fn to_hashable(&self) -> impl Hash { + canonical_double(*self as f64) + } +} + +impl private::Sealed for &str {} + +impl SketchHashable for &str { + fn to_hashable(&self) -> impl Hash { + RawBytes(self.as_bytes()) + } +} + +impl private::Sealed for String {} + +impl SketchHashable for String { + fn to_hashable(&self) -> impl Hash { + RawBytes(self.as_bytes()) + } +} + +impl private::Sealed for &String {} + +impl SketchHashable for &String { + fn to_hashable(&self) -> impl Hash { + RawBytes(self.as_bytes()) + } +} + +impl private::Sealed for &[u8] {} + +impl SketchHashable for &[u8] { + fn to_hashable(&self) -> impl Hash { + RawBytes(self) + } +} + +impl private::Sealed for Vec {} + +impl SketchHashable for Vec { + fn to_hashable(&self) -> impl Hash { + RawBytes(self.as_slice()) + } +} + +impl private::Sealed for &Vec {} + +impl SketchHashable for &Vec { + fn to_hashable(&self) -> impl Hash { + RawBytes(self.as_slice()) + } +} + +impl private::Sealed for [u8; N] {} + +impl SketchHashable for [u8; N] { + fn to_hashable(&self) -> impl Hash { + RawBytes(self.as_slice()) + } +} + +impl private::Sealed for &[u8; N] {} + +impl SketchHashable for &[u8; N] { + fn to_hashable(&self) -> impl Hash { + RawBytes(self.as_slice()) + } +} diff --git a/datasketches/src/hll/mod.rs b/datasketches/src/hll/mod.rs index b3e1b36..f296794 100644 --- a/datasketches/src/hll/mod.rs +++ b/datasketches/src/hll/mod.rs @@ -106,6 +106,7 @@ use std::hash::Hash; use crate::hash::MurmurHash3X64128; +use crate::hash::SketchHashable; mod array4; mod array6; @@ -178,9 +179,9 @@ fn pack_coupon(slot: u32, value: u8) -> u32 { } /// Generate a coupon from a hashable value. -fn coupon(v: H) -> u32 { +fn coupon(v: H) -> u32 { let mut hasher = MurmurHash3X64128::default(); - v.hash(&mut hasher); + v.to_hashable().hash(&mut hasher); let (lo, hi) = hasher.finish128(); let addr26 = lo as u32 & KEY_MASK_26; diff --git a/datasketches/src/hll/sketch.rs b/datasketches/src/hll/sketch.rs index 58c5372..9a78b73 100644 --- a/datasketches/src/hll/sketch.rs +++ b/datasketches/src/hll/sketch.rs @@ -20,14 +20,13 @@ //! This module provides the main [`HllSketch`] struct, which is the primary interface //! for creating and using HLL sketches for cardinality estimation. -use std::hash::Hash; - use crate::codec::SketchSlice; use crate::codec::assert::ensure_serial_version_is; use crate::codec::assert::insufficient_data; use crate::codec::family::Family; use crate::common::NumStdDev; use crate::error::Error; +use crate::hash::SketchHashable; use crate::hll::HllType; use crate::hll::RESIZE_DENOMINATOR; use crate::hll::RESIZE_NUMERATOR; @@ -156,10 +155,9 @@ impl HllSketch { self.lg_config_k } - /// Update the sketch with a value + /// Update the sketch with a value that implements [`SketchHashable`]. /// - /// This accepts any type that implements `Hash`. The value is hashed - /// and converted to a coupon, which is then inserted into the sketch. + /// The value is hashed and converted to a coupon, which is then inserted into the sketch. /// /// # Examples /// @@ -170,7 +168,7 @@ impl HllSketch { /// sketch.update("apple"); /// assert!(sketch.estimate() >= 1.0); /// ``` - pub fn update(&mut self, value: T) { + pub fn update(&mut self, value: T) { let coupon = coupon(value); self.update_with_coupon(coupon); } diff --git a/datasketches/src/hll/union.rs b/datasketches/src/hll/union.rs index 5f3929d..6ec2bfc 100644 --- a/datasketches/src/hll/union.rs +++ b/datasketches/src/hll/union.rs @@ -28,9 +28,8 @@ //! * Different modes (List, Set, Array4/6/8) //! * Different target HLL types -use std::hash::Hash; - use crate::common::NumStdDev; +use crate::hash::SketchHashable; use crate::hll::HllSketch; use crate::hll::HllType; use crate::hll::array4::Array4; @@ -89,10 +88,9 @@ impl HllUnion { Self { lg_max_k, gadget } } - /// Update the union's gadget with a value + /// Update the union's gadget with a value that implements [`SketchHashable`]. /// - /// This accepts any type that implements `Hash`. The value is hashed - /// and converted to a coupon, which is then inserted into the sketch. + /// The value is hashed and converted to a coupon, which is then inserted into the sketch. /// /// # Examples /// @@ -103,7 +101,7 @@ impl HllUnion { /// union.update_value("apple"); /// let _result = union.to_sketch(HllType::Hll8); /// ``` - pub fn update_value(&mut self, value: T) { + pub fn update_value(&mut self, value: T) { self.gadget.update(value); } diff --git a/datasketches/src/lib.rs b/datasketches/src/lib.rs index 02dc692..b1aa681 100644 --- a/datasketches/src/lib.rs +++ b/datasketches/src/lib.rs @@ -37,8 +37,7 @@ pub mod countmin; pub mod cpc; pub mod error; pub mod frequencies; +pub mod hash; pub mod hll; pub mod tdigest; pub mod theta; - -mod hash; diff --git a/datasketches/src/theta/sketch.rs b/datasketches/src/theta/sketch.rs index fdba5fe..9400b5f 100644 --- a/datasketches/src/theta/sketch.rs +++ b/datasketches/src/theta/sketch.rs @@ -20,8 +20,6 @@ //! This module provides ThetaSketch (mutable) and CompactThetaSketch (immutable) //! for cardinality estimation. -use std::hash::Hash; - use crate::codec::SketchBytes; use crate::codec::SketchSlice; use crate::codec::assert::ensure_preamble_longs_in_range; @@ -30,9 +28,9 @@ use crate::codec::family::Family; use crate::common::NumStdDev; use crate::common::ResizeFactor; use crate::common::binomial_bounds; -use crate::common::canonical_double; use crate::error::Error; use crate::hash::DEFAULT_UPDATE_SEED; +use crate::hash::SketchHashable; use crate::hash::compute_seed_hash; use crate::theta::DEFAULT_LG_K; use crate::theta::MAX_LG_K; @@ -105,9 +103,7 @@ impl ThetaSketch { ThetaSketchBuilder::default() } - /// Update the sketch with a hashable value. - /// - /// For `f32`/`f64` values, use `update_f32`/`update_f64` instead. + /// Update the sketch with a value that implements [`SketchHashable`]. /// /// # Examples /// @@ -117,38 +113,8 @@ impl ThetaSketch { /// sketch.update("apple"); /// assert!(sketch.estimate() >= 1.0); /// ``` - pub fn update(&mut self, value: T) { - self.table.try_insert(value); - } - - /// Update the sketch with a f64 value. - /// - /// # Examples - /// - /// ``` - /// # use datasketches::theta::ThetaSketch; - /// let mut sketch = ThetaSketch::builder().build(); - /// sketch.update_f64(1.0); - /// assert!(sketch.estimate() >= 1.0); - /// ``` - pub fn update_f64(&mut self, value: f64) { - // Canonicalize double for compatibility with Java - let canonical = canonical_double(value); - self.update(canonical); - } - - /// Update the sketch with a f32 value. - /// - /// # Examples - /// - /// ``` - /// # use datasketches::theta::ThetaSketch; - /// let mut sketch = ThetaSketch::builder().build(); - /// sketch.update_f32(1.0); - /// assert!(sketch.estimate() >= 1.0); - /// ``` - pub fn update_f32(&mut self, value: f32) { - self.update_f64(value as f64); + pub fn update(&mut self, value: T) { + self.table.try_insert(value.to_hashable()); } /// Return cardinality estimate diff --git a/datasketches/tests/cpc_update_test.rs b/datasketches/tests/cpc_update_test.rs index 7b814f7..c6bd2c3 100644 --- a/datasketches/tests/cpc_update_test.rs +++ b/datasketches/tests/cpc_update_test.rs @@ -24,6 +24,11 @@ use googletest::prelude::near; const RELATIVE_ERROR_FOR_LG_K_11: f64 = 0.02; +fn assert_same_sketch_state(left: &CpcSketch, right: &CpcSketch) { + assert_eq!(left.serialize(), right.serialize()); + assert_eq!(left.estimate(), right.estimate()); +} + #[test] fn test_empty() { let sketch = CpcSketch::new(11); @@ -45,6 +50,54 @@ fn test_one_value() { assert!(sketch.validate()); } +#[test] +fn test_scalar_integer_inputs_are_canonicalized() { + let mut i32_sketch = CpcSketch::new(11); + i32_sketch.update(42i32); + + let mut i64_sketch = CpcSketch::new(11); + i64_sketch.update(42i64); + + assert_same_sketch_state(&i32_sketch, &i64_sketch); +} + +#[test] +fn test_unsigned_narrow_integer_inputs_follow_cpp_signed_path() { + let mut u32_sketch = CpcSketch::new(11); + u32_sketch.update(u32::MAX); + + let mut signed_sketch = CpcSketch::new(11); + signed_sketch.update(-1i64); + + let mut u64_sketch = CpcSketch::new(11); + u64_sketch.update(u32::MAX as u64); + + assert_same_sketch_state(&u32_sketch, &signed_sketch); + assert_ne!(u32_sketch.serialize(), u64_sketch.serialize()); +} + +#[test] +fn test_string_hashes_as_raw_utf8_bytes() { + let mut string_sketch = CpcSketch::new(11); + string_sketch.update("hello"); + + let mut bytes_sketch = CpcSketch::new(11); + bytes_sketch.update("hello".as_bytes()); + + assert_same_sketch_state(&string_sketch, &bytes_sketch); +} + +#[test] +fn test_float_inputs_match_java_cpp_canonicalization_rules() { + let mut f64_sketch = CpcSketch::new(11); + f64_sketch.update(1.5f64); + + let mut f32_sketch = CpcSketch::new(11); + f32_sketch.update(1.5f32); + + assert_same_sketch_state(&f64_sketch, &f32_sketch); +} + #[test] fn test_many_values() { let mut sketch = CpcSketch::new(11); diff --git a/datasketches/tests/hll_update_test.rs b/datasketches/tests/hll_update_test.rs index e72c6fc..80abb70 100644 --- a/datasketches/tests/hll_update_test.rs +++ b/datasketches/tests/hll_update_test.rs @@ -19,6 +19,11 @@ use datasketches::common::NumStdDev; use datasketches::hll::HllSketch; use datasketches::hll::HllType; +fn assert_same_sketch_state(left: &HllSketch, right: &HllSketch) { + assert_eq!(left, right); + assert_eq!(left.serialize(), right.serialize()); +} + #[test] fn test_basic_update() { let mut sketch = HllSketch::new(12, HllType::Hll8); @@ -111,6 +116,54 @@ fn test_different_types() { assert!(estimate >= 5.0, "Should have at least 5 distinct values"); } +#[test] +fn test_scalar_integer_inputs_are_canonicalized() { + let mut i32_sketch = HllSketch::new(10, HllType::Hll8); + i32_sketch.update(42i32); + + let mut i64_sketch = HllSketch::new(10, HllType::Hll8); + i64_sketch.update(42i64); + + assert_same_sketch_state(&i32_sketch, &i64_sketch); +} + +#[test] +fn test_unsigned_narrow_integer_inputs_follow_cpp_signed_path() { + let mut u32_sketch = HllSketch::new(10, HllType::Hll8); + u32_sketch.update(u32::MAX); + + let mut signed_sketch = HllSketch::new(10, HllType::Hll8); + signed_sketch.update(-1i64); + + let mut u64_sketch = HllSketch::new(10, HllType::Hll8); + u64_sketch.update(u32::MAX as u64); + + assert_same_sketch_state(&u32_sketch, &signed_sketch); + assert_ne!(u32_sketch.serialize(), u64_sketch.serialize()); +} + +#[test] +fn test_string_hashes_as_raw_utf8_bytes() { + let mut string_sketch = HllSketch::new(10, HllType::Hll8); + string_sketch.update("hello"); + + let mut bytes_sketch = HllSketch::new(10, HllType::Hll8); + bytes_sketch.update("hello".as_bytes()); + + assert_same_sketch_state(&string_sketch, &bytes_sketch); +} + +#[test] +fn test_float_inputs_match_java_cpp_canonicalization_rules() { + let mut f64_sketch = HllSketch::new(10, HllType::Hll8); + f64_sketch.update(1.5f64); + + let mut f32_sketch = HllSketch::new(10, HllType::Hll8); + f32_sketch.update(1.5f32); + + assert_same_sketch_state(&f64_sketch, &f32_sketch); +} + #[test] fn test_hll4_type() { let mut sketch = HllSketch::new(12, HllType::Hll4); diff --git a/datasketches/tests/theta_sketch_test.rs b/datasketches/tests/theta_sketch_test.rs index 73045cd..3574855 100644 --- a/datasketches/tests/theta_sketch_test.rs +++ b/datasketches/tests/theta_sketch_test.rs @@ -39,16 +39,98 @@ fn test_update_various_types() { sketch.update("string"); sketch.update(42i64); sketch.update(42u64); - sketch.update_f64(3.15); - sketch.update_f64(3.15); - sketch.update_f32(3.15); - sketch.update_f32(3.15); + sketch.update(3.15f64); + sketch.update(3.15f64); + sketch.update(3.15f32); + sketch.update(3.15f32); sketch.update([1u8, 2, 3]); assert!(!sketch.is_empty()); assert_eq!(sketch.estimate(), 5.0); } +#[test] +fn test_scalar_integer_inputs_are_canonicalized() { + let mut i32_sketch = ThetaSketch::builder().build(); + i32_sketch.update(42i32); + + let mut i64_sketch = ThetaSketch::builder().build(); + i64_sketch.update(42i64); + + assert_eq!(i32_sketch.num_retained(), 1); + assert_eq!(i64_sketch.num_retained(), 1); + assert_eq!(i32_sketch.iter().next(), i64_sketch.iter().next()); +} + +#[test] +fn test_unsigned_narrow_integer_inputs_follow_cpp_signed_path() { + let mut u32_sketch = ThetaSketch::builder().build(); + u32_sketch.update(u32::MAX); + + let mut signed_sketch = ThetaSketch::builder().build(); + signed_sketch.update(-1i64); + + let mut u64_sketch = ThetaSketch::builder().build(); + u64_sketch.update(u32::MAX as u64); + + assert_eq!(u32_sketch.iter().next(), signed_sketch.iter().next()); + assert_ne!(u32_sketch.iter().next(), u64_sketch.iter().next()); +} + +#[test] +fn test_string_hashes_as_raw_utf8_bytes() { + let mut string_sketch = ThetaSketch::builder().build(); + string_sketch.update("hello"); + + let mut bytes_sketch = ThetaSketch::builder().build(); + bytes_sketch.update("hello".as_bytes()); + + assert_eq!(string_sketch.iter().next(), bytes_sketch.iter().next()); +} + +#[test] +fn test_byte_inputs_share_the_same_raw_bytes_hash_path() { + let mut array_sketch = ThetaSketch::builder().build(); + array_sketch.update([1u8, 2, 3]); + + let mut slice_sketch = ThetaSketch::builder().build(); + slice_sketch.update([1u8, 2, 3].as_slice()); + + let mut vec_sketch = ThetaSketch::builder().build(); + vec_sketch.update(vec![1u8, 2, 3]); + + assert_eq!(array_sketch.iter().next(), slice_sketch.iter().next()); + assert_eq!(slice_sketch.iter().next(), vec_sketch.iter().next()); +} + +#[test] +fn test_float_inputs_match_java_cpp_canonicalization_rules() { + let mut f64_sketch = ThetaSketch::builder().build(); + f64_sketch.update(1.5f64); + + let mut f32_sketch = ThetaSketch::builder().build(); + f32_sketch.update(1.5f32); + + assert_eq!(f64_sketch.iter().next(), f32_sketch.iter().next()); +} + +#[test] +fn test_default_integer_stream_matches_cpp_cardinality_estimate() { + let mut sketch = ThetaSketch::builder().build(); + let n = 15000; + + for i in 0..n { + sketch.update(i); + } + + assert!( + (sketch.estimate() - n as f64).abs() <= n as f64 * 0.01, + "estimate {} is not within 1% of {}", + sketch.estimate(), + n + ); +} + #[test] fn test_duplicate_updates() { let mut sketch = ThetaSketch::builder().lg_k(12).build();