use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use grit_lib::config::environment::RepositoryOptions;
use grit_lib::config::ConfigSet;
use grit_lib::diff::{diff_trees_with_renames_and_policy, DiffEntry, RENAME_DETECTION_DISABLED};
use grit_lib::objects::{ObjectId, ObjectKind, TreeEntry};
use grit_lib::parallel::ThreadPolicy;
use grit_lib::repo::{InitOptions, Repository};
struct LiveAllocator;
thread_local! {
static LIVE: Cell<isize> = const { Cell::new(0) };
static PEAK: Cell<isize> = const { Cell::new(0) };
}
fn record(delta: isize) {
let _ = LIVE.try_with(|live| {
let bytes = live.get() + delta;
live.set(bytes);
let _ = PEAK.try_with(|peak| peak.set(peak.get().max(bytes)));
});
}
unsafe impl GlobalAlloc for LiveAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() {
record(layout.size() as isize);
}
ptr
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc_zeroed(layout) };
if !ptr.is_null() {
record(layout.size() as isize);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
record(-(layout.size() as isize));
unsafe { System.dealloc(ptr, layout) };
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
let new = unsafe { System.realloc(ptr, layout, size) };
if !new.is_null() {
record(size as isize - layout.size() as isize);
}
new
}
}
#[global_allocator]
static ALLOCATOR: LiveAllocator = LiveAllocator;
const DIRS: usize = 400;
fn write_tree(repo: &Repository, entries: &[TreeEntry]) -> ObjectId {
repo.odb()
.write(
ObjectKind::Tree,
&grit_lib::objects::serialize_tree(entries),
)
.expect("write tree")
}
fn metadata_fixture(
width: usize,
changed: usize,
) -> (tempfile::TempDir, Repository, ObjectId, ObjectId) {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
let old_blob = repo.odb().write(ObjectKind::Blob, b"old\n").expect("blob");
let new_blob = repo.odb().write(ObjectKind::Blob, b"new\n").expect("blob");
let mut old_root = Vec::new();
let mut new_root = Vec::new();
for d in 0..DIRS {
let mut entries: Vec<_> = (0..width)
.map(|f| TreeEntry {
mode: 0o100644,
name: format!("f{d:03}-{f:05}.txt").into_bytes(),
oid: old_blob,
})
.collect();
let old = write_tree(&repo, &entries);
for entry in &mut entries[..changed] {
entry.oid = new_blob;
}
let new = write_tree(&repo, &entries);
old_root.push(TreeEntry {
mode: 0o040000,
name: format!("d{d:03}").into_bytes(),
oid: old,
});
new_root.push(TreeEntry {
oid: new,
..old_root.last().expect("directory").clone()
});
}
let old = write_tree(&repo, &old_root);
let new = write_tree(&repo, &new_root);
drop((old_root, new_root, repo));
let repo = Repository::open(
&dir.path().join(".git"),
&RepositoryOptions::empty().with_work_tree(dir.path()),
)
.expect("reopen without warmed caches");
(dir, repo, old, new)
}
fn peak_during<T>(f: impl FnOnce() -> T) -> (usize, T) {
let start = LIVE.with(Cell::get);
PEAK.with(|peak| peak.set(start));
let value = f();
((PEAK.with(Cell::get) - start) as usize, value)
}
fn peak_for_width(width: usize) -> usize {
let (_dir, repo, old, new) = metadata_fixture(width, 1);
let start = LIVE.with(Cell::get);
let (peak, entries) = peak_during(|| {
repo.diff()
.tree_to_tree(Some(&old), Some(&new))
.expect("diff")
});
assert_eq!(entries.len(), DIRS);
for (d, entry) in entries.iter().enumerate() {
assert_eq!(entry.status.letter(), 'M');
assert_eq!(entry.path(), format!("d{d:03}/f{d:03}-00000.txt"));
}
let with_results = LIVE.with(Cell::get) - start;
drop(entries);
let caches = LIVE.with(Cell::get) - start;
eprintln!(
"width={width}: peak={peak}, retained caches={caches}, result bytes={}",
with_results - caches
);
peak
}
#[test]
fn completed_sibling_trees_do_not_accumulate_in_public_tree_diff() {
let small = peak_for_width(128);
let large = peak_for_width(512);
eprintln!("tree metadata peak live bytes: 51,200 files={small}; 204,800 files={large}");
assert!(
large < small + 8 * 1024 * 1024,
"completed sibling metadata accumulated: small={small}, large={large}"
);
}
#[test]
fn rename_pass_without_pairs_adds_no_copy_of_the_metadata_list() {
let (_dir, repo, old, new) = metadata_fixture(64, 25);
let config = ConfigSet::new();
let policy = ThreadPolicy::default();
let run = |threshold| {
peak_during(|| {
diff_trees_with_renames_and_policy(
repo.odb(),
Some(&old),
Some(&new),
"",
threshold,
&config,
&policy,
)
.expect("diff")
})
};
drop(run(RENAME_DETECTION_DISABLED));
let (plain_peak, plain) = run(RENAME_DETECTION_DISABLED);
let (renames_peak, renamed) = run(50);
assert_eq!(renamed, plain);
assert_eq!(renamed.len(), DIRS * 25);
let list_bytes = plain.capacity() * std::mem::size_of::<DiffEntry>();
eprintln!(
"metadata list={list_bytes}, no renames peak={plain_peak}, renames peak={renames_peak}"
);
assert!(
renames_peak < plain_peak + list_bytes / 8,
"rename pass copied the metadata list: plain={plain_peak}, renames={renames_peak}, list={list_bytes}"
);
}