fd5808c157
Decode JPEGs at the smallest DCT scale (1/8·1/4·1/2·1/1) whose long axis is still ≥ the largest needed thumbnail (800px), via jpeg-decoder, instead of a full-resolution decode through the image crate. The full-res bitmap — the dominant time and RAM cost — is never materialised. PNG/GIF/WebP and unusual JPEG colour spaces (CMYK / 16-bit grey) fall back to a full decode. Extracts the shared decode + EXIF-orientation logic into decode_oriented(), removing the duplication that existed between render_thumbnail_from_data and render_all_thumbnails_from_data. Measured on 14 cores (see benches/BASELINE.md): - render_all 1.8-2.0× faster (12MP 111->61ms, 48MP 398->203ms) - peak heap 5.5-14.8× lower, now decoupled from source MP (~18-25MB regardless) - saturated throughput 3-3.6× (parallel efficiency 4.9×->8.5×) - quality SSIM 0.987-0.999 (>=0.98 gate), PSNR 47-55dB Also adds the Phase 0 benchmark harness (gated behind the `bench` feature, zero prod impact): deterministic image corpus (src/bench_support.rs), criterion latency bench (benches/thumbnails.rs), and a peak-RAM/throughput/SSIM harness (examples/bench_thumbnails_mem.rs). Baseline + before/after in benches/BASELINE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
3.9 KiB
Rust
109 lines
3.9 KiB
Rust
//! Phase 0 — Task 0.2: thumbnail render latency + output-size baseline.
|
||
//!
|
||
//! Measures the CPU-bound render path (decode → EXIF orientation → resize →
|
||
//! JPEG encode) per size and for the all-sizes upload path, across the size/
|
||
//! format corpus. `Throughput::Elements(1)` makes criterion report images/sec
|
||
//! alongside ms/image. Output byte sizes (bandwidth/disk proxy) are printed once
|
||
//! as a table before the timed runs.
|
||
//!
|
||
//! Run: `cargo bench --features bench`
|
||
//! HTML report: `target/criterion/report/index.html`
|
||
|
||
use std::time::Duration;
|
||
|
||
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||
use oxicloud::bench_support::{self, CorpusCase};
|
||
use oxicloud::infrastructure::services::thumbnail_service::{ThumbnailService, ThumbnailSize};
|
||
|
||
const SIZES: [ThumbnailSize; 3] = [
|
||
ThumbnailSize::Icon,
|
||
ThumbnailSize::Preview,
|
||
ThumbnailSize::Large,
|
||
];
|
||
|
||
/// Print the output-size table (per-size encoded JPEG bytes) once. This is the
|
||
/// bandwidth/disk half of the Task 0.2 deliverable.
|
||
fn print_output_sizes(corpus: &[CorpusCase]) {
|
||
println!("\n=== Output size baseline (encoded JPEG bytes per thumbnail) ===");
|
||
println!(
|
||
"| {:<17} | {:<5} | {:>11} | {:>8} | {:>7} | {:>8} | {:>8} |",
|
||
"case", "fmt", "source", "input KB", "icon B", "preview B", "large B"
|
||
);
|
||
println!(
|
||
"|{:-<19}|{:-<7}|{:-<13}|{:-<10}|{:-<9}|{:-<10}|{:-<10}|",
|
||
"", "", "", "", "", "", ""
|
||
);
|
||
for case in corpus {
|
||
let sizes = ThumbnailService::bench_render_all(&case.bytes).unwrap_or_default();
|
||
let get = |want: ThumbnailSize| {
|
||
sizes
|
||
.iter()
|
||
.find(|(s, _)| *s == want)
|
||
.map(|(_, n)| *n)
|
||
.unwrap_or(0)
|
||
};
|
||
println!(
|
||
"| {:<17} | {:<5} | {:>5}×{:<5} | {:>8} | {:>6} | {:>8} | {:>8} |",
|
||
case.name,
|
||
case.format,
|
||
case.width,
|
||
case.height,
|
||
case.bytes.len() / 1024,
|
||
get(ThumbnailSize::Icon),
|
||
get(ThumbnailSize::Preview),
|
||
get(ThumbnailSize::Large),
|
||
);
|
||
}
|
||
println!();
|
||
}
|
||
|
||
fn configure(group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>) {
|
||
// Big images (48 MP) are slow per-iter; keep the suite bounded but stable.
|
||
group
|
||
.sample_size(10)
|
||
.warm_up_time(Duration::from_secs(1))
|
||
.measurement_time(Duration::from_secs(6));
|
||
}
|
||
|
||
fn bench_thumbnails(c: &mut Criterion) {
|
||
let corpus = bench_support::load_or_generate();
|
||
assert!(!corpus.is_empty(), "corpus is empty — generation failed");
|
||
print_output_sizes(&corpus);
|
||
|
||
// Per-size single-thumbnail latency (the lazy request path).
|
||
for size in SIZES {
|
||
let mut group = c.benchmark_group(format!("render_thumbnail/{size:?}"));
|
||
configure(&mut group);
|
||
for case in &corpus {
|
||
group.throughput(Throughput::Elements(1));
|
||
group.bench_with_input(BenchmarkId::from_parameter(case.name), case, |b, case| {
|
||
b.iter(|| {
|
||
let out =
|
||
ThumbnailService::bench_render_thumbnail(black_box(&case.bytes), size)
|
||
.expect("render_thumbnail");
|
||
black_box(out.len())
|
||
});
|
||
});
|
||
}
|
||
group.finish();
|
||
}
|
||
|
||
// All-sizes-in-one-decode latency (the eager upload path).
|
||
let mut group = c.benchmark_group("render_all");
|
||
configure(&mut group);
|
||
for case in &corpus {
|
||
group.throughput(Throughput::Elements(1));
|
||
group.bench_with_input(BenchmarkId::from_parameter(case.name), case, |b, case| {
|
||
b.iter(|| {
|
||
let out =
|
||
ThumbnailService::bench_render_all(black_box(&case.bytes)).expect("render_all");
|
||
black_box(out.len())
|
||
});
|
||
});
|
||
}
|
||
group.finish();
|
||
}
|
||
|
||
criterion_group!(benches, bench_thumbnails);
|
||
criterion_main!(benches);
|