Files
DarkRoom/core/dr-decode/examples/smoke.rs
T
dtourolle 9717e59909 Add RAW decode and a working image viewer
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).
2026-08-09 08:28:26 +02:00

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])
}
}