Skip to main content

running_process/broker/server/
perf_guard.rs

1//! Hello latency budget enforcement for the v1 broker.
2
3use std::time::Duration;
4
5/// Minimum sample count for the CI Hello perf guard.
6pub const HELLO_PERF_SAMPLE_COUNT: usize = 10_000;
7
8/// Environment variable that enables the real local-socket Hello perf gate.
9pub const HELLO_PERF_GUARD_ENV: &str = "RUNNING_PROCESS_BROKER_HELLO_PERF_GUARD";
10
11/// Frozen Hello P50 latency budget.
12pub const HELLO_P50_BUDGET: Duration = Duration::from_micros(200);
13
14/// Frozen Hello P99 latency budget.
15pub const HELLO_P99_BUDGET: Duration = Duration::from_millis(1);
16
17/// Percentile summary for one Hello latency run.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct HelloLatencySummary {
20    /// Number of samples summarized.
21    pub sample_count: usize,
22    /// P50 latency.
23    pub p50: Duration,
24    /// P99 latency.
25    pub p99: Duration,
26}
27
28/// Errors returned when a perf run violates the v1 budget.
29#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
30pub enum PerfGuardError {
31    /// No samples were supplied.
32    #[error("hello perf guard received no samples")]
33    EmptySamples,
34    /// The sample set is too small for CI gating.
35    #[error("hello perf guard needs at least {required} samples, got {actual}")]
36    TooFewSamples {
37        /// Required sample count.
38        required: usize,
39        /// Actual sample count.
40        actual: usize,
41    },
42    /// P50 exceeded the frozen budget.
43    #[error("hello P50 budget exceeded: actual {actual:?}, budget {budget:?}")]
44    P50Exceeded {
45        /// Actual percentile.
46        actual: Duration,
47        /// Frozen budget.
48        budget: Duration,
49    },
50    /// P99 exceeded the frozen budget.
51    #[error("hello P99 budget exceeded: actual {actual:?}, budget {budget:?}")]
52    P99Exceeded {
53        /// Actual percentile.
54        actual: Duration,
55        /// Frozen budget.
56        budget: Duration,
57    },
58}
59
60/// Summarize one non-empty latency sample set.
61pub fn summarize_hello_latencies(
62    samples: &[Duration],
63) -> Result<HelloLatencySummary, PerfGuardError> {
64    if samples.is_empty() {
65        return Err(PerfGuardError::EmptySamples);
66    }
67
68    let mut sorted = samples.to_vec();
69    sorted.sort_unstable();
70    Ok(HelloLatencySummary {
71        sample_count: sorted.len(),
72        p50: percentile_nearest_rank(&sorted, 50),
73        p99: percentile_nearest_rank(&sorted, 99),
74    })
75}
76
77/// Enforce the frozen v1 Hello latency budget.
78pub fn enforce_hello_latency_budget(
79    samples: &[Duration],
80) -> Result<HelloLatencySummary, PerfGuardError> {
81    let summary = summarize_hello_latencies(samples)?;
82    if summary.sample_count < HELLO_PERF_SAMPLE_COUNT {
83        return Err(PerfGuardError::TooFewSamples {
84            required: HELLO_PERF_SAMPLE_COUNT,
85            actual: summary.sample_count,
86        });
87    }
88    if summary.p50 > HELLO_P50_BUDGET {
89        return Err(PerfGuardError::P50Exceeded {
90            actual: summary.p50,
91            budget: HELLO_P50_BUDGET,
92        });
93    }
94    if summary.p99 > HELLO_P99_BUDGET {
95        return Err(PerfGuardError::P99Exceeded {
96            actual: summary.p99,
97            budget: HELLO_P99_BUDGET,
98        });
99    }
100    Ok(summary)
101}
102
103fn percentile_nearest_rank(sorted: &[Duration], percentile: usize) -> Duration {
104    debug_assert!(!sorted.is_empty());
105    debug_assert!((1..=100).contains(&percentile));
106
107    let rank = sorted.len() * percentile;
108    let index = rank.div_ceil(100).saturating_sub(1);
109    sorted[index.min(sorted.len() - 1)]
110}