| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
use oxy_core::repo::Repo; |
| 15 |
use std::path::Path; |
| 16 |
use std::time::{Duration, Instant}; |
| 17 |
|
| 18 |
fn hermetic() { |
| 19 |
static ONCE: std::sync::Once = std::sync::Once::new(); |
| 20 |
static DIR: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new(); |
| 21 |
ONCE.call_once(|| { |
| 22 |
let d = tempfile::tempdir().unwrap(); |
| 23 |
std::env::set_var("OXY_HOME", d.path()); |
| 24 |
DIR.set(d).ok(); |
| 25 |
}); |
| 26 |
} |
| 27 |
|
| 28 |
fn env_usize(name: &str, default: usize) -> usize { |
| 29 |
std::env::var(name) |
| 30 |
.ok() |
| 31 |
.and_then(|v| v.parse().ok()) |
| 32 |
.unwrap_or(default) |
| 33 |
} |
| 34 |
|
| 35 |
fn write(root: &Path, rel: &str, content: &str) { |
| 36 |
let p = root.join(rel); |
| 37 |
std::fs::create_dir_all(p.parent().unwrap()).unwrap(); |
| 38 |
std::fs::write(p, content).unwrap(); |
| 39 |
} |
| 40 |
|
| 41 |
|
| 42 |
fn peak_rss_mib() -> Option<f64> { |
| 43 |
let s = std::fs::read_to_string("/proc/self/status").ok()?; |
| 44 |
let kb: f64 = s |
| 45 |
.lines() |
| 46 |
.find(|l| l.starts_with("VmHWM:"))? |
| 47 |
.split_whitespace() |
| 48 |
.nth(1)? |
| 49 |
.parse() |
| 50 |
.ok()?; |
| 51 |
Some(kb / 1024.0) |
| 52 |
} |
| 53 |
|
| 54 |
struct Row { |
| 55 |
what: String, |
| 56 |
took: Duration, |
| 57 |
limit: Duration, |
| 58 |
} |
| 59 |
|
| 60 |
fn time<T>( |
| 61 |
rows: &mut Vec<Row>, |
| 62 |
what: impl Into<String>, |
| 63 |
limit_ms: u64, |
| 64 |
f: impl FnOnce() -> T, |
| 65 |
) -> T { |
| 66 |
let t = Instant::now(); |
| 67 |
let out = f(); |
| 68 |
rows.push(Row { |
| 69 |
what: what.into(), |
| 70 |
took: t.elapsed(), |
| 71 |
limit: Duration::from_millis(limit_ms), |
| 72 |
}); |
| 73 |
out |
| 74 |
} |
| 75 |
|
| 76 |
fn file_name(i: usize) -> String { |
| 77 |
format!("src/d{:03}/f{:05}.txt", i / 100, i) |
| 78 |
} |
| 79 |
|
| 80 |
#[test] |
| 81 |
#[ignore] |
| 82 |
fn bench_ten_thousand_files_five_thousand_changes() { |
| 83 |
hermetic(); |
| 84 |
let files = env_usize("OXY_BENCH_FILES", 10_000); |
| 85 |
let changes = env_usize("OXY_BENCH_CHANGES", 5_000); |
| 86 |
let dir = match std::env::var_os("OXY_BENCH_DIR") { |
| 87 |
Some(d) => tempfile::tempdir_in(d).unwrap(), |
| 88 |
None => tempfile::tempdir().unwrap(), |
| 89 |
}; |
| 90 |
let root = dir.path(); |
| 91 |
let work = root.join("work"); |
| 92 |
let repo = Repo::init(&work).unwrap().with_home(root.join("home")); |
| 93 |
let mut rows = Vec::new(); |
| 94 |
|
| 95 |
|
| 96 |
|
| 97 |
for i in 0..files { |
| 98 |
write( |
| 99 |
&work, |
| 100 |
&file_name(i), |
| 101 |
&format!("file {i}\n{}", "some line of text\n".repeat(20)), |
| 102 |
); |
| 103 |
} |
| 104 |
write(&work, "hot.txt", ""); |
| 105 |
time( |
| 106 |
&mut rows, |
| 107 |
format!("commit: import {files} files"), |
| 108 |
60_000, |
| 109 |
|| repo.commit("import", &[]).unwrap(), |
| 110 |
); |
| 111 |
let mut hot = String::new(); |
| 112 |
let build = Instant::now(); |
| 113 |
for c in 1..changes { |
| 114 |
if c % 10 == 0 { |
| 115 |
hot.push_str(&format!("line added by change {c}\n")); |
| 116 |
write(&work, "hot.txt", &hot); |
| 117 |
} else { |
| 118 |
let i = (c * 7919) % files; |
| 119 |
write( |
| 120 |
&work, |
| 121 |
&file_name(i), |
| 122 |
&format!( |
| 123 |
"file {i} edited by change {c}\n{}", |
| 124 |
"some line of text\n".repeat(20) |
| 125 |
), |
| 126 |
); |
| 127 |
} |
| 128 |
repo.commit(&format!("change {c}"), &[]).unwrap(); |
| 129 |
} |
| 130 |
let per_commit = build.elapsed() / (changes.max(2) - 1) as u32; |
| 131 |
rows.push(Row { |
| 132 |
what: format!("commit: one file (mean of {})", changes - 1), |
| 133 |
took: per_commit, |
| 134 |
limit: Duration::from_millis(250), |
| 135 |
}); |
| 136 |
|
| 137 |
|
| 138 |
time( |
| 139 |
&mut rows, |
| 140 |
"status: clean working copy (stat crawl)", |
| 141 |
2_000, |
| 142 |
|| assert!(!repo.snapshot("oxy status").unwrap().changed), |
| 143 |
); |
| 144 |
for i in 0..10 { |
| 145 |
write(&work, &file_name(i * 13), &format!("ten-file edit {i}\n")); |
| 146 |
} |
| 147 |
time(&mut rows, "status: 10 files modified", 2_000, || { |
| 148 |
assert!(repo.snapshot("oxy status").unwrap().changed) |
| 149 |
}); |
| 150 |
time(&mut rows, "commit: 10 files", 500, || { |
| 151 |
repo.commit("ten files", &[]).unwrap() |
| 152 |
}); |
| 153 |
let log = time(&mut rows, "log: whole bookmark", 2_000, || { |
| 154 |
repo.log().unwrap() |
| 155 |
}); |
| 156 |
assert!(log.len() >= changes, "{} entries", log.len()); |
| 157 |
let blame = time( |
| 158 |
&mut rows, |
| 159 |
format!("blame: hot file ({} lines)", changes / 10), |
| 160 |
2_000, |
| 161 |
|| repo.blame("hot.txt").unwrap(), |
| 162 |
); |
| 163 |
assert_eq!(blame.len(), (changes - 1) / 10); |
| 164 |
time(&mut rows, "blame: ordinary file", 1_000, || { |
| 165 |
repo.blame(&file_name(1)).unwrap() |
| 166 |
}); |
| 167 |
time(&mut rows, "checkpoint", 10_000, || { |
| 168 |
repo.checkpoint().unwrap() |
| 169 |
}); |
| 170 |
time(&mut rows, "status after checkpoint", 2_000, || { |
| 171 |
repo.snapshot("oxy status").unwrap() |
| 172 |
}); |
| 173 |
|
| 174 |
|
| 175 |
let origin = root.join("origin"); |
| 176 |
Repo::init_bare_unbound(&origin).unwrap(); |
| 177 |
let mut repo = repo; |
| 178 |
repo.config |
| 179 |
.set("remote.origin.url", &origin.to_string_lossy()) |
| 180 |
.unwrap(); |
| 181 |
repo.save_config().unwrap(); |
| 182 |
time( |
| 183 |
&mut rows, |
| 184 |
"push: everything to an empty remote", |
| 185 |
60_000, |
| 186 |
|| assert!(repo.push("origin", false).unwrap().iter().all(|r| r.ok)), |
| 187 |
); |
| 188 |
time(&mut rows, "push: up to date", 1_000, || { |
| 189 |
repo.push("origin", false).unwrap() |
| 190 |
}); |
| 191 |
let clone = time( |
| 192 |
&mut rows, |
| 193 |
"clone: everything, with checkout", |
| 194 |
60_000, |
| 195 |
|| Repo::clone(&origin.to_string_lossy(), &root.join("clone")).unwrap(), |
| 196 |
); |
| 197 |
assert_eq!( |
| 198 |
clone.bookmark_heads().unwrap(), |
| 199 |
repo.bookmark_heads().unwrap() |
| 200 |
); |
| 201 |
let rep = time(&mut rows, "fsck", 60_000, || repo.fsck().unwrap()); |
| 202 |
assert_eq!(rep.errors(), 0, "{:?}", rep.problems); |
| 203 |
time(&mut rows, "gc", 60_000, || repo.gc(true).unwrap()); |
| 204 |
time(&mut rows, "status after gc", 2_000, || { |
| 205 |
repo.snapshot("oxy status").unwrap() |
| 206 |
}); |
| 207 |
|
| 208 |
eprintln!("\n| Operation ({files} files, {changes} changes) | Time | Threshold | |"); |
| 209 |
eprintln!("|---|---:|---:|---|"); |
| 210 |
for r in &rows { |
| 211 |
let verdict = if r.took > r.limit { "over" } else { "ok" }; |
| 212 |
eprintln!( |
| 213 |
"| {} | {:.1} ms | {} ms | {verdict} |", |
| 214 |
r.what, |
| 215 |
r.took.as_secs_f64() * 1000.0, |
| 216 |
r.limit.as_millis() |
| 217 |
); |
| 218 |
} |
| 219 |
if let Some(mib) = peak_rss_mib() { |
| 220 |
eprintln!("\nPeak resident memory of the whole run: {mib:.0} MiB"); |
| 221 |
} |
| 222 |
let slow: Vec<&Row> = rows.iter().filter(|r| r.took > r.limit).collect(); |
| 223 |
assert!( |
| 224 |
slow.is_empty(), |
| 225 |
"over threshold: {:?}", |
| 226 |
slow.iter().map(|r| (&r.what, r.took)).collect::<Vec<_>>() |
| 227 |
); |
| 228 |
} |