use std::{
fs, io,
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant},
};
use djvu_rs::{
djvu_document::{DjVuPage, MmapDocument},
djvu_render::{RenderOptions, render_pixmap},
};
#[cfg(target_os = "macos")]
use std::os::unix::io::AsRawFd;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut file = PathBuf::from("tests/corpus/pathogenic_bacteria_1896.djvu");
let mut mode = "validate".to_string();
let mut iters = 9usize;
let mut page_idx = 0usize;
let mut dwell_ms = 150u64;
let mut delay_ms = 0u64;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--file" => file = PathBuf::from(args.next().ok_or("--file requires a value")?),
"--mode" => mode = args.next().ok_or("--mode requires a value")?,
"--iters" => iters = args.next().ok_or("--iters requires a value")?.parse()?,
"--page" => page_idx = args.next().ok_or("--page requires a value")?.parse()?,
"--dwell-ms" => dwell_ms = args.next().ok_or("--dwell-ms requires a value")?.parse()?,
"--delay-ms" => delay_ms = args.next().ok_or("--delay-ms requires a value")?.parse()?,
other => return Err(format!("unknown argument: {other}").into()),
}
}
if !file.exists() {
return Err(format!(
"corpus file not found: {} (see SOURCES.md / tests/corpus)",
file.display()
)
.into());
}
match mode.as_str() {
"validate" => validate_cold_vs_warm(&file, page_idx, iters)?,
"madvise" => madvise_lever(&file, page_idx, iters, delay_ms)?,
"prefetch" => prefetch_lever(&file, page_idx, iters, dwell_ms)?,
other => {
return Err(format!("unknown --mode {other} (want validate|madvise|prefetch)").into());
}
}
Ok(())
}
fn write_cold(dst: &Path, data: &[u8]) -> io::Result<()> {
let file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(dst)?;
disable_cache(&file);
{
use std::io::Write;
let mut w = &file;
w.write_all(data)?;
w.flush()?;
}
file.sync_all()?;
Ok(())
}
fn make_cold_copy(src: &Path, dir: &Path, tag: u64) -> io::Result<PathBuf> {
let data = fs::read(src)?;
let dst = dir.join(format!("cold_open_bench_{tag}.djvu"));
write_cold(&dst, &data)?;
Ok(dst)
}
#[cfg(target_os = "macos")]
fn disable_cache(file: &fs::File) {
let fd = file.as_raw_fd();
unsafe {
libc::fcntl(fd, libc::F_NOCACHE, 1);
}
}
#[cfg(not(target_os = "macos"))]
fn disable_cache(_file: &fs::File) {}
fn median(samples: &mut [f64]) -> f64 {
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = samples.len();
if n == 0 {
return f64::NAN;
}
if n % 2 == 1 {
samples[n / 2]
} else {
(samples[n / 2 - 1] + samples[n / 2]) / 2.0
}
}
fn mad(samples: &[f64], med: f64) -> f64 {
let mut dev: Vec<f64> = samples.iter().map(|s| (s - med).abs()).collect();
median(&mut dev)
}
fn report(label: &str, mut samples_ms: Vec<f64>) -> f64 {
let med = median(&mut samples_ms);
let d = mad(&samples_ms, med);
let min = samples_ms.iter().cloned().fold(f64::INFINITY, f64::min);
let max = samples_ms.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
println!(
"{label:<24} median={med:>9.3} ms MAD={d:>7.3} ms min={min:>9.3} max={max:>9.3} n={}",
samples_ms.len()
);
med
}
fn native_opts(page: &DjVuPage) -> RenderOptions {
RenderOptions {
width: page.width() as u32,
height: page.height() as u32,
..RenderOptions::default()
}
}
fn render_native(page: &DjVuPage) -> Result<(), Box<dyn std::error::Error>> {
let opts = native_opts(page);
let _pm = render_pixmap(page, &opts)?;
Ok(())
}
fn open_and_render(
path: &Path,
page_idx: usize,
advise: bool,
delay_ms: u64,
) -> Result<Duration, Box<dyn std::error::Error>> {
let start = Instant::now();
let doc = MmapDocument::open(path)?;
if advise {
#[cfg(unix)]
{
let _ = doc.advise_page_willneed(page_idx);
}
}
if delay_ms > 0 {
std::thread::sleep(Duration::from_millis(delay_ms));
}
let page = doc.page(page_idx)?;
render_native(page)?;
Ok(start.elapsed())
}
fn validate_cold_vs_warm(
file: &Path,
page_idx: usize,
iters: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = std::env::temp_dir().join("djvu_cold_open_bench");
fs::create_dir_all(&tmp_dir)?;
println!("=== validate: cold (F_NOCACHE fresh copy) vs warm (page-cache hit) ===");
println!(
"file={} page={page_idx} iters={iters} (harness sanity check — cold should be clearly > warm)",
file.display()
);
let _ = open_and_render(file, page_idx, false, 0)?;
let mut warm = Vec::with_capacity(iters);
for _ in 0..iters {
warm.push(open_and_render(file, page_idx, false, 0)?.as_secs_f64() * 1000.0);
}
let mut cold = Vec::with_capacity(iters);
for i in 0..iters {
let copy_path = make_cold_copy(file, &tmp_dir, i as u64)?;
let d = open_and_render(©_path, page_idx, false, 0)?;
cold.push(d.as_secs_f64() * 1000.0);
let _ = fs::remove_file(©_path);
}
let warm_med = report("warm (same path, hot)", warm);
let cold_med = report("cold (F_NOCACHE copy)", cold);
let ratio = cold_med / warm_med.max(1e-9);
println!("cold/warm ratio: {ratio:.2}x");
if ratio > 1.3 {
println!("VERDICT: harness shows a clear cold >> warm gap — trust the other modes.");
} else {
println!(
"VERDICT: gap is weak/inconclusive on this machine — cross-check with `sudo purge` \
(macOS) + `--mode validate --iters 1` before trusting madvise/prefetch deltas."
);
}
Ok(())
}
fn madvise_lever(
file: &Path,
page_idx: usize,
iters: usize,
delay_ms: u64,
) -> Result<(), Box<dyn std::error::Error>> {
let tmp_dir = std::env::temp_dir().join("djvu_cold_open_bench");
fs::create_dir_all(&tmp_dir)?;
println!("=== B6 madvise(WILLNEED): cold open -> first render, page {page_idx} ===");
println!("file={} iters={iters} delay_ms={delay_ms}", file.display());
let mut no_advise = Vec::with_capacity(iters);
let mut with_advise = Vec::with_capacity(iters);
for i in 0..iters {
let p1 = make_cold_copy(file, &tmp_dir, i as u64 * 2)?;
let d1 = open_and_render(&p1, page_idx, false, delay_ms)?;
no_advise.push(d1.as_secs_f64() * 1000.0);
let _ = fs::remove_file(&p1);
let p2 = make_cold_copy(file, &tmp_dir, i as u64 * 2 + 1)?;
let d2 = open_and_render(&p2, page_idx, true, delay_ms)?;
with_advise.push(d2.as_secs_f64() * 1000.0);
let _ = fs::remove_file(&p2);
}
let base_med = report("no madvise", no_advise);
let hint_med = report("madvise(WILLNEED)", with_advise);
let delta = (base_med - hint_med) / base_med.max(1e-9) * 100.0;
println!("delta: {delta:+.1}% ({}ms -> {}ms)", base_med, hint_med);
Ok(())
}
fn prefetch_lever(
file: &Path,
page_idx: usize,
iters: usize,
dwell_ms: u64,
) -> Result<(), Box<dyn std::error::Error>> {
let _ = fs::read(file)?;
let next_idx = page_idx + 1;
println!("=== B7 prefetch_page: render latency of page {next_idx} after page {page_idx} ===");
println!("file={} iters={iters} dwell_ms={dwell_ms}", file.display());
let mut no_prefetch = Vec::with_capacity(iters);
let mut with_prefetch = Vec::with_capacity(iters);
for _ in 0..iters {
let doc = Arc::new(MmapDocument::open(file)?.into_document());
if next_idx >= doc.page_count() {
return Err("--page + 1 is out of range for this corpus".into());
}
render_native(doc.page(page_idx)?)?;
std::thread::sleep(Duration::from_millis(dwell_ms));
let start = Instant::now();
render_native(doc.page(next_idx)?)?;
no_prefetch.push(start.elapsed().as_secs_f64() * 1000.0);
}
for _ in 0..iters {
let doc = Arc::new(MmapDocument::open(file)?.into_document());
render_native(doc.page(page_idx)?)?;
doc.prefetch_page(next_idx);
std::thread::sleep(Duration::from_millis(dwell_ms));
let start = Instant::now();
render_native(doc.page(next_idx)?)?;
with_prefetch.push(start.elapsed().as_secs_f64() * 1000.0);
}
let base_med = report("no prefetch", no_prefetch);
let pf_med = report("prefetch_page", with_prefetch);
let delta = (base_med - pf_med) / base_med.max(1e-9) * 100.0;
println!("delta: {delta:+.1}% ({}ms -> {}ms)", base_med, pf_med);
Ok(())
}