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.
140 lines
5.8 KiB
Rust
140 lines
5.8 KiB
Rust
//! 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()))
|
||
}
|