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