Skip to content

Commit

Permalink
Implement HistoryBufferView on top of #486
Browse files Browse the repository at this point in the history
  • Loading branch information
sosthene-nitrokey committed Jul 1, 2024
1 parent 4002864 commit 2ae7e8b
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 78 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `StringView`, the `!Sized` version of `String`.
- Added `MpMcQueueView`, the `!Sized` version of `MpMcQueue`.
- Added `LinearMapView`, the `!Sized` version of `LinearMap`.
- Added `HistoryBufferView`, the `!Sized` version of `HistoryBuffer`.

### Changed

Expand Down
253 changes: 181 additions & 72 deletions src/histbuf.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,59 @@
//! A "history buffer", similar to a write-only ring buffer of fixed length.
//!
//! This buffer keeps a fixed number of elements. On write, the oldest element
//! is overwritten. Thus, the buffer is useful to keep a history of values with
//! some desired depth, and for example calculate a rolling average.
//!
//! # Examples
//! ```
//! use heapless::HistoryBuffer;
//!
//! // Initialize a new buffer with 8 elements.
//! let mut buf = HistoryBuffer::<_, 8>::new();
//!
//! // Starts with no data
//! assert_eq!(buf.recent(), None);
//!
//! buf.write(3);
//! buf.write(5);
//! buf.extend(&[4, 4]);
//!
//! // The most recent written element is a four.
//! assert_eq!(buf.recent(), Some(&4));
//!
//! // To access all elements in an unspecified order, use `as_slice()`.
//! for el in buf.as_slice() {
//! println!("{:?}", el);
//! }
//!
//! // Now we can prepare an average of all values, which comes out to 4.
//! let avg = buf.as_slice().iter().sum::<usize>() / buf.len();
//! assert_eq!(avg, 4);
//! ```

use core::borrow::Borrow;
use core::borrow::BorrowMut;
use core::fmt;
use core::marker::PhantomData;
use core::mem::MaybeUninit;
use core::ops::Deref;
use core::ptr;
use core::slice;

use crate::storage::OwnedStorage;
use crate::storage::Storage;
use crate::storage::ViewStorage;

/// Base struct for [`HistoryBuffer`] and [`HistoryBufferView`], generic over the [`Storage`].
///
/// In most cases you should use [`HistoryBuffer`] or [`HistoryBufferView`] directly. Only use this
/// struct if you want to write code that's generic over both.
pub struct HistoryBufferInner<T, S: Storage> {
write_at: usize,
filled: bool,
data: S::Buffer<MaybeUninit<T>>,
}

/// A "history buffer", similar to a write-only ring buffer of fixed length.
///
/// This buffer keeps a fixed number of elements. On write, the oldest element
Expand Down Expand Up @@ -36,11 +86,40 @@ use core::slice;
/// let avg = buf.as_slice().iter().sum::<usize>() / buf.len();
/// assert_eq!(avg, 4);
/// ```
pub struct HistoryBuffer<T, const N: usize> {
data: [MaybeUninit<T>; N],
write_at: usize,
filled: bool,
}
pub type HistoryBuffer<T, const N: usize> = HistoryBufferInner<T, OwnedStorage<N>>;

/// A "view" into a [`HistoryBuffer`]
///
/// Unlike [`HistoryBuffer`], it doesn't have the `const N: usize` in its type signature.
///
/// # Examples
/// ```
/// use heapless::HistoryBuffer;
///
/// // Initialize a new buffer with 8 elements.
/// let mut owned_buf = HistoryBuffer::<_, 8>::new();
/// let buf: &mut HistoryBufferView<_> = &mut owned_buf;
///
/// // Starts with no data
/// assert_eq!(buf.recent(), None);
///
/// buf.write(3);
/// buf.write(5);
/// buf.extend(&[4, 4]);
///
/// // The most recent written element is a four.
/// assert_eq!(buf.recent(), Some(&4));
///
/// // To access all elements in an unspecified order, use `as_slice()`.
/// for el in buf.as_slice() {
/// println!("{:?}", el);
/// }
///
/// // Now we can prepare an average of all values, which comes out to 4.
/// let avg = buf.as_slice().iter().sum::<usize>() / buf.len();
/// assert_eq!(avg, 4);
/// ```
pub type HistoryBufferView<T> = HistoryBufferInner<T, ViewStorage>;

