Skip to content

Commit

Permalink
Merge pull request #1232 from rrybarczyk/utils-buffer-sv2-docs
Browse files Browse the repository at this point in the history
Utils buffer sv2 docs
  • Loading branch information
GitGab19 authored Dec 11, 2024
2 parents 04caafb + 13ab37d commit 5bcd54d
Show file tree
Hide file tree
Showing 9 changed files with 1,079 additions and 178 deletions.
382 changes: 224 additions & 158 deletions utils/buffer/README.md

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions utils/buffer/examples/basic_buffer_pool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// # Simple `BufferPool` Usage
//
// This example showcases how to:
// 1. Creating a `BufferPool`.
// 2. Obtaining a writable buffer.
// 3. Writing data into the buffer.
// 4. Retrieving the data as a referenced slice.
// 5. Retrieving the data as an owned slice.
//
// # Run
//
// ```
// cargo run --example basic_buffer_pool
// ```

use buffer_sv2::{Buffer, BufferPool};

fn main() {
// Create a new BufferPool with a capacity of 32 bytes
let mut buffer_pool = BufferPool::new(32);

// Get a writable buffer from the pool
let data_to_write = b"Ciao, mundo!"; // 12 bytes
let writable = buffer_pool.get_writable(data_to_write.len());

// Write data (12 bytes) into the buffer.
writable.copy_from_slice(data_to_write);
assert_eq!(buffer_pool.len(), 12);

// Retrieve the data as a referenced slice
let _data_slice = buffer_pool.get_data_by_ref(12);
assert_eq!(buffer_pool.len(), 12);

// Retrieve the data as an owned slice
let data_slice = buffer_pool.get_data_owned();
assert_eq!(buffer_pool.len(), 0);

let expect = [67, 105, 97, 111, 44, 32, 109, 117, 110, 100, 111, 33]; // "Ciao, mundo!" ASCII
assert_eq!(data_slice.as_ref(), expect);
}
80 changes: 80 additions & 0 deletions utils/buffer/examples/buffer_pool_exhaustion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// # Handling Buffer Pool Exhaustion and Heap Allocation
//
// This example demonstrates how a buffer pool is filled. The back slots of the buffer pool are
// exhausted first, followed by the front of the buffer pool. Once both the back and front are
// exhausted, data is allocated on the heap at a performance decrease.
//
// 1. Fills up the back slots of the buffer pool until they’re exhausted.
// 2. Releases one slot to allow the buffer pool to switch to front mode.
// 3. Fully fills the front slots of the buffer pool.
// 4. Switches to alloc mode for direct heap allocation when both the buffer pool's back and front
// slots are at capacity.
//
// Below is a visual representation of how the buffer pool evolves as the example progresses:
//
// -------- BACK MODE
// a------- BACK MODE (add "a" via loop)
// aa------ BACK MODE (add "a" via loop)
// aaa----- BACK MODE (add "a" via loop)
// aaaa---- BACK MODE (add "a" via loop)
// -aaa---- BACK MODE (pop front)
// -aaab--- BACK MODE (add "b")
// -aaabc-- BACK MODE (add "c" via loop)
// -aaabcc- BACK MODE (add "c" via loop)
// -aaabccc BACK MODE (add "c" via loop)
// caaabccc BACK MODE (add "c" via loop, which gets added via front mode)
// caaabccc ALLOC MODE (add "d", allocated in a new space in the heap)
//
// # Run
//
// ```
// cargo run --example buffer_pool_exhaustion
// ```

use buffer_sv2::{Buffer, BufferPool};
use std::collections::VecDeque;

fn main() {
// 8 byte capacity
let mut buffer_pool = BufferPool::new(8);
let mut slices = VecDeque::new();

// Write data to fill back slots
for _ in 0..4 {
let data_bytes = b"a"; // 1 byte
let writable = buffer_pool.get_writable(data_bytes.len()); // Mutable slice to internal
// buffer
writable.copy_from_slice(data_bytes);
let data_slice = buffer_pool.get_data_owned(); // Take ownership of allocated segment
slices.push_back(data_slice);
}
assert!(buffer_pool.is_back_mode());

// Release one slice and add another in the back (one slice in back mode must be free to switch
// to front mode)
slices.pop_front(); // Free the slice's associated segment in the buffer pool
let data_bytes = b"b"; // 1 byte
let writable = buffer_pool.get_writable(data_bytes.len());
writable.copy_from_slice(data_bytes);
let data_slice = buffer_pool.get_data_owned();
slices.push_back(data_slice);
assert!(buffer_pool.is_back_mode()); // Still in back mode

// Write data to switch to front mode
for _ in 0..4 {
let data_bytes = b"c"; // 1 byte
let writable = buffer_pool.get_writable(data_bytes.len());
writable.copy_from_slice(data_bytes);
let data_slice = buffer_pool.get_data_owned();
slices.push_back(data_slice);
}
assert!(buffer_pool.is_front_mode()); // Confirm front mode

// Add another slice, causing a switch to alloc mode
let data_bytes = b"d"; // 1 byte
let writable = buffer_pool.get_writable(data_bytes.len());
writable.copy_from_slice(data_bytes);
let data_slice = buffer_pool.get_data_owned();
slices.push_back(data_slice);
assert!(buffer_pool.is_alloc_mode());
}
58 changes: 58 additions & 0 deletions utils/buffer/examples/variable_sized_messages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// # Handling Variable-Sized Messages
//
// This example demonstrates how to the `BufferPool` handles messages of varying sizes.
//
// # Run
//
// ```
// cargo run --example variable_sized_messages
// ```

