From 0331d06d3b7c3ba22093aa0637bcb27dda8ca708 Mon Sep 17 00:00:00 2001 From: Thomas Korrison Date: Thu, 19 Feb 2026 14:20:43 +0000 Subject: [PATCH 1/2] Enhance `FixedHistory` with iterator support and documentation improvements - Introduced `Iter` and `IntoIter` types for iterating over timestamps in most-recently-used (MRU) order, enhancing usability. - Updated documentation to include detailed operation tables with method references, improving clarity and navigation. - Added examples for the new iterator methods, demonstrating their usage in practical scenarios. - Refined `FixedHistory` struct to derive `Copy`, ensuring lightweight copies in concurrent contexts. - Implemented `PartialEq`, `Eq`, and `Hash` traits for `FixedHistory`, allowing logical content comparison and hashability. --- src/ds/fixed_history.rs | 481 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 474 insertions(+), 7 deletions(-) diff --git a/src/ds/fixed_history.rs b/src/ds/fixed_history.rs index 623c99c..fb444ab 100644 --- a/src/ds/fixed_history.rs +++ b/src/ds/fixed_history.rs @@ -50,15 +50,25 @@ //! ## Key Components //! //! - [`FixedHistory`]: Fixed-size ring buffer for timestamp history +//! - [`Iter`]: Borrowed iterator over timestamps in MRU order +//! - [`IntoIter`]: Owning iterator over timestamps in MRU order //! //! ## Operations //! -//! | Operation | Description | Complexity | -//! |-------------------|----------------------------------|------------| -//! | `record` | Add timestamp (overwrites oldest)| O(1) | -//! | `most_recent` | Get most recent timestamp | O(1) | -//! | `kth_most_recent` | Get k-th most recent timestamp | O(1) | -//! | `to_vec_mru` | Get all in MRU order | O(K) | +//! | Operation | Description | Complexity | +//! |-----------------------|----------------------------------|------------| +//! | [`record`] | Add timestamp (overwrites oldest)| O(1) | +//! | [`most_recent`] | Get most recent timestamp | O(1) | +//! | [`kth_most_recent`] | Get k-th most recent timestamp | O(1) | +//! | [`iter`] / [`into_iter`] | Iterate in MRU order | O(K) | +//! | [`to_vec_mru`] | Collect all into a Vec (MRU) | O(K) | +//! +//! [`record`]: FixedHistory::record +//! [`most_recent`]: FixedHistory::most_recent +//! [`kth_most_recent`]: FixedHistory::kth_most_recent +//! [`iter`]: FixedHistory::iter +//! [`into_iter`]: FixedHistory#impl-IntoIterator +//! [`to_vec_mru`]: FixedHistory::to_vec_mru //! //! ## Use Cases //! @@ -151,6 +161,9 @@ /// Stores timestamps in a circular buffer, automatically overwriting the oldest /// entry when full. Provides O(1) access to any of the last K timestamps. /// +/// Implements [`Clone`], [`Copy`], [`Debug`], [`PartialEq`], [`Eq`], [`Hash`], +/// and [`IntoIterator`]. See [`iter`](Self::iter) for borrowed iteration in MRU order. +/// /// # Type Parameters /// /// - `K`: Maximum number of timestamps to retain (const generic) @@ -195,7 +208,7 @@ /// /// assert_eq!(window_duration, 160); // 5 accesses over 160 time units /// ``` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct FixedHistory { data: [u64; K], len: usize, @@ -380,6 +393,30 @@ impl FixedHistory { .collect() } + /// Returns an iterator over recorded timestamps in MRU order (most recent first). + /// + /// Does **not** consume or modify the history. + /// + /// # Example + /// + /// ``` + /// use cachekit::ds::FixedHistory; + /// + /// let mut history = FixedHistory::<4>::new(); + /// history.record(10); + /// history.record(20); + /// history.record(30); + /// + /// let timestamps: Vec<_> = history.iter().collect(); + /// assert_eq!(timestamps, vec![30, 20, 10]); + /// ``` + pub fn iter(&self) -> Iter<'_, K> { + Iter { + history: self, + pos: 1, + } + } + /// Clears the history and resets cursor/length. /// /// # Example @@ -464,6 +501,131 @@ impl Default for FixedHistory { } } +// --------------------------------------------------------------------------- +// PartialEq, Eq, Hash — compare logical content, not the raw backing array +// (raw derive would flag stale slots as differences) +// --------------------------------------------------------------------------- + +impl PartialEq for FixedHistory { + fn eq(&self, other: &Self) -> bool { + if self.len != other.len { + return false; + } + for k in 1..=self.len { + if self.kth_most_recent(k) != other.kth_most_recent(k) { + return false; + } + } + true + } +} + +impl Eq for FixedHistory {} + +impl std::hash::Hash for FixedHistory { + fn hash(&self, state: &mut H) { + self.len.hash(state); + for k in 1..=self.len { + self.kth_most_recent(k).hash(state); + } + } +} + +// --------------------------------------------------------------------------- +// Iterator types (C-ITER-TY: names match the methods that produce them) +// --------------------------------------------------------------------------- + +/// Borrowed iterator over timestamps in a [`FixedHistory`], from most recent to oldest. +/// +/// Created by [`FixedHistory::iter`]. +#[derive(Debug, Clone)] +pub struct Iter<'a, const K: usize> { + history: &'a FixedHistory, + pos: usize, // 1-indexed: 1 = most recent, history.len() = oldest +} + +impl<'a, const K: usize> Iterator for Iter<'a, K> { + type Item = u64; + + fn next(&mut self) -> Option { + let val = self.history.kth_most_recent(self.pos)?; + self.pos += 1; + Some(val) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.history.len().saturating_sub(self.pos - 1); + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for Iter<'_, K> {} + +/// Owning iterator over timestamps in a [`FixedHistory`], from most recent to oldest. +/// +/// Created by calling [`IntoIterator::into_iter`] on a `FixedHistory`. +#[derive(Debug, Clone)] +pub struct IntoIter { + history: FixedHistory, + pos: usize, +} + +impl Iterator for IntoIter { + type Item = u64; + + fn next(&mut self) -> Option { + let val = self.history.kth_most_recent(self.pos)?; + self.pos += 1; + Some(val) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.history.len().saturating_sub(self.pos - 1); + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for IntoIter {} + +// --------------------------------------------------------------------------- +// IntoIterator impls (C-ITER: iter, into_iter) +// --------------------------------------------------------------------------- + +impl IntoIterator for FixedHistory { + type Item = u64; + type IntoIter = IntoIter; + + /// Consumes the history, returning an iterator over timestamps in MRU order. + /// + /// # Example + /// + /// ``` + /// use cachekit::ds::FixedHistory; + /// + /// let mut history = FixedHistory::<3>::new(); + /// history.record(10); + /// history.record(20); + /// + /// let timestamps: Vec<_> = history.into_iter().collect(); + /// assert_eq!(timestamps, vec![20, 10]); + /// ``` + fn into_iter(self) -> Self::IntoIter { + IntoIter { + history: self, + pos: 1, + } + } +} + +impl<'a, const K: usize> IntoIterator for &'a FixedHistory { + type Item = u64; + type IntoIter = Iter<'a, K>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + #[cfg(test)] mod tests { use super::*; @@ -545,6 +707,311 @@ mod tests { history.record(30); assert_eq!(history.debug_snapshot_mru(), vec![30, 20, 10]); } + + // ----------------------------------------------------------------------- + // iter() / IntoIterator tests + // ----------------------------------------------------------------------- + + #[test] + fn iter_yields_mru_order() { + let mut h = FixedHistory::<4>::new(); + h.record(10); + h.record(20); + h.record(30); + + let v: Vec<_> = h.iter().collect(); + assert_eq!(v, vec![30, 20, 10]); + } + + #[test] + fn iter_on_empty() { + let h = FixedHistory::<4>::new(); + assert_eq!(h.iter().count(), 0); + } + + #[test] + fn iter_on_zero_capacity() { + let h = FixedHistory::<0>::new(); + assert_eq!(h.iter().count(), 0); + } + + #[test] + fn iter_after_wrap() { + let mut h = FixedHistory::<3>::new(); + for t in 1..=6 { + h.record(t); + } + // Only last 3: 6, 5, 4 + let v: Vec<_> = h.iter().collect(); + assert_eq!(v, vec![6, 5, 4]); + } + + #[test] + fn iter_partially_filled() { + let mut h = FixedHistory::<5>::new(); + h.record(100); + h.record(200); + + let v: Vec<_> = h.iter().collect(); + assert_eq!(v, vec![200, 100]); + } + + #[test] + fn iter_count_matches_len() { + let mut h = FixedHistory::<4>::new(); + for t in [10, 20, 30, 40, 50] { + h.record(t); + assert_eq!(h.iter().count(), h.len()); + } + } + + #[test] + fn iter_exact_size() { + let mut h = FixedHistory::<3>::new(); + h.record(1); + h.record(2); + + let mut it = h.iter(); + assert_eq!(it.len(), 2); + it.next(); + assert_eq!(it.len(), 1); + it.next(); + assert_eq!(it.len(), 0); + assert!(it.next().is_none()); + } + + #[test] + fn iter_matches_to_vec_mru() { + let mut h = FixedHistory::<5>::new(); + for t in [10, 20, 30, 40, 50, 60] { + h.record(t); + } + let from_iter: Vec<_> = h.iter().collect(); + assert_eq!(from_iter, h.to_vec_mru()); + } + + #[test] + fn ref_into_iter_for_loop() { + let mut h = FixedHistory::<3>::new(); + h.record(10); + h.record(20); + + let mut sum = 0u64; + for t in &h { + sum += t; + } + assert_eq!(sum, 30); + assert_eq!(h.len(), 2); // not consumed + } + + #[test] + fn owned_into_iter_for_loop() { + let mut h = FixedHistory::<3>::new(); + h.record(10); + h.record(20); + h.record(30); + + let mut sum = 0u64; + for t in h { + sum += t; + } + assert_eq!(sum, 60); + } + + #[test] + fn into_iter_exact_size() { + let mut h = FixedHistory::<4>::new(); + h.record(1); + h.record(2); + h.record(3); + + let mut it = h.into_iter(); + assert_eq!(it.len(), 3); + it.next(); + assert_eq!(it.len(), 2); + } + + #[test] + fn into_iter_yields_mru_order() { + let mut h = FixedHistory::<3>::new(); + h.record(7); + h.record(8); + h.record(9); + + let v: Vec<_> = h.into_iter().collect(); + assert_eq!(v, vec![9, 8, 7]); + } + + #[test] + fn iter_after_clear() { + let mut h = FixedHistory::<3>::new(); + h.record(1); + h.record(2); + h.clear(); + + assert_eq!(h.iter().count(), 0); + assert_eq!(h.into_iter().count(), 0); + } + + // ----------------------------------------------------------------------- + // PartialEq / Eq tests + // ----------------------------------------------------------------------- + + #[test] + fn eq_same_entries_same_order() { + let mut a = FixedHistory::<3>::new(); + let mut b = FixedHistory::<3>::new(); + a.record(10); + a.record(20); + a.record(30); + b.record(10); + b.record(20); + b.record(30); + + assert_eq!(a, b); + } + + #[test] + fn eq_different_cursor_same_logical_content() { + // Same logical timestamps but different cursor positions due to wrapping + let mut a = FixedHistory::<3>::new(); + a.record(1); + a.record(2); + a.record(3); + + let mut b = FixedHistory::<3>::new(); + // Insert extra entries to advance cursor, but overwrite with same logical content + b.record(99); + b.record(1); + b.record(2); + b.record(3); + + assert_eq!(a, b); + } + + #[test] + fn ne_different_len() { + let mut a = FixedHistory::<3>::new(); + a.record(10); + let mut b = FixedHistory::<3>::new(); + b.record(10); + b.record(20); + + assert_ne!(a, b); + } + + #[test] + fn ne_different_values() { + let mut a = FixedHistory::<3>::new(); + a.record(10); + a.record(20); + let mut b = FixedHistory::<3>::new(); + b.record(10); + b.record(99); + + assert_ne!(a, b); + } + + #[test] + fn eq_empty_histories() { + let a = FixedHistory::<4>::new(); + let b = FixedHistory::<4>::new(); + assert_eq!(a, b); + } + + #[test] + fn eq_after_clear() { + let mut a = FixedHistory::<3>::new(); + a.record(1); + a.record(2); + a.record(3); + a.clear(); + + let b = FixedHistory::<3>::new(); + assert_eq!(a, b); + } + + // ----------------------------------------------------------------------- + // Hash tests + // ----------------------------------------------------------------------- + + #[test] + fn hash_equal_histories_same_hash() { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut a = FixedHistory::<3>::new(); + a.record(10); + a.record(20); + a.record(30); + + let mut b = FixedHistory::<3>::new(); + // Different cursor position, same logical content + b.record(99); + b.record(10); + b.record(20); + b.record(30); + + let hash_of = |h: &FixedHistory<3>| { + let mut s = DefaultHasher::new(); + h.hash(&mut s); + s.finish() + }; + + assert_eq!(hash_of(&a), hash_of(&b)); + } + + #[test] + fn hash_usable_in_hashmap() { + use std::collections::HashMap; + + let mut a = FixedHistory::<2>::new(); + a.record(1); + a.record(2); + + let mut b = FixedHistory::<2>::new(); + b.record(1); + b.record(2); + + let mut map = HashMap::new(); + map.insert(a, "entry"); + assert_eq!(map.get(&b), Some(&"entry")); + } + + // ----------------------------------------------------------------------- + // Copy tests + // ----------------------------------------------------------------------- + + #[test] + fn copy_produces_independent_value() { + let mut original = FixedHistory::<3>::new(); + original.record(10); + original.record(20); + + // Copy (implicit via binding) + let copy = original; + + // Mutating original after copy doesn't affect copy + original.record(99); + assert_eq!(copy.most_recent(), Some(20)); + assert_eq!(original.most_recent(), Some(99)); + } + + #[test] + fn copy_can_be_passed_by_value() { + fn sum_history(h: FixedHistory<3>) -> u64 { + h.iter().sum() + } + + let mut h = FixedHistory::<3>::new(); + h.record(10); + h.record(20); + h.record(30); + + // Call twice — possible only because FixedHistory is Copy + assert_eq!(sum_history(h), 60); + assert_eq!(sum_history(h), 60); + } } #[cfg(test)] From b3ca74adc1427da0dac811db2aedcee102e530b0 Mon Sep 17 00:00:00 2001 From: Thomas Korrison Date: Thu, 19 Feb 2026 14:30:00 +0000 Subject: [PATCH 2/2] Enhance `FrequencyBuckets` with iterator support and documentation improvements - Introduced `EntryIter` for iterating over all entries in `FrequencyBuckets`, yielding tracked entries in unspecified order. - Updated documentation to include method references in the operations table, improving clarity and navigation. - Added examples for the new `into_inner` method, demonstrating its usage in practical scenarios. - Refined iterator implementations in `SlotArena` to enhance usability and maintain performance efficiency. --- src/ds/frequency_buckets.rs | 84 ++++++++++++++++++++++++++++--------- src/ds/slot_arena.rs | 36 +++++++++++++--- 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/src/ds/frequency_buckets.rs b/src/ds/frequency_buckets.rs index 70d85dc..2db4117 100644 --- a/src/ds/frequency_buckets.rs +++ b/src/ds/frequency_buckets.rs @@ -66,18 +66,23 @@ //! - [`FrequencyBuckets`]: Single-threaded O(1) LFU tracker //! - [`ShardedFrequencyBuckets`]: Concurrent sharded variant //! - [`FrequencyBucketsHandle`]: Handle-based variant for interned keys +//! - [`EntryIter`]: Iterator over all entries; produced by [`FrequencyBuckets::iter_entries`] +//! - [`FrequencyBucketIdIter`]: Iterator over [`SlotId`]s in a bucket; produced by [`FrequencyBuckets::iter_bucket_ids`] +//! - [`FrequencyBucketEntryIter`]: Iterator over `(SlotId, meta)` pairs in a bucket; produced by [`FrequencyBuckets::iter_bucket_entries`] //! //! ## Operations //! //! | Operation | Time | Notes | //! |----------------|-------------|----------------------------------------| -//! | `insert` | O(1) | New key starts at freq=1 | -//! | `touch` | O(1) | Increment frequency, move to MRU | -//! | `remove` | O(1) | Remove from tracking | -//! | `pop_min` | O(1) | Evict LFU (FIFO tie-break) | -//! | `frequency` | O(1) | Query current frequency | -//! | `decay_halve` | O(n) | Halve all frequencies | -//! | `rebase_min_freq` | O(n) | Rebase so min becomes 1 | +//! | [`insert`](FrequencyBuckets::insert) | O(1) | New key starts at freq=1 | +//! | [`touch`](FrequencyBuckets::touch) | O(1) | Increment frequency, move to MRU | +//! | [`remove`](FrequencyBuckets::remove) | O(1) | Remove from tracking | +//! | [`pop_min`](FrequencyBuckets::pop_min) | O(1) | Evict LFU (FIFO tie-break) | +//! | [`frequency`](FrequencyBuckets::frequency) | O(1) | Query current frequency | +//! | [`decay_halve`](FrequencyBuckets::decay_halve) | O(n) | Halve all frequencies | +//! | [`rebase_min_freq`](FrequencyBuckets::rebase_min_freq) | O(n) | Rebase so min becomes 1 | +//! | [`iter_entries`](FrequencyBuckets::iter_entries) | O(n) | Iterate all entries | +//! | [`iter_bucket_ids`](FrequencyBuckets::iter_bucket_ids) | O(k) | Iterate bucket by frequency | //! //! ## Use Cases //! @@ -170,6 +175,7 @@ //! - FIFO within bucket: head=MRU, tail=LRU (evict from tail) //! - `min_freq` pointer enables O(1) eviction //! - `debug_validate_invariants()` available in debug/test builds +//! use rustc_hash::FxHashMap; use std::hash::Hash; @@ -718,6 +724,8 @@ where /// Returns an iterator over all `(SlotId, meta)` entries. /// + /// Yields every tracked entry in unspecified (arena) order. + /// /// # Example /// /// ``` @@ -736,17 +744,10 @@ where /// assert!(keys.contains(&"a")); /// assert!(keys.contains(&"b")); /// ``` - pub fn iter_entries(&self) -> impl Iterator)> { - self.entries.iter().map(|(id, entry)| { - ( - id, - FrequencyBucketEntryMeta { - key: &entry.key, - freq: entry.freq, - last_epoch: entry.last_epoch, - }, - ) - }) + pub fn iter_entries(&self) -> EntryIter<'_, K> { + EntryIter { + inner: self.entries.iter(), + } } /// Inserts a new key with frequency 1. @@ -2423,7 +2424,7 @@ where /// let entries: Vec<_> = freq.iter_entries().collect(); /// assert_eq!(entries.len(), 2); /// ``` - pub fn iter_entries(&self) -> impl Iterator)> { + pub fn iter_entries(&self) -> EntryIter<'_, H> { self.inner.iter_entries() } @@ -2593,6 +2594,24 @@ where self.inner.clear_shrink(); } + /// Consumes the wrapper and returns the inner [`FrequencyBuckets`]. + /// + /// # Example + /// + /// ``` + /// use cachekit::ds::{FrequencyBuckets, FrequencyBucketsHandle}; + /// + /// let mut freq = FrequencyBucketsHandle::new(); + /// freq.insert(1u64); + /// freq.insert(2u64); + /// + /// let inner: FrequencyBuckets = freq.into_inner(); + /// assert_eq!(inner.len(), 2); + /// ``` + pub fn into_inner(self) -> FrequencyBuckets { + self.inner + } + /// Returns an approximate memory footprint in bytes. /// /// # Example @@ -2634,6 +2653,7 @@ where /// /// Created by [`FrequencyBuckets::iter_bucket_ids`]. Yields entries from /// head (MRU) to tail (LRU) within a single frequency bucket. +#[derive(Debug)] pub struct FrequencyBucketIdIter<'a, K> { buckets: &'a FrequencyBuckets, current: Option, @@ -2655,6 +2675,7 @@ impl<'a, K> Iterator for FrequencyBucketIdIter<'a, K> { /// Created by [`FrequencyBuckets::iter_bucket_entries`]. Yields entries from /// head (MRU) to tail (LRU) within a single frequency bucket, including /// metadata like key, frequency, and last_epoch. +#[derive(Debug)] pub struct FrequencyBucketEntryIter<'a, K> { buckets: &'a FrequencyBuckets, current: Option, @@ -2678,6 +2699,31 @@ impl<'a, K> Iterator for FrequencyBucketEntryIter<'a, K> { } } +/// Iterator over all `(SlotId, meta)` entries in a [`FrequencyBuckets`]. +/// +/// Created by [`FrequencyBuckets::iter_entries`]. Yields every tracked entry +/// in unspecified (arena) order. +#[derive(Debug)] +pub struct EntryIter<'a, K> { + inner: crate::ds::slot_arena::Iter<'a, Entry>, +} + +impl<'a, K: 'a> Iterator for EntryIter<'a, K> { + type Item = (SlotId, FrequencyBucketEntryMeta<'a, K>); + + fn next(&mut self) -> Option { + let (id, entry) = self.inner.next()?; + Some(( + id, + FrequencyBucketEntryMeta { + key: &entry.key, + freq: entry.freq, + last_epoch: entry.last_epoch, + }, + )) + } +} + impl Default for FrequencyBuckets where K: Eq + Hash + Clone, diff --git a/src/ds/slot_arena.rs b/src/ds/slot_arena.rs index 04e5772..d97f479 100644 --- a/src/ds/slot_arena.rs +++ b/src/ds/slot_arena.rs @@ -554,11 +554,11 @@ impl SlotArena { /// let values: Vec<_> = arena.iter().map(|(_, v)| *v).collect(); /// assert_eq!(values, vec!["a", "b"]); /// ``` - pub fn iter(&self) -> impl Iterator { - self.slots - .iter() - .enumerate() - .filter_map(|(idx, slot)| slot.as_ref().map(|value| (SlotId(idx), value))) + pub fn iter(&self) -> Iter<'_, T> { + Iter { + slots: &self.slots, + index: 0, + } } /// Iterates over live [`SlotId`]s only. @@ -636,6 +636,32 @@ impl SlotArena { } } +/// Iterator over live `(SlotId, &T)` pairs in a [`SlotArena`]. +/// +/// Created by [`SlotArena::iter`]. Visits occupied slots in index order. +#[derive(Debug, Clone)] +pub struct Iter<'a, T> { + slots: &'a [Option], + index: usize, +} + +impl<'a, T> Iterator for Iter<'a, T> { + type Item = (SlotId, &'a T); + + fn next(&mut self) -> Option { + loop { + if self.index >= self.slots.len() { + return None; + } + let idx = self.index; + self.index += 1; + if let Some(value) = &self.slots[idx] { + return Some((SlotId(idx), value)); + } + } + } +} + #[cfg(any(test, debug_assertions))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SlotArenaSnapshot {