Skip to main content

euv_performance/runner/
fn.rs

1use super::*;
2
3/// Executes the CLI and writes raw measurements without swallowing failures.
4///
5/// # Returns
6///
7/// - `Result<(), Box<dyn Error>>` - Success or the configuration, measurement or output error.
8pub fn run() -> Result<(), Box<dyn Error>> {
9    let config: Config = Config::parse(env::args().skip(1))?;
10    if config.get_help() {
11        println!("{USAGE}");
12        return Ok(());
13    }
14    let Output {
15        status,
16        stdout,
17        stderr,
18    }: Output = Command::new(COMPILER).arg(COMPILER_FLAG).output()?;
19    if !status.success() {
20        return Err(format!(
21            "compiler metadata failed: {}",
22            String::from_utf8_lossy(&stderr)
23        )
24        .into());
25    }
26    let mut measurements: Vec<Measurement> = Vec::with_capacity(config.get_suites().len());
27    for &suite in config.get_suites() {
28        measurements.push(measure(&config, suite)?);
29    }
30    if config.get_suites().as_slice() == DEFAULT_SUITES.as_slice()
31        && measurements.len() != DEFAULT_SUITES.len()
32    {
33        return Err(format!("incomplete default suite coverage: {}", measurements.len()).into());
34    }
35    let mode: &'static str = if cfg!(feature = "allocation-tracking") {
36        MODE_ALLOCATIONS
37    } else {
38        MODE_TIMING
39    };
40    let environment: Environment = Environment::new(
41        SOURCE_ROOT.to_string(),
42        OS.to_string(),
43        ARCH.to_string(),
44        available_parallelism().map(NonZeroUsize::get).ok(),
45        String::from_utf8(stdout)?,
46        SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
47        LIMITATIONS.to_string(),
48    );
49    let report: Report = Report::new(Header::new(2, mode, environment), config, measurements);
50    for measurement in report.get_measurements() {
51        for sample in measurement.get_samples() {
52            eprintln!(
53                "{:?} round={} elapsed_ns={:?} p50_ns={:?} p95_ns={:?} p99_ns={:?} allocations={:?} work=({},{},{}) checksum={}",
54                measurement.get_suite(),
55                sample.get_round(),
56                sample.get_timings().try_get_elapsed_ns(),
57                sample.get_timings().try_get_p50_ns(),
58                sample.get_timings().try_get_p95_ns(),
59                sample.get_timings().try_get_p99_ns(),
60                sample.try_get_allocations(),
61                sample.get_work().get_core_ticks(),
62                sample.get_work().get_engine_ticks(),
63                sample.get_work().get_chart_ticks(),
64                sample.get_work().get_checksum()
65            );
66        }
67    }
68    let json: String = to_string_pretty(&report)?;
69    if let Some(path) = report.get_config().try_get_output() {
70        if let Some(parent) = path.parent()
71            && !parent.as_os_str().is_empty()
72        {
73            fs::create_dir_all(parent)?;
74        }
75        fs::write(path, json)?;
76        eprintln!("report={}", path.display());
77    } else {
78        println!("{json}");
79    }
80    Ok(())
81}
82
83/// Measures a real suite, separating warmup and fixture allocation from ticks.
84///
85/// # Arguments
86///
87/// - `&Config` - Shared timing and scaling options.
88/// - `Suite` - Subsystems to execute.
89///
90/// # Returns
91///
92/// - `Result<Measurement, Box<dyn Error>>` - Verified samples or a coverage failure.
93pub(crate) fn measure(config: &Config, suite: Suite) -> Result<Measurement, Box<dyn Error>> {
94    let mut samples: Vec<Sample> = Vec::with_capacity(config.get_rounds());
95    let mut latencies: Vec<u64> = Vec::with_capacity(config.get_ticks());
96    let before: AllocationStats = AllocationStats::capture(true);
97    let started: Option<Instant> = start_clock();
98    let mut workload: Workload = Workload::create(suite, config.get_scale());
99    let initialization_ns: Option<u64> = elapsed(started);
100    let initialized: AllocationStats = AllocationStats::capture(false);
101    let initialization_allocations: Option<AllocationDelta> =
102        AllocationStats::delta(before, initialized);
103    let warmup_start: AllocationStats = AllocationStats::capture(true);
104    let mut frame: u64 = 0;
105    for _ in 0..config.get_warmup() {
106        black_box(workload.tick(frame));
107        frame += 1;
108    }
109    let warmup_allocations: Option<AllocationDelta> =
110        AllocationStats::delta(warmup_start, AllocationStats::capture(false));
111    for round in 0..config.get_rounds() {
112        latencies.clear();
113        let mut work: Work = Work::default();
114        let start: AllocationStats = AllocationStats::capture(true);
115        let started: Option<Instant> = start_clock();
116        for _ in 0..config.get_ticks() {
117            let tick_start: Option<Instant> = start_clock();
118            let tick_work: Work = workload.tick(frame);
119            if let Some(tick_ns) = elapsed(tick_start) {
120                latencies.push(tick_ns);
121            }
122            work.add(tick_work);
123            frame += 1;
124        }
125        let elapsed_ns: Option<u64> = elapsed(started);
126        let end: AllocationStats = AllocationStats::capture(false);
127        validate_work(suite, config.get_ticks(), work)?;
128        latencies.sort_unstable();
129        let timings: Timings = Timings::new(
130            elapsed_ns,
131            percentile(&latencies, 50),
132            percentile(&latencies, 95),
133            percentile(&latencies, 99),
134            elapsed_ns.map(|nanoseconds: u64| {
135                config.get_ticks() as f64 * 1_000_000_000.0 / nanoseconds.max(1) as f64
136            }),
137        );
138        samples.push(Sample::new(
139            round,
140            config.get_ticks(),
141            timings,
142            AllocationStats::delta(start, end),
143            work,
144        ));
145    }
146    drop(workload);
147    let after_drop: AllocationStats = AllocationStats::capture(false);
148    Ok(Measurement::new(
149        suite,
150        initialization_ns,
151        initialization_allocations,
152        warmup_allocations,
153        cfg!(feature = "allocation-tracking")
154            .then_some(after_drop.get_live() as i64 - before.get_live() as i64),
155        samples,
156    ))
157}
158
159/// Rejects a frame loop that skipped one of its required subsystems.
160///
161/// # Arguments
162///
163/// - `Suite` - Expected subsystems.
164/// - `usize` - Expected frames per subsystem.
165/// - `Work` - Observed work accumulated by the actual frame loop.
166///
167/// # Returns
168///
169/// - `Result<(), Box<dyn Error>>` - Success only when every expected invocation ran.
170pub(crate) fn validate_work(suite: Suite, ticks: usize, work: Work) -> Result<(), Box<dyn Error>> {
171    let expected_core: u64 = if matches!(suite, Suite::Core | Suite::Mixed) {
172        ticks as u64
173    } else {
174        0
175    };
176    let expected_engine: u64 = if matches!(suite, Suite::Engine | Suite::Mixed) {
177        ticks as u64
178    } else {
179        0
180    };
181    let expected_chart: u64 = if matches!(suite, Suite::Chart | Suite::Mixed) {
182        ticks as u64
183    } else {
184        0
185    };
186    if (
187        work.get_core_ticks(),
188        work.get_engine_ticks(),
189        work.get_chart_ticks(),
190    ) != (expected_core, expected_engine, expected_chart)
191        || work.get_checksum() == 0
192    {
193        return Err(format!("missing or unexpected subsystem work in {suite:?}: {work:?}").into());
194    }
195    Ok(())
196}
197
198/// Returns a nearest-rank percentile of already sorted frame latencies.
199///
200/// # Arguments
201///
202/// - `&[u64]` - Sorted nanosecond measurements.
203/// - `usize` - Percentile in the range one through one hundred.
204///
205/// # Returns
206///
207/// - `Option<u64>` - Observed latency, absent when no clock samples were taken.
208fn percentile(sorted: &[u64], percent: usize) -> Option<u64> {
209    let index: usize = (sorted.len() * percent).div_ceil(100).saturating_sub(1);
210    sorted.get(index).copied()
211}
212
213/// Starts a frame clock only in an uninstrumented timing build.
214///
215/// # Returns
216///
217/// - `Option<Instant>` - Clock sample, absent in allocation mode.
218fn start_clock() -> Option<Instant> {
219    if cfg!(feature = "allocation-tracking") {
220        None
221    } else {
222        Some(Instant::now())
223    }
224}
225
226/// Reads elapsed time only when timing was enabled for the phase.
227///
228/// # Arguments
229///
230/// - `Option<Instant>` - Optional clock sample.
231///
232/// # Returns
233///
234/// - `Option<u64>` - Elapsed nanoseconds, absent in allocation mode.
235fn elapsed(started: Option<Instant>) -> Option<u64> {
236    started.map(|started: Instant| started.elapsed().as_nanos() as u64)
237}