Skip to main content

apif_source_row/
schema.rs

1use std::collections::HashMap;
2use std::sync::LazyLock;
3
4// Re-export bench schema constants from parser (format-level validation data)
5pub use apif_parser::validator::{
6    BENCH_ASSERT_MODE_VALUES, BENCH_CACHE_VALUES, BENCH_DURATION_KEYS, BENCH_DURATION_STOP_VALUES,
7    BENCH_LOAD_SCHEDULE_VALUES, BENCH_MODE_VALUES, BENCH_NUMERIC_KEYS, allowed_values_message,
8    canonical_bench_key, is_allowed_value,
9};
10
11pub const BENCH_BOOLEAN_KEYS: &[&str] = &["no_assert", "count_errors_in_latency"];
12
13pub const BENCH_DIRECT_KEYS: &[&str] = &[
14    "mode",
15    "profile",
16    "load_schedule",
17    "name",
18    "assert_mode",
19    "duration_stop",
20    "sample_rate",
21    "cache",
22    "latency_percentiles",
23    "warmup_mode",
24    "load_profile",
25];
26
27pub const BENCH_COMPOUND_KEYS: &[&str] = &["sources"];
28
29pub fn supported_bench_keys() -> Vec<&'static str> {
30    let mut keys: Vec<&'static str> = Vec::new();
31    keys.extend_from_slice(BENCH_DIRECT_KEYS);
32    keys.extend_from_slice(BENCH_NUMERIC_KEYS);
33    keys.extend_from_slice(BENCH_DURATION_KEYS);
34    keys.extend_from_slice(BENCH_BOOLEAN_KEYS);
35    keys.extend_from_slice(BENCH_COMPOUND_KEYS);
36    keys.push("thresholds.*");
37    keys.sort_unstable();
38    keys.dedup();
39    keys
40}
41
42pub fn bench_keys_canonical_order() -> Vec<&'static str> {
43    let mut keys = supported_bench_keys();
44    keys.sort_by(|a, b| {
45        bench_key_rank(a)
46            .cmp(&bench_key_rank(b))
47            .then_with(|| a.cmp(b))
48    });
49    keys
50}
51
52pub fn bench_key_detail(key: &str) -> String {
53    match key {
54        "mode" => format!(
55            "Runtime mode ({})",
56            allowed_values_message(BENCH_MODE_VALUES)
57        ),
58        "profile" => "Bench profile label (e.g. smoke, stress, soak)".to_string(),
59        "concurrency" => "Worker concurrency".to_string(),
60        "requests" => "Total requests stop condition".to_string(),
61        "duration" => "Duration stop condition (e.g. 30s)".to_string(),
62        "max_duration" => "Hard duration cap in requests mode".to_string(),
63        "ramp_up" => "Ramp-up duration".to_string(),
64        "warmup" => "Warmup duration".to_string(),
65        "max_rps" => "Max requests per second".to_string(),
66        "load_schedule" => format!(
67            "Load schedule ({})",
68            allowed_values_message(BENCH_LOAD_SCHEDULE_VALUES)
69        ),
70        "load_start" => "Schedule start RPS".to_string(),
71        "load_step" => "Schedule step delta RPS".to_string(),
72        "load_end" => "Schedule end RPS".to_string(),
73        "load_step_duration" => "Duration per schedule step".to_string(),
74        "load_max_duration" => "Maximum schedule duration".to_string(),
75        "progress_interval" => "Progress heartbeat interval".to_string(),
76        "connections" => "Number of transport connections".to_string(),
77        "connect_timeout" => "gRPC dial timeout".to_string(),
78        "keepalive" => "Transport keepalive interval".to_string(),
79        "cpus" => "CPU pinning hint".to_string(),
80        "name" => "Run name metadata".to_string(),
81        "assert_mode" => format!(
82            "Assertion mode ({})",
83            allowed_values_message(BENCH_ASSERT_MODE_VALUES)
84        ),
85        "no_assert" => "Disable assertions for transport-only benchmark".to_string(),
86        "duration_stop" => format!(
87            "In-flight behavior at duration deadline ({})",
88            allowed_values_message(BENCH_DURATION_STOP_VALUES)
89        ),
90        "sample_rate" => "Sampling rate for sampled assert/details".to_string(),
91        "cache" => format!(
92            "Cache mode ({})",
93            allowed_values_message(BENCH_CACHE_VALUES)
94        ),
95        "skip_first" => "Skip first N samples from stats".to_string(),
96        "count_errors_in_latency" => "Include failed calls in latency stats".to_string(),
97        "latency_percentiles" => "Comma-separated percentile list".to_string(),
98        "sources" => "Data source definitions for bench (file, format, index)".to_string(),
99        "cache_ttl" => "Cache TTL duration".to_string(),
100        "thresholds.*" => "Threshold expressions map".to_string(),
101        "load_midpoint" => "Midpoint RPS for sine schedule".to_string(),
102        "load_amplitude" => "Amplitude for sine schedule".to_string(),
103        "load_frequency" => "Frequency (rad/s) for sine schedule".to_string(),
104        "load_spike_target" => "Target RPS during spike".to_string(),
105        "load_spike_after" => "Seconds before spike starts".to_string(),
106        "load_spike_duration" => "Spike duration in seconds".to_string(),
107        "cool_down" => "Cool-down duration after main test".to_string(),
108        "warmup_mode" => "Warmup mode: warmup (default) or dry_run".to_string(),
109        "load_profile" => "Custom RPS profile (time:rps,time:rps)".to_string(),
110        _ => "BENCH option".to_string(),
111    }
112}
113
114pub fn bench_aliases(_key: &str) -> &'static [&'static str] {
115    &[]
116}
117
118pub fn is_known_bench_key(key: &str) -> bool {
119    if key == "thresholds" || key.starts_with("thresholds.") {
120        return true;
121    }
122    canonical_bench_key(key).is_some()
123}
124
125pub fn suggest_bench_key(raw_key: &str) -> Option<&'static str> {
126    let needle = raw_key.trim().to_ascii_lowercase().replace('-', "_");
127    if needle.is_empty() || needle == "thresholds" || needle.starts_with("thresholds.") {
128        return None;
129    }
130
131    let candidates = bench_keys_canonical_order();
132    let mut best: Option<(&'static str, usize)> = None;
133
134    for key in candidates {
135        if key == "thresholds.*" {
136            continue;
137        }
138        let key_norm = key.to_ascii_lowercase();
139        let Some(score) = bounded_edit_distance(&needle, &key_norm, 3) else {
140            continue;
141        };
142        match best {
143            Some((_, best_score)) if score >= best_score => {}
144            _ => best = Some((key, score)),
145        }
146    }
147
148    best.map(|(k, _)| k)
149}
150
151fn bounded_edit_distance(a: &str, b: &str, max: usize) -> Option<usize> {
152    let a_bytes = a.as_bytes();
153    let b_bytes = b.as_bytes();
154    if a_bytes == b_bytes {
155        return Some(0);
156    }
157    if a_bytes.len().abs_diff(b_bytes.len()) > max {
158        return None;
159    }
160
161    let mut prev: Vec<usize> = (0..=b_bytes.len()).collect();
162    let mut curr = vec![0; b_bytes.len() + 1];
163
164    for (i, &ac) in a_bytes.iter().enumerate() {
165        curr[0] = i + 1;
166        let mut row_min = curr[0];
167        for (j, &bc) in b_bytes.iter().enumerate() {
168            let cost = if ac == bc { 0 } else { 1 };
169            let del = prev[j + 1] + 1;
170            let ins = curr[j] + 1;
171            let sub = prev[j] + cost;
172            let v = del.min(ins).min(sub);
173            curr[j + 1] = v;
174            row_min = row_min.min(v);
175        }
176        if row_min > max {
177            return None;
178        }
179        std::mem::swap(&mut prev, &mut curr);
180    }
181
182    let dist = prev[b_bytes.len()];
183    if dist <= max { Some(dist) } else { None }
184}
185
186pub fn bench_value<'a>(bench: &'a HashMap<String, String>, key: &str) -> Option<&'a String> {
187    if let Some(v) = bench.get(key) {
188        return Some(v);
189    }
190    for alias in bench_aliases(key) {
191        if let Some(v) = bench.get(*alias) {
192            return Some(v);
193        }
194    }
195    None
196}
197
198pub fn bench_key_rank(key: &str) -> usize {
199    let canonical_order = [
200        "mode",
201        "profile",
202        "name",
203        "concurrency",
204        "requests",
205        "duration",
206        "max_duration",
207        "ramp_up",
208        "warmup",
209        "warmup_mode",
210        "cool_down",
211        "max_rps",
212        "load_schedule",
213        "load_start",
214        "load_step",
215        "load_end",
216        "load_step_duration",
217        "load_max_duration",
218        "load_midpoint",
219        "load_amplitude",
220        "load_frequency",
221        "load_spike_target",
222        "load_spike_after",
223        "load_spike_duration",
224        "load_profile",
225        "progress_interval",
226        "connections",
227        "connect_timeout",
228        "keepalive",
229        "cpus",
230        "assert_mode",
231        "no_assert",
232        "sample_rate",
233        "duration_stop",
234        "cache",
235        "cache_ttl",
236        "skip_first",
237        "count_errors_in_latency",
238        "latency_percentiles",
239        "sources",
240    ];
241
242    if let Some((idx, _)) = canonical_order.iter().enumerate().find(|(_, k)| **k == key) {
243        return idx;
244    }
245    if key.starts_with("thresholds.") || key == "thresholds" {
246        return canonical_order.len();
247    }
248    canonical_order.len() + 1
249}
250
251/// Built-in benchmark profiles.
252/// Each profile is a set of key-value pairs that override defaults.
253pub static BUILTIN_PROFILES: LazyLock<HashMap<&'static str, HashMap<&'static str, &'static str>>> =
254    LazyLock::new(|| {
255        let mut m: HashMap<&str, HashMap<&str, &str>> = HashMap::new();
256
257        let mut functional = HashMap::new();
258        functional.insert("description", "Quick functional check");
259        functional.insert("mode", "fixed");
260        functional.insert("concurrency", "1");
261        functional.insert("requests", "100");
262        functional.insert("duration", "30s");
263        m.insert("functional", functional);
264
265        let mut load = HashMap::new();
266        load.insert("description", "Stepped load test 50→200 RPS");
267        load.insert("mode", "stepping");
268        load.insert("concurrency", "10");
269        load.insert("duration", "60s");
270        load.insert("load_schedule", "step");
271        load.insert("load_start", "50");
272        load.insert("load_step", "10");
273        load.insert("load_end", "200");
274        load.insert("load_step_duration", "10s");
275        m.insert("load", load);
276
277        let mut stress = HashMap::new();
278        stress.insert("description", "Linear stress test 10→500 RPS");
279        stress.insert("mode", "stepping");
280        stress.insert("concurrency", "50");
281        stress.insert("duration", "120s");
282        stress.insert("load_schedule", "line");
283        stress.insert("load_start", "10");
284        stress.insert("load_step", "5");
285        stress.insert("load_end", "500");
286        m.insert("stress", stress);
287
288        let mut spike = HashMap::new();
289        spike.insert("description", "Spike test: 10→500→10 RPS");
290        spike.insert("mode", "fixed");
291        spike.insert("concurrency", "100");
292        spike.insert("duration", "60s");
293        spike.insert("load_schedule", "spike");
294        spike.insert("load_start", "10");
295        spike.insert("load_spike_target", "500");
296        spike.insert("load_spike_after", "30");
297        spike.insert("load_spike_duration", "10");
298        m.insert("spike", spike);
299
300        let mut soak = HashMap::new();
301        soak.insert("description", "Long-duration soak at 50 RPS");
302        soak.insert("mode", "fixed");
303        soak.insert("concurrency", "5");
304        soak.insert("duration", "3600s");
305        soak.insert("load_schedule", "const");
306        soak.insert("load_start", "50");
307        m.insert("soak", soak);
308
309        m
310    });
311
312/// Apply a named profile to a BENCH section config.
313/// Returns the list of (key, value) pairs that the profile defines.
314pub fn apply_profile(name: &str) -> Vec<(&'static str, &'static str)> {
315    if let Some(profile) = BUILTIN_PROFILES.get(name) {
316        return profile.iter().map(|(k, v)| (*k, *v)).collect();
317    }
318    Vec::new()
319}
320
321/// Custom profiles loaded from YAML files at runtime.
322static CUSTOM_PROFILES: std::sync::LazyLock<
323    std::sync::RwLock<HashMap<String, HashMap<String, String>>>,
324> = std::sync::LazyLock::new(|| std::sync::RwLock::new(HashMap::new()));
325
326/// Register a custom profile at runtime.
327pub fn register_custom_profile(name: &str, keys: HashMap<String, String>) {
328    CUSTOM_PROFILES
329        .write()
330        .unwrap()
331        .insert(name.to_string(), keys);
332}
333
334/// Apply a custom (or built-in) profile, returning key-value pairs.
335/// Returns empty vec if the profile is not found.
336pub fn apply_profile_dynamic(name: &str) -> Vec<(String, String)> {
337    // Check built-in first
338    let builtin = apply_profile(name);
339    if !builtin.is_empty() {
340        return builtin
341            .into_iter()
342            .map(|(k, v)| (k.to_string(), v.to_string()))
343            .collect();
344    }
345    // Check custom profiles
346    if let Some(keys) = CUSTOM_PROFILES.read().unwrap().get(name) {
347        return keys.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
348    }
349    Vec::new()
350}
351
352/// List all available profiles (built-in + custom).
353pub fn list_profiles() -> Vec<(String, HashMap<String, String>)> {
354    let mut result: Vec<(String, HashMap<String, String>)> = BUILTIN_PROFILES
355        .iter()
356        .map(|(name, keys)| {
357            (
358                name.to_string(),
359                keys.iter()
360                    .map(|(k, v)| (k.to_string(), v.to_string()))
361                    .collect(),
362            )
363        })
364        .collect();
365    for (name, keys) in CUSTOM_PROFILES.read().unwrap().iter() {
366        result.push((name.clone(), keys.clone()));
367    }
368    result.sort_by(|a, b| a.0.cmp(&b.0));
369    result
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn supported_bench_keys_contains_scheduler_and_thresholds() {
378        let keys = supported_bench_keys();
379        assert!(keys.contains(&"load_schedule"));
380        assert!(keys.contains(&"progress_interval"));
381        assert!(keys.contains(&"thresholds.*"));
382    }
383
384    #[test]
385    fn bench_key_rank_orders_core_fields_before_thresholds() {
386        assert!(bench_key_rank("mode") < bench_key_rank("profile"));
387        assert!(bench_key_rank("profile") < bench_key_rank("concurrency"));
388        assert!(bench_key_rank("concurrency") < bench_key_rank("load_schedule"));
389        assert!(bench_key_rank("load_schedule") < bench_key_rank("thresholds.p(95)"));
390        assert!(bench_key_rank("thresholds.p(95)") < bench_key_rank("unknown_key"));
391    }
392
393    #[test]
394    fn bench_value_uses_canonical_keys_only() {
395        let mut bench = HashMap::new();
396        bench.insert("load_schedule".to_string(), "step".to_string());
397        bench.insert("progress_interval".to_string(), "2s".to_string());
398
399        assert_eq!(
400            bench_value(&bench, "load_schedule"),
401            Some(&"step".to_string())
402        );
403        assert_eq!(
404            bench_value(&bench, "progress_interval"),
405            Some(&"2s".to_string())
406        );
407    }
408
409    #[test]
410    fn canonical_bench_key_resolves_known_keys() {
411        assert_eq!(canonical_bench_key("load_schedule"), Some("load_schedule"));
412        assert_eq!(
413            canonical_bench_key("progress_interval"),
414            Some("progress_interval")
415        );
416        assert_eq!(canonical_bench_key("mode"), Some("mode"));
417        assert_eq!(canonical_bench_key("unknown_key"), None);
418    }
419
420    #[test]
421    fn bench_keys_canonical_order_starts_with_mode() {
422        let keys = bench_keys_canonical_order();
423        assert_eq!(keys.first().copied(), Some("mode"));
424        assert!(keys.iter().position(|k| *k == "thresholds.*").is_some());
425    }
426
427    #[test]
428    fn suggest_bench_key_for_typo() {
429        assert_eq!(suggest_bench_key("load_shedule"), Some("load_schedule"));
430        assert_eq!(suggest_bench_key("duraton_stop"), Some("duration_stop"));
431        assert_eq!(suggest_bench_key("thresholds"), None);
432    }
433}