impl<T, const N: usize> HistoryBuffer<T, N> {
const INIT: MaybeUninit<T> = MaybeUninit::uninit();
Expand Down Expand Up @@ -69,12 +148,6 @@ impl<T, const N: usize> HistoryBuffer<T, N> {
filled: false,
}
}

/// Clears the buffer, replacing every element with the default value of
/// type `T`.
pub fn clear(&mut self) {
*self = Self::new();
}
}

impl<T, const N: usize> HistoryBuffer<T, N>
Expand All @@ -101,19 +174,46 @@ where
filled: true,
}
}

}
impl<T: Copy, S: Storage> HistoryBufferInner<T, S> {
/// Clears the buffer, replacing every element with the given value.
pub fn clear_with(&mut self, t: T) {
*self = Self::new_with(t);
// SAFETY: we reset the values just after
unsafe { self.drop_contents() };
self.write_at = 0;
self.filled = true;

for d in self.data.borrow_mut() {
*d = MaybeUninit::new(t);
}
}
}

impl<T, const N: usize> HistoryBuffer<T, N> {
impl<T, S: Storage> HistoryBufferInner<T, S> {
/// Clears the buffer
pub fn clear(&mut self) {
// SAFETY: we reset the values just after
unsafe { self.drop_contents() };
self.write_at = 0;
self.filled = false;
}
}

impl<T, S: Storage> HistoryBufferInner<T, S> {
unsafe fn drop_contents(&mut self) {
unsafe {
ptr::drop_in_place(ptr::slice_from_raw_parts_mut(
self.data.borrow_mut().as_mut_ptr() as *mut T,
self.len(),
))
}
}

/// Returns the current fill level of the buffer.
#[inline]
pub fn len(&self) -> usize {
if self.filled {
N
self.capacity()
} else {
self.write_at
}
Expand All @@ -138,7 +238,7 @@ impl<T, const N: usize> HistoryBuffer<T, N> {
/// underlying backing array.
#[inline]
pub fn capacity(&self) -> usize {
N
self.data.borrow().len()
}

/// Returns whether the buffer is full
Expand All @@ -151,9 +251,9 @@ impl<T, const N: usize> HistoryBuffer<T, N> {
pub fn write(&mut self, t: T) {
if self.filled {
// Drop the old before we overwrite it.
unsafe { ptr::drop_in_place(self.data[self.write_at].as_mut_ptr()) }
unsafe { ptr::drop_in_place(self.data.borrow_mut()[self.write_at].as_mut_ptr()) }
}
self.data[self.write_at] = MaybeUninit::new(t);
self.data.borrow_mut()[self.write_at] = MaybeUninit::new(t);

self.write_at += 1;
if self.write_at == self.capacity() {
Expand Down Expand Up @@ -189,7 +289,7 @@ impl<T, const N: usize> HistoryBuffer<T, N> {
/// ```
pub fn recent(&self) -> Option<&T> {
self.recent_index()
.map(|i| unsafe { &*self.data[i].as_ptr() })
.map(|i| unsafe { &*self.data.borrow()[i].as_ptr() })
}

/// Returns index of the most recently written value in the underlying slice.
Expand Down Expand Up @@ -230,7 +330,7 @@ impl<T, const N: usize> HistoryBuffer<T, N> {
/// ```
pub fn oldest(&self) -> Option<&T> {
self.oldest_index()
.map(|i| unsafe { &*self.data[i].as_ptr() })
.map(|i| unsafe { &*self.data.borrow()[i].as_ptr() })
}

/// Returns index of the oldest value in the underlying slice.
Expand Down Expand Up @@ -258,7 +358,7 @@ impl<T, const N: usize> HistoryBuffer<T, N> {
/// Returns the array slice backing the buffer, without keeping track
/// of the write position. Therefore, the element order is unspecified.
pub fn as_slice(&self) -> &[T] {
unsafe { slice::from_raw_parts(self.data.as_ptr() as *const _, self.len()) }
unsafe { slice::from_raw_parts(self.data.borrow().as_ptr() as *const _, self.len()) }
}

/// Returns a pair of slices which contain, in order, the contents of the buffer.
Expand Down Expand Up @@ -298,20 +398,11 @@ impl<T, const N: usize> HistoryBuffer<T, N> {
/// assert_eq!(x, y)
/// }
/// ```
pub fn oldest_ordered(&self) -> OldestOrdered<'_, T, N> {
match (self.oldest_index(), self.recent_index()) {
(Some(oldest_index), Some(recent_index)) => OldestOrdered {
buf: self,
next: oldest_index,
back: recent_index,
done: false,
},
_ => OldestOrdered {
buf: self,
next: 0,
back: 0,
done: true,
},
pub fn oldest_ordered(&self) -> OldestOrderedInner<'_, T, S> {
let (old, new) = self.as_slices();
OldestOrderedInner {
phantom: PhantomData,
inner: old.iter().chain(new),
}
}
}
Expand Down Expand Up @@ -354,14 +445,9 @@ where
}
}

impl<T, const N: usize> Drop for HistoryBuffer<T, N> {
impl<T, S: Storage> Drop for HistoryBufferInner<T, S> {
fn drop(&mut self) {
unsafe {
ptr::drop_in_place(ptr::slice_from_raw_parts_mut(
self.data.as_mut_ptr() as *mut T,
self.len(),
))
}
unsafe { self.drop_contents() }
}
}

Expand Down Expand Up @@ -404,51 +490,74 @@ where
}
}

/// Base struct for [`OldestOrdered`] and [`OldestOrderedView`], generic over the [`Storage`].
///
/// In most cases you should use [`OldestOrdered`] or [`OldestOrderedView`] directly. Only use this
/// struct if you want to write code that's generic over both.
pub struct OldestOrderedInner<'a, T, S: Storage> {
phantom: PhantomData<S>,
inner: core::iter::Chain<core::slice::Iter<'a, T>, core::slice::Iter<'a, T>>,
}

/// Double ended iterator on the underlying buffer ordered from the oldest data
/// to the newest
#[derive(Clone)]
pub struct OldestOrdered<'a, T, const N: usize> {
buf: &'a HistoryBuffer<T, N>,
next: usize,
back: usize,
done: bool,
}
/// This type exists for backwards compatibility. It is always better to convert it to an [`OldestOrderedView`] with [`into_view`](OldestOrdered::into_view)
pub type OldestOrdered<'a, T, const N: usize> = OldestOrderedInner<'a, T, OwnedStorage<N>>;

impl<'a, T, const N: usize> Iterator for OldestOrdered<'a, T, N> {
type Item = &'a T;
/// Double ended iterator on the underlying buffer ordered from the oldest data
/// to the newest
pub type OldestOrderedView<'a, T> = OldestOrderedInner<'a, T, ViewStorage>;

fn next(&mut self) -> Option<&'a T> {
if self.done {
return None;
impl<'a, T, const N: usize> OldestOrdered<'a, T, N> {
/// Remove the `N` const-generic parameter from the iterator
///
/// For the opposite operation, see [`into_legacy_iter`](OldestOrderedView::into_legacy_iter)
pub fn into_view(self) -> OldestOrderedView<'a, T> {
OldestOrderedView {
phantom: PhantomData,
inner: self.inner,
}
}
}

if self.next == self.back {
self.done = true;
impl<'a, T> OldestOrderedView<'a, T> {
/// Add back the `N` const-generic parameter to use it with APIs expecting the legacy type
///
/// You probably do not need this
///
/// For the opposite operation, see [`into_view`](OldestOrdered::into_view)
pub fn into_legacy_iter<const N: usize>(self) -> OldestOrdered<'a, T, N> {
OldestOrdered {
phantom: PhantomData,
inner: self.inner,
}

let item = &self.buf[self.next];

self.next = if self.next == N - 1 { 0 } else { self.next + 1 };

Some(item)
}
}

impl<'a, T, const N: usize> DoubleEndedIterator for OldestOrdered<'a, T, N> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.done {
return None;
impl<'a, T, S: Storage> Clone for OldestOrderedInner<'a, T, S> {
fn clone(&self) -> Self {
Self {
phantom: PhantomData,
inner: self.inner.clone(),
}
}
}

if self.next == self.back {
self.done = true;
}
impl<'a, T, S: Storage> Iterator for OldestOrderedInner<'a, T, S> {
type Item = &'a T;

let item = &self.buf[self.back];
fn next(&mut self) -> Option<&'a T> {
self.inner.next()
}

self.back = if self.back == 0 { N - 1 } else { self.back - 1 };
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}

Some(item)
impl<'a, T, const N: usize> DoubleEndedIterator for OldestOrdered<'a, T, N> {
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub use vec::{Vec, VecView};
mod test_helpers;

mod deque;
mod histbuf;
pub mod histbuf;
mod indexmap;
mod indexset;
pub mod linear_map;
Expand Down
Loading

0 comments on commit 2ae7e8b

Please sign in to comment.