Files
DarkRoom/tools/bench/src/exporting.rs
T
dtourolle 84fade99ec Put the developer docs under docs/dev and index the folder for users first
docs/ had 26 developer documents flat beside the manual, and the two
audiences are very differently sized: most readers want the manual and
the gesture reference, a few want the register, the designs and the
measurements. The manual and gestures.md stay at the top; everything for
someone changing the code moves to docs/dev/, and the two documents that
name their own successors — the v0.1 milestone and the UI-refinement plan
— go to docs/dev/archive/ rather than being deleted, since both are still
cited. docs/README.md is the index, users first.

Every reference follows: code comments, Cargo manifests, the workflows,
the pre-commit hook, the bench and traceability tools (which locate the
repo root by docs/dev/requirements.md now), packaging, the Docker READMEs,
CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level
deeper and is regenerated. Links out of the moved documents into the tree
gain a level; a link checker over every Markdown file finds none broken.
2026-09-20 21:16:03 +02:00

140 lines
5.8 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.
//! The half of a 24 MP export that needs no GPU.
//!
//! # This cannot certify NFR-P7, and is not tagged as though it could
//!
//! NFR-P7 is "full-resolution export (24 MP, **full chain**) < 2 s". The full
//! chain is decode, demosaic, a GPU render at full resolution, a read-back,
//! and then everything `dr-export` does — resize, output sharpening, encode.
//! Only the last three of those run without an adapter, and the CI runner has
//! none. So what is measured here is the encode half, and no requirement tag
//! anywhere in this crate names NFR-P7.
//!
//! (Written without the tag's own spelling on purpose. `tools/traceability`
//! matches the marker anywhere on a line and parses the identifier after it, so
//! a sentence saying "there is no tag for NFR-P7" would *be* a tag for NFR-P7 —
//! a disclaimer that made itself false.)
//!
//! That is a deliberate refusal rather than an oversight. `CONTRIBUTING.md`
//! asks that a requirement be closed by a test that would fail if the
//! behaviour were removed, and `docs/dev/code-health.md` CH-4 records what the
//! coverage figure looks like when tags are hung on plumbing instead. A tag
//! here would say the export budget is checked; the GPU half of it would still
//! be unchecked.
//!
//! # What the number is still good for
//!
//! It is a **one-sided** gate, and that is worth having. The encode half is a
//! lower bound on the whole: if resizing, sharpening and encoding 24 MP alone
//! take longer than two seconds, NFR-P7 is violated no matter how fast the
//! render is. So the budget in `docs/dev/bench-baseline.json` is the requirement's
//! own 2000 ms, and exceeding it fails the build honestly. Coming in under it
//! proves nothing about the requirement, and the report says so rather than
//! printing a tick.
//!
//! # Two rows
//!
//! `Original` is the one the budget is judged on: it is the archival export,
//! the largest encode, and the case FR-EXP-9 is about. `LongEdge(2048)` is the
//! ordinary web export, where the resample does real work and the encode does
//! very little — it is reported because a regression in `size::resample` would
//! be invisible in the first row, where source and target dimensions are equal.
use std::time::Instant;
use anyhow::Result;
use dr_export::{export, Frame};
use dr_types::{ExportFormat, ExportSettings, OutputSharpening, SizingMode};
use crate::fixture::plausible_frame;
use crate::stats::{ms, Percentiles};
/// The frame every row exports.
///
/// 6000 × 4000 is 24.0 MP — a full-frame body, and the exact figure NFR-P7
/// names. As RGBA8 it is 96 MB, and the export path holds a resized copy and a
/// sharpened copy alongside it, so a run needs roughly 300 MB of headroom.
/// Worth knowing before a small runner reports this as a mysterious kill.
pub const SOURCE: (u32, u32) = (6000, 4000);
/// Measured exports per row. Not a hundred: one 24 MP encode is most of a
/// second, and a hundred of them would be a two-minute CI step to establish
/// what five establish. Nearest-rank p99 of five is the worst of the five,
/// which for a row this expensive is the honest reading anyway.
const RUNS: usize = 5;
/// A row of the export table.
pub struct EncodeRun {
/// The name this row carries in `docs/dev/bench-baseline.json`.
pub key: &'static str,
pub label: &'static str,
pub width: u32,
pub height: u32,
/// Encoded file size, so a row that silently stopped compressing is
/// visible as well as a row that got slow.
pub bytes: usize,
pub times: Percentiles,
}
/// Export the same 24 MP frame at each sizing, timing `dr_export::export`.
pub fn measure() -> Result<Vec<EncodeRun>> {
let (w, h) = SOURCE;
// Detail at every scale, for the same reason the thumbnail fixture has it:
// a flat frame compresses to almost nothing and would make the encoder
// look several times faster than any photograph makes it.
let frame = Frame::new(w, h, plausible_frame(w, h, 0))
.map_err(|e| anyhow::anyhow!("building the 24 MP bench frame: {e}"))?;
let sizings: [(&'static str, &'static str, SizingMode); 2] = [
("export_24mp_original_ms", "original", SizingMode::Original),
(
"export_24mp_long_edge_2048_ms",
"long edge 2048",
SizingMode::LongEdge(2048),
),
];
let mut rows = Vec::with_capacity(sizings.len());
for (key, label, sizing) in sizings {
let settings = ExportSettings {
format: ExportFormat::Jpeg,
// 90 is the default and what a photographer would not need to
// change; quality moves encode time, so it belongs in the record.
quality: 90,
sizing,
sharpening: OutputSharpening::Screen,
..Default::default()
};
// Discarded. The first export of a process grows the allocator to hold
// three 24 MP buffers, which is a cost paid once and not per file in
// the batch export FR-EXP-7 describes.
let warm = run_once(&frame, &settings)?;
let mut samples = Vec::with_capacity(RUNS);
let mut last = warm;
for _ in 0..RUNS {
let started = Instant::now();
last = run_once(&frame, &settings)?;
samples.push(ms(started.elapsed()));
}
rows.push(EncodeRun {
key,
label,
width: last.0,
height: last.1,
bytes: last.2,
times: Percentiles::of(samples),
});
}
Ok(rows)
}
/// One export, returning what it produced rather than the pixels.
fn run_once(frame: &Frame, settings: &ExportSettings) -> Result<(u32, u32, usize)> {
let encoded = export(frame, settings, "bench.jpg".to_string(), None)
.map_err(|e| anyhow::anyhow!("exporting the bench frame: {e}"))?;
Ok((encoded.width, encoded.height, encoded.bytes.len()))
}