1use std::num::NonZeroUsize;
11use std::sync::Arc;
12use std::time::Instant;
13
14use serde::{Deserialize, Serialize};
15
16use super::session::{BoxedRenderer, RenderOutcome, RenderSession};
17use super::{HostIdentity, Material, PresetState, RenderError, RenderJob};
18use crate::cast;
19use crate::dsp::quantile;
20use crate::units::Bpm;
21
22pub const WORKER_COUNTS: [usize; 4] = [1, 2, 4, 8];
24const BPM: f64 = 120.0;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum TimingKind {
31 PluginLoad,
33 StateLoad,
36 Render,
39 ClipLength,
42 RendersPerSecond,
44}
45
46#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
54pub struct TimingRow {
55 pub kind: TimingKind,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub workers: Option<usize>,
60 pub median: f64,
62 pub p95: f64,
64}
65
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68pub struct TimingTable {
69 pub identity: HostIdentity,
71 pub recipe_version: u32,
73 pub runs: usize,
75 pub rows: Vec<TimingRow>,
77}
78
79pub fn measure(
91 spawn: impl Fn(usize) -> Result<BoxedRenderer, RenderError> + Send + Sync + 'static,
92 state: &PresetState,
93 runs: NonZeroUsize,
94) -> Result<TimingTable, RenderError> {
95 let runs = runs.get();
96 let spawn = Arc::new(spawn);
97 let job = RenderJob::for_material(state.clone(), Material::Lead, Bpm::from_const(BPM));
98 let mut loads = Vec::with_capacity(runs);
99 let mut identity = None;
100 for _ in 0..runs {
101 let start = Instant::now();
102 let renderer = spawn(0)?;
103 loads.push(start.elapsed().as_secs_f64());
104 identity.get_or_insert_with(|| renderer.identity());
105 }
106 let mut renderer = spawn(0)?;
107 let mut states = Vec::with_capacity(runs);
108 let mut renders = Vec::with_capacity(runs);
109 let mut clips = Vec::with_capacity(runs);
110 for _ in 0..runs {
111 let start = Instant::now();
112 let clip = renderer.render(&job)?;
113 renders.push(start.elapsed().as_secs_f64());
114 clips.push(clip.record.length.get());
115 let load = renderer.last_state_load().ok_or_else(|| {
116 timing_error(
117 "the renderer does not measure its state loads",
118 "Time the plug-in backend or the fake",
119 )
120 })?;
121 states.push(load.get());
122 }
123 drop(renderer);
124 let mut rows = single_rows(&Samples {
125 loads: &loads,
126 states: &states,
127 renders: &renders,
128 clips: &clips,
129 })
130 .to_vec();
131 for workers in WORKER_COUNTS {
132 let count = NonZeroUsize::new(workers).unwrap_or(NonZeroUsize::MIN);
133 let session = RenderSession::new(count, {
134 let spawn = Arc::clone(&spawn);
135 move |worker| spawn(worker)
136 });
137 session.start_all()?;
138 let batch = vec![job.clone(); workers];
139 let mut times = Vec::with_capacity(runs);
140 for _ in 0..runs {
141 let start = Instant::now();
142 let outcomes = session.render_all(&batch);
143 times.push(start.elapsed().as_secs_f64());
144 if let Some(RenderOutcome::Failed(failure)) = outcomes
145 .into_iter()
146 .find(|outcome| matches!(outcome, RenderOutcome::Failed(_)))
147 {
148 return Err(failure
149 .errors
150 .into_iter()
151 .last()
152 .unwrap_or_else(|| timing_error("a render failed", "Time it again")));
153 }
154 }
155 rows.push(rate_row(workers, ×));
156 }
157 Ok(TimingTable {
158 identity: identity.ok_or_else(|| timing_error("no renderer started", "Time it again"))?,
159 recipe_version: job.recipe().version(),
160 runs,
161 rows,
162 })
163}
164
165struct Samples<'a> {
167 loads: &'a [f64],
168 states: &'a [f64],
169 renders: &'a [f64],
170 clips: &'a [f64],
171}
172
173fn single_rows(samples: &Samples) -> [TimingRow; 4] {
174 [
175 row(TimingKind::PluginLoad, None, samples.loads),
176 row(TimingKind::StateLoad, None, samples.states),
177 row(TimingKind::Render, None, samples.renders),
178 row(TimingKind::ClipLength, None, samples.clips),
179 ]
180}
181
182fn rate_row(workers: usize, batch_seconds: &[f64]) -> TimingRow {
185 let time = row(TimingKind::RendersPerSecond, Some(workers), batch_seconds);
186 let rate = |seconds: f64| cast::usize_to_f64(workers) / seconds;
187 TimingRow {
188 median: rate(time.median),
189 p95: rate(time.p95),
190 ..time
191 }
192}
193
194fn row(kind: TimingKind, workers: Option<usize>, values: &[f64]) -> TimingRow {
195 let mut sorted = values.to_vec();
196 sorted.sort_by(f64::total_cmp);
197 TimingRow {
198 kind,
199 workers,
200 median: quantile(&sorted, 0.5).unwrap_or(f64::NAN),
201 p95: quantile(&sorted, 0.95).unwrap_or(f64::NAN),
202 }
203}
204
205fn timing_error(cause: impl Into<String>, fix: &str) -> RenderError {
206 RenderError::Host {
207 cause: cause.into(),
208 fix: fix.to_owned(),
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn each_single_row_takes_its_own_list() {
218 let rows = single_rows(&Samples {
220 loads: &[1.0],
221 states: &[2.0],
222 renders: &[3.0],
223 clips: &[4.0],
224 });
225 let seen: Vec<(TimingKind, Option<usize>, f64, f64)> = rows
226 .iter()
227 .map(|row| (row.kind, row.workers, row.median, row.p95))
228 .collect();
229 assert_eq!(
230 seen,
231 [
232 (TimingKind::PluginLoad, None, 1.0, 1.0),
233 (TimingKind::StateLoad, None, 2.0, 2.0),
234 (TimingKind::Render, None, 3.0, 3.0),
235 (TimingKind::ClipLength, None, 4.0, 4.0),
236 ]
237 );
238 }
239
240 #[test]
241 fn a_row_sorts_its_values_before_it_takes_the_quantiles() {
242 let row = row(TimingKind::Render, None, &[3.0, 1.0, 2.0]);
243 assert_eq!(row.median, 2.0);
246 assert!((row.p95 - 2.9).abs() < 1e-12, "{row:?}");
247 }
248
249 #[test]
250 fn the_rate_is_the_workers_over_the_batch_time() {
251 for workers in WORKER_COUNTS {
252 let row = rate_row(workers, &[0.5]);
253 assert_eq!(row.kind, TimingKind::RendersPerSecond);
254 assert_eq!(row.workers, Some(workers));
255 let expected = cast::usize_to_f64(workers) / 0.5;
257 assert_eq!((row.median, row.p95), (expected, expected));
258 }
259 }
260
261 #[test]
262 fn the_p95_rate_comes_from_the_slow_end_of_the_batch_times() {
263 let row = rate_row(4, &[2.0, 1.0, 3.0]);
264 assert_eq!(row.median, 4.0 / 2.0);
266 assert!((row.p95 - 4.0 / 2.9).abs() < 1e-12, "{row:?}");
267 assert!(row.p95 < row.median);
268 }
269}