//! 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> { 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())) }