pub struct PreparedEvidenceValidator { /* private fields */ }Expand description
A prepared, immutable evidence-validation snapshot.
Parsing and inference happen before construction. Normalization,
stratification, dataset indexing, and SPARQL-executor construction happen
once here; each validate call creates one reusable
ShapeEvaluator over those retained resources.
Implementations§
Source§impl PreparedEvidenceValidator
impl PreparedEvidenceValidator
Sourcepub fn new(data: &Graph, schema: &Schema) -> Result<Self, NonStratifiable>
pub fn new(data: &Graph, schema: &Schema) -> Result<Self, NonStratifiable>
Prepare embedded data/shapes validation over one graph.
Sourcepub fn with_context(
data: &Graph,
context: &Graph,
schema: &Schema,
) -> Result<Self, NonStratifiable>
pub fn with_context( data: &Graph, context: &Graph, schema: &Schema, ) -> Result<Self, NonStratifiable>
Prepare separate focus and evaluation graphs.
Sourcepub fn with_graphs(
data: &Graph,
shapes: &Graph,
schema: &Schema,
mode: ValidationGraphMode,
) -> Result<Self, NonStratifiable>
pub fn with_graphs( data: &Graph, shapes: &Graph, schema: &Schema, mode: ValidationGraphMode, ) -> Result<Self, NonStratifiable>
Prepare split data/shapes validation in the requested graph mode.
Examples found in repository?
27fn main() {
28 let mut shapes_path = PathBuf::new();
29 let mut data_path = PathBuf::new();
30 let mut no_infer = false;
31 let mut top = 15usize;
32 let mut dump_json: Option<PathBuf> = None;
33 let mut argv = std::env::args().skip(1);
34 while let Some(flag) = argv.next() {
35 let mut value = || {
36 argv.next()
37 .unwrap_or_else(|| panic!("{flag} needs a value"))
38 };
39 match flag.as_str() {
40 "--shapes" => shapes_path = PathBuf::from(value()),
41 "--data" => data_path = PathBuf::from(value()),
42 "--no-infer" => no_infer = true,
43 "--top" => top = value().parse().expect("--top must be an integer"),
44 "--dump-json" => dump_json = Some(PathBuf::from(value())),
45 other => panic!("unknown flag {other}"),
46 }
47 }
48 assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49 assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51 let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53 let parsed = shifty_parse::parse_loaded(&shapes);
54 let raw_schema = parsed.schema.clone();
55 let inference_schema = shifty_opt::normalize(&raw_schema);
56
57 let data_bytes = fs::read(&data_path).expect("cannot read --data");
58 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59 let data_graph = if no_infer {
60 data.graph.clone()
61 } else {
62 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63 .expect("inference")
64 .graph
65 };
66
67 let prepared = PreparedEvidenceValidator::with_graphs(
68 &data_graph,
69 &shapes.graph,
70 &raw_schema,
71 ValidationGraphMode::Union,
72 )
73 .expect("stratifiable");
74 let options = ValidationOptions::default();
75 let scan_options = ConformanceOptions::default();
76
77 println!("model: {}", data_path.display());
78 println!("normalized arena nodes: {}", prepared.schema().arena.len());
79 println!(
80 "normalized statements: {}",
81 prepared.schema().statements.len()
82 );
83 println!();
84
85 // ---- arm A: conformance only ------------------------------------------
86 profile::enable();
87 let _ = profile::take_evidence_visits();
88 let start = Instant::now();
89 let conformance = prepared.validate_conformance(&scan_options);
90 let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91 let conformance_visits = profile::take_evidence_visits();
92 let conformance_probes = profile::take_path_probes();
93 let conformance_cache = profile::take().expect("profiling enabled");
94 let conformance_cache = conformance_cache.shape_cache().clone();
95
96 // ---- arm B: dual evidence ----------------------------------------------
97 profile::enable();
98 let start = Instant::now();
99 let run = prepared.validate(&options);
100 let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101 let evidence_visits = profile::take_evidence_visits();
102 let evidence_probes = profile::take_path_probes();
103 let collector = profile::take().expect("profiling enabled");
104 let evidence_cache = collector.shape_cache().clone();
105
106 // ---- arm C: on-demand ---------------------------------------------------
107 // What a caller pays who wants explanations only for what failed: one
108 // conformance pass that also records the failing pairs, then evidence
109 // materialized for those pairs alone.
110 profile::enable();
111 let start = Instant::now();
112 let (_, failures) = prepared.find_failures(&scan_options);
113 let find_ms = start.elapsed().as_secs_f64() * 1e3;
114 let start = Instant::now();
115 let explained: usize = failures
116 .iter()
117 .map(|pair| prepared.explain(pair).len())
118 .sum();
119 let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120 let _ = profile::take();
121 let _ = profile::take_evidence_visits();
122 let _ = profile::take_path_probes();
123
124 let pairs = conformance.selected_pairs.max(1) as f64;
125 let retained = run.walk().len();
126
127 // ---- compact encoding --------------------------------------------------
128 let full_json = run.to_json().expect("evidence run serializes");
129 // Split so the encoding's own cost is separable from the intermediate
130 // `Value` tree it currently has to be handed: compaction is worth doing
131 // inline only if the total stays under the validation it describes.
132 let to_value_start = Instant::now();
133 let run_value = serde_json::to_value(&run).expect("run serializes");
134 let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135 let intern_start = Instant::now();
136 let interned = shifty_engine::compact_value(run_value, true);
137 let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138 let emit_start = Instant::now();
139 let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140 let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141 let compact_ms = to_value_ms + intern_ms + emit_ms;
142 drop(interned);
143 let compact_no_catalog =
144 shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145 println!();
146 println!(
147 "serialized full: {:>10.2} MB ({:.0} bytes/pair)",
148 full_json.len() as f64 / 1e6,
149 full_json.len() as f64 / pairs
150 );
151 println!(
152 "compact (with catalog): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153 = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154 compact_json.len() as f64 / 1e6,
155 compact_json.len() as f64 / pairs,
156 100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157 );
158 println!(
159 "compact (catalog elided): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller)",
160 compact_no_catalog.len() as f64 / 1e6,
161 compact_no_catalog.len() as f64 / pairs,
162 100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163 );
164
165 if let Some(path) = &dump_json {
166 fs::write(path, run.to_json().expect("evidence run serializes"))
167 .expect("cannot write --dump-json");
168 eprintln!("wrote {}", path.display());
169 }
170
171 println!(" conformance evidence");
172 println!(" wall ms {conformance_ms:>11.1} {evidence_ms:>14.1}");
173 println!(
174 " memo lookups {:>11} {:>14}",
175 conformance_cache.hits + conformance_cache.misses,
176 evidence_cache.hits + evidence_cache.misses
177 );
178 println!(
179 " memo hits {:>11} {:>14}",
180 conformance_cache.hits, evidence_cache.hits
181 );
182 println!(
183 " memo misses {:>11} {:>14}",
184 conformance_cache.misses, evidence_cache.misses
185 );
186 println!(
187 " memo entries (peak) {:>11} {:>14}",
188 conformance_cache.peak_entries, evidence_cache.peak_entries
189 );
190 println!(
191 " back-edges {:>11} {:>14}",
192 conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193 );
194 println!(
195 " uncacheable results {:>11} {:>14}",
196 conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197 );
198 println!(" evidence visits {conformance_visits:>11} {evidence_visits:>14}");
199 println!(" path_support probes {conformance_probes:>11} {evidence_probes:>14}");
200 println!();
201 println!(
202 "path probes per visit: {:.1}",
203 evidence_probes as f64 / evidence_visits.max(1) as f64
204 );
205
206 println!("selected pairs: {}", conformance.selected_pairs);
207 println!("retained evidence nodes: {retained}");
208 println!(
209 "visited / retained: {:.1}x",
210 evidence_visits as f64 / retained.max(1) as f64
211 );
212 println!(
213 "visits per pair: {:.0}",
214 evidence_visits as f64 / pairs
215 );
216 println!(
217 "distinct (shape,node) that a cache could collapse to at most: {}",
218 evidence_cache.peak_entries
219 );
220 println!(
221 "visits / distinct pairs: {:.1}x",
222 evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223 );
224 println!();
225 println!(
226 "per-visit cost: {:.2} us",
227 evidence_ms * 1e3 / evidence_visits.max(1) as f64
228 );
229 println!(
230 "per-memo-lookup cost: {:.2} us",
231 conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232 );
233
234 // ---- attribution: which statements own the time ------------------------
235 let (selection, materialization): (Vec<_>, Vec<_>) = collector
236 .shape_records()
237 .iter()
238 .partition(|record| record.label.starts_with("select:"));
239 let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240 let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242 println!();
243 println!(
244 "target selection total: {:.1} ms over {} statement(s)",
245 selection_us as f64 / 1e3,
246 selection.len()
247 );
248 println!(
249 "materialization total: {:.1} ms over {} statement(s)",
250 materialization_us as f64 / 1e3,
251 materialization.len()
252 );
253 println!();
254 println!(
255 "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256 failures.len(),
257 conformance.selected_pairs,
258 explained
259 );
260 println!(
261 " find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms \
262 ({:.2}x the full evidence run, {:.2}x conformance alone)",
263 find_ms + explain_ms,
264 (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265 (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266 );
267
268 let mut ranked = materialization;
269 ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270 println!();
271 println!("top statements by evidence-materialization time:");
272 println!(
273 " {:>8} {:>7} {:>6} {:>10} {:>12} {:>10} shape",
274 "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275 );
276 for record in ranked.iter().take(top) {
277 println!(
278 " {:>8.1} {:>6.1}% {:>6} {:>10.1} {:>12} {:>10.0} {}",
279 record.total_us as f64 / 1e3,
280 100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281 record.invocations,
282 record.total_us as f64 / record.invocations.max(1) as f64,
283 record.visits,
284 record.visits as f64 / record.invocations.max(1) as f64,
285 record.label,
286 );
287 }
288
289 let mut ranked_selection = selection;
290 ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291 println!();
292 println!("top statements by target-selection time:");
293 for record in ranked_selection.iter().take(5) {
294 println!(
295 " {:>8.1} ms {}",
296 record.total_us as f64 / 1e3,
297 record.label
298 );
299 }
300}More examples
131fn main() {
132 let args = parse_args();
133
134 eprintln!("loading shapes {}…", args.shapes.display());
135 let shapes_bytes = fs::read(&args.shapes).expect("cannot read --shapes");
136 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
137 let parsed = shifty_parse::parse_loaded(&shapes);
138 let raw_schema = parsed.schema.clone();
139 // Inference runs against the normalized schema, exactly as the CLI does.
140 let inference_schema = shifty_opt::normalize(&raw_schema);
141 drop(shapes_bytes);
142
143 if args.header {
144 println!("{COLUMNS}");
145 }
146
147 for model in models_in(&args.models) {
148 let name = model
149 .file_name()
150 .and_then(|name| name.to_str())
151 .unwrap_or("?")
152 .to_string();
153 eprint!("{name}… ");
154
155 // ---- untimed setup: everything common to all workflows --------------
156 let data_bytes = fs::read(&model).expect("cannot read model");
157 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse model");
158 drop(data_bytes);
159 let data_graph = if args.no_infer {
160 data.graph.clone()
161 } else {
162 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
163 .unwrap_or_else(|error| panic!("{name}: inference failed: {error}"))
164 .graph
165 };
166 let data_triples = data_graph.len();
167
168 let prepared = PreparedEvidenceValidator::with_graphs(
169 &data_graph,
170 &shapes.graph,
171 &raw_schema,
172 ValidationGraphMode::Union,
173 )
174 .unwrap_or_else(|error| panic!("{name}: schema is non-stratifiable: {error}"));
175 let options = ValidationOptions::default();
176 let scan_options = ConformanceOptions::default();
177
178 // ---- timed: validation only -----------------------------------------
179 // One warm-up round, discarded: it faults in the caches all arms share.
180 // Its duration is only consulted when `--budget-ms` is in play.
181 let probe = Instant::now();
182 let _ = prepared.validate_conformance(&scan_options);
183 let _ = prepared.validate_canonical(&options);
184 let (_, warm_failures) = prepared.find_failures(&scan_options);
185 for pair in &warm_failures {
186 std::hint::black_box(prepared.explain_canonical(pair));
187 }
188 let probe_ms = probe.elapsed().as_secs_f64() * 1e3;
189 // Every model gets the same iteration count. `--budget-ms` only caps the
190 // count for a corpus slow enough that a fixed count is impractical; it
191 // is off by default, and the emitted `iters` column records what was used.
192 let iters = if args.budget_ms > 0.0 {
193 ((args.budget_ms / probe_ms.max(f64::MIN_POSITIVE)) as usize)
194 .clamp(args.min_iters.min(args.iters), args.iters)
195 } else {
196 args.iters
197 };
198
199 let mut conformance_samples = Vec::with_capacity(iters);
200 let mut evidence_samples = Vec::with_capacity(iters);
201 let mut discovery_samples = Vec::with_capacity(iters);
202 let mut explanation_samples = Vec::with_capacity(iters);
203 let mut on_demand_samples = Vec::with_capacity(iters);
204 let mut last_run = None;
205 let mut last_conformance = None;
206 let mut last_on_demand_conformance = None;
207 let mut last_failures = Vec::new();
208 let mut last_explanations = Vec::new();
209
210 // Rotate A/B/C so no arm systematically inherits the cache, allocator,
211 // scheduler, or thermal state left by another arm.
212 for iteration in 0..iters {
213 for arm in ARM_ORDERS[iteration % ARM_ORDERS.len()] {
214 match arm {
215 Arm::Conformance => {
216 let start = Instant::now();
217 let conformance_run = prepared.validate_conformance(&scan_options);
218 conformance_samples.push(start.elapsed());
219 std::hint::black_box(conformance_run.selected_pairs);
220 last_conformance = Some(conformance_run);
221 }
222 Arm::FullEvidence => {
223 let start = Instant::now();
224 let run = prepared.validate_canonical(&options);
225 evidence_samples.push(start.elapsed());
226 std::hint::black_box(run.statements.len());
227 last_run = Some(run);
228 }
229 Arm::OnDemand => {
230 let start = Instant::now();
231 let (on_demand_conformance, failures) =
232 prepared.find_failures(&scan_options);
233 let discovery = start.elapsed();
234
235 let start = Instant::now();
236 let explanations: Vec<_> = failures
237 .iter()
238 .flat_map(|pair| prepared.explain_canonical(pair))
239 .collect();
240 let explanation = start.elapsed();
241
242 discovery_samples.push(discovery);
243 explanation_samples.push(explanation);
244 on_demand_samples.push(discovery + explanation);
245 std::hint::black_box(explanations.len());
246 last_on_demand_conformance = Some(on_demand_conformance);
247 last_failures = failures;
248 last_explanations = explanations;
249 }
250 }
251 }
252 }
253
254 // ---- untimed: shape of the evidence produced -------------------------
255 // Reuse the final timed results rather than paying for another run.
256 let conformance = last_conformance.expect("at least one iteration runs");
257 let on_demand_conformance =
258 last_on_demand_conformance.expect("at least one iteration runs");
259 let run = last_run.expect("at least one iteration runs");
260 let authored_pairs: usize = run
261 .statements
262 .iter()
263 .map(|statement| statement.selected_foci.len())
264 .sum();
265 let authored_fail_pairs: usize = run
266 .statements
267 .iter()
268 .flat_map(|statement| &statement.selected_foci)
269 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
270 .count();
271
272 // The three interfaces must describe the same normalized selection and
273 // polarity partition. Compare exact failed identities, not just the
274 // report-wide conformance bit.
275 assert_eq!(
276 conformance, on_demand_conformance,
277 "{name}: on-demand counts"
278 );
279 assert_eq!(
280 conformance.conforms, run.conforms,
281 "{name}: conformance bit"
282 );
283 let full_selected: HashSet<_> = run
284 .statements
285 .iter()
286 .flat_map(|statement| {
287 statement.selected_foci.iter().filter_map(|focus| {
288 statement
289 .normalized_statement_id
290 .map(|id| (id, focus.focus.clone()))
291 })
292 })
293 .collect();
294 let full_failures: HashSet<_> = run
295 .statements
296 .iter()
297 .flat_map(|statement| {
298 statement
299 .selected_foci
300 .iter()
301 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
302 .filter_map(|focus| {
303 statement
304 .normalized_statement_id
305 .map(|id| (id, focus.focus.clone()))
306 })
307 })
308 .collect();
309 let discovered_failures: HashSet<_> = last_failures
310 .iter()
311 .map(|pair| (pair.normalized_statement(), pair.focus().clone()))
312 .collect();
313 assert_eq!(
314 full_selected.len(),
315 conformance.selected_pairs,
316 "{name}: selected pairs"
317 );
318 assert_eq!(
319 full_failures.len(),
320 conformance.failed,
321 "{name}: failed pairs"
322 );
323 assert_eq!(
324 full_selected.len() - full_failures.len(),
325 conformance.passed,
326 "{name}: passed pairs"
327 );
328 assert_eq!(
329 full_failures, discovered_failures,
330 "{name}: failed identities"
331 );
332
333 let full_by_authored: HashMap<_, _> = run
334 .statements
335 .iter()
336 .flat_map(|statement| {
337 statement.selected_foci.iter().map(|focus| {
338 (
339 (statement.source_statement_id, focus.focus.clone()),
340 &focus.evidence,
341 )
342 })
343 })
344 .collect();
345 let mut explained_foci = 0;
346 for statement in &last_explanations {
347 for focus in &statement.selected_foci {
348 explained_foci += 1;
349 assert_eq!(
350 focus.status(),
351 shifty_engine::EvaluationStatus::Fail,
352 "{name}: on-demand explanation polarity"
353 );
354 let key = (statement.source_statement_id, focus.focus.clone());
355 assert_eq!(
356 &focus.evidence,
357 *full_by_authored
358 .get(&key)
359 .unwrap_or_else(|| panic!("{name}: unexplained full-run pair {key:?}")),
360 "{name}: full and on-demand evidence differ for {key:?}"
361 );
362 }
363 }
364 assert_eq!(
365 explained_foci, authored_fail_pairs,
366 "{name}: authored explanations"
367 );
368
369 let evidence_nodes = run.walk().len();
370 let evidence_bytes: usize = run
371 .statements
372 .iter()
373 .flat_map(|statement| &statement.selected_foci)
374 .map(|focus| focus.evidence.to_json().expect("evidence serializes").len())
375 .sum();
376 let full_json = run.to_json().expect("evidence run serializes");
377 let full_run_bytes_with_catalog = full_json.len();
378 let mut full_value_no_catalog =
379 serde_json::to_value(&run).expect("evidence run converts to JSON");
380 full_value_no_catalog
381 .as_object_mut()
382 .expect("evidence run is a JSON object")
383 .remove("constraints");
384 let full_run_bytes_no_catalog = serde_json::to_string(&full_value_no_catalog)
385 .expect("catalog-free evidence run serializes")
386 .len();
387
388 // Compare complete runs with the same catalog policy on each side, and
389 // verify both encodings reconstruct the exact typed result.
390 let encoded_with_catalog =
391 shifty_engine::compact(&run, true).expect("compact encoding succeeds");
392 let compact_run_bytes_with_catalog = serde_json::to_string(&encoded_with_catalog)
393 .expect("compact encoding serializes")
394 .len();
395 assert_eq!(
396 shifty_engine::expand(&encoded_with_catalog).expect("compact run expands"),
397 run,
398 "{name}: compact run with catalog is not lossless"
399 );
400 let encoded_no_catalog =
401 shifty_engine::compact(&run, false).expect("compact encoding succeeds");
402 let compact_run_bytes_no_catalog = serde_json::to_string(&encoded_no_catalog)
403 .expect("catalog-free compact encoding serializes")
404 .len();
405 let catalog = serde_json::to_value(&run.constraints).expect("catalog serializes");
406 assert_eq!(
407 shifty_engine::expand_with_catalog(&encoded_no_catalog, catalog)
408 .expect("catalog-free compact run expands"),
409 run,
410 "{name}: compact run without catalog is not lossless"
411 );
412 // Sharing is reported over the evidence alone. The node and term tables
413 // of `encoded` also hold catalog entries, which would understate the
414 // redundancy by adding distinct entries with no evidence occurrences.
415 let sharing = shifty_engine::sharing(&run).expect("sharing measures");
416 let results = shifty_engine::result_sharing(&run);
417
418 // The two passes reach the same judgment nodes by routes that share no
419 // code: `sharing` interns serialized JSON, `result_sharing` walks the
420 // typed evidence. Agreement on both counts is what makes either number
421 // reportable; a mismatch means the node families are misclassified or a
422 // traversal is missing a variant.
423 assert_eq!(
424 sharing.result_occurrences + sharing.support_occurrences,
425 sharing.node_occurrences,
426 "{name}: node families do not partition occurrences"
427 );
428 assert_eq!(
429 sharing.distinct_results + sharing.distinct_support,
430 sharing.distinct_nodes,
431 "{name}: node families do not partition the table"
432 );
433 assert_eq!(
434 sharing.result_occurrences, evidence_nodes,
435 "{name}: interned judgments disagree with the typed walk"
436 );
437 assert_eq!(
438 results.occurrences, evidence_nodes,
439 "{name}: typed sharing pass disagrees with the typed walk"
440 );
441 assert_eq!(
442 results.distinct_payloads, sharing.distinct_results,
443 "{name}: distinct judgments disagree between the two passes"
444 );
445 // Structural equality implies key equality, never the reverse.
446 assert!(
447 results.distinct_keys <= results.distinct_payloads,
448 "{name}: more keys than payloads"
449 );
450 // Requests are the normalized selection the conformance pass counts;
451 // authored records are the source-preserving view over them.
452 assert_eq!(
453 results.normalized_requests, conformance.selected_pairs,
454 "{name}: requests disagree with the conformance selection"
455 );
456 assert_eq!(
457 results.authored_records, authored_pairs,
458 "{name}: authored records disagree with the run"
459 );
460
461 // Serialization is measured separately so its allocation traffic does
462 // not perturb the next validation arm.
463 let mut serialize_samples = Vec::with_capacity(iters);
464 for _ in 0..iters {
465 let start = Instant::now();
466 let json = run.to_json().expect("evidence run serializes");
467 serialize_samples.push(start.elapsed());
468 std::hint::black_box(json.len());
469 }
470
471 // Each ratio compares the corresponding raw samples from one rotated
472 // A/B/C round. The CSV retains those samples for independent checking.
473 let full_ratios: Vec<f64> = conformance_samples
474 .iter()
475 .zip(evidence_samples.iter())
476 .map(|(conformance, evidence)| {
477 evidence.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
478 })
479 .collect();
480 let on_demand_ratios: Vec<f64> = conformance_samples
481 .iter()
482 .zip(on_demand_samples.iter())
483 .map(|(conformance, on_demand)| {
484 on_demand.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
485 })
486 .collect();
487
488 let conformance_ms = summarize_durations(&conformance_samples);
489 let evidence_ms = summarize_durations(&evidence_samples);
490 let discovery_ms = summarize_durations(&discovery_samples);
491 let explanation_ms = summarize_durations(&explanation_samples);
492 let on_demand_ms = summarize_durations(&on_demand_samples);
493 let serialize_ms = summarize_durations(&serialize_samples);
494 let full_ratio = summarize_values(&full_ratios);
495 let on_demand_ratio = summarize_values(&on_demand_ratios);
496 let evaluated_pairs = conformance.selected_pairs.max(1) as f64;
497 let authored_pairs_denominator = authored_pairs.max(1) as f64;
498
499 println!(
500 "{suite},{name},{data_triples},{iters},\
501{c_med:.4},{c_mad:.4},{e_med:.4},{e_mad:.4},{full_ratio:.4},{full_ratio_mad:.4},\
502{find_med:.4},{find_mad:.4},{explain_med:.4},{explain_mad:.4},\
503{ondemand_med:.4},{ondemand_mad:.4},{ondemand_ratio:.4},{ondemand_ratio_mad:.4},\
504{serialize_med:.4},{serialize_mad:.4},\
505{conforms_c},{conforms_e},{conforms_o},\
506{pairs_eval},{pass_pairs},{fail_pairs},{fail_fraction:.6},\
507{authored_pairs},{authored_pass},{authored_fail},\
508{statements},\
509{evidence_nodes},{nodes_per_authored_pair:.3},\
510{evidence_bytes},{bytes_per_authored_pair:.1},\
511{full_with_catalog},{full_no_catalog},{compact_with_catalog},{compact_no_catalog},\
512{node_occurrences},{distinct_nodes},{node_redundancy:.3},\
513{result_occurrences},{distinct_results},{result_redundancy:.3},\
514{support_occurrences},{distinct_support},{support_redundancy:.3},{support_share:.4},\
515{term_occurrences},{distinct_terms},{term_redundancy:.3},\
516{normalized_requests},{duplicate_records},{divergent_duplicates},\
517{canonical_occurrences},{shared_payloads},{shared_payload_fraction:.4},\
518{shared_canonical_occurrences},{shared_occurrence_fraction:.4},\
519{request_reaches},{requests_per_payload:.3},{max_payload_requests},\
520{distinct_keys},{key_redundancy:.3},{multi_occurrence_keys},\
521{divergent_keys},{divergence_fraction:.4},{divergent_occurrences},\
522{distinct_payloads_per_key},{keys_over_payload_cap},{both_polarity_addresses},\
523{conformance_raw},{evidence_raw},{discovery_raw},{explanation_raw},{ondemand_raw}",
524 suite = args.suite,
525 node_occurrences = sharing.node_occurrences,
526 distinct_nodes = sharing.distinct_nodes,
527 node_redundancy = sharing.node_redundancy(),
528 result_occurrences = sharing.result_occurrences,
529 distinct_results = sharing.distinct_results,
530 result_redundancy = sharing.result_redundancy(),
531 support_occurrences = sharing.support_occurrences,
532 distinct_support = sharing.distinct_support,
533 support_redundancy = sharing.support_redundancy(),
534 support_share = sharing.support_share(),
535 term_occurrences = sharing.term_occurrences,
536 distinct_terms = sharing.distinct_terms,
537 term_redundancy = sharing.term_redundancy(),
538 normalized_requests = results.normalized_requests,
539 duplicate_records = results.duplicate_records,
540 divergent_duplicates = results.divergent_duplicates,
541 canonical_occurrences = results.canonical_occurrences,
542 shared_payloads = results.shared_payloads,
543 shared_payload_fraction = results.shared_payload_fraction(),
544 shared_canonical_occurrences = results.shared_canonical_occurrences,
545 shared_occurrence_fraction = results.shared_occurrence_fraction(),
546 request_reaches = results.request_reaches,
547 requests_per_payload = results.requests_per_payload(),
548 max_payload_requests = results.max_payload_requests,
549 distinct_keys = results.distinct_keys,
550 key_redundancy = results.key_redundancy(),
551 multi_occurrence_keys = results.multi_occurrence_keys,
552 divergent_keys = results.divergent_keys,
553 divergence_fraction = results.divergence_fraction(),
554 divergent_occurrences = results.divergent_occurrences,
555 distinct_payloads_per_key = results.distinct_payloads_per_key,
556 keys_over_payload_cap = results.keys_over_payload_cap,
557 both_polarity_addresses = results.both_polarity_addresses,
558 c_med = conformance_ms.median,
559 c_mad = conformance_ms.mad,
560 e_med = evidence_ms.median,
561 e_mad = evidence_ms.mad,
562 full_ratio = full_ratio.median,
563 full_ratio_mad = full_ratio.mad,
564 find_med = discovery_ms.median,
565 find_mad = discovery_ms.mad,
566 explain_med = explanation_ms.median,
567 explain_mad = explanation_ms.mad,
568 ondemand_med = on_demand_ms.median,
569 ondemand_mad = on_demand_ms.mad,
570 ondemand_ratio = on_demand_ratio.median,
571 ondemand_ratio_mad = on_demand_ratio.mad,
572 serialize_med = serialize_ms.median,
573 serialize_mad = serialize_ms.mad,
574 conforms_c = conformance.conforms,
575 conforms_e = run.conforms,
576 conforms_o = on_demand_conformance.conforms,
577 pairs_eval = conformance.selected_pairs,
578 pass_pairs = conformance.passed,
579 fail_pairs = conformance.failed,
580 fail_fraction = conformance.failed as f64 / evaluated_pairs,
581 authored_pass = authored_pairs - authored_fail_pairs,
582 authored_fail = authored_fail_pairs,
583 statements = run.statements.len(),
584 nodes_per_authored_pair = evidence_nodes as f64 / authored_pairs_denominator,
585 bytes_per_authored_pair = evidence_bytes as f64 / authored_pairs_denominator,
586 full_with_catalog = full_run_bytes_with_catalog,
587 full_no_catalog = full_run_bytes_no_catalog,
588 compact_with_catalog = compact_run_bytes_with_catalog,
589 compact_no_catalog = compact_run_bytes_no_catalog,
590 conformance_raw = samples_string(&conformance_samples),
591 evidence_raw = samples_string(&evidence_samples),
592 discovery_raw = samples_string(&discovery_samples),
593 explanation_raw = samples_string(&explanation_samples),
594 ondemand_raw = samples_string(&on_demand_samples),
595 );
596
597 eprintln!(
598 "median: {:.1}ms conformance, {:.1}ms full ({:.2}x), {:.1}ms on-demand ({:.2}x); {} failures / {} pairs; {iters} rounds",
599 conformance_ms.median,
600 evidence_ms.median,
601 full_ratio.median,
602 on_demand_ms.median,
603 on_demand_ratio.median,
604 conformance.failed,
605 conformance.selected_pairs,
606 );
607 }
608}Sourcepub fn data(&self) -> &Graph
pub fn data(&self) -> &Graph
The evaluation data graph this snapshot was prepared over: the dataset
after inference (when requested), exactly as target selection and the
evaluator see it. Borrowed so a caller resolving extra paths (e.g. the
shape-map value_paths feature) evaluates over the same graph the run
used without cloning it.
Sourcepub fn raw_constraint_holds(
&self,
focus: &Term,
constraint: ShapeId,
) -> Option<bool>
pub fn raw_constraint_holds( &self, focus: &Term, constraint: ShapeId, ) -> Option<bool>
Decide one retained authored/raw constraint directly.
Most callers should use normalized constraints and
Self::explain_constraint. This is for source-provenance consumers
that need a child made unreachable when an enclosing constraint was
normalized away (for example, an unbounded qualified property slot).
Sourcepub fn validate_conformance(
&self,
options: &ConformanceOptions,
) -> ConformanceRun
pub fn validate_conformance( &self, options: &ConformanceOptions, ) -> ConformanceRun
Validate the prepared snapshot for conformance only.
Preparation, target selection, and the evaluator are exactly those of
validate; each selected (statement, focus) pair is
instead decided by one short-circuiting satisfaction test. No evidence is
materialized and no authored-statement progress is computed, so the
difference against validate on the same snapshot is
the cost of evidence tracing alone.
Every failing pair makes conforms false: severity filtering requires
the failure evidence this entry point deliberately does not construct.
Examples found in repository?
27fn main() {
28 let mut shapes_path = PathBuf::new();
29 let mut data_path = PathBuf::new();
30 let mut no_infer = false;
31 let mut top = 15usize;
32 let mut dump_json: Option<PathBuf> = None;
33 let mut argv = std::env::args().skip(1);
34 while let Some(flag) = argv.next() {
35 let mut value = || {
36 argv.next()
37 .unwrap_or_else(|| panic!("{flag} needs a value"))
38 };
39 match flag.as_str() {
40 "--shapes" => shapes_path = PathBuf::from(value()),
41 "--data" => data_path = PathBuf::from(value()),
42 "--no-infer" => no_infer = true,
43 "--top" => top = value().parse().expect("--top must be an integer"),
44 "--dump-json" => dump_json = Some(PathBuf::from(value())),
45 other => panic!("unknown flag {other}"),
46 }
47 }
48 assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49 assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51 let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53 let parsed = shifty_parse::parse_loaded(&shapes);
54 let raw_schema = parsed.schema.clone();
55 let inference_schema = shifty_opt::normalize(&raw_schema);
56
57 let data_bytes = fs::read(&data_path).expect("cannot read --data");
58 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59 let data_graph = if no_infer {
60 data.graph.clone()
61 } else {
62 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63 .expect("inference")
64 .graph
65 };
66
67 let prepared = PreparedEvidenceValidator::with_graphs(
68 &data_graph,
69 &shapes.graph,
70 &raw_schema,
71 ValidationGraphMode::Union,
72 )
73 .expect("stratifiable");
74 let options = ValidationOptions::default();
75 let scan_options = ConformanceOptions::default();
76
77 println!("model: {}", data_path.display());
78 println!("normalized arena nodes: {}", prepared.schema().arena.len());
79 println!(
80 "normalized statements: {}",
81 prepared.schema().statements.len()
82 );
83 println!();
84
85 // ---- arm A: conformance only ------------------------------------------
86 profile::enable();
87 let _ = profile::take_evidence_visits();
88 let start = Instant::now();
89 let conformance = prepared.validate_conformance(&scan_options);
90 let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91 let conformance_visits = profile::take_evidence_visits();
92 let conformance_probes = profile::take_path_probes();
93 let conformance_cache = profile::take().expect("profiling enabled");
94 let conformance_cache = conformance_cache.shape_cache().clone();
95
96 // ---- arm B: dual evidence ----------------------------------------------
97 profile::enable();
98 let start = Instant::now();
99 let run = prepared.validate(&options);
100 let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101 let evidence_visits = profile::take_evidence_visits();
102 let evidence_probes = profile::take_path_probes();
103 let collector = profile::take().expect("profiling enabled");
104 let evidence_cache = collector.shape_cache().clone();
105
106 // ---- arm C: on-demand ---------------------------------------------------
107 // What a caller pays who wants explanations only for what failed: one
108 // conformance pass that also records the failing pairs, then evidence
109 // materialized for those pairs alone.
110 profile::enable();
111 let start = Instant::now();
112 let (_, failures) = prepared.find_failures(&scan_options);
113 let find_ms = start.elapsed().as_secs_f64() * 1e3;
114 let start = Instant::now();
115 let explained: usize = failures
116 .iter()
117 .map(|pair| prepared.explain(pair).len())
118 .sum();
119 let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120 let _ = profile::take();
121 let _ = profile::take_evidence_visits();
122 let _ = profile::take_path_probes();
123
124 let pairs = conformance.selected_pairs.max(1) as f64;
125 let retained = run.walk().len();
126
127 // ---- compact encoding --------------------------------------------------
128 let full_json = run.to_json().expect("evidence run serializes");
129 // Split so the encoding's own cost is separable from the intermediate
130 // `Value` tree it currently has to be handed: compaction is worth doing
131 // inline only if the total stays under the validation it describes.
132 let to_value_start = Instant::now();
133 let run_value = serde_json::to_value(&run).expect("run serializes");
134 let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135 let intern_start = Instant::now();
136 let interned = shifty_engine::compact_value(run_value, true);
137 let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138 let emit_start = Instant::now();
139 let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140 let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141 let compact_ms = to_value_ms + intern_ms + emit_ms;
142 drop(interned);
143 let compact_no_catalog =
144 shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145 println!();
146 println!(
147 "serialized full: {:>10.2} MB ({:.0} bytes/pair)",
148 full_json.len() as f64 / 1e6,
149 full_json.len() as f64 / pairs
150 );
151 println!(
152 "compact (with catalog): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153 = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154 compact_json.len() as f64 / 1e6,
155 compact_json.len() as f64 / pairs,
156 100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157 );
158 println!(
159 "compact (catalog elided): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller)",
160 compact_no_catalog.len() as f64 / 1e6,
161 compact_no_catalog.len() as f64 / pairs,
162 100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163 );
164
165 if let Some(path) = &dump_json {
166 fs::write(path, run.to_json().expect("evidence run serializes"))
167 .expect("cannot write --dump-json");
168 eprintln!("wrote {}", path.display());
169 }
170
171 println!(" conformance evidence");
172 println!(" wall ms {conformance_ms:>11.1} {evidence_ms:>14.1}");
173 println!(
174 " memo lookups {:>11} {:>14}",
175 conformance_cache.hits + conformance_cache.misses,
176 evidence_cache.hits + evidence_cache.misses
177 );
178 println!(
179 " memo hits {:>11} {:>14}",
180 conformance_cache.hits, evidence_cache.hits
181 );
182 println!(
183 " memo misses {:>11} {:>14}",
184 conformance_cache.misses, evidence_cache.misses
185 );
186 println!(
187 " memo entries (peak) {:>11} {:>14}",
188 conformance_cache.peak_entries, evidence_cache.peak_entries
189 );
190 println!(
191 " back-edges {:>11} {:>14}",
192 conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193 );
194 println!(
195 " uncacheable results {:>11} {:>14}",
196 conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197 );
198 println!(" evidence visits {conformance_visits:>11} {evidence_visits:>14}");
199 println!(" path_support probes {conformance_probes:>11} {evidence_probes:>14}");
200 println!();
201 println!(
202 "path probes per visit: {:.1}",
203 evidence_probes as f64 / evidence_visits.max(1) as f64
204 );
205
206 println!("selected pairs: {}", conformance.selected_pairs);
207 println!("retained evidence nodes: {retained}");
208 println!(
209 "visited / retained: {:.1}x",
210 evidence_visits as f64 / retained.max(1) as f64
211 );
212 println!(
213 "visits per pair: {:.0}",
214 evidence_visits as f64 / pairs
215 );
216 println!(
217 "distinct (shape,node) that a cache could collapse to at most: {}",
218 evidence_cache.peak_entries
219 );
220 println!(
221 "visits / distinct pairs: {:.1}x",
222 evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223 );
224 println!();
225 println!(
226 "per-visit cost: {:.2} us",
227 evidence_ms * 1e3 / evidence_visits.max(1) as f64
228 );
229 println!(
230 "per-memo-lookup cost: {:.2} us",
231 conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232 );
233
234 // ---- attribution: which statements own the time ------------------------
235 let (selection, materialization): (Vec<_>, Vec<_>) = collector
236 .shape_records()
237 .iter()
238 .partition(|record| record.label.starts_with("select:"));
239 let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240 let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242 println!();
243 println!(
244 "target selection total: {:.1} ms over {} statement(s)",
245 selection_us as f64 / 1e3,
246 selection.len()
247 );
248 println!(
249 "materialization total: {:.1} ms over {} statement(s)",
250 materialization_us as f64 / 1e3,
251 materialization.len()
252 );
253 println!();
254 println!(
255 "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256 failures.len(),
257 conformance.selected_pairs,
258 explained
259 );
260 println!(
261 " find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms \
262 ({:.2}x the full evidence run, {:.2}x conformance alone)",
263 find_ms + explain_ms,
264 (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265 (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266 );
267
268 let mut ranked = materialization;
269 ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270 println!();
271 println!("top statements by evidence-materialization time:");
272 println!(
273 " {:>8} {:>7} {:>6} {:>10} {:>12} {:>10} shape",
274 "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275 );
276 for record in ranked.iter().take(top) {
277 println!(
278 " {:>8.1} {:>6.1}% {:>6} {:>10.1} {:>12} {:>10.0} {}",
279 record.total_us as f64 / 1e3,
280 100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281 record.invocations,
282 record.total_us as f64 / record.invocations.max(1) as f64,
283 record.visits,
284 record.visits as f64 / record.invocations.max(1) as f64,
285 record.label,
286 );
287 }
288
289 let mut ranked_selection = selection;
290 ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291 println!();
292 println!("top statements by target-selection time:");
293 for record in ranked_selection.iter().take(5) {
294 println!(
295 " {:>8.1} ms {}",
296 record.total_us as f64 / 1e3,
297 record.label
298 );
299 }
300}More examples
131fn main() {
132 let args = parse_args();
133
134 eprintln!("loading shapes {}…", args.shapes.display());
135 let shapes_bytes = fs::read(&args.shapes).expect("cannot read --shapes");
136 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
137 let parsed = shifty_parse::parse_loaded(&shapes);
138 let raw_schema = parsed.schema.clone();
139 // Inference runs against the normalized schema, exactly as the CLI does.
140 let inference_schema = shifty_opt::normalize(&raw_schema);
141 drop(shapes_bytes);
142
143 if args.header {
144 println!("{COLUMNS}");
145 }
146
147 for model in models_in(&args.models) {
148 let name = model
149 .file_name()
150 .and_then(|name| name.to_str())
151 .unwrap_or("?")
152 .to_string();
153 eprint!("{name}… ");
154
155 // ---- untimed setup: everything common to all workflows --------------
156 let data_bytes = fs::read(&model).expect("cannot read model");
157 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse model");
158 drop(data_bytes);
159 let data_graph = if args.no_infer {
160 data.graph.clone()
161 } else {
162 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
163 .unwrap_or_else(|error| panic!("{name}: inference failed: {error}"))
164 .graph
165 };
166 let data_triples = data_graph.len();
167
168 let prepared = PreparedEvidenceValidator::with_graphs(
169 &data_graph,
170 &shapes.graph,
171 &raw_schema,
172 ValidationGraphMode::Union,
173 )
174 .unwrap_or_else(|error| panic!("{name}: schema is non-stratifiable: {error}"));
175 let options = ValidationOptions::default();
176 let scan_options = ConformanceOptions::default();
177
178 // ---- timed: validation only -----------------------------------------
179 // One warm-up round, discarded: it faults in the caches all arms share.
180 // Its duration is only consulted when `--budget-ms` is in play.
181 let probe = Instant::now();
182 let _ = prepared.validate_conformance(&scan_options);
183 let _ = prepared.validate_canonical(&options);
184 let (_, warm_failures) = prepared.find_failures(&scan_options);
185 for pair in &warm_failures {
186 std::hint::black_box(prepared.explain_canonical(pair));
187 }
188 let probe_ms = probe.elapsed().as_secs_f64() * 1e3;
189 // Every model gets the same iteration count. `--budget-ms` only caps the
190 // count for a corpus slow enough that a fixed count is impractical; it
191 // is off by default, and the emitted `iters` column records what was used.
192 let iters = if args.budget_ms > 0.0 {
193 ((args.budget_ms / probe_ms.max(f64::MIN_POSITIVE)) as usize)
194 .clamp(args.min_iters.min(args.iters), args.iters)
195 } else {
196 args.iters
197 };
198
199 let mut conformance_samples = Vec::with_capacity(iters);
200 let mut evidence_samples = Vec::with_capacity(iters);
201 let mut discovery_samples = Vec::with_capacity(iters);
202 let mut explanation_samples = Vec::with_capacity(iters);
203 let mut on_demand_samples = Vec::with_capacity(iters);
204 let mut last_run = None;
205 let mut last_conformance = None;
206 let mut last_on_demand_conformance = None;
207 let mut last_failures = Vec::new();
208 let mut last_explanations = Vec::new();
209
210 // Rotate A/B/C so no arm systematically inherits the cache, allocator,
211 // scheduler, or thermal state left by another arm.
212 for iteration in 0..iters {
213 for arm in ARM_ORDERS[iteration % ARM_ORDERS.len()] {
214 match arm {
215 Arm::Conformance => {
216 let start = Instant::now();
217 let conformance_run = prepared.validate_conformance(&scan_options);
218 conformance_samples.push(start.elapsed());
219 std::hint::black_box(conformance_run.selected_pairs);
220 last_conformance = Some(conformance_run);
221 }
222 Arm::FullEvidence => {
223 let start = Instant::now();
224 let run = prepared.validate_canonical(&options);
225 evidence_samples.push(start.elapsed());
226 std::hint::black_box(run.statements.len());
227 last_run = Some(run);
228 }
229 Arm::OnDemand => {
230 let start = Instant::now();
231 let (on_demand_conformance, failures) =
232 prepared.find_failures(&scan_options);
233 let discovery = start.elapsed();
234
235 let start = Instant::now();
236 let explanations: Vec<_> = failures
237 .iter()
238 .flat_map(|pair| prepared.explain_canonical(pair))
239 .collect();
240 let explanation = start.elapsed();
241
242 discovery_samples.push(discovery);
243 explanation_samples.push(explanation);
244 on_demand_samples.push(discovery + explanation);
245 std::hint::black_box(explanations.len());
246 last_on_demand_conformance = Some(on_demand_conformance);
247 last_failures = failures;
248 last_explanations = explanations;
249 }
250 }
251 }
252 }
253
254 // ---- untimed: shape of the evidence produced -------------------------
255 // Reuse the final timed results rather than paying for another run.
256 let conformance = last_conformance.expect("at least one iteration runs");
257 let on_demand_conformance =
258 last_on_demand_conformance.expect("at least one iteration runs");
259 let run = last_run.expect("at least one iteration runs");
260 let authored_pairs: usize = run
261 .statements
262 .iter()
263 .map(|statement| statement.selected_foci.len())
264 .sum();
265 let authored_fail_pairs: usize = run
266 .statements
267 .iter()
268 .flat_map(|statement| &statement.selected_foci)
269 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
270 .count();
271
272 // The three interfaces must describe the same normalized selection and
273 // polarity partition. Compare exact failed identities, not just the
274 // report-wide conformance bit.
275 assert_eq!(
276 conformance, on_demand_conformance,
277 "{name}: on-demand counts"
278 );
279 assert_eq!(
280 conformance.conforms, run.conforms,
281 "{name}: conformance bit"
282 );
283 let full_selected: HashSet<_> = run
284 .statements
285 .iter()
286 .flat_map(|statement| {
287 statement.selected_foci.iter().filter_map(|focus| {
288 statement
289 .normalized_statement_id
290 .map(|id| (id, focus.focus.clone()))
291 })
292 })
293 .collect();
294 let full_failures: HashSet<_> = run
295 .statements
296 .iter()
297 .flat_map(|statement| {
298 statement
299 .selected_foci
300 .iter()
301 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
302 .filter_map(|focus| {
303 statement
304 .normalized_statement_id
305 .map(|id| (id, focus.focus.clone()))
306 })
307 })
308 .collect();
309 let discovered_failures: HashSet<_> = last_failures
310 .iter()
311 .map(|pair| (pair.normalized_statement(), pair.focus().clone()))
312 .collect();
313 assert_eq!(
314 full_selected.len(),
315 conformance.selected_pairs,
316 "{name}: selected pairs"
317 );
318 assert_eq!(
319 full_failures.len(),
320 conformance.failed,
321 "{name}: failed pairs"
322 );
323 assert_eq!(
324 full_selected.len() - full_failures.len(),
325 conformance.passed,
326 "{name}: passed pairs"
327 );
328 assert_eq!(
329 full_failures, discovered_failures,
330 "{name}: failed identities"
331 );
332
333 let full_by_authored: HashMap<_, _> = run
334 .statements
335 .iter()
336 .flat_map(|statement| {
337 statement.selected_foci.iter().map(|focus| {
338 (
339 (statement.source_statement_id, focus.focus.clone()),
340 &focus.evidence,
341 )
342 })
343 })
344 .collect();
345 let mut explained_foci = 0;
346 for statement in &last_explanations {
347 for focus in &statement.selected_foci {
348 explained_foci += 1;
349 assert_eq!(
350 focus.status(),
351 shifty_engine::EvaluationStatus::Fail,
352 "{name}: on-demand explanation polarity"
353 );
354 let key = (statement.source_statement_id, focus.focus.clone());
355 assert_eq!(
356 &focus.evidence,
357 *full_by_authored
358 .get(&key)
359 .unwrap_or_else(|| panic!("{name}: unexplained full-run pair {key:?}")),
360 "{name}: full and on-demand evidence differ for {key:?}"
361 );
362 }
363 }
364 assert_eq!(
365 explained_foci, authored_fail_pairs,
366 "{name}: authored explanations"
367 );
368
369 let evidence_nodes = run.walk().len();
370 let evidence_bytes: usize = run
371 .statements
372 .iter()
373 .flat_map(|statement| &statement.selected_foci)
374 .map(|focus| focus.evidence.to_json().expect("evidence serializes").len())
375 .sum();
376 let full_json = run.to_json().expect("evidence run serializes");
377 let full_run_bytes_with_catalog = full_json.len();
378 let mut full_value_no_catalog =
379 serde_json::to_value(&run).expect("evidence run converts to JSON");
380 full_value_no_catalog
381 .as_object_mut()
382 .expect("evidence run is a JSON object")
383 .remove("constraints");
384 let full_run_bytes_no_catalog = serde_json::to_string(&full_value_no_catalog)
385 .expect("catalog-free evidence run serializes")
386 .len();
387
388 // Compare complete runs with the same catalog policy on each side, and
389 // verify both encodings reconstruct the exact typed result.
390 let encoded_with_catalog =
391 shifty_engine::compact(&run, true).expect("compact encoding succeeds");
392 let compact_run_bytes_with_catalog = serde_json::to_string(&encoded_with_catalog)
393 .expect("compact encoding serializes")
394 .len();
395 assert_eq!(
396 shifty_engine::expand(&encoded_with_catalog).expect("compact run expands"),
397 run,
398 "{name}: compact run with catalog is not lossless"
399 );
400 let encoded_no_catalog =
401 shifty_engine::compact(&run, false).expect("compact encoding succeeds");
402 let compact_run_bytes_no_catalog = serde_json::to_string(&encoded_no_catalog)
403 .expect("catalog-free compact encoding serializes")
404 .len();
405 let catalog = serde_json::to_value(&run.constraints).expect("catalog serializes");
406 assert_eq!(
407 shifty_engine::expand_with_catalog(&encoded_no_catalog, catalog)
408 .expect("catalog-free compact run expands"),
409 run,
410 "{name}: compact run without catalog is not lossless"
411 );
412 // Sharing is reported over the evidence alone. The node and term tables
413 // of `encoded` also hold catalog entries, which would understate the
414 // redundancy by adding distinct entries with no evidence occurrences.
415 let sharing = shifty_engine::sharing(&run).expect("sharing measures");
416 let results = shifty_engine::result_sharing(&run);
417
418 // The two passes reach the same judgment nodes by routes that share no
419 // code: `sharing` interns serialized JSON, `result_sharing` walks the
420 // typed evidence. Agreement on both counts is what makes either number
421 // reportable; a mismatch means the node families are misclassified or a
422 // traversal is missing a variant.
423 assert_eq!(
424 sharing.result_occurrences + sharing.support_occurrences,
425 sharing.node_occurrences,
426 "{name}: node families do not partition occurrences"
427 );
428 assert_eq!(
429 sharing.distinct_results + sharing.distinct_support,
430 sharing.distinct_nodes,
431 "{name}: node families do not partition the table"
432 );
433 assert_eq!(
434 sharing.result_occurrences, evidence_nodes,
435 "{name}: interned judgments disagree with the typed walk"
436 );
437 assert_eq!(
438 results.occurrences, evidence_nodes,
439 "{name}: typed sharing pass disagrees with the typed walk"
440 );
441 assert_eq!(
442 results.distinct_payloads, sharing.distinct_results,
443 "{name}: distinct judgments disagree between the two passes"
444 );
445 // Structural equality implies key equality, never the reverse.
446 assert!(
447 results.distinct_keys <= results.distinct_payloads,
448 "{name}: more keys than payloads"
449 );
450 // Requests are the normalized selection the conformance pass counts;
451 // authored records are the source-preserving view over them.
452 assert_eq!(
453 results.normalized_requests, conformance.selected_pairs,
454 "{name}: requests disagree with the conformance selection"
455 );
456 assert_eq!(
457 results.authored_records, authored_pairs,
458 "{name}: authored records disagree with the run"
459 );
460
461 // Serialization is measured separately so its allocation traffic does
462 // not perturb the next validation arm.
463 let mut serialize_samples = Vec::with_capacity(iters);
464 for _ in 0..iters {
465 let start = Instant::now();
466 let json = run.to_json().expect("evidence run serializes");
467 serialize_samples.push(start.elapsed());
468 std::hint::black_box(json.len());
469 }
470
471 // Each ratio compares the corresponding raw samples from one rotated
472 // A/B/C round. The CSV retains those samples for independent checking.
473 let full_ratios: Vec<f64> = conformance_samples
474 .iter()
475 .zip(evidence_samples.iter())
476 .map(|(conformance, evidence)| {
477 evidence.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
478 })
479 .collect();
480 let on_demand_ratios: Vec<f64> = conformance_samples
481 .iter()
482 .zip(on_demand_samples.iter())
483 .map(|(conformance, on_demand)| {
484 on_demand.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
485 })
486 .collect();
487
488 let conformance_ms = summarize_durations(&conformance_samples);
489 let evidence_ms = summarize_durations(&evidence_samples);
490 let discovery_ms = summarize_durations(&discovery_samples);
491 let explanation_ms = summarize_durations(&explanation_samples);
492 let on_demand_ms = summarize_durations(&on_demand_samples);
493 let serialize_ms = summarize_durations(&serialize_samples);
494 let full_ratio = summarize_values(&full_ratios);
495 let on_demand_ratio = summarize_values(&on_demand_ratios);
496 let evaluated_pairs = conformance.selected_pairs.max(1) as f64;
497 let authored_pairs_denominator = authored_pairs.max(1) as f64;
498
499 println!(
500 "{suite},{name},{data_triples},{iters},\
501{c_med:.4},{c_mad:.4},{e_med:.4},{e_mad:.4},{full_ratio:.4},{full_ratio_mad:.4},\
502{find_med:.4},{find_mad:.4},{explain_med:.4},{explain_mad:.4},\
503{ondemand_med:.4},{ondemand_mad:.4},{ondemand_ratio:.4},{ondemand_ratio_mad:.4},\
504{serialize_med:.4},{serialize_mad:.4},\
505{conforms_c},{conforms_e},{conforms_o},\
506{pairs_eval},{pass_pairs},{fail_pairs},{fail_fraction:.6},\
507{authored_pairs},{authored_pass},{authored_fail},\
508{statements},\
509{evidence_nodes},{nodes_per_authored_pair:.3},\
510{evidence_bytes},{bytes_per_authored_pair:.1},\
511{full_with_catalog},{full_no_catalog},{compact_with_catalog},{compact_no_catalog},\
512{node_occurrences},{distinct_nodes},{node_redundancy:.3},\
513{result_occurrences},{distinct_results},{result_redundancy:.3},\
514{support_occurrences},{distinct_support},{support_redundancy:.3},{support_share:.4},\
515{term_occurrences},{distinct_terms},{term_redundancy:.3},\
516{normalized_requests},{duplicate_records},{divergent_duplicates},\
517{canonical_occurrences},{shared_payloads},{shared_payload_fraction:.4},\
518{shared_canonical_occurrences},{shared_occurrence_fraction:.4},\
519{request_reaches},{requests_per_payload:.3},{max_payload_requests},\
520{distinct_keys},{key_redundancy:.3},{multi_occurrence_keys},\
521{divergent_keys},{divergence_fraction:.4},{divergent_occurrences},\
522{distinct_payloads_per_key},{keys_over_payload_cap},{both_polarity_addresses},\
523{conformance_raw},{evidence_raw},{discovery_raw},{explanation_raw},{ondemand_raw}",
524 suite = args.suite,
525 node_occurrences = sharing.node_occurrences,
526 distinct_nodes = sharing.distinct_nodes,
527 node_redundancy = sharing.node_redundancy(),
528 result_occurrences = sharing.result_occurrences,
529 distinct_results = sharing.distinct_results,
530 result_redundancy = sharing.result_redundancy(),
531 support_occurrences = sharing.support_occurrences,
532 distinct_support = sharing.distinct_support,
533 support_redundancy = sharing.support_redundancy(),
534 support_share = sharing.support_share(),
535 term_occurrences = sharing.term_occurrences,
536 distinct_terms = sharing.distinct_terms,
537 term_redundancy = sharing.term_redundancy(),
538 normalized_requests = results.normalized_requests,
539 duplicate_records = results.duplicate_records,
540 divergent_duplicates = results.divergent_duplicates,
541 canonical_occurrences = results.canonical_occurrences,
542 shared_payloads = results.shared_payloads,
543 shared_payload_fraction = results.shared_payload_fraction(),
544 shared_canonical_occurrences = results.shared_canonical_occurrences,
545 shared_occurrence_fraction = results.shared_occurrence_fraction(),
546 request_reaches = results.request_reaches,
547 requests_per_payload = results.requests_per_payload(),
548 max_payload_requests = results.max_payload_requests,
549 distinct_keys = results.distinct_keys,
550 key_redundancy = results.key_redundancy(),
551 multi_occurrence_keys = results.multi_occurrence_keys,
552 divergent_keys = results.divergent_keys,
553 divergence_fraction = results.divergence_fraction(),
554 divergent_occurrences = results.divergent_occurrences,
555 distinct_payloads_per_key = results.distinct_payloads_per_key,
556 keys_over_payload_cap = results.keys_over_payload_cap,
557 both_polarity_addresses = results.both_polarity_addresses,
558 c_med = conformance_ms.median,
559 c_mad = conformance_ms.mad,
560 e_med = evidence_ms.median,
561 e_mad = evidence_ms.mad,
562 full_ratio = full_ratio.median,
563 full_ratio_mad = full_ratio.mad,
564 find_med = discovery_ms.median,
565 find_mad = discovery_ms.mad,
566 explain_med = explanation_ms.median,
567 explain_mad = explanation_ms.mad,
568 ondemand_med = on_demand_ms.median,
569 ondemand_mad = on_demand_ms.mad,
570 ondemand_ratio = on_demand_ratio.median,
571 ondemand_ratio_mad = on_demand_ratio.mad,
572 serialize_med = serialize_ms.median,
573 serialize_mad = serialize_ms.mad,
574 conforms_c = conformance.conforms,
575 conforms_e = run.conforms,
576 conforms_o = on_demand_conformance.conforms,
577 pairs_eval = conformance.selected_pairs,
578 pass_pairs = conformance.passed,
579 fail_pairs = conformance.failed,
580 fail_fraction = conformance.failed as f64 / evaluated_pairs,
581 authored_pass = authored_pairs - authored_fail_pairs,
582 authored_fail = authored_fail_pairs,
583 statements = run.statements.len(),
584 nodes_per_authored_pair = evidence_nodes as f64 / authored_pairs_denominator,
585 bytes_per_authored_pair = evidence_bytes as f64 / authored_pairs_denominator,
586 full_with_catalog = full_run_bytes_with_catalog,
587 full_no_catalog = full_run_bytes_no_catalog,
588 compact_with_catalog = compact_run_bytes_with_catalog,
589 compact_no_catalog = compact_run_bytes_no_catalog,
590 conformance_raw = samples_string(&conformance_samples),
591 evidence_raw = samples_string(&evidence_samples),
592 discovery_raw = samples_string(&discovery_samples),
593 explanation_raw = samples_string(&explanation_samples),
594 ondemand_raw = samples_string(&on_demand_samples),
595 );
596
597 eprintln!(
598 "median: {:.1}ms conformance, {:.1}ms full ({:.2}x), {:.1}ms on-demand ({:.2}x); {} failures / {} pairs; {iters} rounds",
599 conformance_ms.median,
600 evidence_ms.median,
601 full_ratio.median,
602 on_demand_ms.median,
603 on_demand_ratio.median,
604 conformance.failed,
605 conformance.selected_pairs,
606 );
607 }
608}Sourcepub fn find_failures(
&self,
options: &ConformanceOptions,
) -> (ConformanceRun, Vec<SelectedPair>)
pub fn find_failures( &self, options: &ConformanceOptions, ) -> (ConformanceRun, Vec<SelectedPair>)
Conformance totals together with the pairs that failed.
The same single pass as validate_conformance,
paying only a Term clone per failing pair, and giving the handles
explain needs. On corpora where failures are a small
fraction of selected pairs — 8,047 of 286,705 across the Brick suite —
this plus per-pair explanation is far cheaper than materializing
evidence for everything and discarding the passes.
Examples found in repository?
27fn main() {
28 let mut shapes_path = PathBuf::new();
29 let mut data_path = PathBuf::new();
30 let mut no_infer = false;
31 let mut top = 15usize;
32 let mut dump_json: Option<PathBuf> = None;
33 let mut argv = std::env::args().skip(1);
34 while let Some(flag) = argv.next() {
35 let mut value = || {
36 argv.next()
37 .unwrap_or_else(|| panic!("{flag} needs a value"))
38 };
39 match flag.as_str() {
40 "--shapes" => shapes_path = PathBuf::from(value()),
41 "--data" => data_path = PathBuf::from(value()),
42 "--no-infer" => no_infer = true,
43 "--top" => top = value().parse().expect("--top must be an integer"),
44 "--dump-json" => dump_json = Some(PathBuf::from(value())),
45 other => panic!("unknown flag {other}"),
46 }
47 }
48 assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49 assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51 let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53 let parsed = shifty_parse::parse_loaded(&shapes);
54 let raw_schema = parsed.schema.clone();
55 let inference_schema = shifty_opt::normalize(&raw_schema);
56
57 let data_bytes = fs::read(&data_path).expect("cannot read --data");
58 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59 let data_graph = if no_infer {
60 data.graph.clone()
61 } else {
62 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63 .expect("inference")
64 .graph
65 };
66
67 let prepared = PreparedEvidenceValidator::with_graphs(
68 &data_graph,
69 &shapes.graph,
70 &raw_schema,
71 ValidationGraphMode::Union,
72 )
73 .expect("stratifiable");
74 let options = ValidationOptions::default();
75 let scan_options = ConformanceOptions::default();
76
77 println!("model: {}", data_path.display());
78 println!("normalized arena nodes: {}", prepared.schema().arena.len());
79 println!(
80 "normalized statements: {}",
81 prepared.schema().statements.len()
82 );
83 println!();
84
85 // ---- arm A: conformance only ------------------------------------------
86 profile::enable();
87 let _ = profile::take_evidence_visits();
88 let start = Instant::now();
89 let conformance = prepared.validate_conformance(&scan_options);
90 let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91 let conformance_visits = profile::take_evidence_visits();
92 let conformance_probes = profile::take_path_probes();
93 let conformance_cache = profile::take().expect("profiling enabled");
94 let conformance_cache = conformance_cache.shape_cache().clone();
95
96 // ---- arm B: dual evidence ----------------------------------------------
97 profile::enable();
98 let start = Instant::now();
99 let run = prepared.validate(&options);
100 let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101 let evidence_visits = profile::take_evidence_visits();
102 let evidence_probes = profile::take_path_probes();
103 let collector = profile::take().expect("profiling enabled");
104 let evidence_cache = collector.shape_cache().clone();
105
106 // ---- arm C: on-demand ---------------------------------------------------
107 // What a caller pays who wants explanations only for what failed: one
108 // conformance pass that also records the failing pairs, then evidence
109 // materialized for those pairs alone.
110 profile::enable();
111 let start = Instant::now();
112 let (_, failures) = prepared.find_failures(&scan_options);
113 let find_ms = start.elapsed().as_secs_f64() * 1e3;
114 let start = Instant::now();
115 let explained: usize = failures
116 .iter()
117 .map(|pair| prepared.explain(pair).len())
118 .sum();
119 let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120 let _ = profile::take();
121 let _ = profile::take_evidence_visits();
122 let _ = profile::take_path_probes();
123
124 let pairs = conformance.selected_pairs.max(1) as f64;
125 let retained = run.walk().len();
126
127 // ---- compact encoding --------------------------------------------------
128 let full_json = run.to_json().expect("evidence run serializes");
129 // Split so the encoding's own cost is separable from the intermediate
130 // `Value` tree it currently has to be handed: compaction is worth doing
131 // inline only if the total stays under the validation it describes.
132 let to_value_start = Instant::now();
133 let run_value = serde_json::to_value(&run).expect("run serializes");
134 let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135 let intern_start = Instant::now();
136 let interned = shifty_engine::compact_value(run_value, true);
137 let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138 let emit_start = Instant::now();
139 let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140 let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141 let compact_ms = to_value_ms + intern_ms + emit_ms;
142 drop(interned);
143 let compact_no_catalog =
144 shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145 println!();
146 println!(
147 "serialized full: {:>10.2} MB ({:.0} bytes/pair)",
148 full_json.len() as f64 / 1e6,
149 full_json.len() as f64 / pairs
150 );
151 println!(
152 "compact (with catalog): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153 = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154 compact_json.len() as f64 / 1e6,
155 compact_json.len() as f64 / pairs,
156 100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157 );
158 println!(
159 "compact (catalog elided): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller)",
160 compact_no_catalog.len() as f64 / 1e6,
161 compact_no_catalog.len() as f64 / pairs,
162 100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163 );
164
165 if let Some(path) = &dump_json {
166 fs::write(path, run.to_json().expect("evidence run serializes"))
167 .expect("cannot write --dump-json");
168 eprintln!("wrote {}", path.display());
169 }
170
171 println!(" conformance evidence");
172 println!(" wall ms {conformance_ms:>11.1} {evidence_ms:>14.1}");
173 println!(
174 " memo lookups {:>11} {:>14}",
175 conformance_cache.hits + conformance_cache.misses,
176 evidence_cache.hits + evidence_cache.misses
177 );
178 println!(
179 " memo hits {:>11} {:>14}",
180 conformance_cache.hits, evidence_cache.hits
181 );
182 println!(
183 " memo misses {:>11} {:>14}",
184 conformance_cache.misses, evidence_cache.misses
185 );
186 println!(
187 " memo entries (peak) {:>11} {:>14}",
188 conformance_cache.peak_entries, evidence_cache.peak_entries
189 );
190 println!(
191 " back-edges {:>11} {:>14}",
192 conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193 );
194 println!(
195 " uncacheable results {:>11} {:>14}",
196 conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197 );
198 println!(" evidence visits {conformance_visits:>11} {evidence_visits:>14}");
199 println!(" path_support probes {conformance_probes:>11} {evidence_probes:>14}");
200 println!();
201 println!(
202 "path probes per visit: {:.1}",
203 evidence_probes as f64 / evidence_visits.max(1) as f64
204 );
205
206 println!("selected pairs: {}", conformance.selected_pairs);
207 println!("retained evidence nodes: {retained}");
208 println!(
209 "visited / retained: {:.1}x",
210 evidence_visits as f64 / retained.max(1) as f64
211 );
212 println!(
213 "visits per pair: {:.0}",
214 evidence_visits as f64 / pairs
215 );
216 println!(
217 "distinct (shape,node) that a cache could collapse to at most: {}",
218 evidence_cache.peak_entries
219 );
220 println!(
221 "visits / distinct pairs: {:.1}x",
222 evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223 );
224 println!();
225 println!(
226 "per-visit cost: {:.2} us",
227 evidence_ms * 1e3 / evidence_visits.max(1) as f64
228 );
229 println!(
230 "per-memo-lookup cost: {:.2} us",
231 conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232 );
233
234 // ---- attribution: which statements own the time ------------------------
235 let (selection, materialization): (Vec<_>, Vec<_>) = collector
236 .shape_records()
237 .iter()
238 .partition(|record| record.label.starts_with("select:"));
239 let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240 let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242 println!();
243 println!(
244 "target selection total: {:.1} ms over {} statement(s)",
245 selection_us as f64 / 1e3,
246 selection.len()
247 );
248 println!(
249 "materialization total: {:.1} ms over {} statement(s)",
250 materialization_us as f64 / 1e3,
251 materialization.len()
252 );
253 println!();
254 println!(
255 "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256 failures.len(),
257 conformance.selected_pairs,
258 explained
259 );
260 println!(
261 " find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms \
262 ({:.2}x the full evidence run, {:.2}x conformance alone)",
263 find_ms + explain_ms,
264 (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265 (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266 );
267
268 let mut ranked = materialization;
269 ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270 println!();
271 println!("top statements by evidence-materialization time:");
272 println!(
273 " {:>8} {:>7} {:>6} {:>10} {:>12} {:>10} shape",
274 "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275 );
276 for record in ranked.iter().take(top) {
277 println!(
278 " {:>8.1} {:>6.1}% {:>6} {:>10.1} {:>12} {:>10.0} {}",
279 record.total_us as f64 / 1e3,
280 100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281 record.invocations,
282 record.total_us as f64 / record.invocations.max(1) as f64,
283 record.visits,
284 record.visits as f64 / record.invocations.max(1) as f64,
285 record.label,
286 );
287 }
288
289 let mut ranked_selection = selection;
290 ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291 println!();
292 println!("top statements by target-selection time:");
293 for record in ranked_selection.iter().take(5) {
294 println!(
295 " {:>8.1} ms {}",
296 record.total_us as f64 / 1e3,
297 record.label
298 );
299 }
300}More examples
131fn main() {
132 let args = parse_args();
133
134 eprintln!("loading shapes {}…", args.shapes.display());
135 let shapes_bytes = fs::read(&args.shapes).expect("cannot read --shapes");
136 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
137 let parsed = shifty_parse::parse_loaded(&shapes);
138 let raw_schema = parsed.schema.clone();
139 // Inference runs against the normalized schema, exactly as the CLI does.
140 let inference_schema = shifty_opt::normalize(&raw_schema);
141 drop(shapes_bytes);
142
143 if args.header {
144 println!("{COLUMNS}");
145 }
146
147 for model in models_in(&args.models) {
148 let name = model
149 .file_name()
150 .and_then(|name| name.to_str())
151 .unwrap_or("?")
152 .to_string();
153 eprint!("{name}… ");
154
155 // ---- untimed setup: everything common to all workflows --------------
156 let data_bytes = fs::read(&model).expect("cannot read model");
157 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse model");
158 drop(data_bytes);
159 let data_graph = if args.no_infer {
160 data.graph.clone()
161 } else {
162 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
163 .unwrap_or_else(|error| panic!("{name}: inference failed: {error}"))
164 .graph
165 };
166 let data_triples = data_graph.len();
167
168 let prepared = PreparedEvidenceValidator::with_graphs(
169 &data_graph,
170 &shapes.graph,
171 &raw_schema,
172 ValidationGraphMode::Union,
173 )
174 .unwrap_or_else(|error| panic!("{name}: schema is non-stratifiable: {error}"));
175 let options = ValidationOptions::default();
176 let scan_options = ConformanceOptions::default();
177
178 // ---- timed: validation only -----------------------------------------
179 // One warm-up round, discarded: it faults in the caches all arms share.
180 // Its duration is only consulted when `--budget-ms` is in play.
181 let probe = Instant::now();
182 let _ = prepared.validate_conformance(&scan_options);
183 let _ = prepared.validate_canonical(&options);
184 let (_, warm_failures) = prepared.find_failures(&scan_options);
185 for pair in &warm_failures {
186 std::hint::black_box(prepared.explain_canonical(pair));
187 }
188 let probe_ms = probe.elapsed().as_secs_f64() * 1e3;
189 // Every model gets the same iteration count. `--budget-ms` only caps the
190 // count for a corpus slow enough that a fixed count is impractical; it
191 // is off by default, and the emitted `iters` column records what was used.
192 let iters = if args.budget_ms > 0.0 {
193 ((args.budget_ms / probe_ms.max(f64::MIN_POSITIVE)) as usize)
194 .clamp(args.min_iters.min(args.iters), args.iters)
195 } else {
196 args.iters
197 };
198
199 let mut conformance_samples = Vec::with_capacity(iters);
200 let mut evidence_samples = Vec::with_capacity(iters);
201 let mut discovery_samples = Vec::with_capacity(iters);
202 let mut explanation_samples = Vec::with_capacity(iters);
203 let mut on_demand_samples = Vec::with_capacity(iters);
204 let mut last_run = None;
205 let mut last_conformance = None;
206 let mut last_on_demand_conformance = None;
207 let mut last_failures = Vec::new();
208 let mut last_explanations = Vec::new();
209
210 // Rotate A/B/C so no arm systematically inherits the cache, allocator,
211 // scheduler, or thermal state left by another arm.
212 for iteration in 0..iters {
213 for arm in ARM_ORDERS[iteration % ARM_ORDERS.len()] {
214 match arm {
215 Arm::Conformance => {
216 let start = Instant::now();
217 let conformance_run = prepared.validate_conformance(&scan_options);
218 conformance_samples.push(start.elapsed());
219 std::hint::black_box(conformance_run.selected_pairs);
220 last_conformance = Some(conformance_run);
221 }
222 Arm::FullEvidence => {
223 let start = Instant::now();
224 let run = prepared.validate_canonical(&options);
225 evidence_samples.push(start.elapsed());
226 std::hint::black_box(run.statements.len());
227 last_run = Some(run);
228 }
229 Arm::OnDemand => {
230 let start = Instant::now();
231 let (on_demand_conformance, failures) =
232 prepared.find_failures(&scan_options);
233 let discovery = start.elapsed();
234
235 let start = Instant::now();
236 let explanations: Vec<_> = failures
237 .iter()
238 .flat_map(|pair| prepared.explain_canonical(pair))
239 .collect();
240 let explanation = start.elapsed();
241
242 discovery_samples.push(discovery);
243 explanation_samples.push(explanation);
244 on_demand_samples.push(discovery + explanation);
245 std::hint::black_box(explanations.len());
246 last_on_demand_conformance = Some(on_demand_conformance);
247 last_failures = failures;
248 last_explanations = explanations;
249 }
250 }
251 }
252 }
253
254 // ---- untimed: shape of the evidence produced -------------------------
255 // Reuse the final timed results rather than paying for another run.
256 let conformance = last_conformance.expect("at least one iteration runs");
257 let on_demand_conformance =
258 last_on_demand_conformance.expect("at least one iteration runs");
259 let run = last_run.expect("at least one iteration runs");
260 let authored_pairs: usize = run
261 .statements
262 .iter()
263 .map(|statement| statement.selected_foci.len())
264 .sum();
265 let authored_fail_pairs: usize = run
266 .statements
267 .iter()
268 .flat_map(|statement| &statement.selected_foci)
269 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
270 .count();
271
272 // The three interfaces must describe the same normalized selection and
273 // polarity partition. Compare exact failed identities, not just the
274 // report-wide conformance bit.
275 assert_eq!(
276 conformance, on_demand_conformance,
277 "{name}: on-demand counts"
278 );
279 assert_eq!(
280 conformance.conforms, run.conforms,
281 "{name}: conformance bit"
282 );
283 let full_selected: HashSet<_> = run
284 .statements
285 .iter()
286 .flat_map(|statement| {
287 statement.selected_foci.iter().filter_map(|focus| {
288 statement
289 .normalized_statement_id
290 .map(|id| (id, focus.focus.clone()))
291 })
292 })
293 .collect();
294 let full_failures: HashSet<_> = run
295 .statements
296 .iter()
297 .flat_map(|statement| {
298 statement
299 .selected_foci
300 .iter()
301 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
302 .filter_map(|focus| {
303 statement
304 .normalized_statement_id
305 .map(|id| (id, focus.focus.clone()))
306 })
307 })
308 .collect();
309 let discovered_failures: HashSet<_> = last_failures
310 .iter()
311 .map(|pair| (pair.normalized_statement(), pair.focus().clone()))
312 .collect();
313 assert_eq!(
314 full_selected.len(),
315 conformance.selected_pairs,
316 "{name}: selected pairs"
317 );
318 assert_eq!(
319 full_failures.len(),
320 conformance.failed,
321 "{name}: failed pairs"
322 );
323 assert_eq!(
324 full_selected.len() - full_failures.len(),
325 conformance.passed,
326 "{name}: passed pairs"
327 );
328 assert_eq!(
329 full_failures, discovered_failures,
330 "{name}: failed identities"
331 );
332
333 let full_by_authored: HashMap<_, _> = run
334 .statements
335 .iter()
336 .flat_map(|statement| {
337 statement.selected_foci.iter().map(|focus| {
338 (
339 (statement.source_statement_id, focus.focus.clone()),
340 &focus.evidence,
341 )
342 })
343 })
344 .collect();
345 let mut explained_foci = 0;
346 for statement in &last_explanations {
347 for focus in &statement.selected_foci {
348 explained_foci += 1;
349 assert_eq!(
350 focus.status(),
351 shifty_engine::EvaluationStatus::Fail,
352 "{name}: on-demand explanation polarity"
353 );
354 let key = (statement.source_statement_id, focus.focus.clone());
355 assert_eq!(
356 &focus.evidence,
357 *full_by_authored
358 .get(&key)
359 .unwrap_or_else(|| panic!("{name}: unexplained full-run pair {key:?}")),
360 "{name}: full and on-demand evidence differ for {key:?}"
361 );
362 }
363 }
364 assert_eq!(
365 explained_foci, authored_fail_pairs,
366 "{name}: authored explanations"
367 );
368
369 let evidence_nodes = run.walk().len();
370 let evidence_bytes: usize = run
371 .statements
372 .iter()
373 .flat_map(|statement| &statement.selected_foci)
374 .map(|focus| focus.evidence.to_json().expect("evidence serializes").len())
375 .sum();
376 let full_json = run.to_json().expect("evidence run serializes");
377 let full_run_bytes_with_catalog = full_json.len();
378 let mut full_value_no_catalog =
379 serde_json::to_value(&run).expect("evidence run converts to JSON");
380 full_value_no_catalog
381 .as_object_mut()
382 .expect("evidence run is a JSON object")
383 .remove("constraints");
384 let full_run_bytes_no_catalog = serde_json::to_string(&full_value_no_catalog)
385 .expect("catalog-free evidence run serializes")
386 .len();
387
388 // Compare complete runs with the same catalog policy on each side, and
389 // verify both encodings reconstruct the exact typed result.
390 let encoded_with_catalog =
391 shifty_engine::compact(&run, true).expect("compact encoding succeeds");
392 let compact_run_bytes_with_catalog = serde_json::to_string(&encoded_with_catalog)
393 .expect("compact encoding serializes")
394 .len();
395 assert_eq!(
396 shifty_engine::expand(&encoded_with_catalog).expect("compact run expands"),
397 run,
398 "{name}: compact run with catalog is not lossless"
399 );
400 let encoded_no_catalog =
401 shifty_engine::compact(&run, false).expect("compact encoding succeeds");
402 let compact_run_bytes_no_catalog = serde_json::to_string(&encoded_no_catalog)
403 .expect("catalog-free compact encoding serializes")
404 .len();
405 let catalog = serde_json::to_value(&run.constraints).expect("catalog serializes");
406 assert_eq!(
407 shifty_engine::expand_with_catalog(&encoded_no_catalog, catalog)
408 .expect("catalog-free compact run expands"),
409 run,
410 "{name}: compact run without catalog is not lossless"
411 );
412 // Sharing is reported over the evidence alone. The node and term tables
413 // of `encoded` also hold catalog entries, which would understate the
414 // redundancy by adding distinct entries with no evidence occurrences.
415 let sharing = shifty_engine::sharing(&run).expect("sharing measures");
416 let results = shifty_engine::result_sharing(&run);
417
418 // The two passes reach the same judgment nodes by routes that share no
419 // code: `sharing` interns serialized JSON, `result_sharing` walks the
420 // typed evidence. Agreement on both counts is what makes either number
421 // reportable; a mismatch means the node families are misclassified or a
422 // traversal is missing a variant.
423 assert_eq!(
424 sharing.result_occurrences + sharing.support_occurrences,
425 sharing.node_occurrences,
426 "{name}: node families do not partition occurrences"
427 );
428 assert_eq!(
429 sharing.distinct_results + sharing.distinct_support,
430 sharing.distinct_nodes,
431 "{name}: node families do not partition the table"
432 );
433 assert_eq!(
434 sharing.result_occurrences, evidence_nodes,
435 "{name}: interned judgments disagree with the typed walk"
436 );
437 assert_eq!(
438 results.occurrences, evidence_nodes,
439 "{name}: typed sharing pass disagrees with the typed walk"
440 );
441 assert_eq!(
442 results.distinct_payloads, sharing.distinct_results,
443 "{name}: distinct judgments disagree between the two passes"
444 );
445 // Structural equality implies key equality, never the reverse.
446 assert!(
447 results.distinct_keys <= results.distinct_payloads,
448 "{name}: more keys than payloads"
449 );
450 // Requests are the normalized selection the conformance pass counts;
451 // authored records are the source-preserving view over them.
452 assert_eq!(
453 results.normalized_requests, conformance.selected_pairs,
454 "{name}: requests disagree with the conformance selection"
455 );
456 assert_eq!(
457 results.authored_records, authored_pairs,
458 "{name}: authored records disagree with the run"
459 );
460
461 // Serialization is measured separately so its allocation traffic does
462 // not perturb the next validation arm.
463 let mut serialize_samples = Vec::with_capacity(iters);
464 for _ in 0..iters {
465 let start = Instant::now();
466 let json = run.to_json().expect("evidence run serializes");
467 serialize_samples.push(start.elapsed());
468 std::hint::black_box(json.len());
469 }
470
471 // Each ratio compares the corresponding raw samples from one rotated
472 // A/B/C round. The CSV retains those samples for independent checking.
473 let full_ratios: Vec<f64> = conformance_samples
474 .iter()
475 .zip(evidence_samples.iter())
476 .map(|(conformance, evidence)| {
477 evidence.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
478 })
479 .collect();
480 let on_demand_ratios: Vec<f64> = conformance_samples
481 .iter()
482 .zip(on_demand_samples.iter())
483 .map(|(conformance, on_demand)| {
484 on_demand.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
485 })
486 .collect();
487
488 let conformance_ms = summarize_durations(&conformance_samples);
489 let evidence_ms = summarize_durations(&evidence_samples);
490 let discovery_ms = summarize_durations(&discovery_samples);
491 let explanation_ms = summarize_durations(&explanation_samples);
492 let on_demand_ms = summarize_durations(&on_demand_samples);
493 let serialize_ms = summarize_durations(&serialize_samples);
494 let full_ratio = summarize_values(&full_ratios);
495 let on_demand_ratio = summarize_values(&on_demand_ratios);
496 let evaluated_pairs = conformance.selected_pairs.max(1) as f64;
497 let authored_pairs_denominator = authored_pairs.max(1) as f64;
498
499 println!(
500 "{suite},{name},{data_triples},{iters},\
501{c_med:.4},{c_mad:.4},{e_med:.4},{e_mad:.4},{full_ratio:.4},{full_ratio_mad:.4},\
502{find_med:.4},{find_mad:.4},{explain_med:.4},{explain_mad:.4},\
503{ondemand_med:.4},{ondemand_mad:.4},{ondemand_ratio:.4},{ondemand_ratio_mad:.4},\
504{serialize_med:.4},{serialize_mad:.4},\
505{conforms_c},{conforms_e},{conforms_o},\
506{pairs_eval},{pass_pairs},{fail_pairs},{fail_fraction:.6},\
507{authored_pairs},{authored_pass},{authored_fail},\
508{statements},\
509{evidence_nodes},{nodes_per_authored_pair:.3},\
510{evidence_bytes},{bytes_per_authored_pair:.1},\
511{full_with_catalog},{full_no_catalog},{compact_with_catalog},{compact_no_catalog},\
512{node_occurrences},{distinct_nodes},{node_redundancy:.3},\
513{result_occurrences},{distinct_results},{result_redundancy:.3},\
514{support_occurrences},{distinct_support},{support_redundancy:.3},{support_share:.4},\
515{term_occurrences},{distinct_terms},{term_redundancy:.3},\
516{normalized_requests},{duplicate_records},{divergent_duplicates},\
517{canonical_occurrences},{shared_payloads},{shared_payload_fraction:.4},\
518{shared_canonical_occurrences},{shared_occurrence_fraction:.4},\
519{request_reaches},{requests_per_payload:.3},{max_payload_requests},\
520{distinct_keys},{key_redundancy:.3},{multi_occurrence_keys},\
521{divergent_keys},{divergence_fraction:.4},{divergent_occurrences},\
522{distinct_payloads_per_key},{keys_over_payload_cap},{both_polarity_addresses},\
523{conformance_raw},{evidence_raw},{discovery_raw},{explanation_raw},{ondemand_raw}",
524 suite = args.suite,
525 node_occurrences = sharing.node_occurrences,
526 distinct_nodes = sharing.distinct_nodes,
527 node_redundancy = sharing.node_redundancy(),
528 result_occurrences = sharing.result_occurrences,
529 distinct_results = sharing.distinct_results,
530 result_redundancy = sharing.result_redundancy(),
531 support_occurrences = sharing.support_occurrences,
532 distinct_support = sharing.distinct_support,
533 support_redundancy = sharing.support_redundancy(),
534 support_share = sharing.support_share(),
535 term_occurrences = sharing.term_occurrences,
536 distinct_terms = sharing.distinct_terms,
537 term_redundancy = sharing.term_redundancy(),
538 normalized_requests = results.normalized_requests,
539 duplicate_records = results.duplicate_records,
540 divergent_duplicates = results.divergent_duplicates,
541 canonical_occurrences = results.canonical_occurrences,
542 shared_payloads = results.shared_payloads,
543 shared_payload_fraction = results.shared_payload_fraction(),
544 shared_canonical_occurrences = results.shared_canonical_occurrences,
545 shared_occurrence_fraction = results.shared_occurrence_fraction(),
546 request_reaches = results.request_reaches,
547 requests_per_payload = results.requests_per_payload(),
548 max_payload_requests = results.max_payload_requests,
549 distinct_keys = results.distinct_keys,
550 key_redundancy = results.key_redundancy(),
551 multi_occurrence_keys = results.multi_occurrence_keys,
552 divergent_keys = results.divergent_keys,
553 divergence_fraction = results.divergence_fraction(),
554 divergent_occurrences = results.divergent_occurrences,
555 distinct_payloads_per_key = results.distinct_payloads_per_key,
556 keys_over_payload_cap = results.keys_over_payload_cap,
557 both_polarity_addresses = results.both_polarity_addresses,
558 c_med = conformance_ms.median,
559 c_mad = conformance_ms.mad,
560 e_med = evidence_ms.median,
561 e_mad = evidence_ms.mad,
562 full_ratio = full_ratio.median,
563 full_ratio_mad = full_ratio.mad,
564 find_med = discovery_ms.median,
565 find_mad = discovery_ms.mad,
566 explain_med = explanation_ms.median,
567 explain_mad = explanation_ms.mad,
568 ondemand_med = on_demand_ms.median,
569 ondemand_mad = on_demand_ms.mad,
570 ondemand_ratio = on_demand_ratio.median,
571 ondemand_ratio_mad = on_demand_ratio.mad,
572 serialize_med = serialize_ms.median,
573 serialize_mad = serialize_ms.mad,
574 conforms_c = conformance.conforms,
575 conforms_e = run.conforms,
576 conforms_o = on_demand_conformance.conforms,
577 pairs_eval = conformance.selected_pairs,
578 pass_pairs = conformance.passed,
579 fail_pairs = conformance.failed,
580 fail_fraction = conformance.failed as f64 / evaluated_pairs,
581 authored_pass = authored_pairs - authored_fail_pairs,
582 authored_fail = authored_fail_pairs,
583 statements = run.statements.len(),
584 nodes_per_authored_pair = evidence_nodes as f64 / authored_pairs_denominator,
585 bytes_per_authored_pair = evidence_bytes as f64 / authored_pairs_denominator,
586 full_with_catalog = full_run_bytes_with_catalog,
587 full_no_catalog = full_run_bytes_no_catalog,
588 compact_with_catalog = compact_run_bytes_with_catalog,
589 compact_no_catalog = compact_run_bytes_no_catalog,
590 conformance_raw = samples_string(&conformance_samples),
591 evidence_raw = samples_string(&evidence_samples),
592 discovery_raw = samples_string(&discovery_samples),
593 explanation_raw = samples_string(&explanation_samples),
594 ondemand_raw = samples_string(&on_demand_samples),
595 );
596
597 eprintln!(
598 "median: {:.1}ms conformance, {:.1}ms full ({:.2}x), {:.1}ms on-demand ({:.2}x); {} failures / {} pairs; {iters} rounds",
599 conformance_ms.median,
600 evidence_ms.median,
601 full_ratio.median,
602 on_demand_ms.median,
603 on_demand_ratio.median,
604 conformance.failed,
605 conformance.selected_pairs,
606 );
607 }
608}Sourcepub fn explain(&self, pair: &SelectedPair) -> Vec<StatementEvaluation>
pub fn explain(&self, pair: &SelectedPair) -> Vec<StatementEvaluation>
Materialize evidence for one pair, in the shape validate
would have produced for it.
Returns one StatementEvaluation per authored statement that
normalizes to the pair’s normalized statement and was selected when the
handle was created, each carrying the single focus, so a
caller can treat the result exactly like a slice of a full run. Empty if
the statement index is out of range.
Target selection is not re-run: pair is taken as already selected,
which is the point — re-deriving the selection would cost what the whole
pass costs. Pairs should come from find_failures
or from an earlier run over the same snapshot. A focus this statement
never selected still yields well-defined evidence; it just describes a
pair the run did not contain.
The constraint catalog is not included, since it is fixed per snapshot
rather than per pair; take it once from constraints.
Examples found in repository?
27fn main() {
28 let mut shapes_path = PathBuf::new();
29 let mut data_path = PathBuf::new();
30 let mut no_infer = false;
31 let mut top = 15usize;
32 let mut dump_json: Option<PathBuf> = None;
33 let mut argv = std::env::args().skip(1);
34 while let Some(flag) = argv.next() {
35 let mut value = || {
36 argv.next()
37 .unwrap_or_else(|| panic!("{flag} needs a value"))
38 };
39 match flag.as_str() {
40 "--shapes" => shapes_path = PathBuf::from(value()),
41 "--data" => data_path = PathBuf::from(value()),
42 "--no-infer" => no_infer = true,
43 "--top" => top = value().parse().expect("--top must be an integer"),
44 "--dump-json" => dump_json = Some(PathBuf::from(value())),
45 other => panic!("unknown flag {other}"),
46 }
47 }
48 assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49 assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51 let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53 let parsed = shifty_parse::parse_loaded(&shapes);
54 let raw_schema = parsed.schema.clone();
55 let inference_schema = shifty_opt::normalize(&raw_schema);
56
57 let data_bytes = fs::read(&data_path).expect("cannot read --data");
58 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59 let data_graph = if no_infer {
60 data.graph.clone()
61 } else {
62 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63 .expect("inference")
64 .graph
65 };
66
67 let prepared = PreparedEvidenceValidator::with_graphs(
68 &data_graph,
69 &shapes.graph,
70 &raw_schema,
71 ValidationGraphMode::Union,
72 )
73 .expect("stratifiable");
74 let options = ValidationOptions::default();
75 let scan_options = ConformanceOptions::default();
76
77 println!("model: {}", data_path.display());
78 println!("normalized arena nodes: {}", prepared.schema().arena.len());
79 println!(
80 "normalized statements: {}",
81 prepared.schema().statements.len()
82 );
83 println!();
84
85 // ---- arm A: conformance only ------------------------------------------
86 profile::enable();
87 let _ = profile::take_evidence_visits();
88 let start = Instant::now();
89 let conformance = prepared.validate_conformance(&scan_options);
90 let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91 let conformance_visits = profile::take_evidence_visits();
92 let conformance_probes = profile::take_path_probes();
93 let conformance_cache = profile::take().expect("profiling enabled");
94 let conformance_cache = conformance_cache.shape_cache().clone();
95
96 // ---- arm B: dual evidence ----------------------------------------------
97 profile::enable();
98 let start = Instant::now();
99 let run = prepared.validate(&options);
100 let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101 let evidence_visits = profile::take_evidence_visits();
102 let evidence_probes = profile::take_path_probes();
103 let collector = profile::take().expect("profiling enabled");
104 let evidence_cache = collector.shape_cache().clone();
105
106 // ---- arm C: on-demand ---------------------------------------------------
107 // What a caller pays who wants explanations only for what failed: one
108 // conformance pass that also records the failing pairs, then evidence
109 // materialized for those pairs alone.
110 profile::enable();
111 let start = Instant::now();
112 let (_, failures) = prepared.find_failures(&scan_options);
113 let find_ms = start.elapsed().as_secs_f64() * 1e3;
114 let start = Instant::now();
115 let explained: usize = failures
116 .iter()
117 .map(|pair| prepared.explain(pair).len())
118 .sum();
119 let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120 let _ = profile::take();
121 let _ = profile::take_evidence_visits();
122 let _ = profile::take_path_probes();
123
124 let pairs = conformance.selected_pairs.max(1) as f64;
125 let retained = run.walk().len();
126
127 // ---- compact encoding --------------------------------------------------
128 let full_json = run.to_json().expect("evidence run serializes");
129 // Split so the encoding's own cost is separable from the intermediate
130 // `Value` tree it currently has to be handed: compaction is worth doing
131 // inline only if the total stays under the validation it describes.
132 let to_value_start = Instant::now();
133 let run_value = serde_json::to_value(&run).expect("run serializes");
134 let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135 let intern_start = Instant::now();
136 let interned = shifty_engine::compact_value(run_value, true);
137 let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138 let emit_start = Instant::now();
139 let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140 let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141 let compact_ms = to_value_ms + intern_ms + emit_ms;
142 drop(interned);
143 let compact_no_catalog =
144 shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145 println!();
146 println!(
147 "serialized full: {:>10.2} MB ({:.0} bytes/pair)",
148 full_json.len() as f64 / 1e6,
149 full_json.len() as f64 / pairs
150 );
151 println!(
152 "compact (with catalog): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153 = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154 compact_json.len() as f64 / 1e6,
155 compact_json.len() as f64 / pairs,
156 100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157 );
158 println!(
159 "compact (catalog elided): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller)",
160 compact_no_catalog.len() as f64 / 1e6,
161 compact_no_catalog.len() as f64 / pairs,
162 100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163 );
164
165 if let Some(path) = &dump_json {
166 fs::write(path, run.to_json().expect("evidence run serializes"))
167 .expect("cannot write --dump-json");
168 eprintln!("wrote {}", path.display());
169 }
170
171 println!(" conformance evidence");
172 println!(" wall ms {conformance_ms:>11.1} {evidence_ms:>14.1}");
173 println!(
174 " memo lookups {:>11} {:>14}",
175 conformance_cache.hits + conformance_cache.misses,
176 evidence_cache.hits + evidence_cache.misses
177 );
178 println!(
179 " memo hits {:>11} {:>14}",
180 conformance_cache.hits, evidence_cache.hits
181 );
182 println!(
183 " memo misses {:>11} {:>14}",
184 conformance_cache.misses, evidence_cache.misses
185 );
186 println!(
187 " memo entries (peak) {:>11} {:>14}",
188 conformance_cache.peak_entries, evidence_cache.peak_entries
189 );
190 println!(
191 " back-edges {:>11} {:>14}",
192 conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193 );
194 println!(
195 " uncacheable results {:>11} {:>14}",
196 conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197 );
198 println!(" evidence visits {conformance_visits:>11} {evidence_visits:>14}");
199 println!(" path_support probes {conformance_probes:>11} {evidence_probes:>14}");
200 println!();
201 println!(
202 "path probes per visit: {:.1}",
203 evidence_probes as f64 / evidence_visits.max(1) as f64
204 );
205
206 println!("selected pairs: {}", conformance.selected_pairs);
207 println!("retained evidence nodes: {retained}");
208 println!(
209 "visited / retained: {:.1}x",
210 evidence_visits as f64 / retained.max(1) as f64
211 );
212 println!(
213 "visits per pair: {:.0}",
214 evidence_visits as f64 / pairs
215 );
216 println!(
217 "distinct (shape,node) that a cache could collapse to at most: {}",
218 evidence_cache.peak_entries
219 );
220 println!(
221 "visits / distinct pairs: {:.1}x",
222 evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223 );
224 println!();
225 println!(
226 "per-visit cost: {:.2} us",
227 evidence_ms * 1e3 / evidence_visits.max(1) as f64
228 );
229 println!(
230 "per-memo-lookup cost: {:.2} us",
231 conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232 );
233
234 // ---- attribution: which statements own the time ------------------------
235 let (selection, materialization): (Vec<_>, Vec<_>) = collector
236 .shape_records()
237 .iter()
238 .partition(|record| record.label.starts_with("select:"));
239 let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240 let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242 println!();
243 println!(
244 "target selection total: {:.1} ms over {} statement(s)",
245 selection_us as f64 / 1e3,
246 selection.len()
247 );
248 println!(
249 "materialization total: {:.1} ms over {} statement(s)",
250 materialization_us as f64 / 1e3,
251 materialization.len()
252 );
253 println!();
254 println!(
255 "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256 failures.len(),
257 conformance.selected_pairs,
258 explained
259 );
260 println!(
261 " find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms \
262 ({:.2}x the full evidence run, {:.2}x conformance alone)",
263 find_ms + explain_ms,
264 (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265 (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266 );
267
268 let mut ranked = materialization;
269 ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270 println!();
271 println!("top statements by evidence-materialization time:");
272 println!(
273 " {:>8} {:>7} {:>6} {:>10} {:>12} {:>10} shape",
274 "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275 );
276 for record in ranked.iter().take(top) {
277 println!(
278 " {:>8.1} {:>6.1}% {:>6} {:>10.1} {:>12} {:>10.0} {}",
279 record.total_us as f64 / 1e3,
280 100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281 record.invocations,
282 record.total_us as f64 / record.invocations.max(1) as f64,
283 record.visits,
284 record.visits as f64 / record.invocations.max(1) as f64,
285 record.label,
286 );
287 }
288
289 let mut ranked_selection = selection;
290 ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291 println!();
292 println!("top statements by target-selection time:");
293 for record in ranked_selection.iter().take(5) {
294 println!(
295 " {:>8.1} ms {}",
296 record.total_us as f64 / 1e3,
297 record.label
298 );
299 }
300}Sourcepub fn explain_canonical(&self, pair: &SelectedPair) -> Vec<StatementEvaluation>
pub fn explain_canonical(&self, pair: &SelectedPair) -> Vec<StatementEvaluation>
Materialize only the canonical evidence for one pair.
This omits the optional authored-statement progress view while retaining
the satisfaction trace or failure witness and the authored identities.
It is the per-pair counterpart of validate_canonical.
Examples found in repository?
131fn main() {
132 let args = parse_args();
133
134 eprintln!("loading shapes {}…", args.shapes.display());
135 let shapes_bytes = fs::read(&args.shapes).expect("cannot read --shapes");
136 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
137 let parsed = shifty_parse::parse_loaded(&shapes);
138 let raw_schema = parsed.schema.clone();
139 // Inference runs against the normalized schema, exactly as the CLI does.
140 let inference_schema = shifty_opt::normalize(&raw_schema);
141 drop(shapes_bytes);
142
143 if args.header {
144 println!("{COLUMNS}");
145 }
146
147 for model in models_in(&args.models) {
148 let name = model
149 .file_name()
150 .and_then(|name| name.to_str())
151 .unwrap_or("?")
152 .to_string();
153 eprint!("{name}… ");
154
155 // ---- untimed setup: everything common to all workflows --------------
156 let data_bytes = fs::read(&model).expect("cannot read model");
157 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse model");
158 drop(data_bytes);
159 let data_graph = if args.no_infer {
160 data.graph.clone()
161 } else {
162 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
163 .unwrap_or_else(|error| panic!("{name}: inference failed: {error}"))
164 .graph
165 };
166 let data_triples = data_graph.len();
167
168 let prepared = PreparedEvidenceValidator::with_graphs(
169 &data_graph,
170 &shapes.graph,
171 &raw_schema,
172 ValidationGraphMode::Union,
173 )
174 .unwrap_or_else(|error| panic!("{name}: schema is non-stratifiable: {error}"));
175 let options = ValidationOptions::default();
176 let scan_options = ConformanceOptions::default();
177
178 // ---- timed: validation only -----------------------------------------
179 // One warm-up round, discarded: it faults in the caches all arms share.
180 // Its duration is only consulted when `--budget-ms` is in play.
181 let probe = Instant::now();
182 let _ = prepared.validate_conformance(&scan_options);
183 let _ = prepared.validate_canonical(&options);
184 let (_, warm_failures) = prepared.find_failures(&scan_options);
185 for pair in &warm_failures {
186 std::hint::black_box(prepared.explain_canonical(pair));
187 }
188 let probe_ms = probe.elapsed().as_secs_f64() * 1e3;
189 // Every model gets the same iteration count. `--budget-ms` only caps the
190 // count for a corpus slow enough that a fixed count is impractical; it
191 // is off by default, and the emitted `iters` column records what was used.
192 let iters = if args.budget_ms > 0.0 {
193 ((args.budget_ms / probe_ms.max(f64::MIN_POSITIVE)) as usize)
194 .clamp(args.min_iters.min(args.iters), args.iters)
195 } else {
196 args.iters
197 };
198
199 let mut conformance_samples = Vec::with_capacity(iters);
200 let mut evidence_samples = Vec::with_capacity(iters);
201 let mut discovery_samples = Vec::with_capacity(iters);
202 let mut explanation_samples = Vec::with_capacity(iters);
203 let mut on_demand_samples = Vec::with_capacity(iters);
204 let mut last_run = None;
205 let mut last_conformance = None;
206 let mut last_on_demand_conformance = None;
207 let mut last_failures = Vec::new();
208 let mut last_explanations = Vec::new();
209
210 // Rotate A/B/C so no arm systematically inherits the cache, allocator,
211 // scheduler, or thermal state left by another arm.
212 for iteration in 0..iters {
213 for arm in ARM_ORDERS[iteration % ARM_ORDERS.len()] {
214 match arm {
215 Arm::Conformance => {
216 let start = Instant::now();
217 let conformance_run = prepared.validate_conformance(&scan_options);
218 conformance_samples.push(start.elapsed());
219 std::hint::black_box(conformance_run.selected_pairs);
220 last_conformance = Some(conformance_run);
221 }
222 Arm::FullEvidence => {
223 let start = Instant::now();
224 let run = prepared.validate_canonical(&options);
225 evidence_samples.push(start.elapsed());
226 std::hint::black_box(run.statements.len());
227 last_run = Some(run);
228 }
229 Arm::OnDemand => {
230 let start = Instant::now();
231 let (on_demand_conformance, failures) =
232 prepared.find_failures(&scan_options);
233 let discovery = start.elapsed();
234
235 let start = Instant::now();
236 let explanations: Vec<_> = failures
237 .iter()
238 .flat_map(|pair| prepared.explain_canonical(pair))
239 .collect();
240 let explanation = start.elapsed();
241
242 discovery_samples.push(discovery);
243 explanation_samples.push(explanation);
244 on_demand_samples.push(discovery + explanation);
245 std::hint::black_box(explanations.len());
246 last_on_demand_conformance = Some(on_demand_conformance);
247 last_failures = failures;
248 last_explanations = explanations;
249 }
250 }
251 }
252 }
253
254 // ---- untimed: shape of the evidence produced -------------------------
255 // Reuse the final timed results rather than paying for another run.
256 let conformance = last_conformance.expect("at least one iteration runs");
257 let on_demand_conformance =
258 last_on_demand_conformance.expect("at least one iteration runs");
259 let run = last_run.expect("at least one iteration runs");
260 let authored_pairs: usize = run
261 .statements
262 .iter()
263 .map(|statement| statement.selected_foci.len())
264 .sum();
265 let authored_fail_pairs: usize = run
266 .statements
267 .iter()
268 .flat_map(|statement| &statement.selected_foci)
269 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
270 .count();
271
272 // The three interfaces must describe the same normalized selection and
273 // polarity partition. Compare exact failed identities, not just the
274 // report-wide conformance bit.
275 assert_eq!(
276 conformance, on_demand_conformance,
277 "{name}: on-demand counts"
278 );
279 assert_eq!(
280 conformance.conforms, run.conforms,
281 "{name}: conformance bit"
282 );
283 let full_selected: HashSet<_> = run
284 .statements
285 .iter()
286 .flat_map(|statement| {
287 statement.selected_foci.iter().filter_map(|focus| {
288 statement
289 .normalized_statement_id
290 .map(|id| (id, focus.focus.clone()))
291 })
292 })
293 .collect();
294 let full_failures: HashSet<_> = run
295 .statements
296 .iter()
297 .flat_map(|statement| {
298 statement
299 .selected_foci
300 .iter()
301 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
302 .filter_map(|focus| {
303 statement
304 .normalized_statement_id
305 .map(|id| (id, focus.focus.clone()))
306 })
307 })
308 .collect();
309 let discovered_failures: HashSet<_> = last_failures
310 .iter()
311 .map(|pair| (pair.normalized_statement(), pair.focus().clone()))
312 .collect();
313 assert_eq!(
314 full_selected.len(),
315 conformance.selected_pairs,
316 "{name}: selected pairs"
317 );
318 assert_eq!(
319 full_failures.len(),
320 conformance.failed,
321 "{name}: failed pairs"
322 );
323 assert_eq!(
324 full_selected.len() - full_failures.len(),
325 conformance.passed,
326 "{name}: passed pairs"
327 );
328 assert_eq!(
329 full_failures, discovered_failures,
330 "{name}: failed identities"
331 );
332
333 let full_by_authored: HashMap<_, _> = run
334 .statements
335 .iter()
336 .flat_map(|statement| {
337 statement.selected_foci.iter().map(|focus| {
338 (
339 (statement.source_statement_id, focus.focus.clone()),
340 &focus.evidence,
341 )
342 })
343 })
344 .collect();
345 let mut explained_foci = 0;
346 for statement in &last_explanations {
347 for focus in &statement.selected_foci {
348 explained_foci += 1;
349 assert_eq!(
350 focus.status(),
351 shifty_engine::EvaluationStatus::Fail,
352 "{name}: on-demand explanation polarity"
353 );
354 let key = (statement.source_statement_id, focus.focus.clone());
355 assert_eq!(
356 &focus.evidence,
357 *full_by_authored
358 .get(&key)
359 .unwrap_or_else(|| panic!("{name}: unexplained full-run pair {key:?}")),
360 "{name}: full and on-demand evidence differ for {key:?}"
361 );
362 }
363 }
364 assert_eq!(
365 explained_foci, authored_fail_pairs,
366 "{name}: authored explanations"
367 );
368
369 let evidence_nodes = run.walk().len();
370 let evidence_bytes: usize = run
371 .statements
372 .iter()
373 .flat_map(|statement| &statement.selected_foci)
374 .map(|focus| focus.evidence.to_json().expect("evidence serializes").len())
375 .sum();
376 let full_json = run.to_json().expect("evidence run serializes");
377 let full_run_bytes_with_catalog = full_json.len();
378 let mut full_value_no_catalog =
379 serde_json::to_value(&run).expect("evidence run converts to JSON");
380 full_value_no_catalog
381 .as_object_mut()
382 .expect("evidence run is a JSON object")
383 .remove("constraints");
384 let full_run_bytes_no_catalog = serde_json::to_string(&full_value_no_catalog)
385 .expect("catalog-free evidence run serializes")
386 .len();
387
388 // Compare complete runs with the same catalog policy on each side, and
389 // verify both encodings reconstruct the exact typed result.
390 let encoded_with_catalog =
391 shifty_engine::compact(&run, true).expect("compact encoding succeeds");
392 let compact_run_bytes_with_catalog = serde_json::to_string(&encoded_with_catalog)
393 .expect("compact encoding serializes")
394 .len();
395 assert_eq!(
396 shifty_engine::expand(&encoded_with_catalog).expect("compact run expands"),
397 run,
398 "{name}: compact run with catalog is not lossless"
399 );
400 let encoded_no_catalog =
401 shifty_engine::compact(&run, false).expect("compact encoding succeeds");
402 let compact_run_bytes_no_catalog = serde_json::to_string(&encoded_no_catalog)
403 .expect("catalog-free compact encoding serializes")
404 .len();
405 let catalog = serde_json::to_value(&run.constraints).expect("catalog serializes");
406 assert_eq!(
407 shifty_engine::expand_with_catalog(&encoded_no_catalog, catalog)
408 .expect("catalog-free compact run expands"),
409 run,
410 "{name}: compact run without catalog is not lossless"
411 );
412 // Sharing is reported over the evidence alone. The node and term tables
413 // of `encoded` also hold catalog entries, which would understate the
414 // redundancy by adding distinct entries with no evidence occurrences.
415 let sharing = shifty_engine::sharing(&run).expect("sharing measures");
416 let results = shifty_engine::result_sharing(&run);
417
418 // The two passes reach the same judgment nodes by routes that share no
419 // code: `sharing` interns serialized JSON, `result_sharing` walks the
420 // typed evidence. Agreement on both counts is what makes either number
421 // reportable; a mismatch means the node families are misclassified or a
422 // traversal is missing a variant.
423 assert_eq!(
424 sharing.result_occurrences + sharing.support_occurrences,
425 sharing.node_occurrences,
426 "{name}: node families do not partition occurrences"
427 );
428 assert_eq!(
429 sharing.distinct_results + sharing.distinct_support,
430 sharing.distinct_nodes,
431 "{name}: node families do not partition the table"
432 );
433 assert_eq!(
434 sharing.result_occurrences, evidence_nodes,
435 "{name}: interned judgments disagree with the typed walk"
436 );
437 assert_eq!(
438 results.occurrences, evidence_nodes,
439 "{name}: typed sharing pass disagrees with the typed walk"
440 );
441 assert_eq!(
442 results.distinct_payloads, sharing.distinct_results,
443 "{name}: distinct judgments disagree between the two passes"
444 );
445 // Structural equality implies key equality, never the reverse.
446 assert!(
447 results.distinct_keys <= results.distinct_payloads,
448 "{name}: more keys than payloads"
449 );
450 // Requests are the normalized selection the conformance pass counts;
451 // authored records are the source-preserving view over them.
452 assert_eq!(
453 results.normalized_requests, conformance.selected_pairs,
454 "{name}: requests disagree with the conformance selection"
455 );
456 assert_eq!(
457 results.authored_records, authored_pairs,
458 "{name}: authored records disagree with the run"
459 );
460
461 // Serialization is measured separately so its allocation traffic does
462 // not perturb the next validation arm.
463 let mut serialize_samples = Vec::with_capacity(iters);
464 for _ in 0..iters {
465 let start = Instant::now();
466 let json = run.to_json().expect("evidence run serializes");
467 serialize_samples.push(start.elapsed());
468 std::hint::black_box(json.len());
469 }
470
471 // Each ratio compares the corresponding raw samples from one rotated
472 // A/B/C round. The CSV retains those samples for independent checking.
473 let full_ratios: Vec<f64> = conformance_samples
474 .iter()
475 .zip(evidence_samples.iter())
476 .map(|(conformance, evidence)| {
477 evidence.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
478 })
479 .collect();
480 let on_demand_ratios: Vec<f64> = conformance_samples
481 .iter()
482 .zip(on_demand_samples.iter())
483 .map(|(conformance, on_demand)| {
484 on_demand.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
485 })
486 .collect();
487
488 let conformance_ms = summarize_durations(&conformance_samples);
489 let evidence_ms = summarize_durations(&evidence_samples);
490 let discovery_ms = summarize_durations(&discovery_samples);
491 let explanation_ms = summarize_durations(&explanation_samples);
492 let on_demand_ms = summarize_durations(&on_demand_samples);
493 let serialize_ms = summarize_durations(&serialize_samples);
494 let full_ratio = summarize_values(&full_ratios);
495 let on_demand_ratio = summarize_values(&on_demand_ratios);
496 let evaluated_pairs = conformance.selected_pairs.max(1) as f64;
497 let authored_pairs_denominator = authored_pairs.max(1) as f64;
498
499 println!(
500 "{suite},{name},{data_triples},{iters},\
501{c_med:.4},{c_mad:.4},{e_med:.4},{e_mad:.4},{full_ratio:.4},{full_ratio_mad:.4},\
502{find_med:.4},{find_mad:.4},{explain_med:.4},{explain_mad:.4},\
503{ondemand_med:.4},{ondemand_mad:.4},{ondemand_ratio:.4},{ondemand_ratio_mad:.4},\
504{serialize_med:.4},{serialize_mad:.4},\
505{conforms_c},{conforms_e},{conforms_o},\
506{pairs_eval},{pass_pairs},{fail_pairs},{fail_fraction:.6},\
507{authored_pairs},{authored_pass},{authored_fail},\
508{statements},\
509{evidence_nodes},{nodes_per_authored_pair:.3},\
510{evidence_bytes},{bytes_per_authored_pair:.1},\
511{full_with_catalog},{full_no_catalog},{compact_with_catalog},{compact_no_catalog},\
512{node_occurrences},{distinct_nodes},{node_redundancy:.3},\
513{result_occurrences},{distinct_results},{result_redundancy:.3},\
514{support_occurrences},{distinct_support},{support_redundancy:.3},{support_share:.4},\
515{term_occurrences},{distinct_terms},{term_redundancy:.3},\
516{normalized_requests},{duplicate_records},{divergent_duplicates},\
517{canonical_occurrences},{shared_payloads},{shared_payload_fraction:.4},\
518{shared_canonical_occurrences},{shared_occurrence_fraction:.4},\
519{request_reaches},{requests_per_payload:.3},{max_payload_requests},\
520{distinct_keys},{key_redundancy:.3},{multi_occurrence_keys},\
521{divergent_keys},{divergence_fraction:.4},{divergent_occurrences},\
522{distinct_payloads_per_key},{keys_over_payload_cap},{both_polarity_addresses},\
523{conformance_raw},{evidence_raw},{discovery_raw},{explanation_raw},{ondemand_raw}",
524 suite = args.suite,
525 node_occurrences = sharing.node_occurrences,
526 distinct_nodes = sharing.distinct_nodes,
527 node_redundancy = sharing.node_redundancy(),
528 result_occurrences = sharing.result_occurrences,
529 distinct_results = sharing.distinct_results,
530 result_redundancy = sharing.result_redundancy(),
531 support_occurrences = sharing.support_occurrences,
532 distinct_support = sharing.distinct_support,
533 support_redundancy = sharing.support_redundancy(),
534 support_share = sharing.support_share(),
535 term_occurrences = sharing.term_occurrences,
536 distinct_terms = sharing.distinct_terms,
537 term_redundancy = sharing.term_redundancy(),
538 normalized_requests = results.normalized_requests,
539 duplicate_records = results.duplicate_records,
540 divergent_duplicates = results.divergent_duplicates,
541 canonical_occurrences = results.canonical_occurrences,
542 shared_payloads = results.shared_payloads,
543 shared_payload_fraction = results.shared_payload_fraction(),
544 shared_canonical_occurrences = results.shared_canonical_occurrences,
545 shared_occurrence_fraction = results.shared_occurrence_fraction(),
546 request_reaches = results.request_reaches,
547 requests_per_payload = results.requests_per_payload(),
548 max_payload_requests = results.max_payload_requests,
549 distinct_keys = results.distinct_keys,
550 key_redundancy = results.key_redundancy(),
551 multi_occurrence_keys = results.multi_occurrence_keys,
552 divergent_keys = results.divergent_keys,
553 divergence_fraction = results.divergence_fraction(),
554 divergent_occurrences = results.divergent_occurrences,
555 distinct_payloads_per_key = results.distinct_payloads_per_key,
556 keys_over_payload_cap = results.keys_over_payload_cap,
557 both_polarity_addresses = results.both_polarity_addresses,
558 c_med = conformance_ms.median,
559 c_mad = conformance_ms.mad,
560 e_med = evidence_ms.median,
561 e_mad = evidence_ms.mad,
562 full_ratio = full_ratio.median,
563 full_ratio_mad = full_ratio.mad,
564 find_med = discovery_ms.median,
565 find_mad = discovery_ms.mad,
566 explain_med = explanation_ms.median,
567 explain_mad = explanation_ms.mad,
568 ondemand_med = on_demand_ms.median,
569 ondemand_mad = on_demand_ms.mad,
570 ondemand_ratio = on_demand_ratio.median,
571 ondemand_ratio_mad = on_demand_ratio.mad,
572 serialize_med = serialize_ms.median,
573 serialize_mad = serialize_ms.mad,
574 conforms_c = conformance.conforms,
575 conforms_e = run.conforms,
576 conforms_o = on_demand_conformance.conforms,
577 pairs_eval = conformance.selected_pairs,
578 pass_pairs = conformance.passed,
579 fail_pairs = conformance.failed,
580 fail_fraction = conformance.failed as f64 / evaluated_pairs,
581 authored_pass = authored_pairs - authored_fail_pairs,
582 authored_fail = authored_fail_pairs,
583 statements = run.statements.len(),
584 nodes_per_authored_pair = evidence_nodes as f64 / authored_pairs_denominator,
585 bytes_per_authored_pair = evidence_bytes as f64 / authored_pairs_denominator,
586 full_with_catalog = full_run_bytes_with_catalog,
587 full_no_catalog = full_run_bytes_no_catalog,
588 compact_with_catalog = compact_run_bytes_with_catalog,
589 compact_no_catalog = compact_run_bytes_no_catalog,
590 conformance_raw = samples_string(&conformance_samples),
591 evidence_raw = samples_string(&evidence_samples),
592 discovery_raw = samples_string(&discovery_samples),
593 explanation_raw = samples_string(&explanation_samples),
594 ondemand_raw = samples_string(&on_demand_samples),
595 );
596
597 eprintln!(
598 "median: {:.1}ms conformance, {:.1}ms full ({:.2}x), {:.1}ms on-demand ({:.2}x); {} failures / {} pairs; {iters} rounds",
599 conformance_ms.median,
600 evidence_ms.median,
601 full_ratio.median,
602 on_demand_ms.median,
603 on_demand_ratio.median,
604 conformance.failed,
605 conformance.selected_pairs,
606 );
607 }
608}Sourcepub fn source_statements(&self, normalized: usize) -> &[usize]
pub fn source_statements(&self, normalized: usize) -> &[usize]
The authored statements that normalize to normalized, in source order.
Common-subexpression elimination makes this one-to-many: several authored
statements can share one normalized statement, which is why
explain returns one evaluation per authored statement
rather than one per pair. Empty if normalized is out of range.
Sourcepub fn explain_constraint(
&self,
focus: &Term,
constraint: ShapeId,
) -> Option<Evidence>
pub fn explain_constraint( &self, focus: &Term, constraint: ShapeId, ) -> Option<Evidence>
Materialize evidence for one focus against one normalized constraint — any arena id, not just a statement’s top-level shape.
This is the sub-statement counterpart of explain: a
failing conjunction’s Witness carries only its failing children, so a
caller reconstructing per-property coverage uses the run’s
EvaluationProgress to learn which children passed and this method to
materialize the satisfaction evidence the witness elided. Like
explain, no target selection is involved: the pair is
taken as given, and a focus no statement selects still yields
well-defined evidence. Returns None when constraint is not an arena
id of the normalized schema (see schema).
Sourcepub fn constraints(&self) -> ConstraintCatalog
pub fn constraints(&self) -> ConstraintCatalog
The constraint catalogs an EvidenceRun carries.
Fixed for the snapshot, so a caller explaining pairs one at a time takes this once rather than paying for it per pair — on a small 223P model the catalog is 57% of a whole run’s serialized bytes.
Sourcepub fn validate(&self, options: &ValidationOptions) -> EvidenceRun
pub fn validate(&self, options: &ValidationOptions) -> EvidenceRun
Validate the prepared snapshot and return its complete coverage horizon.
Examples found in repository?
27fn main() {
28 let mut shapes_path = PathBuf::new();
29 let mut data_path = PathBuf::new();
30 let mut no_infer = false;
31 let mut top = 15usize;
32 let mut dump_json: Option<PathBuf> = None;
33 let mut argv = std::env::args().skip(1);
34 while let Some(flag) = argv.next() {
35 let mut value = || {
36 argv.next()
37 .unwrap_or_else(|| panic!("{flag} needs a value"))
38 };
39 match flag.as_str() {
40 "--shapes" => shapes_path = PathBuf::from(value()),
41 "--data" => data_path = PathBuf::from(value()),
42 "--no-infer" => no_infer = true,
43 "--top" => top = value().parse().expect("--top must be an integer"),
44 "--dump-json" => dump_json = Some(PathBuf::from(value())),
45 other => panic!("unknown flag {other}"),
46 }
47 }
48 assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49 assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51 let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53 let parsed = shifty_parse::parse_loaded(&shapes);
54 let raw_schema = parsed.schema.clone();
55 let inference_schema = shifty_opt::normalize(&raw_schema);
56
57 let data_bytes = fs::read(&data_path).expect("cannot read --data");
58 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59 let data_graph = if no_infer {
60 data.graph.clone()
61 } else {
62 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63 .expect("inference")
64 .graph
65 };
66
67 let prepared = PreparedEvidenceValidator::with_graphs(
68 &data_graph,
69 &shapes.graph,
70 &raw_schema,
71 ValidationGraphMode::Union,
72 )
73 .expect("stratifiable");
74 let options = ValidationOptions::default();
75 let scan_options = ConformanceOptions::default();
76
77 println!("model: {}", data_path.display());
78 println!("normalized arena nodes: {}", prepared.schema().arena.len());
79 println!(
80 "normalized statements: {}",
81 prepared.schema().statements.len()
82 );
83 println!();
84
85 // ---- arm A: conformance only ------------------------------------------
86 profile::enable();
87 let _ = profile::take_evidence_visits();
88 let start = Instant::now();
89 let conformance = prepared.validate_conformance(&scan_options);
90 let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91 let conformance_visits = profile::take_evidence_visits();
92 let conformance_probes = profile::take_path_probes();
93 let conformance_cache = profile::take().expect("profiling enabled");
94 let conformance_cache = conformance_cache.shape_cache().clone();
95
96 // ---- arm B: dual evidence ----------------------------------------------
97 profile::enable();
98 let start = Instant::now();
99 let run = prepared.validate(&options);
100 let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101 let evidence_visits = profile::take_evidence_visits();
102 let evidence_probes = profile::take_path_probes();
103 let collector = profile::take().expect("profiling enabled");
104 let evidence_cache = collector.shape_cache().clone();
105
106 // ---- arm C: on-demand ---------------------------------------------------
107 // What a caller pays who wants explanations only for what failed: one
108 // conformance pass that also records the failing pairs, then evidence
109 // materialized for those pairs alone.
110 profile::enable();
111 let start = Instant::now();
112 let (_, failures) = prepared.find_failures(&scan_options);
113 let find_ms = start.elapsed().as_secs_f64() * 1e3;
114 let start = Instant::now();
115 let explained: usize = failures
116 .iter()
117 .map(|pair| prepared.explain(pair).len())
118 .sum();
119 let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120 let _ = profile::take();
121 let _ = profile::take_evidence_visits();
122 let _ = profile::take_path_probes();
123
124 let pairs = conformance.selected_pairs.max(1) as f64;
125 let retained = run.walk().len();
126
127 // ---- compact encoding --------------------------------------------------
128 let full_json = run.to_json().expect("evidence run serializes");
129 // Split so the encoding's own cost is separable from the intermediate
130 // `Value` tree it currently has to be handed: compaction is worth doing
131 // inline only if the total stays under the validation it describes.
132 let to_value_start = Instant::now();
133 let run_value = serde_json::to_value(&run).expect("run serializes");
134 let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135 let intern_start = Instant::now();
136 let interned = shifty_engine::compact_value(run_value, true);
137 let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138 let emit_start = Instant::now();
139 let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140 let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141 let compact_ms = to_value_ms + intern_ms + emit_ms;
142 drop(interned);
143 let compact_no_catalog =
144 shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145 println!();
146 println!(
147 "serialized full: {:>10.2} MB ({:.0} bytes/pair)",
148 full_json.len() as f64 / 1e6,
149 full_json.len() as f64 / pairs
150 );
151 println!(
152 "compact (with catalog): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153 = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154 compact_json.len() as f64 / 1e6,
155 compact_json.len() as f64 / pairs,
156 100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157 );
158 println!(
159 "compact (catalog elided): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller)",
160 compact_no_catalog.len() as f64 / 1e6,
161 compact_no_catalog.len() as f64 / pairs,
162 100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163 );
164
165 if let Some(path) = &dump_json {
166 fs::write(path, run.to_json().expect("evidence run serializes"))
167 .expect("cannot write --dump-json");
168 eprintln!("wrote {}", path.display());
169 }
170
171 println!(" conformance evidence");
172 println!(" wall ms {conformance_ms:>11.1} {evidence_ms:>14.1}");
173 println!(
174 " memo lookups {:>11} {:>14}",
175 conformance_cache.hits + conformance_cache.misses,
176 evidence_cache.hits + evidence_cache.misses
177 );
178 println!(
179 " memo hits {:>11} {:>14}",
180 conformance_cache.hits, evidence_cache.hits
181 );
182 println!(
183 " memo misses {:>11} {:>14}",
184 conformance_cache.misses, evidence_cache.misses
185 );
186 println!(
187 " memo entries (peak) {:>11} {:>14}",
188 conformance_cache.peak_entries, evidence_cache.peak_entries
189 );
190 println!(
191 " back-edges {:>11} {:>14}",
192 conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193 );
194 println!(
195 " uncacheable results {:>11} {:>14}",
196 conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197 );
198 println!(" evidence visits {conformance_visits:>11} {evidence_visits:>14}");
199 println!(" path_support probes {conformance_probes:>11} {evidence_probes:>14}");
200 println!();
201 println!(
202 "path probes per visit: {:.1}",
203 evidence_probes as f64 / evidence_visits.max(1) as f64
204 );
205
206 println!("selected pairs: {}", conformance.selected_pairs);
207 println!("retained evidence nodes: {retained}");
208 println!(
209 "visited / retained: {:.1}x",
210 evidence_visits as f64 / retained.max(1) as f64
211 );
212 println!(
213 "visits per pair: {:.0}",
214 evidence_visits as f64 / pairs
215 );
216 println!(
217 "distinct (shape,node) that a cache could collapse to at most: {}",
218 evidence_cache.peak_entries
219 );
220 println!(
221 "visits / distinct pairs: {:.1}x",
222 evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223 );
224 println!();
225 println!(
226 "per-visit cost: {:.2} us",
227 evidence_ms * 1e3 / evidence_visits.max(1) as f64
228 );
229 println!(
230 "per-memo-lookup cost: {:.2} us",
231 conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232 );
233
234 // ---- attribution: which statements own the time ------------------------
235 let (selection, materialization): (Vec<_>, Vec<_>) = collector
236 .shape_records()
237 .iter()
238 .partition(|record| record.label.starts_with("select:"));
239 let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240 let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242 println!();
243 println!(
244 "target selection total: {:.1} ms over {} statement(s)",
245 selection_us as f64 / 1e3,
246 selection.len()
247 );
248 println!(
249 "materialization total: {:.1} ms over {} statement(s)",
250 materialization_us as f64 / 1e3,
251 materialization.len()
252 );
253 println!();
254 println!(
255 "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256 failures.len(),
257 conformance.selected_pairs,
258 explained
259 );
260 println!(
261 " find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms \
262 ({:.2}x the full evidence run, {:.2}x conformance alone)",
263 find_ms + explain_ms,
264 (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265 (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266 );
267
268 let mut ranked = materialization;
269 ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270 println!();
271 println!("top statements by evidence-materialization time:");
272 println!(
273 " {:>8} {:>7} {:>6} {:>10} {:>12} {:>10} shape",
274 "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275 );
276 for record in ranked.iter().take(top) {
277 println!(
278 " {:>8.1} {:>6.1}% {:>6} {:>10.1} {:>12} {:>10.0} {}",
279 record.total_us as f64 / 1e3,
280 100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281 record.invocations,
282 record.total_us as f64 / record.invocations.max(1) as f64,
283 record.visits,
284 record.visits as f64 / record.invocations.max(1) as f64,
285 record.label,
286 );
287 }
288
289 let mut ranked_selection = selection;
290 ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291 println!();
292 println!("top statements by target-selection time:");
293 for record in ranked_selection.iter().take(5) {
294 println!(
295 " {:>8.1} ms {}",
296 record.total_us as f64 / 1e3,
297 record.label
298 );
299 }
300}Sourcepub fn validate_canonical(&self, options: &ValidationOptions) -> EvidenceRun
pub fn validate_canonical(&self, options: &ValidationOptions) -> EvidenceRun
Validate the prepared snapshot and return canonical evidence only.
The optional authored-statement progress view is omitted. This is the evidence interface benchmarked against conformance-only validation.
Examples found in repository?
131fn main() {
132 let args = parse_args();
133
134 eprintln!("loading shapes {}…", args.shapes.display());
135 let shapes_bytes = fs::read(&args.shapes).expect("cannot read --shapes");
136 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
137 let parsed = shifty_parse::parse_loaded(&shapes);
138 let raw_schema = parsed.schema.clone();
139 // Inference runs against the normalized schema, exactly as the CLI does.
140 let inference_schema = shifty_opt::normalize(&raw_schema);
141 drop(shapes_bytes);
142
143 if args.header {
144 println!("{COLUMNS}");
145 }
146
147 for model in models_in(&args.models) {
148 let name = model
149 .file_name()
150 .and_then(|name| name.to_str())
151 .unwrap_or("?")
152 .to_string();
153 eprint!("{name}… ");
154
155 // ---- untimed setup: everything common to all workflows --------------
156 let data_bytes = fs::read(&model).expect("cannot read model");
157 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse model");
158 drop(data_bytes);
159 let data_graph = if args.no_infer {
160 data.graph.clone()
161 } else {
162 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
163 .unwrap_or_else(|error| panic!("{name}: inference failed: {error}"))
164 .graph
165 };
166 let data_triples = data_graph.len();
167
168 let prepared = PreparedEvidenceValidator::with_graphs(
169 &data_graph,
170 &shapes.graph,
171 &raw_schema,
172 ValidationGraphMode::Union,
173 )
174 .unwrap_or_else(|error| panic!("{name}: schema is non-stratifiable: {error}"));
175 let options = ValidationOptions::default();
176 let scan_options = ConformanceOptions::default();
177
178 // ---- timed: validation only -----------------------------------------
179 // One warm-up round, discarded: it faults in the caches all arms share.
180 // Its duration is only consulted when `--budget-ms` is in play.
181 let probe = Instant::now();
182 let _ = prepared.validate_conformance(&scan_options);
183 let _ = prepared.validate_canonical(&options);
184 let (_, warm_failures) = prepared.find_failures(&scan_options);
185 for pair in &warm_failures {
186 std::hint::black_box(prepared.explain_canonical(pair));
187 }
188 let probe_ms = probe.elapsed().as_secs_f64() * 1e3;
189 // Every model gets the same iteration count. `--budget-ms` only caps the
190 // count for a corpus slow enough that a fixed count is impractical; it
191 // is off by default, and the emitted `iters` column records what was used.
192 let iters = if args.budget_ms > 0.0 {
193 ((args.budget_ms / probe_ms.max(f64::MIN_POSITIVE)) as usize)
194 .clamp(args.min_iters.min(args.iters), args.iters)
195 } else {
196 args.iters
197 };
198
199 let mut conformance_samples = Vec::with_capacity(iters);
200 let mut evidence_samples = Vec::with_capacity(iters);
201 let mut discovery_samples = Vec::with_capacity(iters);
202 let mut explanation_samples = Vec::with_capacity(iters);
203 let mut on_demand_samples = Vec::with_capacity(iters);
204 let mut last_run = None;
205 let mut last_conformance = None;
206 let mut last_on_demand_conformance = None;
207 let mut last_failures = Vec::new();
208 let mut last_explanations = Vec::new();
209
210 // Rotate A/B/C so no arm systematically inherits the cache, allocator,
211 // scheduler, or thermal state left by another arm.
212 for iteration in 0..iters {
213 for arm in ARM_ORDERS[iteration % ARM_ORDERS.len()] {
214 match arm {
215 Arm::Conformance => {
216 let start = Instant::now();
217 let conformance_run = prepared.validate_conformance(&scan_options);
218 conformance_samples.push(start.elapsed());
219 std::hint::black_box(conformance_run.selected_pairs);
220 last_conformance = Some(conformance_run);
221 }
222 Arm::FullEvidence => {
223 let start = Instant::now();
224 let run = prepared.validate_canonical(&options);
225 evidence_samples.push(start.elapsed());
226 std::hint::black_box(run.statements.len());
227 last_run = Some(run);
228 }
229 Arm::OnDemand => {
230 let start = Instant::now();
231 let (on_demand_conformance, failures) =
232 prepared.find_failures(&scan_options);
233 let discovery = start.elapsed();
234
235 let start = Instant::now();
236 let explanations: Vec<_> = failures
237 .iter()
238 .flat_map(|pair| prepared.explain_canonical(pair))
239 .collect();
240 let explanation = start.elapsed();
241
242 discovery_samples.push(discovery);
243 explanation_samples.push(explanation);
244 on_demand_samples.push(discovery + explanation);
245 std::hint::black_box(explanations.len());
246 last_on_demand_conformance = Some(on_demand_conformance);
247 last_failures = failures;
248 last_explanations = explanations;
249 }
250 }
251 }
252 }
253
254 // ---- untimed: shape of the evidence produced -------------------------
255 // Reuse the final timed results rather than paying for another run.
256 let conformance = last_conformance.expect("at least one iteration runs");
257 let on_demand_conformance =
258 last_on_demand_conformance.expect("at least one iteration runs");
259 let run = last_run.expect("at least one iteration runs");
260 let authored_pairs: usize = run
261 .statements
262 .iter()
263 .map(|statement| statement.selected_foci.len())
264 .sum();
265 let authored_fail_pairs: usize = run
266 .statements
267 .iter()
268 .flat_map(|statement| &statement.selected_foci)
269 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
270 .count();
271
272 // The three interfaces must describe the same normalized selection and
273 // polarity partition. Compare exact failed identities, not just the
274 // report-wide conformance bit.
275 assert_eq!(
276 conformance, on_demand_conformance,
277 "{name}: on-demand counts"
278 );
279 assert_eq!(
280 conformance.conforms, run.conforms,
281 "{name}: conformance bit"
282 );
283 let full_selected: HashSet<_> = run
284 .statements
285 .iter()
286 .flat_map(|statement| {
287 statement.selected_foci.iter().filter_map(|focus| {
288 statement
289 .normalized_statement_id
290 .map(|id| (id, focus.focus.clone()))
291 })
292 })
293 .collect();
294 let full_failures: HashSet<_> = run
295 .statements
296 .iter()
297 .flat_map(|statement| {
298 statement
299 .selected_foci
300 .iter()
301 .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
302 .filter_map(|focus| {
303 statement
304 .normalized_statement_id
305 .map(|id| (id, focus.focus.clone()))
306 })
307 })
308 .collect();
309 let discovered_failures: HashSet<_> = last_failures
310 .iter()
311 .map(|pair| (pair.normalized_statement(), pair.focus().clone()))
312 .collect();
313 assert_eq!(
314 full_selected.len(),
315 conformance.selected_pairs,
316 "{name}: selected pairs"
317 );
318 assert_eq!(
319 full_failures.len(),
320 conformance.failed,
321 "{name}: failed pairs"
322 );
323 assert_eq!(
324 full_selected.len() - full_failures.len(),
325 conformance.passed,
326 "{name}: passed pairs"
327 );
328 assert_eq!(
329 full_failures, discovered_failures,
330 "{name}: failed identities"
331 );
332
333 let full_by_authored: HashMap<_, _> = run
334 .statements
335 .iter()
336 .flat_map(|statement| {
337 statement.selected_foci.iter().map(|focus| {
338 (
339 (statement.source_statement_id, focus.focus.clone()),
340 &focus.evidence,
341 )
342 })
343 })
344 .collect();
345 let mut explained_foci = 0;
346 for statement in &last_explanations {
347 for focus in &statement.selected_foci {
348 explained_foci += 1;
349 assert_eq!(
350 focus.status(),
351 shifty_engine::EvaluationStatus::Fail,
352 "{name}: on-demand explanation polarity"
353 );
354 let key = (statement.source_statement_id, focus.focus.clone());
355 assert_eq!(
356 &focus.evidence,
357 *full_by_authored
358 .get(&key)
359 .unwrap_or_else(|| panic!("{name}: unexplained full-run pair {key:?}")),
360 "{name}: full and on-demand evidence differ for {key:?}"
361 );
362 }
363 }
364 assert_eq!(
365 explained_foci, authored_fail_pairs,
366 "{name}: authored explanations"
367 );
368
369 let evidence_nodes = run.walk().len();
370 let evidence_bytes: usize = run
371 .statements
372 .iter()
373 .flat_map(|statement| &statement.selected_foci)
374 .map(|focus| focus.evidence.to_json().expect("evidence serializes").len())
375 .sum();
376 let full_json = run.to_json().expect("evidence run serializes");
377 let full_run_bytes_with_catalog = full_json.len();
378 let mut full_value_no_catalog =
379 serde_json::to_value(&run).expect("evidence run converts to JSON");
380 full_value_no_catalog
381 .as_object_mut()
382 .expect("evidence run is a JSON object")
383 .remove("constraints");
384 let full_run_bytes_no_catalog = serde_json::to_string(&full_value_no_catalog)
385 .expect("catalog-free evidence run serializes")
386 .len();
387
388 // Compare complete runs with the same catalog policy on each side, and
389 // verify both encodings reconstruct the exact typed result.
390 let encoded_with_catalog =
391 shifty_engine::compact(&run, true).expect("compact encoding succeeds");
392 let compact_run_bytes_with_catalog = serde_json::to_string(&encoded_with_catalog)
393 .expect("compact encoding serializes")
394 .len();
395 assert_eq!(
396 shifty_engine::expand(&encoded_with_catalog).expect("compact run expands"),
397 run,
398 "{name}: compact run with catalog is not lossless"
399 );
400 let encoded_no_catalog =
401 shifty_engine::compact(&run, false).expect("compact encoding succeeds");
402 let compact_run_bytes_no_catalog = serde_json::to_string(&encoded_no_catalog)
403 .expect("catalog-free compact encoding serializes")
404 .len();
405 let catalog = serde_json::to_value(&run.constraints).expect("catalog serializes");
406 assert_eq!(
407 shifty_engine::expand_with_catalog(&encoded_no_catalog, catalog)
408 .expect("catalog-free compact run expands"),
409 run,
410 "{name}: compact run without catalog is not lossless"
411 );
412 // Sharing is reported over the evidence alone. The node and term tables
413 // of `encoded` also hold catalog entries, which would understate the
414 // redundancy by adding distinct entries with no evidence occurrences.
415 let sharing = shifty_engine::sharing(&run).expect("sharing measures");
416 let results = shifty_engine::result_sharing(&run);
417
418 // The two passes reach the same judgment nodes by routes that share no
419 // code: `sharing` interns serialized JSON, `result_sharing` walks the
420 // typed evidence. Agreement on both counts is what makes either number
421 // reportable; a mismatch means the node families are misclassified or a
422 // traversal is missing a variant.
423 assert_eq!(
424 sharing.result_occurrences + sharing.support_occurrences,
425 sharing.node_occurrences,
426 "{name}: node families do not partition occurrences"
427 );
428 assert_eq!(
429 sharing.distinct_results + sharing.distinct_support,
430 sharing.distinct_nodes,
431 "{name}: node families do not partition the table"
432 );
433 assert_eq!(
434 sharing.result_occurrences, evidence_nodes,
435 "{name}: interned judgments disagree with the typed walk"
436 );
437 assert_eq!(
438 results.occurrences, evidence_nodes,
439 "{name}: typed sharing pass disagrees with the typed walk"
440 );
441 assert_eq!(
442 results.distinct_payloads, sharing.distinct_results,
443 "{name}: distinct judgments disagree between the two passes"
444 );
445 // Structural equality implies key equality, never the reverse.
446 assert!(
447 results.distinct_keys <= results.distinct_payloads,
448 "{name}: more keys than payloads"
449 );
450 // Requests are the normalized selection the conformance pass counts;
451 // authored records are the source-preserving view over them.
452 assert_eq!(
453 results.normalized_requests, conformance.selected_pairs,
454 "{name}: requests disagree with the conformance selection"
455 );
456 assert_eq!(
457 results.authored_records, authored_pairs,
458 "{name}: authored records disagree with the run"
459 );
460
461 // Serialization is measured separately so its allocation traffic does
462 // not perturb the next validation arm.
463 let mut serialize_samples = Vec::with_capacity(iters);
464 for _ in 0..iters {
465 let start = Instant::now();
466 let json = run.to_json().expect("evidence run serializes");
467 serialize_samples.push(start.elapsed());
468 std::hint::black_box(json.len());
469 }
470
471 // Each ratio compares the corresponding raw samples from one rotated
472 // A/B/C round. The CSV retains those samples for independent checking.
473 let full_ratios: Vec<f64> = conformance_samples
474 .iter()
475 .zip(evidence_samples.iter())
476 .map(|(conformance, evidence)| {
477 evidence.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
478 })
479 .collect();
480 let on_demand_ratios: Vec<f64> = conformance_samples
481 .iter()
482 .zip(on_demand_samples.iter())
483 .map(|(conformance, on_demand)| {
484 on_demand.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
485 })
486 .collect();
487
488 let conformance_ms = summarize_durations(&conformance_samples);
489 let evidence_ms = summarize_durations(&evidence_samples);
490 let discovery_ms = summarize_durations(&discovery_samples);
491 let explanation_ms = summarize_durations(&explanation_samples);
492 let on_demand_ms = summarize_durations(&on_demand_samples);
493 let serialize_ms = summarize_durations(&serialize_samples);
494 let full_ratio = summarize_values(&full_ratios);
495 let on_demand_ratio = summarize_values(&on_demand_ratios);
496 let evaluated_pairs = conformance.selected_pairs.max(1) as f64;
497 let authored_pairs_denominator = authored_pairs.max(1) as f64;
498
499 println!(
500 "{suite},{name},{data_triples},{iters},\
501{c_med:.4},{c_mad:.4},{e_med:.4},{e_mad:.4},{full_ratio:.4},{full_ratio_mad:.4},\
502{find_med:.4},{find_mad:.4},{explain_med:.4},{explain_mad:.4},\
503{ondemand_med:.4},{ondemand_mad:.4},{ondemand_ratio:.4},{ondemand_ratio_mad:.4},\
504{serialize_med:.4},{serialize_mad:.4},\
505{conforms_c},{conforms_e},{conforms_o},\
506{pairs_eval},{pass_pairs},{fail_pairs},{fail_fraction:.6},\
507{authored_pairs},{authored_pass},{authored_fail},\
508{statements},\
509{evidence_nodes},{nodes_per_authored_pair:.3},\
510{evidence_bytes},{bytes_per_authored_pair:.1},\
511{full_with_catalog},{full_no_catalog},{compact_with_catalog},{compact_no_catalog},\
512{node_occurrences},{distinct_nodes},{node_redundancy:.3},\
513{result_occurrences},{distinct_results},{result_redundancy:.3},\
514{support_occurrences},{distinct_support},{support_redundancy:.3},{support_share:.4},\
515{term_occurrences},{distinct_terms},{term_redundancy:.3},\
516{normalized_requests},{duplicate_records},{divergent_duplicates},\
517{canonical_occurrences},{shared_payloads},{shared_payload_fraction:.4},\
518{shared_canonical_occurrences},{shared_occurrence_fraction:.4},\
519{request_reaches},{requests_per_payload:.3},{max_payload_requests},\
520{distinct_keys},{key_redundancy:.3},{multi_occurrence_keys},\
521{divergent_keys},{divergence_fraction:.4},{divergent_occurrences},\
522{distinct_payloads_per_key},{keys_over_payload_cap},{both_polarity_addresses},\
523{conformance_raw},{evidence_raw},{discovery_raw},{explanation_raw},{ondemand_raw}",
524 suite = args.suite,
525 node_occurrences = sharing.node_occurrences,
526 distinct_nodes = sharing.distinct_nodes,
527 node_redundancy = sharing.node_redundancy(),
528 result_occurrences = sharing.result_occurrences,
529 distinct_results = sharing.distinct_results,
530 result_redundancy = sharing.result_redundancy(),
531 support_occurrences = sharing.support_occurrences,
532 distinct_support = sharing.distinct_support,
533 support_redundancy = sharing.support_redundancy(),
534 support_share = sharing.support_share(),
535 term_occurrences = sharing.term_occurrences,
536 distinct_terms = sharing.distinct_terms,
537 term_redundancy = sharing.term_redundancy(),
538 normalized_requests = results.normalized_requests,
539 duplicate_records = results.duplicate_records,
540 divergent_duplicates = results.divergent_duplicates,
541 canonical_occurrences = results.canonical_occurrences,
542 shared_payloads = results.shared_payloads,
543 shared_payload_fraction = results.shared_payload_fraction(),
544 shared_canonical_occurrences = results.shared_canonical_occurrences,
545 shared_occurrence_fraction = results.shared_occurrence_fraction(),
546 request_reaches = results.request_reaches,
547 requests_per_payload = results.requests_per_payload(),
548 max_payload_requests = results.max_payload_requests,
549 distinct_keys = results.distinct_keys,
550 key_redundancy = results.key_redundancy(),
551 multi_occurrence_keys = results.multi_occurrence_keys,
552 divergent_keys = results.divergent_keys,
553 divergence_fraction = results.divergence_fraction(),
554 divergent_occurrences = results.divergent_occurrences,
555 distinct_payloads_per_key = results.distinct_payloads_per_key,
556 keys_over_payload_cap = results.keys_over_payload_cap,
557 both_polarity_addresses = results.both_polarity_addresses,
558 c_med = conformance_ms.median,
559 c_mad = conformance_ms.mad,
560 e_med = evidence_ms.median,
561 e_mad = evidence_ms.mad,
562 full_ratio = full_ratio.median,
563 full_ratio_mad = full_ratio.mad,
564 find_med = discovery_ms.median,
565 find_mad = discovery_ms.mad,
566 explain_med = explanation_ms.median,
567 explain_mad = explanation_ms.mad,
568 ondemand_med = on_demand_ms.median,
569 ondemand_mad = on_demand_ms.mad,
570 ondemand_ratio = on_demand_ratio.median,
571 ondemand_ratio_mad = on_demand_ratio.mad,
572 serialize_med = serialize_ms.median,
573 serialize_mad = serialize_ms.mad,
574 conforms_c = conformance.conforms,
575 conforms_e = run.conforms,
576 conforms_o = on_demand_conformance.conforms,
577 pairs_eval = conformance.selected_pairs,
578 pass_pairs = conformance.passed,
579 fail_pairs = conformance.failed,
580 fail_fraction = conformance.failed as f64 / evaluated_pairs,
581 authored_pass = authored_pairs - authored_fail_pairs,
582 authored_fail = authored_fail_pairs,
583 statements = run.statements.len(),
584 nodes_per_authored_pair = evidence_nodes as f64 / authored_pairs_denominator,
585 bytes_per_authored_pair = evidence_bytes as f64 / authored_pairs_denominator,
586 full_with_catalog = full_run_bytes_with_catalog,
587 full_no_catalog = full_run_bytes_no_catalog,
588 compact_with_catalog = compact_run_bytes_with_catalog,
589 compact_no_catalog = compact_run_bytes_no_catalog,
590 conformance_raw = samples_string(&conformance_samples),
591 evidence_raw = samples_string(&evidence_samples),
592 discovery_raw = samples_string(&discovery_samples),
593 explanation_raw = samples_string(&explanation_samples),
594 ondemand_raw = samples_string(&on_demand_samples),
595 );
596
597 eprintln!(
598 "median: {:.1}ms conformance, {:.1}ms full ({:.2}x), {:.1}ms on-demand ({:.2}x); {} failures / {} pairs; {iters} rounds",
599 conformance_ms.median,
600 evidence_ms.median,
601 full_ratio.median,
602 on_demand_ms.median,
603 on_demand_ratio.median,
604 conformance.failed,
605 conformance.selected_pairs,
606 );
607 }
608}Sourcepub fn schema(&self) -> &Schema
pub fn schema(&self) -> &Schema
The normalized schema whose ids appear in returned evidence.
Examples found in repository?
27fn main() {
28 let mut shapes_path = PathBuf::new();
29 let mut data_path = PathBuf::new();
30 let mut no_infer = false;
31 let mut top = 15usize;
32 let mut dump_json: Option<PathBuf> = None;
33 let mut argv = std::env::args().skip(1);
34 while let Some(flag) = argv.next() {
35 let mut value = || {
36 argv.next()
37 .unwrap_or_else(|| panic!("{flag} needs a value"))
38 };
39 match flag.as_str() {
40 "--shapes" => shapes_path = PathBuf::from(value()),
41 "--data" => data_path = PathBuf::from(value()),
42 "--no-infer" => no_infer = true,
43 "--top" => top = value().parse().expect("--top must be an integer"),
44 "--dump-json" => dump_json = Some(PathBuf::from(value())),
45 other => panic!("unknown flag {other}"),
46 }
47 }
48 assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49 assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51 let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52 let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53 let parsed = shifty_parse::parse_loaded(&shapes);
54 let raw_schema = parsed.schema.clone();
55 let inference_schema = shifty_opt::normalize(&raw_schema);
56
57 let data_bytes = fs::read(&data_path).expect("cannot read --data");
58 let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59 let data_graph = if no_infer {
60 data.graph.clone()
61 } else {
62 shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63 .expect("inference")
64 .graph
65 };
66
67 let prepared = PreparedEvidenceValidator::with_graphs(
68 &data_graph,
69 &shapes.graph,
70 &raw_schema,
71 ValidationGraphMode::Union,
72 )
73 .expect("stratifiable");
74 let options = ValidationOptions::default();
75 let scan_options = ConformanceOptions::default();
76
77 println!("model: {}", data_path.display());
78 println!("normalized arena nodes: {}", prepared.schema().arena.len());
79 println!(
80 "normalized statements: {}",
81 prepared.schema().statements.len()
82 );
83 println!();
84
85 // ---- arm A: conformance only ------------------------------------------
86 profile::enable();
87 let _ = profile::take_evidence_visits();
88 let start = Instant::now();
89 let conformance = prepared.validate_conformance(&scan_options);
90 let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91 let conformance_visits = profile::take_evidence_visits();
92 let conformance_probes = profile::take_path_probes();
93 let conformance_cache = profile::take().expect("profiling enabled");
94 let conformance_cache = conformance_cache.shape_cache().clone();
95
96 // ---- arm B: dual evidence ----------------------------------------------
97 profile::enable();
98 let start = Instant::now();
99 let run = prepared.validate(&options);
100 let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101 let evidence_visits = profile::take_evidence_visits();
102 let evidence_probes = profile::take_path_probes();
103 let collector = profile::take().expect("profiling enabled");
104 let evidence_cache = collector.shape_cache().clone();
105
106 // ---- arm C: on-demand ---------------------------------------------------
107 // What a caller pays who wants explanations only for what failed: one
108 // conformance pass that also records the failing pairs, then evidence
109 // materialized for those pairs alone.
110 profile::enable();
111 let start = Instant::now();
112 let (_, failures) = prepared.find_failures(&scan_options);
113 let find_ms = start.elapsed().as_secs_f64() * 1e3;
114 let start = Instant::now();
115 let explained: usize = failures
116 .iter()
117 .map(|pair| prepared.explain(pair).len())
118 .sum();
119 let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120 let _ = profile::take();
121 let _ = profile::take_evidence_visits();
122 let _ = profile::take_path_probes();
123
124 let pairs = conformance.selected_pairs.max(1) as f64;
125 let retained = run.walk().len();
126
127 // ---- compact encoding --------------------------------------------------
128 let full_json = run.to_json().expect("evidence run serializes");
129 // Split so the encoding's own cost is separable from the intermediate
130 // `Value` tree it currently has to be handed: compaction is worth doing
131 // inline only if the total stays under the validation it describes.
132 let to_value_start = Instant::now();
133 let run_value = serde_json::to_value(&run).expect("run serializes");
134 let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135 let intern_start = Instant::now();
136 let interned = shifty_engine::compact_value(run_value, true);
137 let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138 let emit_start = Instant::now();
139 let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140 let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141 let compact_ms = to_value_ms + intern_ms + emit_ms;
142 drop(interned);
143 let compact_no_catalog =
144 shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145 println!();
146 println!(
147 "serialized full: {:>10.2} MB ({:.0} bytes/pair)",
148 full_json.len() as f64 / 1e6,
149 full_json.len() as f64 / pairs
150 );
151 println!(
152 "compact (with catalog): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153 = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154 compact_json.len() as f64 / 1e6,
155 compact_json.len() as f64 / pairs,
156 100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157 );
158 println!(
159 "compact (catalog elided): {:>10.2} MB ({:.0} bytes/pair, {:.1}% smaller)",
160 compact_no_catalog.len() as f64 / 1e6,
161 compact_no_catalog.len() as f64 / pairs,
162 100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163 );
164
165 if let Some(path) = &dump_json {
166 fs::write(path, run.to_json().expect("evidence run serializes"))
167 .expect("cannot write --dump-json");
168 eprintln!("wrote {}", path.display());
169 }
170
171 println!(" conformance evidence");
172 println!(" wall ms {conformance_ms:>11.1} {evidence_ms:>14.1}");
173 println!(
174 " memo lookups {:>11} {:>14}",
175 conformance_cache.hits + conformance_cache.misses,
176 evidence_cache.hits + evidence_cache.misses
177 );
178 println!(
179 " memo hits {:>11} {:>14}",
180 conformance_cache.hits, evidence_cache.hits
181 );
182 println!(
183 " memo misses {:>11} {:>14}",
184 conformance_cache.misses, evidence_cache.misses
185 );
186 println!(
187 " memo entries (peak) {:>11} {:>14}",
188 conformance_cache.peak_entries, evidence_cache.peak_entries
189 );
190 println!(
191 " back-edges {:>11} {:>14}",
192 conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193 );
194 println!(
195 " uncacheable results {:>11} {:>14}",
196 conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197 );
198 println!(" evidence visits {conformance_visits:>11} {evidence_visits:>14}");
199 println!(" path_support probes {conformance_probes:>11} {evidence_probes:>14}");
200 println!();
201 println!(
202 "path probes per visit: {:.1}",
203 evidence_probes as f64 / evidence_visits.max(1) as f64
204 );
205
206 println!("selected pairs: {}", conformance.selected_pairs);
207 println!("retained evidence nodes: {retained}");
208 println!(
209 "visited / retained: {:.1}x",
210 evidence_visits as f64 / retained.max(1) as f64
211 );
212 println!(
213 "visits per pair: {:.0}",
214 evidence_visits as f64 / pairs
215 );
216 println!(
217 "distinct (shape,node) that a cache could collapse to at most: {}",
218 evidence_cache.peak_entries
219 );
220 println!(
221 "visits / distinct pairs: {:.1}x",
222 evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223 );
224 println!();
225 println!(
226 "per-visit cost: {:.2} us",
227 evidence_ms * 1e3 / evidence_visits.max(1) as f64
228 );
229 println!(
230 "per-memo-lookup cost: {:.2} us",
231 conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232 );
233
234 // ---- attribution: which statements own the time ------------------------
235 let (selection, materialization): (Vec<_>, Vec<_>) = collector
236 .shape_records()
237 .iter()
238 .partition(|record| record.label.starts_with("select:"));
239 let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240 let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242 println!();
243 println!(
244 "target selection total: {:.1} ms over {} statement(s)",
245 selection_us as f64 / 1e3,
246 selection.len()
247 );
248 println!(
249 "materialization total: {:.1} ms over {} statement(s)",
250 materialization_us as f64 / 1e3,
251 materialization.len()
252 );
253 println!();
254 println!(
255 "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256 failures.len(),
257 conformance.selected_pairs,
258 explained
259 );
260 println!(
261 " find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms \
262 ({:.2}x the full evidence run, {:.2}x conformance alone)",
263 find_ms + explain_ms,
264 (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265 (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266 );
267
268 let mut ranked = materialization;
269 ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270 println!();
271 println!("top statements by evidence-materialization time:");
272 println!(
273 " {:>8} {:>7} {:>6} {:>10} {:>12} {:>10} shape",
274 "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275 );
276 for record in ranked.iter().take(top) {
277 println!(
278 " {:>8.1} {:>6.1}% {:>6} {:>10.1} {:>12} {:>10.0} {}",
279 record.total_us as f64 / 1e3,
280 100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281 record.invocations,
282 record.total_us as f64 / record.invocations.max(1) as f64,
283 record.visits,
284 record.visits as f64 / record.invocations.max(1) as f64,
285 record.label,
286 );
287 }
288
289 let mut ranked_selection = selection;
290 ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291 println!();
292 println!("top statements by target-selection time:");
293 for record in ranked_selection.iter().take(5) {
294 println!(
295 " {:>8.1} ms {}",
296 record.total_us as f64 / 1e3,
297 record.label
298 );
299 }
300}