Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utils buffer sv2 docs #1232

Merged
merged 12 commits into from
Dec 11, 2024
382 changes: 224 additions & 158 deletions utils/buffer/README.md
plebhash marked this conversation as resolved.
Show resolved Hide resolved

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
plebhash marked this conversation as resolved.
Show resolved Hide resolved
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.
//
rrybarczyk marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be for _ in 0..8 ? Otherwise only 4 bytes are going to be written, but you defined a 8 bytes capacity. Maybe I'm missing something

Copy link
Collaborator

@plebhash plebhash Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is 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)

Copy link
Collaborator

@plebhash plebhash Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rrybarczyk it could be interesting to add this explanation above into the header comments

https://github.com/stratum-mining/stratum/pull/1232/files#r1865240236

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe you're adding the last 4 bytes here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
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.
rrybarczyk marked this conversation as resolved.
Show resolved Hide resolved
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
Loading