use buffer_sv2::{Buffer, BufferPool};
use std::collections::VecDeque;

fn main() {
// Initialize a BufferPool with a capacity of 32 bytes
let mut buffer_pool = BufferPool::new(32);
let mut slices = VecDeque::new();

// Function to write data to the buffer pool and store the slice
let write_data = |pool: &mut BufferPool<_>, data: &[u8], slices: &mut VecDeque<_>| {
let writable = pool.get_writable(data.len());
writable.copy_from_slice(data);
let data_slice = pool.get_data_owned();
slices.push_back(data_slice);
println!("{:?}", &pool);
println!("");
};

// Write a small message to the first slot
let small_message = b"Hello";
write_data(&mut buffer_pool, small_message, &mut slices);
assert!(buffer_pool.is_back_mode());
assert_eq!(slices.back().unwrap().as_ref(), small_message);

// Write a medium-sized message to the second slot
let medium_message = b"Rust programming";
write_data(&mut buffer_pool, medium_message, &mut slices);
assert!(buffer_pool.is_back_mode());
assert_eq!(slices.back().unwrap().as_ref(), medium_message);

// Write a large message that exceeds the remaining pool capacity
let large_message = b"This message is larger than the remaining buffer pool capacity.";
write_data(&mut buffer_pool, large_message, &mut slices);
assert!(buffer_pool.is_alloc_mode());
assert_eq!(slices.back().unwrap().as_ref(), large_message);

while let Some(slice) = slices.pop_front() {
drop(slice);
}

// Write another small message
let another_small_message = b"Hi";
write_data(&mut buffer_pool, another_small_message, &mut slices);
assert_eq!(slices.back().unwrap().as_ref(), another_small_message);

// Verify that the buffer pool has returned to back mode for the last write
assert!(buffer_pool.is_back_mode());
}
69 changes: 68 additions & 1 deletion utils/buffer/src/buffer.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
// # Buffer from System Memory
//
// Provides memory management for encoding and transmitting message frames between Sv2 roles when
// buffer pools have been exhausted.
//
// `BufferFromSystemMemory` serves as a fallback when a `BufferPool` is full or unable to allocate
// memory fast enough. Instead of relying on pre-allocated memory, it dynamically allocates memory
// on the heap using a `Vec<u8>`, ensuring that message frames can still be processed.
//
// This fallback mechanism allows the buffer to resize dynamically based on data needs, making it
// suitable for scenarios where message sizes vary. However, it introduces performance trade-offs
// such as slower allocation, increased memory fragmentation, and higher system overhead compared
// to using pre-allocated buffers.

use crate::Buffer;
use aes_gcm::aead::Buffer as AeadBuffer;
use alloc::vec::Vec;

/// Manages a dynamically growing buffer in system memory using an internal [`Vec<u8>`].
///
/// Operates on a dynamically sized buffer and provides utilities for writing, reading, and
/// manipulating data. It tracks the current position where data is written, and resizes the buffer
/// as needed.
#[derive(Debug)]
pub struct BufferFromSystemMemory {
// Underlying buffer storing the data.
inner: Vec<u8>,

// Current cursor indicating where the next byte should be written.
cursor: usize,

// Starting index for the buffer. Useful for scenarios where part of the buffer is skipped or
// invalid.
start: usize,
}

