blob: aa84101b77ab6007eac0aefc3388979b8a13403d [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::memory_pool::{ALIGNMENT, AlignedBuffer, AlignedBufferExt, memory_pool};
use bytes::Bytes;
use compio::buf::{IoBuf, IoBufMut, SetLen};
use std::{
mem::MaybeUninit,
ops::{Deref, DerefMut},
};
/// A buffer wrapper that participates in memory pooling.
///
/// This buffer automatically acquires memory from the global memory pool
/// and returns it when dropped. It also tracks resize events to keep
/// pool accounting accurate.
#[derive(Debug)]
pub struct PooledBuffer {
from_pool: bool,
original_capacity: usize,
original_bucket_idx: Option<usize>,
inner: AlignedBuffer,
}
impl Default for PooledBuffer {
fn default() -> Self {
Self::empty()
}
}
impl PooledBuffer {
/// Creates a new pooled buffer with the specified capacity.
///
/// # Arguments
///
/// * `capacity` - The capacity of the buffer
pub fn with_capacity(capacity: usize) -> Self {
let (buffer, was_pool_allocated) = memory_pool().acquire_buffer(capacity.max(ALIGNMENT));
let original_capacity = buffer.capacity();
let original_bucket_idx = if was_pool_allocated {
memory_pool().best_fit(original_capacity)
} else {
None
};
debug_assert_eq!(
buffer.as_ptr() as usize % ALIGNMENT,
0,
"PooledBuffer not aligned to {} bytes",
ALIGNMENT
);
Self {
from_pool: was_pool_allocated,
original_capacity,
original_bucket_idx,
inner: buffer,
}
}
/// Creates a new pooled buffer from an existing `AlignedBuffer`.
///
/// # Arguments
///
/// * `existing` - The existing `AlignedBuffer` buffer
pub fn from_existing(existing: AlignedBuffer) -> Self {
Self {
from_pool: false,
original_capacity: existing.capacity(),
original_bucket_idx: None,
inner: existing,
}
}
/// Creates an empty pooled buffer.
pub fn empty() -> Self {
Self {
from_pool: false,
original_capacity: 0,
original_bucket_idx: None,
inner: AlignedBuffer::new(ALIGNMENT),
}
}
/// Checks if the buffer needs to be resized and updates the memory pool accordingly.
/// This shall be called after operations that might cause a resize.
pub fn check_for_resize(&mut self) {
if !self.from_pool {
return;
}
let current_capacity = self.inner.capacity();
if current_capacity != self.original_capacity {
memory_pool().inc_resize_events();
if let Some(orig_idx) = self.original_bucket_idx {
memory_pool().dec_bucket_in_use(orig_idx);
if let Some(new_idx) = memory_pool().best_fit(current_capacity) {
// Track as a new allocation in the new bucket
memory_pool().inc_bucket_alloc(new_idx);
memory_pool().inc_bucket_in_use(new_idx);
self.original_bucket_idx = Some(new_idx);
} else {
// Track as an external allocation if no bucket fits
memory_pool().inc_external_allocations();
self.original_bucket_idx = None;
}
}
self.original_capacity = current_capacity;
}
}
/// Wrapper for reserve which might cause resize
pub fn reserve(&mut self, additional: usize) {
let before_cap = self.inner.capacity();
self.inner.reserve(additional);
if self.inner.capacity() != before_cap {
self.check_for_resize();
}
}
/// Split the buffer at given position, returning a new PooledBuffer
/// containing byte [0, at) and leaving [at, len)
///
/// # Panic
/// Panics if at > len
pub fn split_to(&mut self, at: usize) -> PooledBuffer {
assert!(
at <= self.len(),
"split_to out of bounds: at={}, len={}",
at,
self.len()
);
let mut new_buff = PooledBuffer::with_capacity(at);
new_buff.inner.extend_from_slice(&self.inner[..at]);
// SAFETY:
// - `self.inner.as_ptr().add(at)` is valid for `new_len` because
// `at + new_len === old_len <= cap`. Similar with `self.inner.as_mut_ptr()`
//
// - source range is `[at, at + new_len)` and the destination is
// `[0, new_len)`. These ranges do not overlap when `at > 0`.
// - when `at == 0`, the operation is noop
let new_len = self.len() - at;
if new_len > 0 {
unsafe {
// move [at..] to [0..]
std::ptr::copy(
self.inner.as_ptr().add(at),
self.inner.as_mut_ptr(),
new_len,
);
self.inner.set_len(new_len);
}
} else {
self.inner.clear();
}
new_buff
}
pub fn put<T: AsRef<[u8]>>(&mut self, src: T) {
self.extend_from_slice(src.as_ref());
}
/// Wrapper for extend_from_slice which might cause resize
pub fn extend_from_slice(&mut self, extend_from: &[u8]) {
let before_cap = self.inner.capacity();
self.inner.extend_from_slice(extend_from);
if self.inner.capacity() != before_cap {
self.check_for_resize();
}
}
/// Wrapper for put_bytes which might cause resize
pub fn put_bytes(&mut self, byte: u8, len: usize) {
let before_cap = self.inner.capacity();
let start = self.inner.len();
self.inner.resize(start + len, byte);
if self.inner.capacity() != before_cap {
self.check_for_resize();
}
}
/// Wrapper for put_slice which might cause resize
pub fn put_slice(&mut self, src: &[u8]) {
self.extend_from_slice(src);
// let before_cap = self.inner.capacity();
//
// if self.inner.capacity() != before_cap {
// self.check_for_resize();
// }
}
/// Wrapper for put_u32_le which might cause resize
pub fn put_u32_le(&mut self, value: u32) {
let before_cap = self.inner.capacity();
self.inner.extend_from_slice(&value.to_le_bytes());
if self.inner.capacity() != before_cap {
self.check_for_resize();
}
}
/// Wrapper for put_u64_le which might cause resize
pub fn put_u64_le(&mut self, value: u64) {
let before_cap = self.inner.capacity();
self.inner.extend_from_slice(&value.to_le_bytes());
if self.inner.capacity() != before_cap {
self.check_for_resize();
}
}
/// Returns the capacity of the inner buffer
pub fn capacity(&self) -> usize {
self.inner.capacity()
}
/// Returns the length of the inner buffer
pub fn len(&self) -> usize {
self.inner.len()
}
/// Returns true if the buffer is empty
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// Consumes the PooledBuffer and returns the inner AlignedBuffer.
/// Note: This bypasses pool return logic, use with caution.
pub fn into_inner(self) -> AlignedBuffer {
let mut this = std::mem::ManuallyDrop::new(self);
std::mem::replace(&mut this.inner, AlignedBuffer::new(ALIGNMENT))
}
/// Freezes the buffer, converting it to an immutable `Bytes`.
///
/// After calling this method, the PooledBuffer becomes empty and will not
/// return memory to the pool on drop (the frozen Bytes owns the allocation).
/// The returned `Bytes` is Arc-backed, allowing cheap clones.
pub fn freeze(&mut self) -> Bytes {
let buf = std::mem::replace(&mut self.inner, AlignedBuffer::new(ALIGNMENT));
// Update pool accounting
if self.from_pool
&& let Some(bucket_idx) = self.original_bucket_idx
{
memory_pool().dec_bucket_in_use(bucket_idx);
}
self.from_pool = false;
self.original_capacity = 0;
self.original_bucket_idx = None;
// Zero copy: Bytes takes ownership of the AlignedBuffer
// and will drop it when refcount reaches zero
Bytes::from_owner(buf)
}
}
impl Deref for PooledBuffer {
type Target = AlignedBuffer;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for PooledBuffer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl Drop for PooledBuffer {
fn drop(&mut self) {
if self.from_pool {
let buf = std::mem::replace(&mut self.inner, AlignedBuffer::new(ALIGNMENT));
buf.return_to_pool(self.original_capacity, true);
}
}
}
impl From<&[u8]> for PooledBuffer {
fn from(slice: &[u8]) -> Self {
let mut buf = PooledBuffer::with_capacity(slice.len());
buf.inner.extend_from_slice(slice);
buf
}
}
impl AsRef<[u8]> for PooledBuffer {
fn as_ref(&self) -> &[u8] {
&self.inner
}
}
impl From<AlignedBuffer> for PooledBuffer {
fn from(buffer: AlignedBuffer) -> Self {
Self::from_existing(buffer)
}
}
impl SetLen for PooledBuffer {
unsafe fn set_len(&mut self, len: usize) {
unsafe { self.inner.set_len(len) };
}
}
impl IoBuf for PooledBuffer {
fn as_init(&self) -> &[u8] {
&self.inner[..]
}
}
impl IoBufMut for PooledBuffer {
fn as_uninit(&mut self) -> &mut [MaybeUninit<u8>] {
let ptr = self.inner.as_mut_ptr().cast::<MaybeUninit<u8>>();
let cap = self.inner.capacity();
unsafe { std::slice::from_raw_parts_mut(ptr, cap) }
}
}
#[cfg(test)]
mod miri_tests {
//! Miri targets the 3 unsafe sites: `IoBufMut::as_uninit`
//! (`from_raw_parts_mut` ptr cast), `SetLen::set_len`, `split_to`
//! (`ptr::copy` forward-overlap + `set_len`).
//!
//! Pool-free helper avoids the global `MEMORY_POOL` (a `OnceCell` whose
//! `ArrayQueue` buckets leak under Miri) and `serial_test` (pulls
//! `sdd`/`scc` with int→ptr casts rejected by `-Zmiri-strict-provenance`).
//! `split_to` tests need the pool internally; gated `#[cfg(not(miri))]`.
use super::*;
use aligned_vec::{AVec, ConstAlign};
/// Build `PooledBuffer` with `from_pool == false` to skip the global pool.
fn pool_free_with_capacity(cap: usize) -> PooledBuffer {
let v: AVec<u8, ConstAlign<ALIGNMENT>> = AVec::with_capacity(ALIGNMENT, cap);
PooledBuffer::from_existing(v)
}
// IoBufMut::as_uninit
#[test]
fn as_uninit_returns_slice_of_full_capacity() {
let mut buf = pool_free_with_capacity(256);
let cap_before = buf.capacity();
let uninit = buf.as_uninit();
assert_eq!(
uninit.len(),
cap_before,
"as_uninit must expose the full capacity, not just initialized len",
);
}
#[test]
fn as_uninit_pointer_is_4096_aligned() {
let mut buf = pool_free_with_capacity(8192);
let addr = buf.as_uninit().as_mut_ptr() as usize;
assert_eq!(addr % ALIGNMENT, 0);
}
#[test]
fn as_uninit_write_then_set_len_observes_writes() {
let mut buf = pool_free_with_capacity(128);
{
let uninit = buf.as_uninit();
for (i, slot) in uninit.iter_mut().take(16).enumerate() {
slot.write(u8::try_from(i).unwrap());
}
}
// SAFETY: 16 bytes initialized above.
unsafe { <PooledBuffer as SetLen>::set_len(&mut buf, 16) };
assert_eq!(
buf.as_init(),
&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
);
}
// SetLen::set_len
#[test]
fn set_len_to_full_capacity_after_uninit_fill() {
// Fill entire capacity then `set_len(cap)`: Miri verifies every
// byte initialized before read via `as_init()`.
let mut buf = pool_free_with_capacity(256);
let cap = buf.capacity();
{
let uninit = buf.as_uninit();
assert_eq!(uninit.len(), cap);
for (i, slot) in uninit.iter_mut().enumerate() {
slot.write(u8::try_from(i & 0xff).unwrap());
}
}
// SAFETY: 0..cap initialized above.
unsafe { <PooledBuffer as SetLen>::set_len(&mut buf, cap) };
assert_eq!(buf.len(), cap);
assert_eq!(buf.as_init()[0], 0);
assert_eq!(
buf.as_init()[cap - 1],
u8::try_from((cap - 1) & 0xff).unwrap()
);
}
// split_to: `ptr::copy` (forward-overlap capable) + `set_len`.
// Needs global pool; skip Miri (ArrayQueue retention reads as leak,
// serial_test fails strict-provenance).
#[cfg(not(miri))]
mod split_to {
use super::*;
use crate::memory_pool::{MemoryPool, MemoryPoolConfigOther};
use iggy_common::IggyByteSize;
use serial_test::serial;
use std::str::FromStr;
use std::sync::Once;
static MIRI_POOL_INIT: Once = Once::new();
fn init_pool_for_split_to_tests() {
MIRI_POOL_INIT.call_once(|| {
let config = MemoryPoolConfigOther {
enabled: true,
size: IggyByteSize::from_str("64MiB").unwrap(),
bucket_capacity: 16,
};
MemoryPool::init_pool(&config);
});
}
#[test]
#[serial(memory_pool)]
fn basic_split() {
init_pool_for_split_to_tests();
let mut buf = pool_free_with_capacity(64);
buf.extend_from_slice(b"abcdefghij");
let prefix = buf.split_to(4);
assert_eq!(prefix.as_ref(), b"abcd");
assert_eq!(buf.as_ref(), b"efghij");
}
#[test]
#[serial(memory_pool)]
fn at_zero_yields_empty_prefix_and_unchanged_self() {
init_pool_for_split_to_tests();
let mut buf = pool_free_with_capacity(32);
buf.extend_from_slice(b"abcd");
let prefix = buf.split_to(0);
assert!(prefix.is_empty());
assert_eq!(buf.as_ref(), b"abcd");
}
#[test]
#[serial(memory_pool)]
fn at_len_yields_full_prefix_and_empty_self() {
init_pool_for_split_to_tests();
let mut buf = pool_free_with_capacity(32);
buf.extend_from_slice(b"abcd");
let prefix = buf.split_to(4);
assert_eq!(prefix.as_ref(), b"abcd");
assert!(buf.is_empty());
}
#[test]
#[serial(memory_pool)]
fn forward_overlap_preserves_bytes() {
// at=2, len=8: source [2..8) overlaps destination [0..6).
// `ptr::copy` handles overlap; `copy_nonoverlapping` would be UB.
init_pool_for_split_to_tests();
let mut buf = pool_free_with_capacity(32);
buf.extend_from_slice(b"ABCDEFGH");
let prefix = buf.split_to(2);
assert_eq!(prefix.as_ref(), b"AB");
assert_eq!(buf.as_ref(), b"CDEFGH");
}
}
// Drop short-circuit for `from_pool == false`. Miri leak detector enforces.
#[test]
fn pool_free_buffer_drop_does_not_leak() {
let mut buf = pool_free_with_capacity(256);
buf.extend_from_slice(&[0xaa; 200]);
drop(buf);
}
}