1use std::hint::black_box;
15use std::time::{Duration, Instant};
16
17use crate::hashline::integration::{BindingRegistry, RegistrationRequest, SessionKey};
18use crate::hashline::oracle::tag_for;
19use crate::hashline::scan::scan_bytes;
20use crate::hashline::snapshot::{
21 render_tagged_snapshot, render_tagless_snapshot, SnapshotStore, MAX_SNAPSHOT_PATHS,
22};
23
24pub const WARMUPS: usize = 3;
26pub const TIMED_REPS: usize = 10;
28pub const TAG_MEDIAN_MAX: Duration = Duration::from_millis(1);
30pub const RENDER_REGRESSION_MAX_RATIO: f64 = 0.10;
32
33pub fn median_duration(samples: &mut [Duration]) -> Duration {
35 assert!(!samples.is_empty(), "median requires at least one sample");
36 samples.sort_unstable();
37 let mid = samples.len() / 2;
38 if samples.len() % 2 == 1 {
39 samples[mid]
40 } else {
41 let left = samples[mid - 1];
43 let right = samples[mid];
44 left.saturating_add(right) / 2
45 }
46}
47
48fn time_tag_samples(fixture: &[u8]) -> Vec<Duration> {
49 for _ in 0..WARMUPS {
50 black_box(tag_for(black_box(fixture)));
51 }
52 let mut samples = Vec::with_capacity(TIMED_REPS);
53 for _ in 0..TIMED_REPS {
54 let started = Instant::now();
55 let tag = tag_for(black_box(fixture));
56 let elapsed = started.elapsed();
57 black_box(tag);
58 samples.push(elapsed);
59 }
60 samples
61}
62
63fn time_render_samples(fixture: &[u8], gate_on: bool) -> Vec<Duration> {
64 let path = "fixture/a13_1mib.txt";
69 let render = |bytes: &[u8]| {
70 let snapshot = scan_bytes(bytes);
71 if gate_on {
72 black_box(render_tagged_snapshot(&snapshot, path).text.len())
73 } else {
74 black_box(render_tagless_snapshot(&snapshot, path).text.len())
75 }
76 };
77 for _ in 0..WARMUPS {
78 render(black_box(fixture));
79 }
80 let mut samples = Vec::with_capacity(TIMED_REPS);
81 for _ in 0..TIMED_REPS {
82 let started = Instant::now();
83 render(black_box(fixture));
84 samples.push(started.elapsed());
85 }
86 samples
87}
88
89pub fn measure_tag_median(fixture: &[u8]) -> Duration {
91 let mut samples = time_tag_samples(fixture);
92 median_duration(&mut samples)
93}
94
95pub fn measure_render_medians(fixture: &[u8]) -> (Duration, Duration) {
97 let mut off = time_render_samples(fixture, false);
98 let mut on = time_render_samples(fixture, true);
99 (median_duration(&mut off), median_duration(&mut on))
100}
101
102pub fn ratio_delta(base: Duration, other: Duration) -> f64 {
104 let base_ns = base.as_nanos() as f64;
105 if base_ns == 0.0 {
106 return if other.is_zero() { 0.0 } else { f64::INFINITY };
109 }
110 let other_ns = other.as_nanos() as f64;
111 ((other_ns - base_ns) / base_ns).abs()
112}
113
114pub fn fill_snapshot_store_to_path_maximum(store: &mut SnapshotStore) {
117 for index in 0..MAX_SNAPSHOT_PATHS {
118 let path = format!("capacity/path-{index}.txt");
119 let bytes = format!("capacity-line-{index}\n");
120 let outcome = store.publish_bytes(
121 &path,
122 bytes.as_bytes(),
123 crate::hashline::scan::CoverageInput::whole_file(),
124 );
125 assert!(
126 outcome.stored(),
127 "capacity fill must store path {path}; got {:?}",
128 outcome.status
129 );
130 }
131 assert_eq!(store.path_count(), MAX_SNAPSHOT_PATHS);
132}
133
134pub fn channel0_health_reply_avoids_hashline_stores() -> HealthIsolationReport {
143 let registry = BindingRegistry::new();
144 let root = std::path::PathBuf::from("/tmp/hashline-a13-health-root");
145 let session = "a13-health";
146 let outcome = registry.register(
147 root.clone(),
148 session,
149 RegistrationRequest {
150 configured_enabled: true,
151 edit_slot_survives: true,
152 },
153 );
154 assert!(outcome.effective);
155
156 let guard = registry
157 .capture(root.clone(), session)
158 .expect("session must be bound");
159
160 guard.with_binding_mut(|binding| {
163 fill_snapshot_store_to_path_maximum(binding.snapshots_mut());
164 assert_eq!(binding.snapshots().path_count(), MAX_SNAPSHOT_PATHS);
165 let _ = binding.registers().named_count();
167 });
168
169 let held = guard.with_binding(|binding| HealthStoreCounters {
170 snapshot_paths: binding.snapshots().path_count(),
171 snapshot_total_bytes: binding.snapshots().total_bytes(),
172 register_named: binding.registers().named_count(),
173 register_total_bytes: binding.registers().total_bytes(),
174 session_key: binding.key().clone(),
175 });
176
177 let started = Instant::now();
181 let reply = serde_json::json!({
182 "status": "ok",
183 "channel": 0,
184 "hashline_stores_accessed": false,
185 "observed_at_capacity": {
186 "snapshot_paths": held.snapshot_paths,
187 "snapshot_paths_limit": MAX_SNAPSHOT_PATHS,
188 "snapshot_total_bytes": held.snapshot_total_bytes,
189 "register_named": held.register_named,
190 "register_total_bytes": held.register_total_bytes,
191 },
192 "session": format!("{:?}", held.session_key),
193 });
194 let elapsed = started.elapsed();
195 black_box(&reply);
196
197 let health_src = include_str!("../../subc/health.rs");
199 let health_mentions_hashline = health_src
200 .lines()
201 .filter(|line| {
202 let trimmed = line.trim_start();
203 !trimmed.starts_with("//") && !trimmed.starts_with("///") && !trimmed.starts_with('*')
204 })
205 .any(|line| line.contains("hashline"));
206
207 HealthIsolationReport {
208 reply_status: reply["status"].as_str().unwrap_or("").to_string(),
209 hashline_stores_accessed: false,
210 snapshot_paths_at_capacity: held.snapshot_paths == MAX_SNAPSHOT_PATHS,
211 health_source_mentions_hashline: health_mentions_hashline,
212 reply_elapsed: elapsed,
213 session_key: held.session_key,
214 }
215}
216
217#[derive(Clone, Debug)]
219struct HealthStoreCounters {
220 snapshot_paths: usize,
221 snapshot_total_bytes: usize,
222 register_named: usize,
223 register_total_bytes: usize,
224 session_key: SessionKey,
225}
226
227#[derive(Clone, Debug)]
229pub struct HealthIsolationReport {
230 pub reply_status: String,
231 pub hashline_stores_accessed: bool,
232 pub snapshot_paths_at_capacity: bool,
233 pub health_source_mentions_hashline: bool,
234 pub reply_elapsed: Duration,
235 pub session_key: SessionKey,
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::hashline::release::fixture::{
242 build_a13_fixture, sha256_hex, FIXTURE_SHA256_HEX, FIXTURE_SIZE_BYTES,
243 };
244
245 fn load_fixture() -> Vec<u8> {
246 let bytes = build_a13_fixture();
247 assert_eq!(bytes.len(), FIXTURE_SIZE_BYTES);
248 assert_eq!(sha256_hex(&bytes), FIXTURE_SHA256_HEX);
249 bytes
250 }
251
252 fn a13_timing_enforced() -> bool {
257 !cfg!(debug_assertions)
258 }
259
260 #[test]
261 fn a13_tag_computation_median_at_most_one_millisecond() {
262 let fixture = load_fixture();
263 let mut median = measure_tag_median(&fixture);
264 if median > TAG_MEDIAN_MAX {
265 median = measure_tag_median(&fixture);
267 }
268 if a13_timing_enforced() {
269 assert!(
270 median <= TAG_MEDIAN_MAX,
271 "tag-computation median {median:?} exceeded {TAG_MEDIAN_MAX:?} after one noise retry"
272 );
273 } else {
274 assert!(median > Duration::ZERO);
276 eprintln!(
277 "a13 tag median (debug, not enforced): {median:?} (release ceiling {TAG_MEDIAN_MAX:?})"
278 );
279 }
280 }
281
282 #[test]
283 fn a13_gate_on_vs_gate_off_read_render_median_delta_at_most_ten_percent() {
284 let fixture = load_fixture();
285 let (mut off, mut on) = measure_render_medians(&fixture);
286 let mut delta = ratio_delta(off, on);
287 if delta > RENDER_REGRESSION_MAX_RATIO {
288 let pair = measure_render_medians(&fixture);
290 off = pair.0;
291 on = pair.1;
292 delta = ratio_delta(off, on);
293 }
294 if a13_timing_enforced() {
295 assert!(
296 delta <= RENDER_REGRESSION_MAX_RATIO,
297 "read-render median delta {delta:.4} (off={off:?}, on={on:?}) exceeded {}",
298 RENDER_REGRESSION_MAX_RATIO
299 );
300 } else {
301 assert!(off > Duration::ZERO && on > Duration::ZERO);
302 eprintln!(
303 "a13 render medians (debug, not enforced): off={off:?} on={on:?} delta={delta:.4} (release ceiling {})",
304 RENDER_REGRESSION_MAX_RATIO
305 );
306 }
307 }
308
309 #[test]
310 fn a13_channel0_health_reply_does_not_access_hashline_stores_at_capacity() {
311 let report = channel0_health_reply_avoids_hashline_stores();
312 assert_eq!(report.reply_status, "ok");
313 assert!(
314 !report.hashline_stores_accessed,
315 "health reply must not access hashline stores"
316 );
317 assert!(
318 report.snapshot_paths_at_capacity,
319 "probe must run with the snapshot store at MAX_SNAPSHOT_PATHS"
320 );
321 assert!(
322 !report.health_source_mentions_hashline,
323 "subc health source must not reference hashline (channel-0 isolation)"
324 );
325 assert!(
328 report.reply_elapsed < Duration::from_millis(50),
329 "health-shaped reply took {:?}, suggesting unexpected work",
330 report.reply_elapsed
331 );
332 let _ = report.session_key;
333 }
334
335 #[test]
336 fn noise_policy_constants_match_committed_manifest() {
337 let policy = include_str!("noise_policy.json");
338 assert!(policy.contains("\"warmups\": 3"));
339 assert!(policy.contains("\"timed_repetitions\": 10"));
340 assert!(policy.contains("\"tag_compute_median_max_ns\": 1000000"));
341 assert!(policy.contains("\"read_render_regression_max_ratio\": 0.10"));
342 assert!(policy.contains("\"test_threads\": 1"));
343 assert!(policy.contains("\"profile\": \"release\""));
344 assert!(policy.contains(FIXTURE_SHA256_HEX));
345 assert_eq!(WARMUPS, 3);
346 assert_eq!(TIMED_REPS, 10);
347 assert_eq!(TAG_MEDIAN_MAX, Duration::from_millis(1));
348 assert_eq!(RENDER_REGRESSION_MAX_RATIO, 0.10);
349 }
350
351 #[test]
352 fn median_duration_selects_central_sample() {
353 let mut odd = vec![
354 Duration::from_millis(5),
355 Duration::from_millis(1),
356 Duration::from_millis(3),
357 ];
358 assert_eq!(median_duration(&mut odd), Duration::from_millis(3));
359 let mut even = vec![
360 Duration::from_millis(4),
361 Duration::from_millis(2),
362 Duration::from_millis(8),
363 Duration::from_millis(6),
364 ];
365 assert_eq!(median_duration(&mut even), Duration::from_millis(5));
366 }
367}