Skip to content

Commit 3ef4950

Browse files
authored
feat: multi-size benchmarks (50MB, 200MB, 500MB) (#8)
## Summary - Run read benchmarks on 3 file sizes (50MB, 200MB, 500MB) instead of just 50MB - All files uploaded to a single bucket, single mount per backend - One comparison table per size in the output - Write benchmark unchanged (500MB) - CI posts all tables as PR comment
1 parent 2719b79 commit 3ef4950

4 files changed

Lines changed: 309 additions & 354 deletions

File tree

.github/workflows/ci.yml

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ concurrency:
1010
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
1111
cancel-in-progress: true
1212

13+
permissions:
14+
contents: read
15+
pull-requests: write
16+
1317
env:
1418
CARGO_TERM_COLOR: always
1519

@@ -101,30 +105,47 @@ jobs:
101105
- name: Build release binaries
102106
run: cargo build --release --features nfs
103107

108+
- name: Install fio
109+
run: sudo apt-get install -y fio
110+
104111
- name: Run benchmarks
105-
run: cargo test --release --test bench -- --nocapture 2>&1 | tee bench_output.txt
112+
timeout-minutes: 10
113+
run: cargo test --release --test bench --test fio_bench -- --nocapture 2>&1 | tee bench_output.txt
106114

107115
- name: Post benchmark results
108116
if: github.event_name == 'pull_request'
109117
env:
110118
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
111119
run: |
112-
# Extract the benchmark table from output
113-
TABLE=$(sed -n '/^ =====/,/^ =====/p' bench_output.txt)
114-
if [ -z "$TABLE" ]; then
120+
# Strip ANSI codes, then extract only ==== delimited blocks
121+
CLEAN=$(sed 's/\x1b\[[0-9;]*m//g' bench_output.txt)
122+
TABLES=$(echo "$CLEAN" | awk '
123+
/^=====*$/ { inside=!inside; print; next }
124+
inside { print }
125+
')
126+
if [ -z "$TABLES" ]; then
115127
echo "No benchmark table found in output"
116128
exit 0
117129
fi
118130
119-
# Build markdown comment
120-
BODY=$(cat <<'HEADER'
131+
PR="${{ github.event.pull_request.number }}"
132+
MARKER="<!-- bench-results -->"
133+
BODY="${MARKER}
121134
## Benchmark Results
122135
123-
```
124-
HEADER
125-
)
126-
BODY="${BODY}
127-
${TABLE}
136+
\`\`\`
137+
${TABLES}
128138
\`\`\`"
129139
130-
gh pr comment "${{ github.event.pull_request.number }}" --body "$BODY"
140+
# Find existing comment with our marker
141+
COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR}/comments" \
142+
--jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" | head -1)
143+
144+
if [ -n "$COMMENT_ID" ]; then
145+
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
146+
-X PATCH -f body="$BODY"
147+
echo "Updated existing comment $COMMENT_ID"
148+
else
149+
gh pr comment "$PR" --body "$BODY"
150+
echo "Created new comment"
151+
fi

tests/bench.rs

Lines changed: 114 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,73 @@
11
mod common;
22

3-
const FILE_SIZE: usize = 50 * 1024 * 1024; // 50 MB
4-
const WRITE_SIZE: usize = 500 * 1024 * 1024; // 500 MB for write benchmarks
3+
const BENCH_SIZES: &[(usize, &str)] = &[
4+
(50 * 1024 * 1024, "50MB"),
5+
(200 * 1024 * 1024, "200MB"),
6+
(500 * 1024 * 1024, "500MB"),
7+
];
58

69
#[tokio::test]
710
async fn test_bench() {
8-
let test_filename = format!("bench_{}.bin", std::process::id());
9-
let expected = common::generate_pattern(FILE_SIZE);
10-
11-
let (token, bucket_id, hub) = match common::setup_bucket_with_file("bench", &test_filename, &expected).await {
11+
// Upload files of each size to the same bucket
12+
let (token, bucket_id, hub) = match common::setup_bucket("bench").await {
1213
Some(cfg) => cfg,
1314
None => return,
1415
};
1516

17+
let mut files: Vec<(String, Vec<u8>)> = Vec::new();
18+
for &(size, label) in BENCH_SIZES {
19+
let filename = format!("bench_{}.bin", label);
20+
let data = common::generate_pattern(size);
21+
let write_config = common::build_write_config(&hub).await;
22+
23+
let tmp_dir = std::env::temp_dir().join(format!("hf-mount-bench-setup-{}", label));
24+
std::fs::create_dir_all(&tmp_dir).ok();
25+
let staging_path = tmp_dir.join(&filename);
26+
std::fs::write(&staging_path, &data).expect("write staging file");
27+
28+
let file_info = common::upload_file(write_config, &staging_path).await;
29+
let xet_hash = file_info.hash().to_string();
30+
eprintln!("Uploaded {}: xet_hash={}", label, xet_hash);
31+
32+
let mtime_ms = std::time::SystemTime::now()
33+
.duration_since(std::time::UNIX_EPOCH)
34+
.unwrap_or_default()
35+
.as_millis() as u64;
36+
37+
hub.batch_operations(&[hf_mount::hub_api::BatchOp::AddFile {
38+
path: filename.clone(),
39+
xet_hash,
40+
mtime: mtime_ms,
41+
content_type: None,
42+
}])
43+
.await
44+
.expect("batch add failed");
45+
46+
std::fs::remove_dir_all(&tmp_dir).ok();
47+
files.push((filename, data));
48+
}
49+
1650
let pid = std::process::id();
1751

18-
// --- FUSE benchmark (read + write) ---
52+
// --- FUSE benchmark (read + write per size) ---
1953
let fuse_mount = format!("/tmp/hf-bench-fuse-{}", pid);
2054
let fuse_cache = format!("/tmp/hf-bench-fuse-cache-{}", pid);
2155

2256
let fuse_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2357
let child = common::mount_bucket(&bucket_id, &fuse_mount, &fuse_cache, &[]);
24-
let r = common::bench::run_read_benchmarks(&fuse_mount, &test_filename, &expected);
2558

26-
// Write benchmark: 50 MB sequential write
27-
let write_result = common::bench::run_write_benchmark(&fuse_mount, WRITE_SIZE);
59+
let mut reads = Vec::new();
60+
for (filename, expected) in &files {
61+
reads.push(common::bench::run_read_benchmarks(&fuse_mount, filename, expected));
62+
}
63+
64+
let mut writes = Vec::new();
65+
for &(size, _) in BENCH_SIZES {
66+
writes.push(common::bench::run_write_benchmark(&fuse_mount, size));
67+
}
2868

2969
common::unmount(&fuse_mount, child, 60);
30-
(r, write_result)
70+
(reads, writes)
3171
}));
3272

3373
std::fs::remove_dir_all(&fuse_mount).ok();
@@ -39,161 +79,87 @@ async fn test_bench() {
3979

4080
let nfs_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4181
let child = common::mount_bucket_nfs(&bucket_id, &nfs_mount, &nfs_cache, &["--read-only"]);
42-
let r = common::bench::run_read_benchmarks(&nfs_mount, &test_filename, &expected);
82+
let mut reads = Vec::new();
83+
for (filename, expected) in &files {
84+
reads.push(common::bench::run_read_benchmarks(&nfs_mount, filename, expected));
85+
}
4386
common::unmount_nfs(&nfs_mount, child, 5);
44-
r
87+
reads
4588
}));
4689

4790
std::fs::remove_dir_all(&nfs_mount).ok();
4891
std::fs::remove_dir_all(&nfs_cache).ok();
4992

50-
// --- mount-s3 reference benchmark (read only, requires mount-s3 + AWS creds) ---
51-
let s3_result = if common::s3_bench_available() {
52-
let s3_bucket = common::setup_s3_bench(&expected);
53-
let s3_mount = format!("/tmp/hf-bench-s3-{}", pid);
54-
55-
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
56-
let child = common::mount_s3(&s3_bucket, &s3_mount);
57-
let r = common::bench::run_read_benchmarks(&s3_mount, "bench_file.bin", &expected);
58-
common::cleanup_s3_bench(&s3_mount, child, &s3_bucket);
59-
r
60-
}));
61-
62-
match r {
63-
Ok(Ok(bench)) => Some(bench),
64-
Ok(Err(e)) => {
65-
eprintln!("mount-s3 bench error: {}", e);
66-
None
67-
}
68-
Err(e) => std::panic::resume_unwind(e),
69-
}
70-
} else {
71-
eprintln!("Skipping mount-s3 bench (mount-s3 binary or AWS creds not available)");
72-
None
73-
};
74-
75-
// --- Raw cleaner benchmark (no FUSE, no kernel) ---
76-
let raw_mbps = {
77-
let write_config = common::build_write_config(&hub).await;
78-
let download_session = data::FileDownloadSession::new(write_config.clone(), None, None)
79-
.await
80-
.expect("download session");
81-
let xet_sessions = hf_mount::xet::XetSessions::new(download_session, Some(write_config));
82-
match common::bench::run_raw_cleaner_benchmark(&xet_sessions, WRITE_SIZE).await {
83-
Ok(mbps) => Some(mbps),
84-
Err(e) => {
85-
eprintln!("Raw cleaner bench error: {}", e);
86-
None
87-
}
88-
}
89-
};
90-
91-
// Cleanup bucket
93+
// Cleanup
9294
common::delete_bucket(&common::endpoint(), &token, &bucket_id).await;
9395

9496
// --- Extract results ---
95-
let (fuse, write) = match fuse_result {
96-
Ok((Ok(r), w)) => (Some(r), w),
97-
Ok((Err(e), _)) => {
98-
eprintln!("FUSE bench error: {}", e);
99-
(None, Err(e))
100-
}
97+
let (fuse_reads, fuse_writes) = match fuse_result {
98+
Ok((reads, writes)) => (reads, writes),
10199
Err(e) => std::panic::resume_unwind(e),
102100
};
103101

104-
let nfs = match nfs_result {
105-
Ok(Ok(r)) => Some(r),
106-
Ok(Err(e)) => {
107-
eprintln!("NFS bench error: {}", e);
108-
None
109-
}
102+
let nfs_reads = match nfs_result {
103+
Ok(reads) => reads,
110104
Err(e) => std::panic::resume_unwind(e),
111105
};
112106

113-
// --- Print comparison table ---
114-
eprintln!("\n============================================================");
115-
eprintln!(" FUSE vs NFS vs mount-s3 Benchmark — 50 MB file");
116-
eprintln!("============================================================");
117-
eprintln!(" {:30} {:>12} {:>12} {:>12}", "Metric", "FUSE", "NFS", "mount-s3");
118-
eprintln!(" {:-<30} {:-<12} {:-<12} {:-<12}", "", "", "", "");
119-
120-
if let (Some(f), Some(n)) = (&fuse, &nfs) {
121-
let s3_read = s3_result.as_ref().map(|s| format!("{:>9.1} MB/s", s.seq_read_mbps));
122-
let s3_reread = s3_result.as_ref().map(|s| format!("{:>9.1} MB/s", s.seq_reread_mbps));
123-
let s3_range = s3_result.as_ref().map(|s| format!("{:>9.1} ms", s.range_read_ms));
124-
let s3_rand = s3_result.as_ref().map(|s| format!("{:>9.1} ms", s.random_avg_ms));
125-
let dash = " —".to_string();
126-
127-
eprintln!(
128-
" {:30} {:>9.1} MB/s {:>9.1} MB/s {:>12}",
129-
"Sequential read",
130-
f.seq_read_mbps,
131-
n.seq_read_mbps,
132-
s3_read.as_deref().unwrap_or(&dash)
133-
);
134-
eprintln!(
135-
" {:30} {:>9.1} MB/s {:>9.1} MB/s {:>12}",
136-
"Sequential re-read",
137-
f.seq_reread_mbps,
138-
n.seq_reread_mbps,
139-
s3_reread.as_deref().unwrap_or(&dash)
140-
);
141-
eprintln!(
142-
" {:30} {:>9.1} ms {:>9.1} ms {:>12}",
143-
"Range read (1MB@25MB)",
144-
f.range_read_ms,
145-
n.range_read_ms,
146-
s3_range.as_deref().unwrap_or(&dash)
147-
);
148-
eprintln!(
149-
" {:30} {:>9.1} ms {:>9.1} ms {:>12}",
150-
"Random reads (100x4KB avg)",
151-
f.random_avg_ms,
152-
n.random_avg_ms,
153-
s3_rand.as_deref().unwrap_or(&dash)
154-
);
155-
let f_speedup = if f.neg_cache_second_ms > 0.0 {
156-
f.neg_cache_first_ms / f.neg_cache_second_ms
157-
} else {
158-
f64::INFINITY
159-
};
160-
let n_speedup = if n.neg_cache_second_ms > 0.0 {
161-
n.neg_cache_first_ms / n.neg_cache_second_ms
107+
// --- Print tables per size ---
108+
for (i, &(_, label)) in BENCH_SIZES.iter().enumerate() {
109+
let fuse = fuse_reads[i].as_ref().ok();
110+
let nfs = nfs_reads[i].as_ref().ok();
111+
let write = fuse_writes[i].as_ref().ok();
112+
113+
eprintln!("\n============================================================");
114+
eprintln!(" Benchmark — {}", label);
115+
eprintln!("------------------------------------------------------------");
116+
eprintln!(" {:30} {:>12} {:>12}", "Metric", "FUSE", "NFS");
117+
eprintln!(" {:-<30} {:-<12} {:-<12}", "", "", "");
118+
119+
if let (Some(f), Some(n)) = (fuse, nfs) {
120+
eprintln!(
121+
" {:30} {:>9.1} MB/s {:>9.1} MB/s",
122+
"Sequential read", f.seq_read_mbps, n.seq_read_mbps
123+
);
124+
eprintln!(
125+
" {:30} {:>9.1} MB/s {:>9.1} MB/s",
126+
"Sequential re-read", f.seq_reread_mbps, n.seq_reread_mbps
127+
);
128+
eprintln!(
129+
" {:30} {:>9.1} ms {:>9.1} ms",
130+
"Range read (1MB@25MB)", f.range_read_ms, n.range_read_ms
131+
);
132+
eprintln!(
133+
" {:30} {:>9.1} ms {:>9.1} ms",
134+
"Random reads (100x4KB avg)", f.random_avg_ms, n.random_avg_ms
135+
);
162136
} else {
163-
f64::INFINITY
164-
};
165-
let s3_speedup = s3_result.as_ref().map(|s| {
166-
if s.neg_cache_second_ms > 0.0 {
167-
format!("{:>9.1}x", s.neg_cache_first_ms / s.neg_cache_second_ms)
168-
} else {
169-
" inf".to_string()
137+
if fuse.is_none() {
138+
eprintln!(" FUSE read: FAILED");
170139
}
171-
});
172-
eprintln!(
173-
" {:30} {:>9.1}x {:>9.1}x {:>12}",
174-
"Neg cache speedup",
175-
f_speedup,
176-
n_speedup,
177-
s3_speedup.as_deref().unwrap_or(&dash)
178-
);
179-
}
140+
if nfs.is_none() {
141+
eprintln!(" NFS read: FAILED");
142+
}
143+
}
180144

181-
if let Ok(ref w) = write {
182-
eprintln!(" {:30} {:>9.1} MB/s", "Sequential write (500MB)", w.write_mbps);
183-
eprintln!(" {:30} {:>9.3} s", "Close latency (CAS+Hub)", w.close_secs);
184-
eprintln!(" {:30} {:>9.1} MB/s", "Write end-to-end", w.total_mbps);
185-
eprintln!(" {:30} {:>9.1} MB/s", "Dedup write (same data)", w.dedup_write_mbps);
186-
eprintln!(" {:30} {:>9.3} s", "Dedup close latency", w.dedup_close_secs);
187-
eprintln!(" {:30} {:>9.1} MB/s", "Dedup end-to-end", w.dedup_total_mbps);
188-
} else {
189-
eprintln!(" {:30} {:>12}", "Write benchmark", "FAIL");
190-
}
191-
if let Some(raw) = raw_mbps {
192-
eprintln!(" {:30} {:>9.1} MB/s", "Raw add_data() (no FUSE)", raw);
145+
if let Some(w) = write {
146+
eprintln!(" {:30} {:>9.1} MB/s", "Sequential write (FUSE)", w.write_mbps);
147+
eprintln!(" {:30} {:>9.3} s", "Close latency (CAS+Hub)", w.close_secs);
148+
eprintln!(" {:30} {:>9.1} MB/s", "Write end-to-end", w.total_mbps);
149+
eprintln!(" {:30} {:>9.1} MB/s", "Dedup write", w.dedup_write_mbps);
150+
eprintln!(" {:30} {:>9.3} s", "Dedup close latency", w.dedup_close_secs);
151+
eprintln!(" {:30} {:>9.1} MB/s", "Dedup end-to-end", w.dedup_total_mbps);
152+
} else {
153+
eprintln!(" Write: FAILED");
154+
}
155+
eprintln!("============================================================");
193156
}
194-
eprintln!("============================================================\n");
157+
eprintln!();
195158

196-
assert!(fuse.is_some(), "FUSE benchmark failed");
197-
assert!(nfs.is_some(), "NFS benchmark failed");
198-
write.expect("Write benchmark failed");
159+
// Assertions
160+
for (i, &(_, label)) in BENCH_SIZES.iter().enumerate() {
161+
assert!(fuse_reads[i].is_ok(), "FUSE read benchmark failed for {}", label);
162+
assert!(nfs_reads[i].is_ok(), "NFS read benchmark failed for {}", label);
163+
assert!(fuse_writes[i].is_ok(), "Write benchmark failed for {}", label);
164+
}
199165
}

0 commit comments

Comments
 (0)