#![allow(clippy::unwrap_used, reason = "tests")]
#![allow(clippy::expect_used, reason = "tests")]
#![allow(clippy::missing_panics_doc, reason = "tests")]
#![allow(clippy::str_to_string, reason = "tests")]
#![allow(clippy::doc_markdown, reason = "tests")]
#![allow(clippy::uninlined_format_args, reason = "tests")]
#![allow(clippy::arithmetic_side_effects, reason = "tests")]
#![allow(
clippy::shadow_unrelated,
clippy::shadow_reuse,
reason = "test readability: `rows` reshadowed after construction"
)]
#![allow(
clippy::used_underscore_binding,
reason = "_force_promotion is deliberately named to signal intent"
)]
#![allow(
clippy::disallowed_types,
reason = "tests need std Mutex for global allocator gate"
)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::convert::Infallible;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use futures::TryStreamExt;
use mnesis::Version;
use mnesis_store::codec::{Decode, Encode};
use mnesis_store::envelope::PersistedEnvelope;
use thiserror::Error;
#[derive(Debug, Error)]
enum FoldErr {
#[error("stream")]
Stream(#[from] Infallible),
#[error("decode")]
Decode,
}
impl From<Utf8Err> for FoldErr {
fn from(_: Utf8Err) -> Self {
Self::Decode
}
}
struct CountingAllocator;
static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
static ALLOC_GATE: AtomicUsize = AtomicUsize::new(0);
static ALLOC_LOCK: Mutex<()> = Mutex::new(());
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if ALLOC_GATE.load(Ordering::Relaxed) > 0 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
}
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static GLOBAL: CountingAllocator = CountingAllocator;
async fn measure_async<R>(fut: impl std::future::Future<Output = R>) -> (R, usize, usize) {
ALLOC_COUNT.store(0, Ordering::SeqCst);
ALLOC_BYTES.store(0, Ordering::SeqCst);
ALLOC_GATE.fetch_add(1, Ordering::SeqCst);
let r = fut.await;
ALLOC_GATE.fetch_sub(1, Ordering::SeqCst);
let count = ALLOC_COUNT.load(Ordering::SeqCst);
let bytes = ALLOC_BYTES.load(Ordering::SeqCst);
(r, count, bytes)
}
#[derive(Clone)]
struct PrebuiltRow {
version: u64,
value: bytes::Bytes,
event_type_range: std::ops::Range<u32>,
payload_range: std::ops::Range<u32>,
}
struct VecStream {
rows: Vec<PrebuiltRow>,
pos: usize,
}
impl VecStream {
fn new(rows: Vec<(u64, String, Vec<u8>)>) -> Self {
let rows = rows
.into_iter()
.map(|(v, et, pl)| {
let mut buf = Vec::with_capacity(et.len() + pl.len());
buf.extend_from_slice(et.as_bytes());
buf.extend_from_slice(&pl);
let value = bytes::Bytes::from(buf);
let _force_promotion = value.clone();
drop(_force_promotion);
let et_end = u32::try_from(et.len()).expect("fits u32");
let pl_end = u32::try_from(et.len() + pl.len()).expect("fits u32");
PrebuiltRow {
version: v,
value,
event_type_range: 0..et_end,
payload_range: et_end..pl_end,
}
})
.collect();
Self { rows, pos: 0 }
}
}
impl futures::Stream for VecStream {
type Item = Result<PersistedEnvelope, Infallible>;
fn poll_next(
mut self: core::pin::Pin<&mut Self>,
_cx: &mut core::task::Context<'_>,
) -> core::task::Poll<Option<Self::Item>> {
if self.pos >= self.rows.len() {
return core::task::Poll::Ready(None);
}
let row = self.rows[self.pos].clone();
self.pos += 1;
let env = PersistedEnvelope::try_new(
Version::new(row.version).expect("non-zero"),
row.value,
mnesis_store::value::SchemaVersion::INITIAL,
row.event_type_range,
row.payload_range,
None,
)
.expect("test fixture");
core::task::Poll::Ready(Some(Ok(env)))
}
}
#[derive(Debug, Error)]
#[error("utf-8 decode failed")]
struct Utf8Err;
struct StringOwningCodec;
impl Encode<String> for StringOwningCodec {
type Error = Utf8Err;
fn encode(&self, value: &String) -> Result<bytes::Bytes, Self::Error> {
Ok(bytes::Bytes::copy_from_slice(value.as_bytes()))
}
}
impl Decode<String> for StringOwningCodec {
type Output<'a> = String;
type Error = Utf8Err;
fn decode<'a>(&'a self, env: &'a PersistedEnvelope) -> Result<String, Self::Error> {
std::str::from_utf8(env.payload())
.map(std::borrow::ToOwned::to_owned)
.map_err(|_| Utf8Err)
}
}
struct StrBorrowingCodec;
impl Encode<str> for StrBorrowingCodec {
type Error = Utf8Err;
fn encode(&self, value: &str) -> Result<bytes::Bytes, Self::Error> {
Ok(bytes::Bytes::copy_from_slice(value.as_bytes()))
}
}
impl Decode<str> for StrBorrowingCodec {
type Output<'a> = &'a str;
type Error = Utf8Err;
fn decode<'a>(&'a self, env: &'a PersistedEnvelope) -> Result<&'a str, Self::Error> {
std::str::from_utf8(env.payload()).map_err(|_| Utf8Err)
}
}
#[test]
fn borrowed_fold_is_constant_regardless_of_stream_length() {
let _guard = ALLOC_LOCK.lock().expect("alloc lock");
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("rt");
let payload = b"hello".to_vec();
let codec = StrBorrowingCodec;
let codec_ref = &codec;
let run = |n: u64| {
let rows: Vec<_> = (1..=n).map(|v| (v, "E".into(), payload.clone())).collect();
let stream = VecStream::new(rows);
rt.block_on(async {
let s = stream;
let fut = s
.map_err(FoldErr::from)
.try_fold(0usize, |acc, env| async move {
let bytes: &str = codec_ref.decode(&env)?;
Ok::<_, FoldErr>(acc + bytes.len())
});
let (r, c, b) = measure_async(fut).await;
(r.expect("fold ok"), c, b)
})
};
let _ = run(4);
let (_, c_small, b_small) = run(16);
let (_, c_large, b_large) = run(1024);
assert!(
b_large <= b_small.saturating_mul(4) + 4_096,
"borrowed fold allocations look linear in N: small={}B (n=16), large={}B (n=1024)",
b_small,
b_large,
);
assert!(
c_large <= c_small.saturating_mul(4) + 32,
"borrowed fold alloc count looks linear in N: small={} (n=16), large={} (n=1024)",
c_small,
c_large,
);
}
#[test]
fn owning_string_codec_scales_linearly_with_stream_length() {
let _guard = ALLOC_LOCK.lock().expect("alloc lock");
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("rt");
let payload = b"some text payload longer than sso".to_vec();
let codec = StringOwningCodec;
let codec_ref = &codec;
let run = |n: u64| {
let rows: Vec<_> = (1..=n).map(|v| (v, "E".into(), payload.clone())).collect();
let stream = VecStream::new(rows);
rt.block_on(async {
let s = stream;
let fut = s
.map_err(FoldErr::from)
.try_fold(0usize, |acc, env| async move {
let decoded: String = codec_ref.decode(&env)?;
Ok::<_, FoldErr>(acc + decoded.len())
});
let (r, c, b) = measure_async(fut).await;
(r.expect("fold ok"), c, b)
})
};
let _ = run(4);
let (_, c_small, b_small) = run(16);
let (_, c_large, b_large) = run(1024);
assert!(
c_large >= c_small + 900,
"owning codec did not scale linearly: small={} (n=16), large={} (n=1024)",
c_small,
c_large,
);
assert!(
b_large >= b_small + 900 * payload.len(),
"owning codec bytes did not scale: small={}B (n=16), large={}B (n=1024)",
b_small,
b_large,
);
}