use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use axum::body::Body;
use axum::http::{Request, StatusCode};
use bytes::Bytes;
use futures::{StreamExt, stream};
use sui_cache::config::{BackendConfig, CacheConfig};
use sui_cache::{AppState, LocalStorage, StorageBackend, TieredBackend, build_router};
use tower::ServiceExt;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
struct CountingAlloc;
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc(layout) };
if !p.is_null() {
note_alloc(layout.size());
}
p
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
LIVE.fetch_sub(layout.size(), Ordering::Relaxed);
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let p = unsafe { System.realloc(ptr, layout, new_size) };
if !p.is_null() {
LIVE.fetch_sub(layout.size(), Ordering::Relaxed);
note_alloc(new_size);
}
p
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc_zeroed(layout) };
if !p.is_null() {
note_alloc(layout.size());
}
p
}
}
fn note_alloc(size: usize) {
let live = LIVE.fetch_add(size, Ordering::Relaxed) + size;
PEAK.fetch_max(live, Ordering::Relaxed);
}
#[global_allocator]
static ALLOC: CountingAlloc = CountingAlloc;
async fn peak_growth<F, Fut, T>(f: F) -> (usize, T)
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = T>,
{
let base = LIVE.load(Ordering::Relaxed);
PEAK.store(base, Ordering::Relaxed);
let out = f().await;
let peak = PEAK.load(Ordering::Relaxed);
(peak.saturating_sub(base), out)
}
const NAR_BYTES: usize = 256 * 1024 * 1024;
const FRAME_BYTES: usize = 64 * 1024;
const PEAK_BUDGET: usize = 64 * 1024 * 1024;
fn synthetic_nar(total: usize) -> impl futures::Stream<Item = Result<Bytes, std::io::Error>> {
stream::unfold(0usize, move |sent| async move {
if sent >= total {
return None;
}
let n = (total - sent).min(FRAME_BYTES);
let fill = (sent / FRAME_BYTES % 251) as u8;
Some((Ok(Bytes::from(vec![fill; n])), sent + n))
})
}
fn fold_checksum(acc: u64, chunk: &[u8]) -> u64 {
chunk.iter().fold(acc, |a, b| {
a.wrapping_mul(1_000_003).wrapping_add(u64::from(*b))
})
}
fn expected_checksum(total: usize) -> u64 {
let mut acc = 0u64;
let mut sent = 0usize;
while sent < total {
let n = (total - sent).min(FRAME_BYTES);
let fill = (sent / FRAME_BYTES % 251) as u8;
for _ in 0..n {
acc = acc.wrapping_mul(1_000_003).wrapping_add(u64::from(fill));
}
sent += n;
}
acc
}
fn tiered_router(root: &std::path::Path) -> axum::Router {
let l1: Arc<dyn StorageBackend> = Arc::new(LocalStorage::new(root.join("l1")));
let l2: Arc<dyn StorageBackend> = Arc::new(LocalStorage::new(root.join("l2")));
let l3: Arc<dyn StorageBackend> = Arc::new(LocalStorage::new(root.join("l3")));
let storage: Arc<dyn StorageBackend> = Arc::new(TieredBackend::new(l1, l2, l3));
let config = CacheConfig {
listen: "127.0.0.1:0".to_string(),
backend: BackendConfig::Local {
path: root.to_path_buf(),
},
priority: 40,
want_mass_query: true,
store_dir: "/nix/store".to_string(),
signing_key: None,
require_sigs: false,
..CacheConfig::default()
};
build_router(AppState {
storage,
config,
signer: None,
})
}
#[tokio::test(flavor = "current_thread")]
async fn a_large_nar_moves_through_the_cache_without_becoming_resident() {
let (control_peak, control_len) = peak_growth(|| async {
let mut s = Box::pin(synthetic_nar(NAR_BYTES));
let mut buf: Vec<u8> = Vec::new();
while let Some(c) = s.next().await {
buf.extend_from_slice(&c.unwrap());
}
buf.len()
})
.await;
assert_eq!(control_len, NAR_BYTES);
assert!(
control_peak > PEAK_BUDGET,
"POSITIVE CONTROL FAILED: collecting {NAR_BYTES} bytes registered a peak of \
{control_peak}, under the {PEAK_BUDGET}-byte budget. The allocator meter is not \
measuring what it claims to, so the gate below proves nothing. Fix the meter \
before trusting any result from this file.",
);
let dir = tempfile::tempdir().unwrap();
let app = tiered_router(dir.path());
let (write_peak, status) = peak_growth(|| async {
let req = Request::builder()
.method("PUT")
.uri("/nar/big.nar.xz")
.body(Body::from_stream(synthetic_nar(NAR_BYTES)))
.unwrap();
app.clone().oneshot(req).await.unwrap().status()
})
.await;
assert_eq!(status, StatusCode::OK, "the upload must actually be stored");
assert!(
write_peak <= PEAK_BUDGET,
"WRITE PATH REGRESSED: peak heap grew {write_peak} bytes moving a {NAR_BYTES}-byte \
NAR, over the {PEAK_BUDGET}-byte budget. A peak that tracks NAR size means \
something on the ingest path is materializing the whole blob again — check that \
the handler still streams the body and that every tier still declares \
NarResidency::Streaming.",
);
let (read_peak, (len, checksum)) = peak_growth(|| async {
let req = Request::builder()
.method("GET")
.uri("/nar/big.nar.xz")
.body(Body::empty())
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let mut body = resp.into_body().into_data_stream();
let mut len = 0usize;
let mut sum = 0u64;
while let Some(c) = body.next().await {
let c = c.unwrap();
len += c.len();
sum = fold_checksum(sum, &c);
}
(len, sum)
})
.await;
assert_eq!(len, NAR_BYTES, "the served NAR must be byte-complete");
assert_eq!(
checksum,
expected_checksum(NAR_BYTES),
"the served NAR must be byte-identical — a chunked write that reordered or \
spliced would show up here and nowhere else",
);
assert!(
read_peak <= PEAK_BUDGET,
"READ PATH REGRESSED: peak heap grew {read_peak} bytes serving a {NAR_BYTES}-byte \
NAR, over the {PEAK_BUDGET}-byte budget.",
);
println!(
"nar={NAR_BYTES} budget={PEAK_BUDGET} control_peak={control_peak} \
write_peak={write_peak} read_peak={read_peak}",
);
}