impl BufferFromSystemMemory {
/// Creates a new buffer with no initial data.
pub fn new(_: usize) -> Self {
Self {
inner: Vec::new(),
Expand All @@ -20,6 +46,7 @@ impl BufferFromSystemMemory {
}

impl Default for BufferFromSystemMemory {
// Creates a new buffer with no initial data.
fn default() -> Self {
Self::new(0)
}
Expand All @@ -28,57 +55,81 @@ impl Default for BufferFromSystemMemory {
impl Buffer for BufferFromSystemMemory {
type Slice = Vec<u8>;

// Dynamically allocates or resizes the internal `Vec<u8>` to ensure there is enough space for
// writing.
#[inline]
fn get_writable(&mut self, len: usize) -> &mut [u8] {
let cursor = self.cursor;

// Reserve space in the buffer for writing based on the requested `len`
let len = self.cursor + len;

// If the internal buffer is not large enough to hold the new data, resize it
if len > self.inner.len() {
self.inner.resize(len, 0)
};

self.cursor = len;

// Portion of the buffer where data can be written
&mut self.inner[cursor..len]
}

// Splits off the written portion of the buffer, returning it as a new `Vec<u8>`. Swaps the
// internal buffer with a newly allocated empty one, effectively returning ownership of the
// written data while resetting the internal buffer for future use.
#[inline]
fn get_data_owned(&mut self) -> Vec<u8> {
// Split the internal buffer at the cursor position
let mut tail = self.inner.split_off(self.cursor);

// Swap the data after the cursor (tail) with the remaining buffer
core::mem::swap(&mut tail, &mut self.inner);

// Move ownership of the buffer content up to the cursor, resetting the internal buffer
// state for future writes
let head = tail;
self.cursor = 0;
head
}

// Returns a mutable reference to the written portion of the internal buffer that has been
// filled up with data, up to the specified length (`len`).
#[inline]
fn get_data_by_ref(&mut self, len: usize) -> &mut [u8] {
&mut self.inner[..usize::min(len, self.cursor)]
}

// Returns an immutable reference to the written portion of the internal buffer that has been
// filled up with data, up to the specified length (`len`).
#[inline]
fn get_data_by_ref_(&self, len: usize) -> &[u8] {
&self.inner[..usize::min(len, self.cursor)]
}

// Returns the current write position (cursor) in the buffer, representing how much of the
// internal buffer has been filled with data.
#[inline]
fn len(&self) -> usize {
self.cursor
}

// Sets the start index for the buffer, adjusting where reads and writes begin. Used to discard
// part of the buffer by adjusting the starting point for future operations.
#[inline]
fn danger_set_start(&mut self, index: usize) {
self.start = index;
}

// Indicates that the buffer is always safe to drop, as `Vec<u8>` manages memory internally.
#[inline]
fn is_droppable(&self) -> bool {
true
}
}

// Used to test if `BufferPool` tries to allocate from system memory.
#[cfg(test)]
// Used to test if BufferPool try to allocate from system memory
pub struct TestBufferFromMemory(pub Vec<u8>);

#[cfg(test)]
Expand All @@ -96,39 +147,55 @@ impl Buffer for TestBufferFromMemory {
fn get_data_by_ref(&mut self, _len: usize) -> &mut [u8] {
&mut self.0[0..0]
}

fn get_data_by_ref_(&self, _len: usize) -> &[u8] {
&self.0[0..0]
}

fn len(&self) -> usize {
0
}

fn danger_set_start(&mut self, _index: usize) {
todo!()
}

fn is_droppable(&self) -> bool {
true
}
}

impl AsRef<[u8]> for BufferFromSystemMemory {
/// Returns a reference to the internal buffer as a byte slice, starting from the specified
/// `start` index. Provides an immutable view into the buffer's contents, allowing it to be
/// used as a regular slice for reading.
fn as_ref(&self) -> &[u8] {
let start = self.start;
&self.get_data_by_ref_(Buffer::len(self))[start..]
}
}

impl AsMut<[u8]> for BufferFromSystemMemory {
/// Returns a mutable reference to the internal buffer as a byte slice, starting from the
/// specified `start` index. Allows direct modification of the buffer's contents, while
/// restricting access to the data after the `start` index.
fn as_mut(&mut self) -> &mut [u8] {
let start = self.start;
self.get_data_by_ref(Buffer::len(self))[start..].as_mut()
}
}

impl AeadBuffer for BufferFromSystemMemory {
/// Extends the internal buffer by appending the given byte slice. Dynamically resizes the
/// internal buffer to accommodate the new data and copies the contents of `other` into it.
fn extend_from_slice(&mut self, other: &[u8]) -> aes_gcm::aead::Result<()> {
self.get_writable(other.len()).copy_from_slice(other);
Ok(())
}

/// Truncates the internal buffer to the specified length, adjusting for the `start` index.
/// Resets the buffer cursor to reflect the new size, effectively discarding any data beyond
/// the truncated length.
fn truncate(&mut self, len: usize) {
let len = len + self.start;
self.cursor = len;
Expand Down
Loading

0 comments on commit 5bcd54d

Please sign in to comment.