Skip to main content

forge/bench/
runner.rs

1// #t(file: rust_alloc_in_loop) CLI binary, allocations are fine for readability.
2// #t(file: rust_clone_in_loop) CLI binary, cloning strings in display loops is fine.
3
4#![expect(
5    clippy::cast_possible_truncation,
6    clippy::cast_possible_wrap,
7    clippy::cast_precision_loss,
8    reason = "benchmark durations, iteration counts, and thread indices are bounded by CLI inputs and serialized report fields"
9)]
10
11//! Benchmark execution: discover specs, run simulations, collect results.
12
13use std::{cell::Cell, time::Duration};
14
15use anyhow::{Context, Result};
16use prost::Message;
17use rayon::prelude::*;
18use wowlab_common::{output, sys, time::Instant};
19use wowlab_engine_adapter_data::{LocalCsvResolver, OverlayResolver};
20use wowlab_engine_application::{
21    ApplicationError, ChunkRun, IntentOverrides, ResolvedSetup, build_handler_from_setup,
22    resolve_setup, run_chunk,
23};
24use wowlab_engine_ports::{ChunkAssignment, NoopProgress, SpecDescriptor, content_catalog};
25use wowlab_fs::file;
26use wowlab_types::{constants::MS_PER_SECOND, game::SpecId};
27
28use super::types::{
29    BenchRun, BenchmarkBackend, BenchmarkSample, ScalingPoint, ScalingResult, SpecBenchResult,
30};
31use crate::{
32    constants::{DEFAULT_SEED, default_data_dir, engine_dir},
33    encounter_fixture::EncounterFixture,
34    provider::ComparisonConfig,
35    run::RunParameters,
36    sim,
37};
38
39const DECIMAL_BASE: f64 = 10.0;
40const THREAD_STEP_FACTOR: usize = 2;
41const DECIMALS_IPS: u32 = 1;
42const DECIMALS_MULTIPLIER: u32 = 2;
43const DECIMALS_RATIO: u32 = 3;
44const WARMUP_SAMPLES: u32 = 1;
45const NANOS_PER_MILLISECOND: f64 = 1_000_000.0;
46const ROUNDING_HALF_MILLISECOND_MICROS: u64 = 500;
47const MEDIAN_PAIR_SIZE: usize = 2;
48const MEDIAN_PAIR_DIVISOR: f64 = 2.0;
49const SQUARE_EXPONENT: i32 = 2;
50
51#[derive(Clone, Copy)]
52struct BenchExecution<'a> {
53    rt: &'a tokio::runtime::Runtime,
54    data_dir: &'a str,
55    parameters: RunParameters,
56    samples: u32,
57    encounter_fixture: Option<EncounterFixture>,
58}
59
60#[derive(Clone, Copy)]
61struct SampleContext<'a> {
62    rt: &'a tokio::runtime::Runtime,
63    catalog: &'a wowlab_engine_ports::ContentCatalog,
64    sim_config: &'a str,
65    chunk: &'a ChunkAssignment,
66    resolver: &'a wowlab_engine_ports::DynDataResolver<'a>,
67    parameters: RunParameters,
68    slug: &'a str,
69}
70
71fn round(v: f64, decimals: u32) -> f64 {
72    let factor = DECIMAL_BASE.powi(decimals as i32);
73
74    (v * factor).round() / factor
75}
76
77pub(super) fn discover_specs(filter: &[String]) -> Result<Vec<(&'static SpecDescriptor, String)>> {
78    let rotation_dir = engine_dir().join("examples/rotations");
79    let catalog = content_catalog().context("engine content catalog is unavailable")?;
80
81    let mut all_dps: Vec<_> = catalog
82        .descriptors()
83        .iter()
84        .filter(|d| d.spec_id.is_dps())
85        .collect();
86
87    all_dps.sort_by_key(|d| d.spec_id.slug());
88
89    let mut specs = Vec::new();
90
91    for desc in all_dps {
92        let slug = desc.spec_id.slug();
93
94        if !filter.is_empty() && !filter.iter().any(|f| f == slug) {
95            continue;
96        }
97
98        // Skip specs without comparison-profile gear; gearless throughput is not comparable.
99
100        if crate::simc::simc_profile_path(desc.spec_id).is_none() {
101            continue;
102        }
103
104        let rotation_path = rotation_dir.join(format!("{slug}_assisted.json"));
105
106        if let Ok(script) = file::read_text(&rotation_path) {
107            specs.push((desc, script));
108        }
109    }
110
111    Ok(specs)
112}
113
114pub(super) fn run_benchmarks(
115    specs: &[(&'static SpecDescriptor, String)],
116    parameters: RunParameters,
117    samples: u32,
118    encounter_fixture: Option<EncounterFixture>,
119) -> Result<(Vec<SpecBenchResult>, tokio::runtime::Runtime)> {
120    let iterations = parameters.iterations();
121    let duration = parameters.fight_duration_secs();
122    let data_dir = std::env::var("WOWLAB_DATA_DIR").unwrap_or_else(|_| default_data_dir());
123    let rt = tokio::runtime::Runtime::new().context("failed to create tokio runtime")?;
124
125    output::header(&format!(
126        "Single-threaded benchmark: {} specs (1 warmup + {} samples, {} iterations/sample, {}s duration)",
127        specs.len(),
128        samples,
129        iterations,
130        duration,
131    ));
132    output::blank();
133
134    let mut results = Vec::new();
135    let mut failed: Vec<(String, String)> = Vec::new();
136
137    for (desc, rotation_script) in specs {
138        let slug = desc.spec_id.slug();
139
140        let execution = BenchExecution {
141            rt: &rt,
142            data_dir: &data_dir,
143            parameters,
144            samples,
145            encounter_fixture,
146        };
147
148        match bench_spec_single(&execution, desc, rotation_script) {
149            Ok(result) => {
150                output::success(&format!(
151                    "{:<30} {:>10.1} median iter/s  {:>8.1} std dev  {:>10} DPS  fingerprint {}",
152                    result.slug,
153                    result.median_iterations_per_sec,
154                    result.population_stddev_iterations_per_sec,
155                    output::fmt_number(result.mean_dps),
156                    result.semantic_fingerprint,
157                ));
158                results.push(result);
159            }
160            Err(e) => {
161                output::error(&format!("{slug:<30} FAILED: {e:#}"));
162                failed.push((slug.to_string(), format!("{e:#}")));
163            }
164        }
165    }
166
167    output::blank();
168
169    if !failed.is_empty() {
170        output::kv("Skipped", &format!("{} specs failed", failed.len()));
171    }
172
173    print_summary(&results);
174
175    Ok((results, rt))
176}
177
178pub(super) fn run_scaling(
179    rt: &tokio::runtime::Runtime,
180    specs: &[(&'static SpecDescriptor, String)],
181    single_results: &[SpecBenchResult],
182    parameters: RunParameters,
183    encounter_fixture: Option<EncounterFixture>,
184) -> Vec<ScalingResult> {
185    let catalog = match content_catalog() {
186        Ok(catalog) => catalog,
187        Err(error) => {
188            output::error(&format!("engine content catalog is unavailable: {error}"));
189
190            return Vec::new();
191        }
192    };
193    let iterations = parameters.iterations();
194    let data_dir = std::env::var("WOWLAB_DATA_DIR").unwrap_or_else(|_| default_data_dir());
195    let profile = sys::os_profile();
196    let thread_counts = compute_thread_counts(&profile);
197
198    output::blank();
199    output::header(&format!(
200        "Thread scaling benchmark: {} specs, thread counts {:?}",
201        specs.len(),
202        thread_counts,
203    ));
204    output::blank();
205
206    let mut scaling_results = Vec::new();
207
208    for (desc, rotation_script) in specs {
209        let slug = desc.spec_id.slug();
210
211        if !single_results.iter().any(|r| r.slug == slug) {
212            output::detail(&format!("{slug:<30} skipped (no single-threaded baseline)"));
213            continue;
214        }
215
216        let rotation_id = format!("{slug}_assisted");
217
218        let resolver = OverlayResolver::new(LocalCsvResolver::new(data_dir.as_str()))
219            .with_rotation_script(&rotation_id, rotation_script.clone());
220        let resolver_dyn = wowlab_engine_ports::DynDataResolver::from_ref(&resolver);
221        let sim_config = match rt.block_on(bench_sim_config(
222            desc.spec_id,
223            &rotation_id,
224            parameters,
225            encounter_fixture,
226            resolver_dyn,
227        )) {
228            Ok(c) => c,
229            Err(e) => {
230                output::error(&format!("{slug:<30} FAILED: {e}"));
231                continue;
232            }
233        };
234        let setup = match rt.block_on(resolve_setup(
235            catalog,
236            &sim_config,
237            resolver_dyn,
238            &IntentOverrides::default(),
239        )) {
240            Ok(s) => s,
241            Err(e) => {
242                output::error(&format!("{slug:<30} FAILED: {e}"));
243                continue;
244            }
245        };
246
247        let baseline_ips = single_results
248            .iter()
249            .find(|r| r.slug == slug)
250            .map_or(1.0, |r| r.iterations_per_sec);
251
252        output::subheader(&format!("{slug} (baseline: {baseline_ips:.0} iter/s)"));
253
254        let mut points = Vec::with_capacity(thread_counts.len());
255        let mut prev_ips = baseline_ips;
256
257        for &n in &thread_counts {
258            let start = Instant::now();
259            // PANIC-BOUNDARY: isolate one scaling run so the CLI can report the failed spec.
260            let par_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
261                run_scaling_dispatch(&setup, DEFAULT_SEED, parameters, n)
262            }));
263
264            match par_result {
265                Ok(Err(e)) => {
266                    output::error(&format!("  {n:>3} threads: FAILED: {e}"));
267                    break;
268                }
269                Err(_panic) => {
270                    output::error(&format!("  {n:>3} threads: PANICKED"));
271                    break;
272                }
273                Ok(Ok(())) => {}
274            }
275
276            let elapsed_ms = start.elapsed().as_millis() as u64;
277
278            let ips = if elapsed_ms > 0 {
279                f64::from(iterations) / (elapsed_ms as f64 / MS_PER_SECOND)
280            } else {
281                0.0
282            };
283            let multiplier = ips / baseline_ips;
284            let efficiency = multiplier / n as f64;
285            let marginal_gain = if prev_ips > 0.0 {
286                (ips - prev_ips) / prev_ips
287            } else {
288                0.0
289            };
290
291            output::detail(&format!(
292                "  {n:>3} threads: {:>10.0} iter/s  {multiplier:>5.2}x  eff {:.0}%  marginal +{:.1}%",
293                ips,
294                efficiency * wowlab_types::constants::HUNDRED,
295                marginal_gain * wowlab_types::constants::HUNDRED,
296            ));
297
298            points.push(ScalingPoint {
299                threads: n,
300                elapsed_ms,
301                iterations_per_sec: round(ips, DECIMALS_IPS),
302                multiplier: round(multiplier, DECIMALS_MULTIPLIER),
303                efficiency: round(efficiency, DECIMALS_RATIO),
304                marginal_gain: round(marginal_gain, DECIMALS_RATIO),
305                is_p_core_range: n <= profile.p_cores,
306            });
307
308            prev_ips = ips;
309        }
310
311        scaling_results.push(ScalingResult {
312            slug: slug.to_string(),
313            iterations,
314            points,
315        });
316    }
317
318    scaling_results
319}
320
321fn compute_thread_counts(profile: &sys::OsProfile) -> Vec<usize> {
322    let mut counts = vec![1_usize];
323    let mut n = THREAD_STEP_FACTOR;
324
325    while n < profile.p_cores {
326        counts.push(n);
327        n *= THREAD_STEP_FACTOR;
328    }
329
330    if profile.p_cores > 1 && !counts.contains(&profile.p_cores) {
331        counts.push(profile.p_cores);
332    }
333
334    let p_plus_e = profile.p_cores + profile.e_cores;
335
336    if profile.e_cores > 0 && p_plus_e != profile.p_cores {
337        counts.push(p_plus_e);
338    }
339
340    if profile.logical_cores > p_plus_e && !counts.contains(&profile.logical_cores) {
341        counts.push(profile.logical_cores);
342    }
343
344    counts.sort_unstable();
345    counts.dedup();
346
347    counts
348}
349
350fn run_scaling_dispatch(
351    setup: &ResolvedSetup,
352    seed_base: u64,
353    parameters: RunParameters,
354    num_threads: usize,
355) -> Result<()> {
356    let iterations = parameters.iterations();
357    let pool = rayon::ThreadPoolBuilder::new()
358        .num_threads(num_threads)
359        .build()
360        .context("Failed to create thread pool")?;
361
362    let base_count = iterations / num_threads as u32;
363    let remainder = iterations % num_threads as u32;
364
365    let assignments: Vec<ChunkAssignment> = (0..num_threads)
366        .map(|t| {
367            let t_u32 = t as u32;
368            let my_count = base_count + u32::from(t_u32 < remainder);
369            let mut assignment = ChunkAssignment::single_indexed("scaling", t_u32, my_count);
370
371            assignment.chunk_id = format!("scaling-{t}");
372
373            assignment
374        })
375        .collect();
376
377    let results: Vec<Result<_, ApplicationError>> = pool.install(|| {
378        assignments
379            .par_iter()
380            .map(|assignment| {
381                run_chunk(
382                    ChunkRun {
383                        assignment,
384                        duration_s: setup.duration_s,
385                        seed_base,
386                    },
387                    &NoopProgress,
388                    || build_handler_from_setup(setup),
389                )
390            })
391            .collect()
392    });
393
394    for r in results {
395        r.context("Worker failed")?;
396    }
397
398    Ok(())
399}
400
401async fn bench_sim_config(
402    spec: SpecId,
403    rotation_id: &str,
404    parameters: RunParameters,
405    encounter_fixture: Option<EncounterFixture>,
406    resolver: &wowlab_engine_ports::DynDataResolver<'_>,
407) -> Result<String> {
408    let config = ComparisonConfig {
409        spec,
410        parameters,
411        race: None,
412        bugs: true,
413        simc_debug: false,
414    };
415
416    crate::wowlab::wowlab_profile_config_with_fixture(
417        &config,
418        rotation_id,
419        encounter_fixture,
420        resolver,
421    )
422    .await
423}
424
425fn bench_spec_single(
426    execution: &BenchExecution<'_>,
427    desc: &SpecDescriptor,
428    rotation_script: &str,
429) -> Result<SpecBenchResult> {
430    let BenchExecution {
431        rt,
432        data_dir,
433        parameters,
434        samples,
435        encounter_fixture,
436    } = *execution;
437    let catalog = content_catalog().context("engine content catalog is unavailable")?;
438    let iterations = parameters.iterations();
439    let duration = parameters.fight_duration_secs();
440    let slug = desc.spec_id.slug();
441    let rotation_id = format!("{slug}_assisted");
442
443    let resolver = OverlayResolver::new(LocalCsvResolver::new(data_dir))
444        .with_rotation_script(&rotation_id, rotation_script.to_string());
445    let resolver_dyn = wowlab_engine_ports::DynDataResolver::from_ref(&resolver);
446    let sim_config = rt.block_on(bench_sim_config(
447        desc.spec_id,
448        &rotation_id,
449        parameters,
450        encounter_fixture,
451        resolver_dyn,
452    ))?;
453
454    let chunk_label = format!("bench-{slug}");
455    let chunk = sim::make_chunk("bench", iterations, Some(&chunk_label));
456    let sample_context = SampleContext {
457        rt,
458        catalog,
459        sim_config: &sim_config,
460        chunk: &chunk,
461        resolver: resolver_dyn,
462        parameters,
463        slug,
464    };
465
466    for _ in 0..WARMUP_SAMPLES {
467        let _ = bench_sample(&sample_context, 0)?;
468    }
469
470    let mut measured = Vec::with_capacity(samples as usize);
471
472    for sample in 1..=samples {
473        let result = bench_sample(&sample_context, sample)?;
474
475        output::detail(&format!(
476            "  sample {sample}: {:.1} iter/s, {:.3}ms build, {:.3}ms reset, {} events, {} allocations, fingerprint {}",
477            result.iterations_per_sec,
478            result.build_time_ms,
479            result.reset_time_ms,
480            result.counters.events,
481            result
482                .allocations
483                .map_or(0, |allocations| allocations.count),
484            result.semantic_fingerprint,
485        ));
486        measured.push(result);
487    }
488
489    let fingerprint = measured
490        .first()
491        .map(|sample| sample.semantic_fingerprint.clone())
492        .context("benchmark produced no measured samples")?;
493
494    if let Some(mismatch) = measured
495        .iter()
496        .find(|sample| sample.semantic_fingerprint != fingerprint)
497    {
498        anyhow::bail!(
499            "semantic fingerprint drift for {slug}: expected {fingerprint}, sample {} produced {}",
500            mismatch.sample,
501            mismatch.semantic_fingerprint,
502        );
503    }
504
505    let throughputs: Vec<f64> = measured
506        .iter()
507        .map(|sample| sample.iterations_per_sec)
508        .collect();
509    let mean_throughput = mean(&throughputs);
510    let median_throughput = median(&throughputs);
511    let throughput_stddev = population_stddev(&throughputs, mean_throughput);
512    let average_dps = mean(
513        &measured
514            .iter()
515            .map(|sample| sample.mean_dps)
516            .collect::<Vec<_>>(),
517    );
518    let total_elapsed_ms = measured.iter().map(|sample| sample.elapsed_ms).sum::<f64>();
519
520    Ok(SpecBenchResult {
521        slug: slug.to_string(),
522        encounter_fixture: encounter_fixture
523            .map_or("patchwerk", EncounterFixture::slug)
524            .to_string(),
525        backend: benchmark_backend(),
526        iterations,
527        duration_secs: duration,
528        warmups: WARMUP_SAMPLES,
529        samples: measured,
530        elapsed_ms: rounded_milliseconds(total_elapsed_ms)?,
531        iterations_per_sec: round(mean_throughput, DECIMALS_IPS),
532        mean_iterations_per_sec: mean_throughput,
533        median_iterations_per_sec: median_throughput,
534        population_stddev_iterations_per_sec: throughput_stddev,
535        mean_dps: round(average_dps, DECIMALS_IPS),
536        semantic_fingerprint: fingerprint,
537    })
538}
539
540fn rounded_milliseconds(milliseconds: f64) -> Result<u64> {
541    let elapsed = Duration::try_from_secs_f64(milliseconds / MS_PER_SECOND)
542        .context("benchmark elapsed time must be finite and nonnegative")?;
543    let rounded = elapsed
544        .checked_add(Duration::from_micros(ROUNDING_HALF_MILLISECOND_MICROS))
545        .context("benchmark elapsed time exceeds Duration capacity")?;
546
547    u64::try_from(rounded.as_millis()).context("benchmark elapsed milliseconds exceed u64 capacity")
548}
549
550fn bench_sample(context: &SampleContext<'_>, sample: u32) -> Result<BenchmarkSample> {
551    let SampleContext {
552        rt,
553        catalog,
554        sim_config,
555        chunk,
556        resolver,
557        parameters,
558        slug,
559    } = *context;
560    let build_started_at = Instant::now();
561    let setup = rt
562        .block_on(resolve_setup(
563            catalog,
564            sim_config,
565            resolver,
566            &IntentOverrides::default(),
567        ))
568        .with_context(|| format!("setup resolution failed for {slug}, sample {sample}"))?;
569    let setup_time_ms = build_started_at.elapsed().as_secs_f64() * MS_PER_SECOND;
570    let handler_build_ns = Cell::new(0_u64);
571    let run_started_at = Instant::now();
572
573    // PANIC-BOUNDARY: isolate one benchmark sample so the CLI reports its exact scenario.
574    let sim_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
575        run_chunk(
576            ChunkRun {
577                assignment: chunk,
578                duration_s: setup.duration_s,
579                seed_base: DEFAULT_SEED,
580            },
581            &NoopProgress,
582            || {
583                let (handler, elapsed) =
584                    measure_build_operation(|| build_handler_from_setup(&setup));
585
586                handler_build_ns.set(elapsed);
587
588                if handler.is_ok() {
589                    wowlab_types::bench_metrics::reset();
590                    crate::GLOBAL.start();
591                }
592
593                handler
594            },
595        )
596    }));
597    let allocation_snapshot = crate::GLOBAL.stop();
598    let total_run_ms = run_started_at.elapsed().as_secs_f64() * MS_PER_SECOND;
599    let handler_time_ms = handler_build_ns.get() as f64 / NANOS_PER_MILLISECOND;
600    let elapsed_ms = (total_run_ms - handler_time_ms).max(0.0);
601    let report = match sim_result {
602        Ok(Ok(report)) => report,
603        Ok(Err(error)) => {
604            return Err(anyhow::Error::new(error)
605                .context(format!("simulation failed for {slug}, sample {sample}")));
606        }
607        Err(panic) => {
608            let message = panic
609                .downcast_ref::<String>()
610                .map(String::as_str)
611                .or_else(|| panic.downcast_ref::<&str>().copied())
612                .unwrap_or("unknown panic");
613
614            anyhow::bail!("panic during {slug}, sample {sample}: {message}");
615        }
616    };
617    let counters = wowlab_types::bench_metrics::snapshot();
618    let telemetry = sim::decode_telemetry(&report)?;
619    let iterations_per_sec = if elapsed_ms > 0.0 {
620        f64::from(parameters.iterations()) / (elapsed_ms / MS_PER_SECOND)
621    } else {
622        0.0
623    };
624
625    Ok(BenchmarkSample {
626        sample,
627        elapsed_ms,
628        iterations_per_sec,
629        mean_dps: sim::mean_dps(&telemetry),
630        semantic_fingerprint: semantic_fingerprint(&telemetry),
631        counters,
632        build_time_ms: setup_time_ms + handler_time_ms,
633        reset_time_ms: reset_time_ms(counters),
634        allocations: Some(allocation_snapshot),
635    })
636}
637
638fn measure_build_operation<T>(operation: impl FnOnce() -> T) -> (T, u64) {
639    let started_at = Instant::now();
640    let value = operation();
641    let elapsed = u64::try_from(started_at.elapsed().as_nanos()).unwrap_or(u64::MAX);
642
643    (value, elapsed)
644}
645
646const fn reset_time_ms(counters: wowlab_types::bench_metrics::BenchmarkCounters) -> f64 {
647    counters.reset_time_ns as f64 / NANOS_PER_MILLISECOND
648}
649
650fn semantic_fingerprint(telemetry: &wowlab_types::proto::ChunkTelemetry) -> String {
651    blake3::hash(&telemetry.encode_to_vec())
652        .to_hex()
653        .to_string()
654}
655
656const fn benchmark_backend() -> BenchmarkBackend {
657    if cfg!(feature = "jit") {
658        BenchmarkBackend::Jit
659    } else {
660        BenchmarkBackend::Portable
661    }
662}
663
664fn mean(values: &[f64]) -> f64 {
665    values.iter().sum::<f64>() / values.len() as f64
666}
667
668fn median(values: &[f64]) -> f64 {
669    let mut sorted = values.to_vec();
670
671    sorted.sort_by(f64::total_cmp);
672    let middle = sorted.len() / MEDIAN_PAIR_SIZE;
673
674    let upper = sorted.get(middle).copied().unwrap_or_default();
675
676    if sorted.len().is_multiple_of(MEDIAN_PAIR_SIZE) {
677        let lower = sorted
678            .get(middle.saturating_sub(1))
679            .copied()
680            .unwrap_or(upper);
681
682        (lower + upper) / MEDIAN_PAIR_DIVISOR
683    } else {
684        upper
685    }
686}
687
688fn population_stddev(values: &[f64], mean: f64) -> f64 {
689    let variance = values
690        .iter()
691        .map(|value| (value - mean).powi(SQUARE_EXPONENT))
692        .sum::<f64>()
693        / values.len() as f64;
694
695    variance.sqrt()
696}
697
698fn print_summary(results: &[SpecBenchResult]) {
699    let total_iters: u64 = results
700        .iter()
701        .map(|result| u64::from(result.iterations) * result.samples.len() as u64)
702        .sum();
703    let total_ms: u64 = results.iter().map(|r| r.elapsed_ms).sum();
704    let overall_ips = if total_ms > 0 {
705        total_iters as f64 / (total_ms as f64 / MS_PER_SECOND)
706    } else {
707        0.0
708    };
709
710    output::kv("Total iterations", &output::fmt_number(total_iters as f64));
711    output::kv("Total time", &format!("{total_ms}ms"));
712    output::kv("Overall throughput", &format!("{overall_ips:.0} iter/s"));
713}
714
715pub(super) fn build_bench_run(
716    results: Vec<SpecBenchResult>,
717    scaling: Vec<ScalingResult>,
718) -> BenchRun {
719    BenchRun {
720        engine_version: wowlab_engine::composition::EngineBuildMetadata::CURRENT
721            .version()
722            .to_string(),
723        timestamp: shell_output("date", &["-u", "+%Y-%m-%dT%H:%M:%SZ"]),
724        git_hash: shell_output("git", &["rev-parse", "--short", "HEAD"]),
725        git_branch: shell_output("git", &["rev-parse", "--abbrev-ref", "HEAD"]),
726        rustc_version: shell_output("rustc", &["--version"]),
727        features: if cfg!(feature = "jit") {
728            vec!["jit".to_string()]
729        } else {
730            Vec::new()
731        },
732        backend: benchmark_backend(),
733        system: sys::os_profile(),
734        results,
735        scaling,
736    }
737}
738
739fn shell_output(cmd: &str, args: &[&str]) -> String {
740    let output = std::process::Command::new(cmd)
741        .args(args)
742        .output()
743        .ok()
744        .and_then(|o| String::from_utf8(o.stdout).ok());
745
746    output.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string())
747}
748
749#[cfg(test)]
750mod tests {
751    use googletest::{Result as GtestResult, prelude::*};
752
753    use super::*;
754
755    #[gtest]
756    fn canonical_empty_telemetry_has_pinned_blake3_fingerprint() -> GtestResult<()> {
757        let telemetry = wowlab_types::proto::ChunkTelemetry::default();
758
759        verify_that!(
760            semantic_fingerprint(&telemetry),
761            eq("af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262")
762        )
763    }
764
765    #[gtest]
766    fn fingerprint_is_stable_after_canonical_protobuf_round_trip() -> GtestResult<()> {
767        let telemetry = wowlab_types::proto::ChunkTelemetry {
768            iterations: 17,
769            mean_dps_x10: 123_456,
770            ..Default::default()
771        };
772        let bytes = telemetry.encode_to_vec();
773        let decoded = wowlab_types::proto::ChunkTelemetry::decode(bytes.as_slice()).or_fail()?;
774
775        verify_that!(
776            semantic_fingerprint(&telemetry),
777            eq(&semantic_fingerprint(&decoded))
778        )
779    }
780
781    #[gtest]
782    fn distribution_uses_population_standard_deviation_and_even_median() -> GtestResult<()> {
783        let values = [1.0, 2.0, 3.0, 4.0];
784        let average = mean(&values);
785
786        verify_that!(average, near(2.5, f64::EPSILON))?;
787        verify_that!(median(&values), near(2.5, f64::EPSILON))?;
788
789        verify_that!(
790            population_stddev(&values, average),
791            near(1.118_033_988_749_895, f64::EPSILON)
792        )
793    }
794
795    #[gtest]
796    fn benchmark_boundaries_measure_real_build_and_reset_operations() -> GtestResult<()> {
797        let (built, build_time_ns) = measure_build_operation(|| {
798            (0_u64..4_096).map(std::hint::black_box).collect::<Vec<_>>()
799        });
800
801        verify_that!(built.len(), eq(4_096))?;
802        verify_that!(build_time_ns, gt(0))?;
803
804        let mut resettable = built;
805
806        wowlab_types::bench_metrics::reset();
807        wowlab_types::bench_metrics::measure_reset(|| resettable.clear());
808
809        let counters = wowlab_types::bench_metrics::snapshot();
810
811        verify_that!(resettable, is_empty())?;
812        verify_that!(counters.resets, eq(1))?;
813        verify_that!(counters.reset_time_ns, gt(0))?;
814
815        verify_that!(reset_time_ms(counters), gt(0.0))
816    }
817}