Skip to main content

crabscore_analysis/
metrics.rs

1//! Performance metrics collection utilities for CrabScore analysis.
2
3use anyhow::Result;
4use crabscore_core::metrics::{
5    LatencyMetrics, PerformanceMetrics, ResourceMetrics, ScalabilityMetrics, ThroughputMetrics,
6};
7use std::time::Instant;
8use tokio::process::Command;
9
10/// Options controlling how benchmarks are executed.
11#[derive(Debug, Clone)]
12pub struct BenchmarkOptions {
13    /// Number of warm-up iterations (not recorded).
14    pub warmup: u32,
15    /// Number of measured iterations.
16    pub iterations: u32,
17    /// Arguments to pass to the executable.
18    pub args: Vec<String>,
19}
20
21impl Default for BenchmarkOptions {
22    fn default() -> Self {
23        Self {
24            warmup: 1,
25            iterations: 5,
26            args: Vec::new(),
27        }
28    }
29}
30
31/// Runs a target executable multiple times and aggregates latency statistics.
32#[derive(Default)]
33pub struct BenchmarkRunner {
34    opts: BenchmarkOptions,
35}
36
37impl BenchmarkRunner {
38    /// Create a new BenchmarkMetrics with the given options.
39    pub fn new(opts: BenchmarkOptions) -> Self {
40        Self { opts }
41    }
42
43    /// Benchmark the given executable and return `PerformanceMetrics`.
44    pub async fn benchmark<P: AsRef<std::path::Path>>(
45        &self,
46        executable: P,
47    ) -> Result<PerformanceMetrics> {
48        let exe = executable.as_ref();
49        let mut samples = Vec::with_capacity(self.opts.iterations as usize);
50
51        // Warm-up runs (ignored)
52        for _ in 0..self.opts.warmup {
53            let _ = Command::new(exe).args(&self.opts.args).status().await?;
54        }
55
56        // Measured runs
57        for _ in 0..self.opts.iterations {
58            let start = Instant::now();
59            let status = Command::new(exe).args(&self.opts.args).status().await?;
60            let elapsed = start.elapsed();
61            if status.success() {
62                samples.push(elapsed.as_secs_f64() * 1000.0); // ms
63            }
64        }
65
66        if samples.is_empty() {
67            return Ok(PerformanceMetrics::default());
68        }
69
70        samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
71        let idx =
72            |p: f64| ((p * (samples.len() as f64 - 1.0)).round() as usize).min(samples.len() - 1);
73
74        let latency = LatencyMetrics {
75            p50_ms: samples[idx(0.50)],
76            p95_ms: samples[idx(0.95)],
77            p99_ms: samples[idx(0.99)],
78            cold_start_ms: samples[0],
79            ttfb_ms: 0.0, // not measured here
80        };
81
82        // Throughput: ops per second = 1000 / median latency
83        let throughput = ThroughputMetrics {
84            requests_per_second: if latency.p50_ms > 0.0 {
85                1000.0 / latency.p50_ms
86            } else {
87                0.0
88            },
89            mb_per_second: 0.0,
90            concurrent_connections: 0,
91            queue_depth: 0.0,
92        };
93
94        let perf = PerformanceMetrics {
95            latency,
96            throughput,
97            resource_usage: ResourceMetrics::default(),
98            scalability: ScalabilityMetrics::default(),
99        };
100
101        Ok(perf)
102    }
103}