dr-decode exposes four entry points rather than one decode, because callers differ sharply in what they need (ARCH §3.2): culling wants a preview, the grid wants metadata, only develop and export touch sensor data. Fusing them forces a full decode where a header read suffices, which is why Lightroom stalls ~2s per image while culling. Smoke-tested against 1,852 real Canon CR2 files (EOS 6D, ~27MB each): metadata 0.2ms from a 256KB header read, no full decode preview ~250ms 5472x3648, downscaled to 2048 for display jpeg 3.0ms Two findings worth recording: rawler 0.7.2's CR2 decoder implements only full_image; thumbnail_image and preview_image are unimplemented trait defaults returning None. So every rung of the preview ladder resolves to a full-resolution decode at ~250ms — 5x over NFR-P13's 50ms budget. CR2 does carry smaller IFDs, so the fix is our own IFD walk or an upstream contribution. The ladder is written now so that fixing it is a decoder change, not a change to every caller. Recorded in milestone-v0.1 risks. Preview.downscale_to bounds memory: a 5472x3648 RGBA preview is 79.8MB, which exhausts a phone's budget after a handful of images. Box-filtered so downscaled thumbnails do not alias. Also fixed a RefCell double-borrow that panicked on first navigation — `*x.borrow_mut() = *x.borrow() + 1` holds both borrows at once. Verified with 10,000 programmatic navigations. 58 tests passing. Traceability 20.3% (29/143).
147 lines
3.9 KiB
Rust
147 lines
3.9 KiB
Rust
//! Smoke test against real RAW files.
|
|
//!
|
|
//! cargo run -p dr-decode --example smoke -- <file-or-dir>...
|
|
//!
|
|
//! Reports, per file, what each entry point costs — which is the whole reason
|
|
//! they are separate (ARCH §3.2).
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Instant;
|
|
|
|
fn main() {
|
|
env_logger::init();
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
if args.is_empty() {
|
|
eprintln!("usage: smoke <file-or-dir>...");
|
|
std::process::exit(2);
|
|
}
|
|
|
|
let mut files = Vec::new();
|
|
for a in &args {
|
|
let p = PathBuf::from(a);
|
|
if p.is_dir() {
|
|
collect(&p, &mut files);
|
|
} else {
|
|
files.push(p);
|
|
}
|
|
}
|
|
files.sort();
|
|
files.truncate(8);
|
|
|
|
println!(
|
|
"{:<20} {:>7} {:>8} {:>9} {:>13} {:>9} {:>13}",
|
|
"file", "size", "meta", "thumb", "thumb dims", "full", "full dims"
|
|
);
|
|
println!("{}", "-".repeat(88));
|
|
|
|
let (mut ok, mut failed) = (0, 0);
|
|
for f in &files {
|
|
match run_one(f) {
|
|
Ok(line) => {
|
|
println!("{line}");
|
|
ok += 1;
|
|
}
|
|
Err(e) => {
|
|
println!("{:<22} {e}", truncate(&name(f), 22));
|
|
failed += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
println!("\n{ok} ok, {failed} failed");
|
|
if failed > 0 {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn run_one(path: &Path) -> Result<String, String> {
|
|
let size = std::fs::metadata(path).map_err(|e| e.to_string())?.len();
|
|
|
|
// The culling path: read only the header region, not the whole file.
|
|
let probe_bytes =
|
|
read_prefix(path, dr_decode::PREVIEW_PROBE_BYTES).map_err(|e| e.to_string())?;
|
|
let t0 = Instant::now();
|
|
let fmt = dr_decode::probe(&probe_bytes);
|
|
let meta = dr_decode::metadata(&probe_bytes).ok();
|
|
let meta_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
|
|
|
let all = std::fs::read(path).map_err(|e| e.to_string())?;
|
|
|
|
// The culling rung.
|
|
let t1 = Instant::now();
|
|
let thumb = dr_decode::extract_preview(&all, dr_decode::PreviewSize::Thumbnail)
|
|
.map_err(|e| format!("thumb: {e}"))?;
|
|
let thumb_ms = t1.elapsed().as_secs_f64() * 1000.0;
|
|
|
|
// The full-resolution rung, for comparison.
|
|
let t2 = Instant::now();
|
|
let full = dr_decode::extract_preview(&all, dr_decode::PreviewSize::Full)
|
|
.map_err(|e| format!("full: {e}"))?;
|
|
let full_ms = t2.elapsed().as_secs_f64() * 1000.0;
|
|
|
|
let model = meta
|
|
.as_ref()
|
|
.and_then(|m| m.model.clone())
|
|
.unwrap_or_else(|| "?".into());
|
|
let budget = if thumb_ms <= 50.0 {
|
|
""
|
|
} else {
|
|
" OVER BUDGET"
|
|
};
|
|
|
|
Ok(format!(
|
|
"{:<20} {:>6.1}M {:>6.1}ms {:>7.1}ms {:>7}x{:<5} {:>7.1}ms {:>7}x{:<5} {:?} {}{}",
|
|
truncate(&name(path), 20),
|
|
size as f64 / 1e6,
|
|
meta_ms,
|
|
thumb_ms,
|
|
thumb.width,
|
|
thumb.height,
|
|
full_ms,
|
|
full.width,
|
|
full.height,
|
|
fmt,
|
|
model.trim(),
|
|
budget,
|
|
))
|
|
}
|
|
|
|
fn read_prefix(path: &Path, n: u64) -> std::io::Result<Vec<u8>> {
|
|
use std::io::Read;
|
|
let mut f = std::fs::File::open(path)?;
|
|
let mut buf = vec![0u8; n as usize];
|
|
let read = f.read(&mut buf)?;
|
|
buf.truncate(read);
|
|
Ok(buf)
|
|
}
|
|
|
|
fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
|
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
return;
|
|
};
|
|
for e in entries.flatten() {
|
|
let p = e.path();
|
|
if p.is_file() {
|
|
let ext = p
|
|
.extension()
|
|
.map(|s| s.to_string_lossy().to_ascii_lowercase())
|
|
.unwrap_or_default();
|
|
if dr_types::Format::from_extension(&ext).is_some() {
|
|
out.push(p);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn name(p: &Path) -> String {
|
|
p.file_name().unwrap_or_default().to_string_lossy().into()
|
|
}
|
|
|
|
fn truncate(s: &str, n: usize) -> String {
|
|
if s.len() <= n {
|
|
s.to_string()
|
|
} else {
|
|
format!("{}…", &s[..n - 1])
|
|
}
|
|
}
|