use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use git_storage_trait::GitOps as _;
use znippy_plugin_git::exploded_cache::MAX_CACHED_BYTES;
use znippy_plugin_git::git_ops::GitStore;
struct Counting;
static ON: AtomicBool = AtomicBool::new(false);
static BYTES: AtomicU64 = AtomicU64::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
if ON.load(Ordering::Relaxed) {
BYTES.fetch_add(l.size() as u64, Ordering::Relaxed);
}
unsafe { System.alloc(l) }
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
unsafe { System.dealloc(p, l) }
}
unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 {
if ON.load(Ordering::Relaxed) && new > l.size() {
BYTES.fetch_add((new - l.size()) as u64, Ordering::Relaxed);
}
unsafe { System.realloc(p, l, new) }
}
}
#[global_allocator]
static A: Counting = Counting;
fn allocated<T>(f: impl FnOnce() -> T) -> (T, u64) {
BYTES.store(0, Ordering::SeqCst);
ON.store(true, Ordering::SeqCst);
let out = f();
ON.store(false, Ordering::SeqCst);
(out, BYTES.load(Ordering::SeqCst))
}
fn one_blob_pack(body: &[u8]) -> Vec<u8> {
use std::io::Write;
let mut pack = b"PACK".to_vec();
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&1u32.to_be_bytes());
let mut size = body.len() as u64;
let mut header = vec![(3u8 << 4) | (size as u8 & 0x0f)];
size >>= 4;
while size > 0 {
let last = header.len() - 1;
header[last] |= 0x80;
header.push((size & 0x7f) as u8);
size >>= 7;
}
pack.extend_from_slice(&header);
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(body).unwrap();
pack.extend_from_slice(&e.finish().unwrap());
let sum = {
use sha1::{Digest, Sha1};
let mut h = Sha1::new();
h.update(&pack);
h.finalize().to_vec()
};
pack.extend_from_slice(&sum);
pack
}
fn tmpdir(name: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("znippy-read-copies-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn reading_a_large_object_allocates_it_once_and_the_cache_takes_no_copy() {
const SIZE: usize = 3 * 1024 * 1024;
assert!(
SIZE as u64 > MAX_CACHED_BYTES,
"the fixture must be over the cache's size cap or this test asserts nothing"
);
let dir = tmpdir("large");
let store = GitStore::open(&dir, "rickard").unwrap();
let body: Vec<u8> = (0..SIZE).map(|i| (i % 251) as u8).collect();
store.put_pack(&one_blob_pack(&body)).unwrap();
store.wait_indexed();
let oid = store
.hash_kind()
.oid_of(&znippy_plugin_git::object::canonical(
znippy_plugin_git::object::GitObjectKind::Blob,
&body,
));
assert!(store.content(&oid).unwrap().is_some());
let (got, bytes) = allocated(|| store.content(&oid).unwrap());
let (kind, payload) = got.expect("the object is absent");
assert_eq!(kind, znippy_plugin_git::object::GitObjectKind::Blob);
assert_eq!(payload, body, "the read returned the wrong bytes");
assert!(
bytes < (SIZE as u64 * 3) / 2,
"reading a {SIZE} B object allocated {bytes} B — {:.2} copies. The exploded tier is \
taking a copy of bytes the caller already has; a large whole object is one linear \
inflate and must be declined before anything is copied",
bytes as f64 / SIZE as f64
);
assert!(
bytes >= SIZE as u64,
"reading a {SIZE} B object allocated only {bytes} B, which is less than the object — \
the instrument is not measuring the read"
);
let band = store.explode_cache_use();
assert_eq!(
band.lazy_bytes, 0,
"a {SIZE} B object is in the cache; {band:?}"
);
assert!(band.direct_reads >= 2, "the declines were not counted: {band:?}");
eprintln!(
"a {SIZE} B object read: {bytes} B allocated ({:.2} copies), cache took {} B, \
{} direct reads",
bytes as f64 / SIZE as f64,
band.lazy_bytes,
band.direct_reads
);
}
#[test]
fn a_declined_write_back_allocates_nothing_and_an_admitted_one_costs_one_copy() {
let dir = tmpdir("admitted");
let cache = znippy_plugin_git::exploded_cache::ExplodedCache::open(
&dir.join("objects.exploded"),
znippy_plugin_git::exploded_arrow::ExplodePolicy::Commits,
0,
)
.unwrap();
let body = vec![7u8; 64 * 1024];
let oid = |n: u32| {
let mut o = vec![0u8; 20];
o[..4].copy_from_slice(&n.to_be_bytes());
o
};
let dear = znippy_plugin_git::resolve::ReadCost {
depth: znippy_plugin_git::exploded_cache::CHEAP_DEPTH + 1,
inflated: body.len() as u64,
external_base: false,
};
let cheap = znippy_plugin_git::resolve::ReadCost {
depth: znippy_plugin_git::exploded_cache::CHEAP_DEPTH,
inflated: body.len() as u64,
external_base: false,
};
const N: u32 = 256;
let cheap_oids: Vec<Vec<u8>> = (0..N).map(oid).collect();
let dear_oids: Vec<Vec<u8>> = (0..N).map(|i| oid(1000 + i)).collect();
let (_, declined_bytes) = allocated(|| {
for o in &cheap_oids {
cache
.write_back(o, znippy_plugin_git::object::GitObjectKind::Blob, &body, cheap)
.unwrap();
}
});
assert!(
declined_bytes < body.len() as u64,
"{N} DECLINED write-backs of {} B allocated {declined_bytes} B — more than one \
object, so the cache is taking bytes it decided it did not want",
body.len()
);
assert_eq!(cache.use_now().direct_reads, N as u64);
let (_, admitted_bytes) = allocated(|| {
for o in &dear_oids {
cache
.write_back(o, znippy_plugin_git::object::GitObjectKind::Blob, &body, dear)
.unwrap();
}
});
let total = N as u64 * body.len() as u64;
assert!(
admitted_bytes < total * 8 / 5,
"{N} ADMITTED write-backs of {} B allocated {admitted_bytes} B against {total} B of \
payload = {:.2} copies; the arena is not holding them once",
body.len(),
admitted_bytes as f64 / total as f64
);
eprintln!(
"{N} write-backs of {} B: DECLINED {declined_bytes} B, ADMITTED {admitted_bytes} B \
({:.2} copies)",
body.len(),
admitted_bytes as f64 / total as f64
);
}