OXYGENHUB
# cf1b0412f142 oxygen / crates / oxy-core / tests / bench.rs
7.0 KiB · Rust · last changed Benchmark harness and first numbers 1 month ago source blame history raw
1
//! Benchmark harness (spec §25): a repository of 10 000 files and 5 000
2
//! changes, then the everyday operations timed against thresholds.
3
//!
4
//! Not part of the ordinary test run. Run it with
5
//!
6
//!     cargo test --release -p oxy-core --test bench -- --ignored --nocapture
7
//!
8
//! `OXY_BENCH_FILES` and `OXY_BENCH_CHANGES` change the size and
9
//! `OXY_BENCH_DIR` the disk. The table it prints is what
10
//! `docs/benchmarks.md` records. Thresholds are for the default size on a
11
//! developer machine; they catch regressions of an order of magnitude,
12
//! not noise.
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
/// Peak resident memory of this process, from /proc (Linux only).
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
    // Build: one import of every file, then small changes; one file in
96
    // ten changes is the hot file, so blame has a long history to walk.
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
    // Everyday operations on the finished repository.
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
    // Sync over a local path: a full push into an empty hosted repository, then a clone.
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
}