use std::{env, hint::black_box, path::Path};
use cli::bench::{detect_renames_for_bench, find_merge_base_for_bench, three_way_merge_for_bench};
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use objects::{
object::{Blob, ChangeId, MarkerName, ThreadName, Tree, TreeEntry},
store::{InMemoryStore, ObjectStore},
};
use refs::{Head, RefExpectation, RefManager};
use repo::{
FsMonitorMode, FsMonitorSettings, Repository, WorktreeStatusOptions, run_local_monitor_helper,
};
use semantic::{
SemanticDiffOptions, SemanticParseCache, SimilarityMethod, semantic_check_only, semantic_diff,
semantic_diff_summary,
};
use tempfile::TempDir;
#[derive(Clone, Copy, Debug)]
enum SyntheticShape {
EvenSpread,
Wide,
Deep,
ManySmallDirectories,
FewHugeDirectories,
}
impl SyntheticShape {
fn name(self) -> &'static str {
match self {
Self::EvenSpread => "even_spread",
Self::Wide => "wide",
Self::Deep => "deep",
Self::ManySmallDirectories => "many_small_dirs",
Self::FewHugeDirectories => "few_huge_dirs",
}
}
}
fn synthetic_file_counts() -> Vec<usize> {
let mut counts = vec![1_000, 10_000];
if let Ok(value) = env::var("HEDDLE_BENCH_LARGE_SYNTHETIC")
&& let Ok(parsed) = value.parse::<usize>()
&& parsed > 10_000
{
counts.push(parsed);
}
counts
}
fn write_files(root: &Path, count: usize, prefix: &str) {
write_shape(root, count, prefix, SyntheticShape::EvenSpread);
}
fn write_flat_files(root: &Path, count: usize) {
for i in 0..count {
std::fs::write(
root.join(format!("file_{i:05}.txt")),
format!("flat file {i}\n{}\n", "x".repeat(64)),
)
.unwrap();
}
}
fn write_shape(root: &Path, count: usize, prefix: &str, shape: SyntheticShape) {
match shape {
SyntheticShape::EvenSpread => {
for i in 0..count {
let dir = root.join(format!("{prefix}/dir-{:02}", i % 20));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join(format!("file-{i:05}.txt")),
format!("{prefix} file {i}\n{}\n", "x".repeat(64)),
)
.unwrap();
}
}
SyntheticShape::Wide => {
for i in 0..count {
let dir = root.join(format!("{prefix}/dir-{i:05}"));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("entry.txt"),
format!("wide file {i}\n{}\n", "x".repeat(64)),
)
.unwrap();
}
}
SyntheticShape::Deep => {
for i in 0..count {
let dir = root.join(prefix).join(format!(
"l0-{:02}/l1-{:02}/l2-{:02}/l3-{:02}/l4-{:02}/l5-{:02}",
i % 8,
(i / 8) % 8,
(i / 64) % 8,
(i / 512) % 8,
(i / 4_096) % 8,
(i / 32_768) % 8,
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join(format!("file-{i:05}.txt")),
format!("deep file {i}\n{}\n", "x".repeat(64)),
)
.unwrap();
}
}
SyntheticShape::ManySmallDirectories => {
for i in 0..count {
let dir = root.join(format!("{prefix}/shard-{:03}/item-{i:05}", i % 256));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("entry.txt"),
format!("small dir file {i}\n{}\n", "x".repeat(64)),
)
.unwrap();
}
}
SyntheticShape::FewHugeDirectories => {
for i in 0..count {
let dir = root.join(format!("{prefix}/bucket-{:02}", i % 4));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join(format!("file-{i:05}.txt")),
format!("huge dir file {i}\n{}\n", "x".repeat(64)),
)
.unwrap();
}
}
}
}
fn setup_repo_with_files(count: usize) -> (TempDir, Repository) {
let temp = TempDir::new().unwrap();
let repo = Repository::init_default(temp.path()).unwrap();
write_files(temp.path(), count, "tracked");
(temp, repo)
}
fn setup_flat_repo_with_files(count: usize) -> (TempDir, Repository) {
let temp = TempDir::new().unwrap();
let repo = Repository::init_default(temp.path()).unwrap();
write_flat_files(temp.path(), count);
(temp, repo)
}
fn setup_repo_with_shape(shape: SyntheticShape, count: usize) -> (TempDir, Repository) {
let temp = TempDir::new().unwrap();
let repo = Repository::init_default(temp.path()).unwrap();
write_shape(temp.path(), count, "tracked", shape);
(temp, repo)
}
fn setup_repo_with_many_untracked_dirs(
tracked_count: usize,
untracked_dir_count: usize,
) -> (TempDir, Repository) {
let temp = TempDir::new().unwrap();
let repo = Repository::init_default(temp.path()).unwrap();
write_files(temp.path(), tracked_count, "tracked");
for i in 0..untracked_dir_count {
let dir = temp
.path()
.join(format!("scratch/bucket-{:03}/dir-{i:05}", i % 128));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("note.txt"),
format!("untracked dir {i}\n{}\n", "z".repeat(48)),
)
.unwrap();
}
(temp, repo)
}
fn ref_scale_counts() -> [usize; 4] {
[1_000, 10_000, 50_000, 100_000]
}
fn setup_ref_manager() -> (TempDir, RefManager) {
let temp = TempDir::new().unwrap();
let heddle_dir = temp.path().join(".heddle");
std::fs::create_dir_all(&heddle_dir).unwrap();
let refs = RefManager::new(&heddle_dir);
refs.init().unwrap();
(temp, refs)
}
fn populate_threads(refs: &RefManager, count: usize) {
for index in 0..count {
refs.set_thread(
&ThreadName::new(format!("branch-{index:05}")),
&ChangeId::generate(),
)
.unwrap();
}
}
fn populate_markers(refs: &RefManager, count: usize) {
for index in 0..count {
refs.create_marker(
&MarkerName::new(format!("marker-{index:05}")),
&ChangeId::generate(),
)
.unwrap();
}
}
fn populate_remote_threads(refs: &RefManager, remote: &str, count: usize) {
for index in 0..count {
refs.set_remote_thread(
remote,
&ThreadName::new(format!("branch-{index:05}")),
&ChangeId::generate(),
)
.unwrap();
}
}
#[cfg(unix)]
fn setup_repo_with_symlinks(count: usize) -> (TempDir, Repository) {
let temp = TempDir::new().unwrap();
let repo = Repository::init_default(temp.path()).unwrap();
let target_root = temp.path().join("targets");
std::fs::create_dir_all(&target_root).unwrap();
for i in 0..count {
let target = target_root.join(format!("target-{i:05}.txt"));
std::fs::write(&target, format!("target {i}\n")).unwrap();
let link_dir = temp.path().join(format!("links/dir-{:02}", i % 32));
std::fs::create_dir_all(&link_dir).unwrap();
std::os::unix::fs::symlink(
Path::new("..")
.join("..")
.join("targets")
.join(format!("target-{i:05}.txt")),
link_dir.join(format!("link-{i:05}")),
)
.unwrap();
}
(temp, repo)
}
fn bench_build_tree(c: &mut Criterion) {
let mut group = c.benchmark_group("build_tree");
for &file_count in &[100usize, 1_000] {
let (_temp, repo) = setup_repo_with_files(file_count);
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
let tree = repo.build_tree(repo.root()).unwrap();
black_box(tree);
});
},
);
}
group.finish();
}
fn bench_build_tree_cold_shapes(c: &mut Criterion) {
let mut group = c.benchmark_group("build_tree_cold_shape");
for shape in [
SyntheticShape::Wide,
SyntheticShape::Deep,
SyntheticShape::ManySmallDirectories,
SyntheticShape::FewHugeDirectories,
] {
for file_count in synthetic_file_counts() {
let (_temp, repo) = setup_repo_with_shape(shape, file_count);
group.bench_with_input(
BenchmarkId::new(shape.name(), file_count),
&file_count,
|b, _| {
b.iter(|| {
let tree = repo.build_tree(repo.root()).unwrap();
black_box(tree);
});
},
);
}
}
group.finish();
}
fn bench_compare_worktree(c: &mut Criterion) {
let mut group = c.benchmark_group("compare_worktree");
for &file_count in &[100usize, 1_000] {
let (_temp, repo) = setup_repo_with_files(file_count);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
std::fs::write(
repo.root().join("tracked/dir-00/file-00000.txt"),
"modified\n",
)
.unwrap();
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
let status = repo.compare_worktree_cached(&tree).unwrap();
black_box(status.change_count());
});
},
);
}
group.finish();
}
fn bench_snapshot_profile_flat_repo(c: &mut Criterion) {
let mut group = c.benchmark_group("snapshot_profile_flat_repo");
let file_count = 1_000usize;
group.throughput(Throughput::Elements(file_count as u64));
let sample = {
let (_temp, repo) = setup_flat_repo_with_files(file_count);
let attribution = repo.get_attribution().unwrap();
repo.snapshot_with_attribution_profiled(Some("bench".to_string()), None, attribution)
.unwrap()
.profile
};
eprintln!(
"snapshot_profile_flat_repo sample: files={} tree_walk_ms={} blob_prep_ms={} blob_write_ms={} tree_write_ms={} state_ref_oplog_ms={}",
file_count,
sample.tree_walk_ms,
sample.blob_prep_ms,
sample.blob_write_ms,
sample.tree_write_ms,
sample.state_ref_oplog_ms
);
group.bench_function(BenchmarkId::new("profiled_snapshot", file_count), |b| {
b.iter_batched(
|| setup_flat_repo_with_files(file_count),
|(_temp, repo)| {
let attribution = repo.get_attribution().unwrap();
let execution = repo
.snapshot_with_attribution_profiled(
Some("bench".to_string()),
None,
attribution,
)
.unwrap();
black_box((
execution.state.change_id,
execution.profile.tree_walk_ms,
execution.profile.blob_prep_ms,
execution.profile.blob_write_ms,
execution.profile.tree_write_ms,
execution.profile.state_ref_oplog_ms,
));
},
BatchSize::PerIteration,
);
});
group.finish();
}
fn bench_compare_worktree_many_added_files(c: &mut Criterion) {
let mut group = c.benchmark_group("compare_worktree_many_added_files");
for &added_count in &[1_000usize, 10_000] {
let (_temp, repo) = setup_repo_with_files(1_000);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
write_shape(
repo.root(),
added_count,
"new-files",
SyntheticShape::FewHugeDirectories,
);
group.bench_with_input(
BenchmarkId::from_parameter(added_count),
&added_count,
|b, _| {
b.iter(|| {
let status = repo.compare_worktree_cached(&tree).unwrap();
black_box((
status.added.len(),
status.modified.len(),
status.deleted.len(),
));
});
},
);
}
group.finish();
}
fn bench_compare_worktree_untracked_dirs_with_tracked_changes(c: &mut Criterion) {
let mut group = c.benchmark_group("compare_worktree_untracked_dirs_with_tracked_changes");
for &(tracked_count, untracked_dirs) in &[(1_000usize, 1_000usize), (1_000usize, 10_000usize)] {
let (_temp, repo) = setup_repo_with_many_untracked_dirs(tracked_count, untracked_dirs);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
for path in [
"tracked/dir-00/file-00000.txt",
"tracked/dir-01/file-00001.txt",
"tracked/dir-02/file-00002.txt",
] {
std::fs::write(repo.root().join(path), format!("edited {path}\n")).unwrap();
}
group.bench_with_input(
BenchmarkId::new(
format!("tracked_{tracked_count}_untracked_dirs_{untracked_dirs}"),
tracked_count,
),
&untracked_dirs,
|b, _| {
b.iter(|| {
let status = repo.compare_worktree_cached(&tree).unwrap();
black_box((status.added.len(), status.modified.len()));
});
},
);
}
group.finish();
}
fn bench_compare_worktree_untracked_dirs_second_run(c: &mut Criterion) {
let mut group = c.benchmark_group("compare_worktree_untracked_dirs_second_run");
for &(tracked_count, untracked_dirs) in &[(1_000usize, 1_000usize), (1_000usize, 10_000usize)] {
let (_temp, repo) = setup_repo_with_many_untracked_dirs(tracked_count, untracked_dirs);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
for path in [
"tracked/dir-00/file-00000.txt",
"tracked/dir-01/file-00001.txt",
"tracked/dir-02/file-00002.txt",
] {
std::fs::write(repo.root().join(path), format!("edited {path}\n")).unwrap();
}
let warmup = repo.compare_worktree_cached(&tree).unwrap();
black_box((warmup.added.len(), warmup.modified.len()));
group.bench_with_input(
BenchmarkId::new(
format!("tracked_{tracked_count}_untracked_dirs_{untracked_dirs}"),
tracked_count,
),
&untracked_dirs,
|b, _| {
b.iter(|| {
let status = repo.compare_worktree_cached(&tree).unwrap();
black_box((status.added.len(), status.modified.len()));
});
},
);
}
group.finish();
}
fn bench_refs_list_threads_summary(c: &mut Criterion) {
let mut group = c.benchmark_group("refs_list_threads_summary");
for count in ref_scale_counts() {
let (_temp, refs) = setup_ref_manager();
populate_threads(&refs, count);
refs.rebuild_ref_summary_index().unwrap();
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
b.iter(|| {
black_box(refs.list_threads().unwrap().len());
});
});
}
group.finish();
}
fn bench_refs_list_markers_summary(c: &mut Criterion) {
let mut group = c.benchmark_group("refs_list_markers_summary");
for count in ref_scale_counts() {
let (_temp, refs) = setup_ref_manager();
populate_markers(&refs, count);
refs.rebuild_ref_summary_index().unwrap();
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
b.iter(|| {
black_box(refs.list_markers().unwrap().len());
});
});
}
group.finish();
}
fn bench_refs_list_remote_threads_summary(c: &mut Criterion) {
let mut group = c.benchmark_group("refs_list_remote_threads_summary");
for count in ref_scale_counts() {
let (_temp, refs) = setup_ref_manager();
populate_remote_threads(&refs, "origin", count);
refs.rebuild_ref_summary_index().unwrap();
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
b.iter(|| {
black_box(refs.list_remote_threads("origin").unwrap().len());
});
});
}
group.finish();
}
fn bench_refs_update_thread_rebuild_summary(c: &mut Criterion) {
let mut group = c.benchmark_group("refs_update_thread_rebuild_summary");
for count in ref_scale_counts() {
let (_temp, refs) = setup_ref_manager();
populate_threads(&refs, count);
let hot = ThreadName::new(format!("branch-{:05}", count / 2));
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
b.iter(|| {
refs.set_thread(&hot, &ChangeId::generate()).unwrap();
});
});
}
group.finish();
}
fn bench_refs_update_marker_rebuild_summary(c: &mut Criterion) {
let mut group = c.benchmark_group("refs_update_marker_rebuild_summary");
for count in ref_scale_counts() {
let (_temp, refs) = setup_ref_manager();
populate_markers(&refs, count);
let hot = MarkerName::new(format!("marker-{:05}", count / 2));
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
b.iter(|| {
refs.set_marker_cas(&hot, RefExpectation::Any, &ChangeId::generate())
.unwrap();
});
});
}
group.finish();
}
fn bench_refs_update_remote_thread_rebuild_summary(c: &mut Criterion) {
let mut group = c.benchmark_group("refs_update_remote_thread_rebuild_summary");
for count in ref_scale_counts() {
let (_temp, refs) = setup_ref_manager();
populate_remote_threads(&refs, "origin", count);
let hot = ThreadName::new(format!("branch-{:05}", count / 2));
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
b.iter(|| {
refs.set_remote_thread("origin", &hot, &ChangeId::generate())
.unwrap();
});
});
}
group.finish();
}
fn bench_worktree_is_clean_untracked_dirs_second_run(c: &mut Criterion) {
let mut group = c.benchmark_group("worktree_is_clean_untracked_dirs_second_run");
for &(tracked_count, untracked_dirs) in &[(1_000usize, 1_000usize), (1_000usize, 10_000usize)] {
let (_temp, repo) = setup_repo_with_many_untracked_dirs(tracked_count, untracked_dirs);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
let warmup = repo.worktree_is_clean_cached(&tree).unwrap();
black_box(warmup);
group.bench_with_input(
BenchmarkId::new(
format!("tracked_{tracked_count}_untracked_dirs_{untracked_dirs}"),
tracked_count,
),
&untracked_dirs,
|b, _| {
b.iter(|| {
let clean = repo.worktree_is_clean_cached(&tree).unwrap();
black_box(clean);
});
},
);
}
group.finish();
}
#[cfg(unix)]
fn bench_compare_worktree_symlink_heavy(c: &mut Criterion) {
let mut group = c.benchmark_group("compare_worktree_symlink_heavy");
for &symlink_count in &[1_000usize, 10_000] {
let (_temp, repo) = setup_repo_with_symlinks(symlink_count);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
std::os::unix::fs::symlink(
Path::new("..")
.join("..")
.join("targets")
.join("target-00000.txt"),
repo.root().join("links/dir-00/extra-link"),
)
.unwrap();
group.bench_with_input(
BenchmarkId::from_parameter(symlink_count),
&symlink_count,
|b, _| {
b.iter(|| {
let status = repo.compare_worktree_cached(&tree).unwrap();
black_box((status.added.len(), status.modified.len()));
});
},
);
}
group.finish();
}
fn bench_worktree_clean(c: &mut Criterion) {
let mut group = c.benchmark_group("worktree_is_clean");
for &file_count in &[100usize, 1_000] {
let (_temp, repo) = setup_repo_with_files(file_count);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
let clean = repo.worktree_is_clean_cached(&tree).unwrap();
black_box(clean);
});
},
);
}
group.finish();
}
fn bench_worktree_clean_modes(c: &mut Criterion) {
let mut group = c.benchmark_group("worktree_is_clean_mode");
let file_count = 1_000usize;
let (_temp, repo) = setup_repo_with_files(file_count);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
let off_options = WorktreeStatusOptions {
fsmonitor: FsMonitorSettings {
mode: FsMonitorMode::Off,
},
};
group.bench_with_input(BenchmarkId::new("off", file_count), &file_count, |b, _| {
b.iter(|| {
let clean = repo
.worktree_is_clean_cached_with_options(&tree, &off_options)
.unwrap();
black_box(clean);
});
});
let native_options = WorktreeStatusOptions {
fsmonitor: FsMonitorSettings {
mode: FsMonitorMode::Native,
},
};
let root = repo.root().to_path_buf();
std::thread::spawn(move || {
let _ = run_local_monitor_helper(&root);
});
std::thread::sleep(std::time::Duration::from_millis(200));
let _ = repo
.worktree_is_clean_cached_with_options(&tree, &native_options)
.unwrap();
group.bench_with_input(
BenchmarkId::new("native", file_count),
&file_count,
|b, _| {
b.iter(|| {
let clean = repo
.worktree_is_clean_cached_with_options(&tree, &native_options)
.unwrap();
black_box(clean);
});
},
);
group.finish();
}
fn bench_worktree_clean_native_profile(c: &mut Criterion) {
let mut group = c.benchmark_group("worktree_is_clean_native_profile");
let file_count = 1_000usize;
let (_temp, repo) = setup_repo_with_files(file_count);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
let native_options = WorktreeStatusOptions {
fsmonitor: FsMonitorSettings {
mode: FsMonitorMode::Native,
},
};
let root = repo.root().to_path_buf();
std::thread::spawn(move || {
let _ = run_local_monitor_helper(&root);
});
std::thread::sleep(std::time::Duration::from_millis(200));
let _ = repo
.compare_worktree_cached_profiled_with_options(&tree, &native_options)
.unwrap();
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
let (status, profile) = repo
.compare_worktree_cached_profiled_with_options(&tree, &native_options)
.unwrap();
black_box((
status.is_clean(),
profile.monitor_prepare_ms,
profile.compare_ms,
profile.monitor_persist_ms,
));
});
},
);
group.finish();
}
fn bench_goto_same_tree(c: &mut Criterion) {
let mut group = c.benchmark_group("goto_same_tree");
for &file_count in &[100usize, 1_000] {
let (_temp, repo) = setup_repo_with_files(file_count);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
repo.goto(&state.change_id).unwrap();
black_box(state.change_id);
});
},
);
}
group.finish();
}
fn bench_goto_small_delta(c: &mut Criterion) {
let mut group = c.benchmark_group("goto_small_delta");
let file_count = 1_000usize;
let (_temp, repo) = setup_repo_with_files(file_count);
let base = repo.snapshot(Some("base".to_string()), None).unwrap();
std::fs::write(
repo.root().join("tracked/dir-00/file-00000.txt"),
"small delta\n",
)
.unwrap();
let delta = repo.snapshot(Some("delta".to_string()), None).unwrap();
repo.goto(&base.change_id).unwrap();
let mut target = delta.change_id;
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
repo.goto(&target).unwrap();
target = if target == delta.change_id {
base.change_id
} else {
delta.change_id
};
black_box(target);
});
},
);
group.finish();
}
fn bench_goto_large_delta(c: &mut Criterion) {
let mut group = c.benchmark_group("goto_large_delta");
for &file_count in &[1_000usize] {
let (_temp, repo) = setup_repo_with_files(file_count);
let base = repo.snapshot(Some("base".to_string()), None).unwrap();
for i in 0..file_count {
std::fs::write(
repo.root()
.join(format!("tracked/dir-{:02}/file-{i:05}.txt", i % 20)),
format!("rewritten file {i}\n{}\n", "y".repeat(64)),
)
.unwrap();
}
let delta = repo.snapshot(Some("delta".to_string()), None).unwrap();
repo.goto(&base.change_id).unwrap();
let mut target = delta.change_id;
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
repo.goto(&target).unwrap();
target = if target == delta.change_id {
base.change_id
} else {
delta.change_id
};
black_box(target);
});
},
);
}
group.finish();
}
fn bench_goto_same_tree_then_status(c: &mut Criterion) {
let mut group = c.benchmark_group("goto_same_tree_then_status");
let file_count = 1_000usize;
let (_temp, repo) = setup_repo_with_files(file_count);
let state = repo.snapshot(Some("base".to_string()), None).unwrap();
let tree = repo.store().get_tree(&state.tree).unwrap().unwrap();
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
repo.goto(&state.change_id).unwrap();
let clean = repo.worktree_is_clean_cached(&tree).unwrap();
black_box((state.change_id, clean));
});
},
);
group.finish();
}
fn bench_goto_small_delta_then_status(c: &mut Criterion) {
let mut group = c.benchmark_group("goto_small_delta_then_status");
let file_count = 1_000usize;
let (_temp, repo) = setup_repo_with_files(file_count);
let base = repo.snapshot(Some("base".to_string()), None).unwrap();
std::fs::write(
repo.root().join("tracked/dir-00/file-00000.txt"),
"small delta\n",
)
.unwrap();
let delta = repo.snapshot(Some("delta".to_string()), None).unwrap();
let base_tree = repo.store().get_tree(&base.tree).unwrap().unwrap();
let delta_tree = repo.store().get_tree(&delta.tree).unwrap().unwrap();
repo.goto(&base.change_id).unwrap();
let mut target = delta.change_id;
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
repo.goto(&target).unwrap();
let clean = if target == delta.change_id {
repo.worktree_is_clean_cached(&delta_tree).unwrap()
} else {
repo.worktree_is_clean_cached(&base_tree).unwrap()
};
target = if target == delta.change_id {
base.change_id
} else {
delta.change_id
};
black_box((target, clean));
});
},
);
group.finish();
}
fn bench_full_rematerialize_then_status(c: &mut Criterion) {
let mut group = c.benchmark_group("full_rematerialize_then_status");
let file_count = 1_000usize;
let (_temp, repo) = setup_repo_with_files(file_count);
let base = repo.snapshot(Some("base".to_string()), None).unwrap();
std::fs::write(
repo.root().join("tracked/dir-00/file-00000.txt"),
"delta version\n",
)
.unwrap();
let delta = repo.snapshot(Some("delta".to_string()), None).unwrap();
let base_tree = repo.store().get_tree(&base.tree).unwrap().unwrap();
let delta_tree = repo.store().get_tree(&delta.tree).unwrap().unwrap();
repo.goto(&base.change_id).unwrap();
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
std::fs::write(
repo.root().join("tracked/dir-03/file-00003.txt"),
"dirty worktree forces fallback\n",
)
.unwrap();
repo.goto(&delta.change_id).unwrap();
let delta_clean = repo.worktree_is_clean_cached(&delta_tree).unwrap();
repo.goto(&base.change_id).unwrap();
let base_clean = repo.worktree_is_clean_cached(&base_tree).unwrap();
black_box((delta_clean, base_clean));
});
},
);
group.finish();
}
fn setup_divergent_history() -> (
TempDir,
Repository,
objects::object::ChangeId,
objects::object::ChangeId,
objects::object::ChangeId,
) {
let temp = TempDir::new().unwrap();
let repo = Repository::init_default(temp.path()).unwrap();
std::fs::write(temp.path().join("shared.txt"), "base\n").unwrap();
let base = repo.snapshot(Some("base".to_string()), None).unwrap();
repo.refs()
.set_thread(&ThreadName::new("topic"), &base.change_id)
.unwrap();
std::fs::write(temp.path().join("main-only.txt"), "main\n").unwrap();
let main_tip = repo.snapshot(Some("main tip".to_string()), None).unwrap();
repo.refs()
.write_head(&Head::Attached {
thread: ThreadName::new("topic"),
})
.unwrap();
repo.goto(&base.change_id).unwrap();
std::fs::write(temp.path().join("topic-only.txt"), "topic\n").unwrap();
let topic_tip = repo.snapshot(Some("topic tip".to_string()), None).unwrap();
repo.refs()
.write_head(&Head::Attached {
thread: ThreadName::new("main"),
})
.unwrap();
repo.goto(&main_tip.change_id).unwrap();
(
temp,
repo,
base.change_id,
main_tip.change_id,
topic_tip.change_id,
)
}
fn bench_find_merge_base(c: &mut Criterion) {
let (_temp, repo, _base, main_tip, topic_tip) = setup_divergent_history();
c.bench_function("find_merge_base", |b| {
b.iter(|| {
let merge_base = find_merge_base_for_bench(&repo, &main_tip, &topic_tip).unwrap();
black_box(merge_base);
});
});
}
fn bench_three_way_merge(c: &mut Criterion) {
let (_temp, repo, base_id, main_tip, topic_tip) = setup_divergent_history();
let base_tree = repo.get_tree_for_state(&base_id).unwrap().unwrap();
let main_tree = repo.get_tree_for_state(&main_tip).unwrap().unwrap();
let topic_tree = repo.get_tree_for_state(&topic_tip).unwrap().unwrap();
c.bench_function("three_way_merge", |b| {
b.iter(|| {
let result =
three_way_merge_for_bench(&repo, &base_tree, &main_tree, &topic_tree).unwrap();
black_box(result.1 + result.2 + result.3);
});
});
}
fn make_bench_tree(store: &InMemoryStore, files: &[(&str, &[u8])]) -> Tree {
let entries: Vec<TreeEntry> = files
.iter()
.map(|(path, content)| {
let hash = store.put_blob(&Blob::new(content.to_vec())).unwrap();
TreeEntry::file((*path).to_string(), hash, false).unwrap()
})
.collect();
Tree::from_entries(entries)
}
fn bench_detect_renames(c: &mut Criterion) {
let mut group = c.benchmark_group("detect_renames");
for &file_count in &[100usize, 500] {
let store = InMemoryStore::new();
let mut base_files = Vec::with_capacity(file_count);
let mut branch_files = Vec::with_capacity(file_count);
for i in 0..file_count {
let content = format!(
"fn item_{i}() {{\n let value = {i};\n println!(\"{{}}\", value);\n}}\n{}",
"x".repeat(96)
)
.into_bytes();
base_files.push((format!("original_{i}.rs"), content.clone()));
branch_files.push((format!("renamed_{i}.rs"), content));
}
let base_refs: Vec<(&str, &[u8])> = base_files
.iter()
.map(|(path, content)| (path.as_str(), content.as_slice()))
.collect();
let branch_refs: Vec<(&str, &[u8])> = branch_files
.iter()
.map(|(path, content)| (path.as_str(), content.as_slice()))
.collect();
let base_tree = make_bench_tree(&store, &base_refs);
let branch_tree = make_bench_tree(&store, &branch_refs);
group.bench_with_input(
BenchmarkId::from_parameter(file_count),
&file_count,
|b, _| {
b.iter(|| {
let result =
detect_renames_for_bench(&store, &base_tree, &branch_tree).unwrap();
black_box(result);
});
},
);
}
group.finish();
}
fn setup_semantic_history() -> (
TempDir,
Repository,
objects::object::ContentHash,
objects::object::ContentHash,
) {
let temp = TempDir::new().unwrap();
let repo = Repository::init_default(temp.path()).unwrap();
std::fs::write(
temp.path().join("lib.rs"),
"fn add(a: i32, b: i32) -> i32 { a + b }\n",
)
.unwrap();
let base = repo.snapshot(Some("base".to_string()), None).unwrap();
std::fs::write(
temp.path().join("lib.rs"),
"fn add(a:i32,b:i32)->i32 {\n a + b\n}\n",
)
.unwrap();
let formatted = repo.snapshot(Some("formatted".to_string()), None).unwrap();
(temp, repo, base.tree, formatted.tree)
}
fn bench_semantic_diff(c: &mut Criterion) {
let (_temp, repo, base_tree, formatted_tree) = setup_semantic_history();
let options = SemanticDiffOptions {
similarity_method: SimilarityMethod::Ast,
..Default::default()
};
c.bench_function("semantic_diff", |b| {
b.iter(|| {
let result =
semantic_diff(repo.store(), &base_tree, &formatted_tree, &options).unwrap();
black_box(result.changes.len());
});
});
}
fn bench_semantic_check_only(c: &mut Criterion) {
let (_temp, repo, base_tree, formatted_tree) = setup_semantic_history();
let options = SemanticDiffOptions {
similarity_method: SimilarityMethod::Ast,
..Default::default()
};
c.bench_function("semantic_check_only", |b| {
b.iter(|| {
let result =
semantic_check_only(repo.store(), &base_tree, &formatted_tree, &options).unwrap();
black_box(result.status);
});
});
}
fn bench_semantic_summary(c: &mut Criterion) {
let (_temp, repo, base_tree, formatted_tree) = setup_semantic_history();
let options = SemanticDiffOptions {
similarity_method: SimilarityMethod::Ast,
..Default::default()
};
c.bench_function("semantic_diff_summary", |b| {
b.iter(|| {
let result =
semantic_diff_summary(repo.store(), &base_tree, &formatted_tree, &options).unwrap();
black_box(result.file_renames.len());
});
});
}
fn bench_semantic_parse_cache_warm(c: &mut Criterion) {
let cache = SemanticParseCache::shared();
cache.clear();
let (_temp, repo, base_tree, formatted_tree) = setup_semantic_history();
let options = SemanticDiffOptions {
similarity_method: SimilarityMethod::Ast,
..Default::default()
};
let _ = semantic_diff(repo.store(), &base_tree, &formatted_tree, &options).unwrap();
c.bench_function("semantic_parse_cache_warm", |b| {
b.iter(|| {
let before = cache.stats();
let result =
semantic_diff(repo.store(), &base_tree, &formatted_tree, &options).unwrap();
let after = cache.stats();
black_box((result.changes.len(), after.hits.saturating_sub(before.hits)));
});
});
}
fn bench_semantic_parse_cache_cold(c: &mut Criterion) {
let cache = SemanticParseCache::shared();
let (_temp, repo, base_tree, formatted_tree) = setup_semantic_history();
let options = SemanticDiffOptions {
similarity_method: SimilarityMethod::Ast,
..Default::default()
};
c.bench_function("semantic_parse_cache_cold", |b| {
b.iter(|| {
cache.clear();
let before = cache.stats();
let result =
semantic_diff(repo.store(), &base_tree, &formatted_tree, &options).unwrap();
let after = cache.stats();
black_box((
result.changes.len(),
after.misses.saturating_sub(before.misses),
));
});
});
}
criterion_group!(
local_ops,
bench_build_tree,
bench_build_tree_cold_shapes,
bench_snapshot_profile_flat_repo,
bench_compare_worktree,
bench_compare_worktree_many_added_files,
bench_compare_worktree_untracked_dirs_with_tracked_changes,
bench_compare_worktree_untracked_dirs_second_run,
bench_refs_list_threads_summary,
bench_refs_list_markers_summary,
bench_refs_list_remote_threads_summary,
bench_refs_update_thread_rebuild_summary,
bench_refs_update_marker_rebuild_summary,
bench_refs_update_remote_thread_rebuild_summary,
bench_worktree_is_clean_untracked_dirs_second_run,
bench_worktree_clean,
bench_worktree_clean_modes,
bench_worktree_clean_native_profile,
bench_goto_same_tree,
bench_goto_small_delta,
bench_goto_large_delta,
bench_goto_same_tree_then_status,
bench_goto_small_delta_then_status,
bench_full_rematerialize_then_status,
bench_find_merge_base,
bench_three_way_merge,
bench_detect_renames,
bench_semantic_check_only,
bench_semantic_summary,
bench_semantic_diff,
bench_semantic_parse_cache_warm,
bench_semantic_parse_cache_cold
);
#[cfg(unix)]
criterion_group!(unix_status_ops, bench_compare_worktree_symlink_heavy);
#[cfg(unix)]
criterion_main!(local_ops, unix_status_ops);
#[cfg(not(unix))]
criterion_main!(local_ops);