#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
use cas_kit::BlobStore;
use tempfile::TempDir;
static ALLOCATED_BYTES: AtomicUsize = AtomicUsize::new(0);
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
ALLOCATED_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) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
ALLOCATED_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
if new_size >= layout.size() {
ALLOCATED_BYTES.fetch_add(new_size - layout.size(), Ordering::Relaxed);
} else {
ALLOCATED_BYTES.fetch_sub(layout.size() - new_size, Ordering::Relaxed);
}
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
fn allocated_bytes() -> usize {
ALLOCATED_BYTES.load(Ordering::Relaxed)
}
fn allocations() -> usize {
ALLOCATIONS.load(Ordering::Relaxed)
}
fn gen_data(size: usize, seed: u64) -> Vec<u8> {
let mut buf = vec![0u8; size];
let mut state = seed;
for chunk in buf.chunks_mut(8) {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut word = state;
word = (word ^ (word >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
word = (word ^ (word >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
word ^= word >> 31;
for (i, byte) in chunk.iter_mut().enumerate() {
*byte = (word >> (i * 8)) as u8;
}
}
buf
}
#[test]
fn dedup_hit_allocation_claims() {
let dir = TempDir::new().unwrap();
let store = BlobStore::new(dir.path()).unwrap();
let blob_1mib = gen_data(1024 * 1024, 1);
let before = allocated_bytes();
store.put_blob(&blob_1mib).unwrap();
let cold_bytes = allocated_bytes() - before;
#[cfg(feature = "zstd")]
assert!(
cold_bytes >= 1024 * 1024,
"cold put of incompressible 1 MiB must allocate at least the payload \
(allocated {cold_bytes} bytes — counter sanity check)"
);
#[cfg(not(feature = "zstd"))]
assert!(
cold_bytes < 4096,
"raw cold put (no zstd) must not allocate content-proportionally \
(allocated {cold_bytes} bytes)"
);
let before = allocated_bytes();
let probe = vec![0u8; 1024 * 1024];
assert!(
allocated_bytes() - before >= 1024 * 1024,
"counting allocator must count allocations (counter sanity check)"
);
drop(probe);
let before = allocated_bytes();
let hits_before = allocations();
assert_eq!(
store.put_blob(&blob_1mib).unwrap(),
cas_kit::hash_bytes(&blob_1mib)
);
let hit_bytes = allocated_bytes() - before;
let hit_count = allocations() - hits_before;
assert!(
hit_bytes < 4096,
"dedup hit must not allocate content-proportionally \
(1 MiB blob hit allocated {hit_bytes} bytes)"
);
assert!(
hit_count < 32,
"dedup hit allocation count must be a small constant (got {hit_count})"
);
let blob_1kib = gen_data(1024, 2);
store.put_blob(&blob_1kib).unwrap();
let before = allocated_bytes();
store.put_blob(&blob_1mib).unwrap();
let hit_1mib = allocated_bytes() - before;
let before = allocated_bytes();
store.put_blob(&blob_1kib).unwrap();
let hit_1kib = allocated_bytes() - before;
assert!(
hit_1mib < 4096 && hit_1kib < 4096,
"hit-path allocation must be size-independent \
(1 KiB: {hit_1kib} B, 1 MiB: {hit_1mib} B)"
);
let corpus: Vec<Vec<u8>> = (0..100u64).map(|i| gen_data(16 * 1024, 100 + i)).collect();
for blob in &corpus {
store.put_blob(blob).unwrap();
}
let before = allocated_bytes();
for blob in &corpus {
store.put_blob(blob).unwrap();
}
let reingest_bytes = allocated_bytes() - before;
assert!(
reingest_bytes < 100 * 4096,
"full-corpus re-ingest (all hits) must stay in the constant-per-hit \
budget (allocated {reingest_bytes} bytes for 100 x 16 KiB)"
);
}