use std::time::Instant;
#[derive(Clone, Copy)]
enum ReadMode {
Fixed(usize),
TwoStage,
}
fn main() {
let n: usize = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(10_000);
let thumb: Vec<u8> = (0..6306).map(|i: usize| u8::try_from(i.wrapping_mul(37) % 251).unwrap_or(0)).collect();
let key = omy_core::crypto::SecretKey::from_bytes(core::array::from_fn(|i| {
u8::try_from(i).unwrap_or(0).wrapping_mul(7) ^ 0x3D
}));
let keks = [omy_core::crypto::Kek::from_key(key)];
let salt = [0x11u8; 16];
let small = build_corpus("omy-bench-small", n, 16, &thumb, &keks, &salt);
let big = build_corpus("omy-bench-big", 200, 4 << 20, &thumb, &keks, &salt);
for (label, dir, cnt) in [("小文件", &small, n), ("4 MiB 文件", &big, 200usize)] {
println!("\n=== {label} x{cnt} ===");
for mode in [
ReadMode::Fixed(32 * 1024),
ReadMode::Fixed(1 << 20),
ReadMode::TwoStage,
] {
let (hit, elapsed, maxlen) = measure(dir, cnt, mode, &keks);
let per_ms = elapsed.as_secs_f64() / cnt as f64 * 1000.0;
let total_10k_s = per_ms * 10.0;
let name = match mode {
ReadMode::Fixed(c) => format!("固定 {} KiB", c.saturating_div(1024)),
ReadMode::TwoStage => String::from("两段读"),
};
let verdict = if total_10k_s < 5.0 { "预算内" } else { "**超预算**" };
println!(
" {name:<14} {hit}/{cnt} 命中 {per_ms:.3} ms/文件 \
推算 10000 个 {total_10k_s:.2} s {verdict}"
);
if matches!(mode, ReadMode::TwoStage) {
println!(" (实际 header_len 最大 {maxlen} 字节)");
}
}
}
let _ = std::fs::remove_dir_all(&small);
let _ = std::fs::remove_dir_all(&big);
}
fn build_corpus(
name: &str,
count: usize,
payload_len: usize,
thumb: &[u8],
keks: &[omy_core::crypto::Kek],
salt: &[u8; 16],
) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(name);
let _ = std::fs::remove_dir_all(&dir);
if std::fs::create_dir_all(&dir).is_err() {
return dir;
}
let payload: Vec<u8> = (0..payload_len)
.map(|i: usize| u8::try_from(i.wrapping_mul(31) % 251).unwrap_or(0))
.collect();
println!("正在造 {count} 个文件(载荷 {payload_len} 字节)……");
let t = Instant::now();
for i in 0..count {
let opts = omy_core::file::EncryptOptions {
filename: Some(format!("f{i}.png")),
thumbnail: Some(thumb.to_vec()),
media_meta: Some(br#"{"container":"png_pipe"}"#.to_vec()),
..omy_core::file::EncryptOptions::default()
};
let rnd = omy_core::file::RandomMaterial::generate();
if let Ok(enc) = omy_core::file::encrypt(&payload, keks, salt, &opts, &rnd) {
let _ = std::fs::write(dir.join(format!("f{i:05}.omy")), &enc.bytes);
}
let _ = std::fs::read(dir.join(format!("f{i:05}.omy")));
}
println!(" 建库耗时 {:?}", t.elapsed());
dir
}
fn measure(
dir: &std::path::Path,
count: usize,
mode: ReadMode,
keks: &[omy_core::crypto::Kek],
) -> (usize, std::time::Duration, usize) {
let t = Instant::now();
let mut hit = 0usize;
let mut maxlen = 0usize;
for i in 0..count {
let p = dir.join(format!("f{i:05}.omy"));
let bytes = match mode {
ReadMode::Fixed(cap) => read_prefix(&p, cap).unwrap_or_default(),
ReadMode::TwoStage => {
let (b, hl) = read_header_area(&p);
maxlen = maxlen.max(hl);
b
}
};
if omy_core::file::open(&bytes, keks).is_ok_and(|o| o.thumbnail().is_ok()) {
hit = hit.saturating_add(1);
}
}
(hit, t.elapsed(), maxlen)
}
fn read_header_area(p: &std::path::Path) -> (Vec<u8>, usize) {
let off = omy_core::header::HEADER_LEN_FIELD_OFFSET;
let head = read_prefix(p, off.saturating_add(4)).unwrap_or_default();
let Some(field) = head
.get(off..off.saturating_add(4))
.and_then(|s| <[u8; 4]>::try_from(s).ok())
else {
return (head, 0);
};
let claimed = u32::from_le_bytes(field) as usize;
let actual = std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
let cap = claimed.min(usize::try_from(actual).unwrap_or(usize::MAX));
(read_prefix(p, cap).unwrap_or_default(), claimed)
}
fn read_prefix(p: &std::path::Path, n: usize) -> std::io::Result<Vec<u8>> {
use std::io::Read as _;
let f = std::fs::File::open(p)?;
let mut buf = Vec::new();
f.take(n as u64).read_to_end(&mut buf)?;
Ok(buf)
}