Skip to main content

p6_scale/
p6_scale.rs

1//! ADR-119 P6 scale-certification harness.
2//!
3//! Run against a disposable generated corpus. The harness restores every
4//! mutation it makes, but the cache directory is intentionally retained for
5//! inspection.
6
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::time::Instant;
10
11use rac_engine::freshness::FreshnessTracker;
12use serde_json::{json, Value};
13
14fn elapsed_ms(start: Instant) -> f64 {
15    start.elapsed().as_secs_f64() * 1000.0
16}
17
18fn timed_read(tracker: &mut FreshnessTracker, verify: bool) -> f64 {
19    let start = Instant::now();
20    tracker.read_model(verify);
21    elapsed_ms(start)
22}
23
24fn distribution(values: &[f64]) -> Value {
25    let mut sorted = values.to_vec();
26    sorted.sort_by(f64::total_cmp);
27    let percentile = |p: f64| {
28        let index = ((sorted.len() as f64 * p).ceil() as usize)
29            .saturating_sub(1)
30            .min(sorted.len().saturating_sub(1));
31        sorted.get(index).copied().unwrap_or(0.0)
32    };
33    json!({"p50_ms": percentile(0.50), "p95_ms": percentile(0.95)})
34}
35
36fn first_markdown(root: &Path) -> PathBuf {
37    rac_engine::walk::find_markdown_files(&root.to_string_lossy(), true)
38        .into_iter()
39        .next()
40        .map(|entry| root.join(entry.components.join("/")))
41        .expect("corpus must contain at least one markdown file")
42}
43
44fn main() {
45    let args: Vec<String> = std::env::args().collect();
46    if args.len() < 4 || !matches!(args[1].as_str(), "snapshot" | "delta") {
47        eprintln!("usage: p6_scale <snapshot|delta> <corpus> <cache-dir> [iterations]");
48        std::process::exit(2);
49    }
50    let mode = &args[1];
51    let root = PathBuf::from(&args[2]);
52    let cache = PathBuf::from(&args[3]);
53    let iterations: usize = args.get(4).and_then(|v| v.parse().ok()).unwrap_or(7);
54    let root_text = root.to_string_lossy().into_owned();
55    let file_count = rac_engine::walk::find_markdown_files(&root_text, true).len();
56    let threshold = file_count.saturating_add(iterations).saturating_add(10);
57    let mut tracker = if mode == "delta" {
58        FreshnessTracker::new(cache.clone(), &root_text, Some(threshold))
59    } else {
60        FreshnessTracker::new_snapshot(cache.clone(), &root_text, Some(threshold))
61    };
62
63    let cold_ms = timed_read(&mut tracker, true);
64    let warm_ms: Vec<f64> = (0..iterations)
65        .map(|_| timed_read(&mut tracker, false))
66        .collect();
67
68    let target = first_markdown(&root);
69    let original = fs::read(&target).expect("read mutation target");
70    let mut edited = original.clone();
71    edited.extend_from_slice(b"\n<!-- p6-scale-edit -->\n");
72    let mut edit_ms = Vec::new();
73    for _ in 0..iterations {
74        fs::write(&target, &edited).expect("write edit");
75        edit_ms.push(timed_read(&mut tracker, true));
76        fs::write(&target, &original).expect("restore edit");
77        timed_read(&mut tracker, true);
78    }
79
80    let added = root.join("p6-scale-added.md");
81    let added_text = "---\nschema_version: 1\nid: RAC-Z9SCAE000001\ntype: decision\n---\n# Scale Probe\n\n## Context\n\nBenchmark.\n\n## Decision\n\nMeasure.\n\n## Consequences\n\nEvidence.\n\n## Status\n\nAccepted\n";
82    let mut add_ms = Vec::new();
83    let mut delete_ms = Vec::new();
84    for _ in 0..iterations {
85        fs::write(&added, added_text).expect("write added probe");
86        add_ms.push(timed_read(&mut tracker, true));
87        fs::remove_file(&added).expect("remove added probe");
88        delete_ms.push(timed_read(&mut tracker, true));
89    }
90
91    let renamed = target.with_extension("p6-scale-renamed.md");
92    let mut rename_ms = Vec::new();
93    for _ in 0..iterations {
94        fs::rename(&target, &renamed).expect("rename probe");
95        rename_ms.push(timed_read(&mut tracker, true));
96        fs::rename(&renamed, &target).expect("restore rename probe");
97        timed_read(&mut tracker, true);
98    }
99
100    let mut compact_tracker = if mode == "delta" {
101        FreshnessTracker::new(cache.join("compact"), &root_text, Some(1))
102    } else {
103        FreshnessTracker::new_snapshot(cache.join("compact"), &root_text, Some(1))
104    };
105    timed_read(&mut compact_tracker, true);
106    fs::write(&target, &edited).expect("write compaction edit");
107    let compact_ms = timed_read(&mut compact_tracker, true);
108    fs::write(&target, &original).expect("restore compaction edit");
109
110    let summary = json!({
111        "warm": distribution(&warm_ms),
112        "edit": distribution(&edit_ms),
113        "add": distribution(&add_ms),
114        "delete": distribution(&delete_ms),
115        "rename": distribution(&rename_ms),
116    });
117    let output: Value = json!({
118        "schema_version": "1",
119        "mode": mode,
120        "corpus": root_text,
121        "files": file_count,
122        "iterations": iterations,
123        "cold_ms": cold_ms,
124        "warm_ms": warm_ms,
125        "edit_ms": edit_ms,
126        "add_ms": add_ms,
127        "delete_ms": delete_ms,
128        "rename_ms": rename_ms,
129        "compact_ms": compact_ms,
130        "summary": summary,
131        "last_parse_files": tracker.last_parse_files(),
132        "delta_size": tracker.delta_size(),
133    });
134    println!("{}", serde_json::to_string_pretty(&output).unwrap());
135}