1use 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}