Skip to main content

cuckoo_clock/
data_block.rs

1use crate::{
2    config::{BitCount, CounterConfig, CuckooConfiguration, LruConfig, TtlConfig},
3    filter::CuckooFilter,
4};
5use std::{
6    borrow::Borrow,
7    hash::{BuildHasher, Hash},
8};
9use std::{
10    borrow::BorrowMut,
11    ops::{Range, RangeInclusive},
12};
13
14/// Represents a fingerprint of an item, stored in the [`crate::CuckooFilter`].
15///
16/// Generally, fingerprint access should not be needed for regular filter usage, but it can be
17/// useful to confirm if the evicted fingerprint matches a known item.
18///
19/// # Examples
20///
21/// ```ignore
22/// use cuckoo_clock::{CuckooFilter, Fingerprint, config::CuckooConfiguration};
23///
24/// let filter = CuckooFilter::new_random(CuckooConfiguration::builder(100_000).build()?);
25///
26/// // ... use filter
27///
28/// if let Some(evicted_fp: Fingerprint) = filter.insert("new_item") {
29///     if evicted_fp.matches_key("known_item", &filter) {
30///         println!("Known item was evicted!");
31///     }
32/// }
33/// # Ok::<(), Box<dyn std::error::Error>>(())
34/// ```
35#[derive(Debug, Hash, Clone, PartialEq, Eq)]
36pub struct Fingerprint {
37    data: u32,
38}
39
40impl Fingerprint {
41    pub(crate) const fn new(hash: u32, mask: u32) -> Self {
42        let mut fingerprint = hash & mask;
43        // Since fingerprint 0 represents and empty slot, we just shift it up by 1
44        // This will result in slightly higher collision rate
45        if fingerprint == 0 {
46            fingerprint = 1;
47        }
48
49        Self { data: fingerprint }
50    }
51
52    /// Returns true if this represents and empty fingerprint (an empty slot).
53    pub(crate) const fn is_empty(&self) -> bool {
54        self.data == 0
55    }
56
57    /// Returns the underlying data.
58    pub(crate) const fn data(&self) -> u32 {
59        self.data
60    }
61
62    /// Compares this fingerprint to a key. This will only produce correct results if the same
63    /// filter that produced this fingerprint is passed into this method.
64    ///
65    /// Returns true if this fingerprint matches the key.
66    pub fn matches_key<K: Hash + ?Sized, H: BuildHasher>(
67        &self,
68        key: &K,
69        filter: &CuckooFilter<H>,
70    ) -> bool {
71        filter.get_fingerprint(key) == *self
72    }
73}
74
75impl From<u32> for Fingerprint {
76    fn from(value: u32) -> Self {
77        Self { data: value }
78    }
79}
80
81/// Configuration for a field in a [`DataBlock`] (a single slot in a bucket).
82#[derive(Clone, Debug, Eq, PartialEq)]
83pub(crate) struct DataBlockFieldConfiguration {
84    /// Range of bytes that contain this field.
85    /// Not all bytes are actually exclusive to this field.
86    /// The specific bits that are used for this field are accessed using
87    /// [`DataBlockFieldConfiguration::mask`].
88    bytes: RangeInclusive<usize>,
89    /// Mask for this field. When applied to [`DataBlockFieldConfiguration::bytes`] range of bytes
90    /// from the data block, stored field can be retrieved.
91    // Mask has to be u64, because even though max value can fit in u32, it can spread over 4 bytes
92    // due to layout
93    mask: u64,
94    /// Since the field doesn't have to be aligned at byte, shift is used to correct the value
95    /// after masking.
96    ///
97    /// A simplified example using a shorter mask would be:
98    /// mask = 00111100
99    ///
100    /// In this case, shift = 2 and can be used after masking to move masked bits into correct
101    /// position.
102    shift: usize,
103    /// Mask for incoming values. Maximum bits is [`BitCount::MAX`] (32), so u32 is big enough.
104    /// This is used to mask incoming values, to ensure they don't overflow into other
105    /// fields when writing into the [`DataBlock`].
106    in_value_mask: u32,
107}
108
109impl DataBlockFieldConfiguration {
110    /// Creates a new [`DataBlockFieldConfiguration`] from the range of bits that it takes up. Bit
111    /// indexes start from the beginning of the [`DataBlock`], meaning from the start of the first
112    /// field (which will always be the [`Fingerprint`]). It is limited to
113    /// [`BitCount::MAX`]
114    pub(crate) fn new(bits: Range<usize>) -> Self {
115        // This should be handled at `BitCount` validation
116        debug_assert!(bits.len() <= BitCount::MAX.into());
117        let start_byte = bits.start / 8; // Round down to take the lower byte
118        let end_byte = (bits.end - 1) / 8;
119        let bytes = start_byte..=end_byte;
120        let shift = (end_byte + 1) * 8 - bits.end;
121        Self {
122            bytes,
123            shift,
124            mask: ((1u64 << bits.len()) - 1) << shift,
125            // u64 is used to prevent overflow when bits.len() == 32
126            // The final value will be at most u32::MAX, since bits.len() is limited to be <= 32
127            #[expect(clippy::cast_possible_truncation)]
128            in_value_mask: ((1u64 << bits.len()) - 1) as u32,
129        }
130    }
131
132    /// Mask for incoming values. Maximum bits is [`crate::config::BitCount::MAX`] (32), so u32 is
133    /// big enough. This is used to mask incoming values, to ensure they don't overflow into other
134    /// fields when writing into the [`DataBlock`].
135    pub(crate) const fn value_mask(&self) -> u32 {
136        self.in_value_mask
137    }
138}
139
140/// Simple wrapper for a slot in a bucket. Enables access to the fields stored in the slot.
141pub(crate) struct DataBlock<T: Borrow<[u8]>>(T);
142
143impl<'a> From<&'a mut [u8]> for DataBlock<&'a mut [u8]> {
144    fn from(value: &'a mut [u8]) -> Self {
145        Self(value)
146    }
147}
148
149impl<'a> From<&'a [u8]> for DataBlock<&'a [u8]> {
150    fn from(value: &'a [u8]) -> Self {
151        Self(value)
152    }
153}
154
155impl From<Vec<u8>> for DataBlock<Vec<u8>> {
156    fn from(value: Vec<u8>) -> Self {
157        Self(value)
158    }
159}
160
161impl<T: Borrow<[u8]>> DataBlock<T> {
162    /// Provides read-only access to underlying bytes.
163    pub(crate) fn inner(&self) -> &[u8] {
164        self.0.borrow()
165    }
166
167    /// Loads bits defined by the [`DataBlockFieldConfiguration`] and converts them to [`u32`].
168    #[expect(clippy::cast_possible_truncation)]
169    pub(crate) fn load_bits(&self, config: &DataBlockFieldConfiguration) -> u32 {
170        let loaded = &self.0.borrow()[config.bytes.clone()];
171        let mut loaded_u64 = 0;
172        let len = loaded.len();
173        for (i, b) in loaded.iter().enumerate() {
174            loaded_u64 += (*b as u64) << ((len - (i + 1)) * 8)
175        }
176        ((loaded_u64 & config.mask) >> config.shift) as u32
177    }
178
179    /// Loads fingerprint based on fingerprint configuration in the [`CuckooConfiguration`].
180    pub(crate) fn get_fingerprint(&self, configuration: &CuckooConfiguration) -> Fingerprint {
181        self.load_bits(&configuration.fingerprint_field_config)
182            .into()
183    }
184
185    /// Loads LRU based on the provided [`DataBlockFieldConfiguration`].
186    pub(crate) fn get_lru_counter(
187        &self,
188        configuration: &(LruConfig, DataBlockFieldConfiguration),
189    ) -> u32 {
190        self.load_bits(&configuration.1)
191    }
192
193    /// Loads generic counter based on the provided [`DataBlockFieldConfiguration`].
194    pub(crate) fn get_counter(
195        &self,
196        configuration: &(CounterConfig, DataBlockFieldConfiguration),
197    ) -> u32 {
198        self.load_bits(&configuration.1)
199    }
200
201    /// Loads TTL based on the provided [`DataBlockFieldConfiguration`].
202    pub(crate) fn get_ttl(&self, configuration: &(TtlConfig, DataBlockFieldConfiguration)) -> u32 {
203        self.load_bits(&configuration.1)
204    }
205
206    /// Returns true if this slot is occupied
207    pub(crate) fn occupied(&self, configuration: &CuckooConfiguration) -> bool {
208        !self.get_fingerprint(configuration).is_empty()
209    }
210}
211
212impl<T: BorrowMut<[u8]>> DataBlock<T> {
213    /// Stores the value as bits defined by the [`DataBlockFieldConfiguration`].
214    pub(crate) fn store_bits(&mut self, config: &DataBlockFieldConfiguration, value: u32) {
215        let masked_new_value = value & config.in_value_mask;
216        let loaded = &self.0.borrow()[config.bytes.clone()];
217        let len = loaded.len();
218        let mut loaded_u64 = 0;
219        for (i, b) in loaded.iter().enumerate() {
220            loaded_u64 += (*b as u64) << ((len - (i + 1)) * 8)
221        }
222        let masked_old_value = loaded_u64 & !config.mask;
223        let final_value = masked_old_value | ((masked_new_value as u64) << config.shift);
224        self.0.borrow_mut()[config.bytes.clone()]
225            .copy_from_slice(&final_value.to_be_bytes()[(8 - len)..]);
226    }
227
228    /// Stores the fingerprint based on fingerprint configuration in the [`CuckooConfiguration`].
229    pub(crate) fn store_fingerprint(
230        &mut self,
231        fingerprint: &Fingerprint,
232        configuration: &CuckooConfiguration,
233    ) {
234        self.store_bits(&configuration.fingerprint_field_config, fingerprint.data);
235    }
236
237    /// Resets this data block by setting all bits to 0.
238    pub(crate) fn reset(&mut self) {
239        self.0.borrow_mut().fill(0u8);
240    }
241
242    /// Swaps this data block with another data block.
243    ///
244    /// # Panics
245    ///
246    /// Panics if the data blocks have different lengths. Both data blocks should come from the
247    /// same filter.
248    pub(crate) fn swap<U: BorrowMut<[u8]>>(&mut self, other: &mut DataBlock<U>) {
249        self.0.borrow_mut().swap_with_slice(other.0.borrow_mut());
250    }
251
252    /// Copy data from another data block.
253    ///
254    /// # Panics
255    ///
256    /// Panics if the data blocks have different lengths. Both data blocks should come from the
257    /// same filter.
258    pub(crate) fn copy_from<U: Borrow<[u8]>>(&mut self, other: &DataBlock<U>) {
259        self.0.borrow_mut().copy_from_slice(other.0.borrow());
260    }
261
262    /// Merges data from another data block.
263    ///
264    /// # Panics
265    ///
266    /// Panics if the data blocks have different lengths. Both data blocks should come from the
267    /// same filter.
268    pub(crate) fn merge_associated_from<U: Borrow<[u8]>>(
269        &mut self,
270        other: &DataBlock<U>,
271        configuration: &CuckooConfiguration,
272    ) {
273        if let Some(lru) = &configuration.lru_field_config {
274            let counter = self.load_bits(&lru.1);
275            let other = other.load_bits(&lru.1);
276            let mut new_counter = counter.saturating_add(other);
277            // Value mask is also the max possible value
278            if new_counter > lru.1.value_mask() {
279                new_counter = lru.1.value_mask();
280            }
281            self.store_bits(&lru.1, new_counter);
282        }
283        if let Some(ttl) = &configuration.ttl_field_config {
284            let this_ttl = self.load_bits(&ttl.1);
285            let other = other.load_bits(&ttl.1);
286            self.store_bits(&ttl.1, this_ttl.max(other));
287        }
288        if let Some(counter_config) = &configuration.counter_field_config {
289            let counter = self.load_bits(&counter_config.1);
290            let other = other.load_bits(&counter_config.1);
291            let mut new_counter = counter.saturating_add(other);
292            // Value mask is also the max possible value
293            if new_counter > counter_config.1.value_mask() {
294                new_counter = counter_config.1.value_mask();
295            }
296            self.store_bits(&counter_config.1, new_counter);
297        }
298    }
299
300    /// Initializes the LRU counter, based on the provided [`DataBlockFieldConfiguration`].
301    pub(crate) fn init_lru_counter(
302        &mut self,
303        configuration: &(LruConfig, DataBlockFieldConfiguration),
304    ) {
305        let mut value = configuration.0.starting_value;
306        // Value mask is also the max possible value
307        if value > configuration.1.value_mask() {
308            value = configuration.1.value_mask();
309        }
310        self.store_bits(&configuration.1, value);
311    }
312
313    /// Increments the LRU counter, based on the provided [`DataBlockFieldConfiguration`].
314    pub(crate) fn inc_lru_counter(
315        &mut self,
316        configuration: &(LruConfig, DataBlockFieldConfiguration),
317    ) {
318        let counter = self.load_bits(&configuration.1);
319        let mut new_counter = counter.saturating_add(configuration.0.increment);
320        // Value mask is also the max possible value
321        if new_counter > configuration.1.value_mask() {
322            new_counter = configuration.1.value_mask();
323        }
324        self.store_bits(&configuration.1, new_counter);
325    }
326
327    /// Ages the LRU counter, based on the provided [`DataBlockFieldConfiguration`].
328    ///
329    /// Aging is done by halving the LRU counter value.
330    pub(crate) fn age_lru_counter(
331        &mut self,
332        configuration: &(LruConfig, DataBlockFieldConfiguration),
333    ) -> bool {
334        let counter = self.load_bits(&configuration.1);
335        if configuration.0.remove_on_zero && counter == 0 {
336            self.reset();
337            true
338        } else {
339            self.store_bits(
340                &configuration.1,
341                configuration.0.aging_strategy.age_value(counter),
342            );
343            false
344        }
345    }
346
347    /// Ages the TTL counter, based on the provided [`DataBlockFieldConfiguration`] and clears out
348    /// the data if it reaches 0.
349    ///
350    /// Aging is done by reducing the TTL counter by 1.
351    /// Returns true if this resulted in removal of this item.
352    pub(crate) fn age_ttl_counter(
353        &mut self,
354        configuration: &(TtlConfig, DataBlockFieldConfiguration),
355    ) -> bool {
356        let mut counter = self.load_bits(&configuration.1);
357        counter = counter.saturating_sub(1);
358        self.store_bits(&configuration.1, counter);
359        if counter == 0 {
360            self.reset();
361        }
362        counter == 0
363    }
364
365    /// Increments the generic counter, based on the provided [`DataBlockFieldConfiguration`], by
366    /// the provided value.
367    pub(crate) fn update_counter(
368        &mut self,
369        configuration: &(CounterConfig, DataBlockFieldConfiguration),
370        by: i32,
371    ) {
372        let counter = self.load_bits(&configuration.1);
373        let mut new_counter = counter.saturating_add_signed(by);
374        // Value mask is also the max possible value
375        if new_counter > configuration.1.value_mask() {
376            new_counter = configuration.1.value_mask();
377        }
378        self.store_bits(&configuration.1, new_counter);
379    }
380
381    /// Sets the TTL counter, based on the provided [`DataBlockFieldConfiguration`].
382    pub(crate) fn set_ttl(
383        &mut self,
384        configuration: &(TtlConfig, DataBlockFieldConfiguration),
385        ttl: u32,
386    ) {
387        self.store_bits(&configuration.1, ttl);
388    }
389}
390
391#[cfg(test)]
392#[expect(clippy::unwrap_used)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn test_load_store_for_each_bit_count() {
398        let mut data = [0u8; 4];
399        let mut data_block = DataBlock::from(&mut data[..]);
400        for i in 1usize..=32usize {
401            let field_config = DataBlockFieldConfiguration::new(0..i);
402            // Ensure we are using the max value possible for set bit count
403            let value: u32 = ((1u64 << i) - 1).try_into().unwrap();
404            data_block.reset();
405            data_block.store_bits(&field_config, value);
406            assert_eq!(
407                data_block.load_bits(&field_config),
408                value,
409                "Loaded value was different for bit count {i}"
410            );
411        }
412    }
413
414    #[test]
415    fn test_load_store_at_non_byte_boundary() {
416        // Let's assume we have 13 bit fingerprint and additional data after it
417        let data_start = 13;
418        let fp_config = DataBlockFieldConfiguration::new(0..data_start);
419        let fp_value = 0b1010101010101u32;
420        let mut data = [0u8; 7];
421        let mut data_block = DataBlock::from(&mut data[..]);
422        data_block.store_bits(&fp_config, fp_value);
423        for i in 1usize..=32usize {
424            let field_config = DataBlockFieldConfiguration::new(data_start..data_start + i);
425            // Ensure we are using the max value possible for set bit count
426            let value: u32 = ((1u64 << i) - 1).try_into().unwrap();
427            data_block.store_bits(&field_config, 0);
428            data_block.store_bits(&field_config, value);
429            assert_eq!(
430                data_block.load_bits(&field_config),
431                value,
432                "Loaded value was different for bit count {i}"
433            );
434        }
435        assert_eq!(
436            data_block.load_bits(&fp_config),
437            fp_value,
438            "Loads/stores wrote outside of their bits"
439        );
440    }
441
442    #[test]
443    fn test_load_store_full_config() {
444        let config = CuckooConfiguration::builder(100_000)
445            .fingerprint_bits(32.try_into().unwrap())
446            .with_lru(LruConfig::default())
447            .with_ttl(TtlConfig {
448                ttl: 10.try_into().unwrap(),
449                ttl_bits: 4.try_into().unwrap(),
450            })
451            .build()
452            .unwrap();
453        let mut data = [0u8; 6];
454        let mut data_block = DataBlock::from(&mut data[..]);
455        let fp_value = 0b1010101010101u32;
456
457        data_block.store_fingerprint(&Fingerprint { data: fp_value }, &config);
458        data_block.inc_lru_counter(config.lru_field_config.as_ref().unwrap());
459        data_block.set_ttl(config.ttl_field_config.as_ref().unwrap(), 10);
460
461        data_block.age_lru_counter(config.lru_field_config.as_ref().unwrap());
462        data_block.age_ttl_counter(config.ttl_field_config.as_ref().unwrap());
463
464        assert_eq!(data_block.get_fingerprint(&config).data(), fp_value);
465        assert_eq!(
466            data_block.get_ttl(config.ttl_field_config.as_ref().unwrap()),
467            9
468        );
469        assert_eq!(
470            data_block.get_lru_counter(config.lru_field_config.as_ref().unwrap()),
471            0
472        );
473    }
474
475    #[test]
476    fn test_inc_counter_saturation() {
477        let config = CuckooConfiguration::builder(100_000)
478            .fingerprint_bits(8.try_into().unwrap())
479            .with_lru(LruConfig {
480                counter_bits: 2.try_into().unwrap(),
481                ..Default::default()
482            })
483            .build()
484            .unwrap();
485
486        let mut data = [0u8; 2];
487        let mut data_block = DataBlock::from(&mut data[..]);
488
489        let lru_config = config.lru_field_config.as_ref().unwrap();
490
491        data_block.inc_lru_counter(lru_config);
492        data_block.inc_lru_counter(lru_config);
493        data_block.inc_lru_counter(lru_config);
494
495        assert_eq!(data_block.get_lru_counter(lru_config), 3);
496
497        // Since counter is limited at 2 bits, it shouldn't be able to go over 3
498        data_block.inc_lru_counter(lru_config);
499        assert_eq!(data_block.get_lru_counter(lru_config), 3);
500
501        data_block.age_lru_counter(lru_config);
502        assert_eq!(data_block.get_lru_counter(lru_config), 1);
503    }
504}