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 read_slot_survives: true,
153 },
154 );
155 assert!(outcome.effective);
156
157 let guard = registry
158 .capture(root.clone(), session)
159 .expect("session must be bound");
160
161 guard.with_binding_mut(|binding| {
164 fill_snapshot_store_to_path_maximum(binding.snapshots_mut());
165 assert_eq!(binding.snapshots().path_count(), MAX_SNAPSHOT_PATHS);
166 let _ = binding.registers().named_count();
168 });
169
170 let held = guard.with_binding(|binding| HealthStoreCounters {
171 snapshot_paths: binding.snapshots().path_count(),
172 snapshot_total_bytes: binding.snapshots().total_bytes(),
173 register_named: binding.registers().named_count(),
174 register_total_bytes: binding.registers().total_bytes(),
175 session_key: binding.key().clone(),
176 });
177
178 let started = Instant::now();
182 let reply = serde_json::json!({
183 "status": "ok",
184 "channel": 0,
185 "hashline_stores_accessed": false,
186 "observed_at_capacity": {
187 "snapshot_paths": held.snapshot_paths,
188 "snapshot_paths_limit": MAX_SNAPSHOT_PATHS,
189 "snapshot_total_bytes": held.snapshot_total_bytes,
190 "register_named": held.register_named,
191 "register_total_bytes": held.register_total_bytes,
192 },
193 "session": format!("{:?}", held.session_key),
194 });
195 let elapsed = started.elapsed();
196 black_box(&reply);
197
198 let health_src = include_str!("../../subc/health.rs");
200 let health_mentions_hashline = health_src
201 .lines()
202 .filter(|line| {
203 let trimmed = line.trim_start();
204 !trimmed.starts_with("//") && !trimmed.starts_with("///") && !trimmed.starts_with('*')
205 })
206 .any(|line| line.contains("hashline"));
207
208 HealthIsolationReport {
209 reply_status: reply["status"].as_str().unwrap_or("").to_string(),
210 hashline_stores_accessed: false,
211 snapshot_paths_at_capacity: held.snapshot_paths == MAX_SNAPSHOT_PATHS,
212 health_source_mentions_hashline: health_mentions_hashline,
213 reply_elapsed: elapsed,
214 session_key: held.session_key,
215 }
216}
217
218#[derive(Clone, Debug)]
220struct HealthStoreCounters {
221 snapshot_paths: usize,
222 snapshot_total_bytes: usize,
223 register_named: usize,
224 register_total_bytes: usize,
225 session_key: SessionKey,
226}
227
228#[derive(Clone, Debug)]
230pub struct HealthIsolationReport {
231 pub reply_status: String,
232 pub hashline_stores_accessed: bool,
233 pub snapshot_paths_at_capacity: bool,
234 pub health_source_mentions_hashline: bool,
235 pub reply_elapsed: Duration,
236 pub session_key: SessionKey,
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::hashline::release::fixture::{
243 build_a13_fixture, sha256_hex, FIXTURE_SHA256_HEX, FIXTURE_SIZE_BYTES,
244 };
245
246 fn load_fixture() -> Vec<u8> {
247 let bytes = build_a13_fixture();
248 assert_eq!(bytes.len(), FIXTURE_SIZE_BYTES);
249 assert_eq!(sha256_hex(&bytes), FIXTURE_SHA256_HEX);
250 bytes
251 }
252
253 fn a13_timing_enforced() -> bool {
258 !cfg!(debug_assertions)
259 }
260
261 #[test]
262 fn a13_tag_computation_median_at_most_one_millisecond() {
263 let fixture = load_fixture();
264 let mut median = measure_tag_median(&fixture);
265 if median > TAG_MEDIAN_MAX {
266 median = measure_tag_median(&fixture);
268 }
269 if a13_timing_enforced() {
270 assert!(
271 median <= TAG_MEDIAN_MAX,
272 "tag-computation median {median:?} exceeded {TAG_MEDIAN_MAX:?} after one noise retry"
273 );
274 } else {
275 assert!(median > Duration::ZERO);
277 eprintln!(
278 "a13 tag median (debug, not enforced): {median:?} (release ceiling {TAG_MEDIAN_MAX:?})"
279 );
280 }
281 }
282
283 #[test]
284 fn a13_gate_on_vs_gate_off_read_render_median_delta_at_most_ten_percent() {
285 let fixture = load_fixture();
286 let (mut off, mut on) = measure_render_medians(&fixture);
287 let mut delta = ratio_delta(off, on);
288 if delta > RENDER_REGRESSION_MAX_RATIO {
289 let pair = measure_render_medians(&fixture);
291 off = pair.0;
292 on = pair.1;
293 delta = ratio_delta(off, on);
294 }
295 if a13_timing_enforced() {
296 assert!(
297 delta <= RENDER_REGRESSION_MAX_RATIO,
298 "read-render median delta {delta:.4} (off={off:?}, on={on:?}) exceeded {}",
299 RENDER_REGRESSION_MAX_RATIO
300 );
301 } else {
302 assert!(off > Duration::ZERO && on > Duration::ZERO);
303 eprintln!(
304 "a13 render medians (debug, not enforced): off={off:?} on={on:?} delta={delta:.4} (release ceiling {})",
305 RENDER_REGRESSION_MAX_RATIO
306 );
307 }
308 }
309
310 #[test]
311 fn a13_channel0_health_reply_does_not_access_hashline_stores_at_capacity() {
312 let report = channel0_health_reply_avoids_hashline_stores();
313 assert_eq!(report.reply_status, "ok");
314 assert!(
315 !report.hashline_stores_accessed,
316 "health reply must not access hashline stores"
317 );
318 assert!(
319 report.snapshot_paths_at_capacity,
320 "probe must run with the snapshot store at MAX_SNAPSHOT_PATHS"
321 );
322 assert!(
323 !report.health_source_mentions_hashline,
324 "subc health source must not reference hashline (channel-0 isolation)"
325 );
326 assert!(
329 report.reply_elapsed < Duration::from_millis(50),
330 "health-shaped reply took {:?}, suggesting unexpected work",
331 report.reply_elapsed
332 );
333 let _ = report.session_key;
334 }
335
336 #[test]
337 fn noise_policy_constants_match_committed_manifest() {
338 let policy = include_str!("noise_policy.json");
339 assert!(policy.contains("\"warmups\": 3"));
340 assert!(policy.contains("\"timed_repetitions\": 10"));
341 assert!(policy.contains("\"tag_compute_median_max_ns\": 1000000"));
342 assert!(policy.contains("\"read_render_regression_max_ratio\": 0.10"));
343 assert!(policy.contains("\"test_threads\": 1"));
344 assert!(policy.contains("\"profile\": \"release\""));
345 assert!(policy.contains(FIXTURE_SHA256_HEX));
346 assert_eq!(WARMUPS, 3);
347 assert_eq!(TIMED_REPS, 10);
348 assert_eq!(TAG_MEDIAN_MAX, Duration::from_millis(1));
349 assert_eq!(RENDER_REGRESSION_MAX_RATIO, 0.10);
350 }
351
352 #[test]
353 fn median_duration_selects_central_sample() {
354 let mut odd = vec![
355 Duration::from_millis(5),
356 Duration::from_millis(1),
357 Duration::from_millis(3),
358 ];
359 assert_eq!(median_duration(&mut odd), Duration::from_millis(3));
360 let mut even = vec![
361 Duration::from_millis(4),
362 Duration::from_millis(2),
363 Duration::from_millis(8),
364 Duration::from_millis(6),
365 ];
366 assert_eq!(median_duration(&mut even), Duration::from_millis(5));
367 }
368}