Files
Oxicloud/benches/thumbnails.rs
T
DioCrafts fd5808c157 perf(thumbnails): shrink-on-load JPEG decode (1.8-2× faster, 5-15× less RAM)
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>
2026-06-21 15:13:03 +02:00

109 lines
3.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);