Skip to main content

PreparedEvidenceValidator

Struct PreparedEvidenceValidator 

Source
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

Source

pub fn new(data: &Graph, schema: &Schema) -> Result<Self, NonStratifiable>

Prepare embedded data/shapes validation over one graph.

Source

pub fn with_context( data: &Graph, context: &Graph, schema: &Schema, ) -> Result<Self, NonStratifiable>

Prepare separate focus and evaluation graphs.

Source

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

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.

Source

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).

Source

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

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

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?
examples/probe_evidence_cost.rs (line 117)
27fn main() {
28    let mut shapes_path = PathBuf::new();
29    let mut data_path = PathBuf::new();
30    let mut no_infer = false;
31    let mut top = 15usize;
32    let mut dump_json: Option<PathBuf> = None;
33    let mut argv = std::env::args().skip(1);
34    while let Some(flag) = argv.next() {
35        let mut value = || {
36            argv.next()
37                .unwrap_or_else(|| panic!("{flag} needs a value"))
38        };
39        match flag.as_str() {
40            "--shapes" => shapes_path = PathBuf::from(value()),
41            "--data" => data_path = PathBuf::from(value()),
42            "--no-infer" => no_infer = true,
43            "--top" => top = value().parse().expect("--top must be an integer"),
44            "--dump-json" => dump_json = Some(PathBuf::from(value())),
45            other => panic!("unknown flag {other}"),
46        }
47    }
48    assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49    assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51    let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52    let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53    let parsed = shifty_parse::parse_loaded(&shapes);
54    let raw_schema = parsed.schema.clone();
55    let inference_schema = shifty_opt::normalize(&raw_schema);
56
57    let data_bytes = fs::read(&data_path).expect("cannot read --data");
58    let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59    let data_graph = if no_infer {
60        data.graph.clone()
61    } else {
62        shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63            .expect("inference")
64            .graph
65    };
66
67    let prepared = PreparedEvidenceValidator::with_graphs(
68        &data_graph,
69        &shapes.graph,
70        &raw_schema,
71        ValidationGraphMode::Union,
72    )
73    .expect("stratifiable");
74    let options = ValidationOptions::default();
75    let scan_options = ConformanceOptions::default();
76
77    println!("model: {}", data_path.display());
78    println!("normalized arena nodes: {}", prepared.schema().arena.len());
79    println!(
80        "normalized statements:  {}",
81        prepared.schema().statements.len()
82    );
83    println!();
84
85    // ---- arm A: conformance only ------------------------------------------
86    profile::enable();
87    let _ = profile::take_evidence_visits();
88    let start = Instant::now();
89    let conformance = prepared.validate_conformance(&scan_options);
90    let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91    let conformance_visits = profile::take_evidence_visits();
92    let conformance_probes = profile::take_path_probes();
93    let conformance_cache = profile::take().expect("profiling enabled");
94    let conformance_cache = conformance_cache.shape_cache().clone();
95
96    // ---- arm B: dual evidence ----------------------------------------------
97    profile::enable();
98    let start = Instant::now();
99    let run = prepared.validate(&options);
100    let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101    let evidence_visits = profile::take_evidence_visits();
102    let evidence_probes = profile::take_path_probes();
103    let collector = profile::take().expect("profiling enabled");
104    let evidence_cache = collector.shape_cache().clone();
105
106    // ---- arm C: on-demand ---------------------------------------------------
107    // What a caller pays who wants explanations only for what failed: one
108    // conformance pass that also records the failing pairs, then evidence
109    // materialized for those pairs alone.
110    profile::enable();
111    let start = Instant::now();
112    let (_, failures) = prepared.find_failures(&scan_options);
113    let find_ms = start.elapsed().as_secs_f64() * 1e3;
114    let start = Instant::now();
115    let explained: usize = failures
116        .iter()
117        .map(|pair| prepared.explain(pair).len())
118        .sum();
119    let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120    let _ = profile::take();
121    let _ = profile::take_evidence_visits();
122    let _ = profile::take_path_probes();
123
124    let pairs = conformance.selected_pairs.max(1) as f64;
125    let retained = run.walk().len();
126
127    // ---- compact encoding --------------------------------------------------
128    let full_json = run.to_json().expect("evidence run serializes");
129    // Split so the encoding's own cost is separable from the intermediate
130    // `Value` tree it currently has to be handed: compaction is worth doing
131    // inline only if the total stays under the validation it describes.
132    let to_value_start = Instant::now();
133    let run_value = serde_json::to_value(&run).expect("run serializes");
134    let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135    let intern_start = Instant::now();
136    let interned = shifty_engine::compact_value(run_value, true);
137    let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138    let emit_start = Instant::now();
139    let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140    let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141    let compact_ms = to_value_ms + intern_ms + emit_ms;
142    drop(interned);
143    let compact_no_catalog =
144        shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145    println!();
146    println!(
147        "serialized full:           {:>10.2} MB  ({:.0} bytes/pair)",
148        full_json.len() as f64 / 1e6,
149        full_json.len() as f64 / pairs
150    );
151    println!(
152        "compact (with catalog):    {:>10.2} MB  ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153         = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154        compact_json.len() as f64 / 1e6,
155        compact_json.len() as f64 / pairs,
156        100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157    );
158    println!(
159        "compact (catalog elided):  {:>10.2} MB  ({:.0} bytes/pair, {:.1}% smaller)",
160        compact_no_catalog.len() as f64 / 1e6,
161        compact_no_catalog.len() as f64 / pairs,
162        100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163    );
164
165    if let Some(path) = &dump_json {
166        fs::write(path, run.to_json().expect("evidence run serializes"))
167            .expect("cannot write --dump-json");
168        eprintln!("wrote {}", path.display());
169    }
170
171    println!("                        conformance        evidence");
172    println!("  wall ms               {conformance_ms:>11.1}  {evidence_ms:>14.1}");
173    println!(
174        "  memo lookups          {:>11}  {:>14}",
175        conformance_cache.hits + conformance_cache.misses,
176        evidence_cache.hits + evidence_cache.misses
177    );
178    println!(
179        "  memo hits             {:>11}  {:>14}",
180        conformance_cache.hits, evidence_cache.hits
181    );
182    println!(
183        "  memo misses           {:>11}  {:>14}",
184        conformance_cache.misses, evidence_cache.misses
185    );
186    println!(
187        "  memo entries (peak)   {:>11}  {:>14}",
188        conformance_cache.peak_entries, evidence_cache.peak_entries
189    );
190    println!(
191        "  back-edges            {:>11}  {:>14}",
192        conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193    );
194    println!(
195        "  uncacheable results   {:>11}  {:>14}",
196        conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197    );
198    println!("  evidence visits       {conformance_visits:>11}  {evidence_visits:>14}");
199    println!("  path_support probes   {conformance_probes:>11}  {evidence_probes:>14}");
200    println!();
201    println!(
202        "path probes per visit:     {:.1}",
203        evidence_probes as f64 / evidence_visits.max(1) as f64
204    );
205
206    println!("selected pairs:            {}", conformance.selected_pairs);
207    println!("retained evidence nodes:   {retained}");
208    println!(
209        "visited / retained:        {:.1}x",
210        evidence_visits as f64 / retained.max(1) as f64
211    );
212    println!(
213        "visits per pair:           {:.0}",
214        evidence_visits as f64 / pairs
215    );
216    println!(
217        "distinct (shape,node) that a cache could collapse to at most: {}",
218        evidence_cache.peak_entries
219    );
220    println!(
221        "visits / distinct pairs:   {:.1}x",
222        evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223    );
224    println!();
225    println!(
226        "per-visit cost:            {:.2} us",
227        evidence_ms * 1e3 / evidence_visits.max(1) as f64
228    );
229    println!(
230        "per-memo-lookup cost:      {:.2} us",
231        conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232    );
233
234    // ---- attribution: which statements own the time ------------------------
235    let (selection, materialization): (Vec<_>, Vec<_>) = collector
236        .shape_records()
237        .iter()
238        .partition(|record| record.label.starts_with("select:"));
239    let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240    let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242    println!();
243    println!(
244        "target selection total:    {:.1} ms over {} statement(s)",
245        selection_us as f64 / 1e3,
246        selection.len()
247    );
248    println!(
249        "materialization total:     {:.1} ms over {} statement(s)",
250        materialization_us as f64 / 1e3,
251        materialization.len()
252    );
253    println!();
254    println!(
255        "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256        failures.len(),
257        conformance.selected_pairs,
258        explained
259    );
260    println!(
261        "  find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms  \
262         ({:.2}x the full evidence run, {:.2}x conformance alone)",
263        find_ms + explain_ms,
264        (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265        (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266    );
267
268    let mut ranked = materialization;
269    ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270    println!();
271    println!("top statements by evidence-materialization time:");
272    println!(
273        "  {:>8}  {:>7}  {:>6}  {:>10}  {:>12}  {:>10}  shape",
274        "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275    );
276    for record in ranked.iter().take(top) {
277        println!(
278            "  {:>8.1}  {:>6.1}%  {:>6}  {:>10.1}  {:>12}  {:>10.0}  {}",
279            record.total_us as f64 / 1e3,
280            100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281            record.invocations,
282            record.total_us as f64 / record.invocations.max(1) as f64,
283            record.visits,
284            record.visits as f64 / record.invocations.max(1) as f64,
285            record.label,
286        );
287    }
288
289    let mut ranked_selection = selection;
290    ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291    println!();
292    println!("top statements by target-selection time:");
293    for record in ranked_selection.iter().take(5) {
294        println!(
295            "  {:>8.1} ms  {}",
296            record.total_us as f64 / 1e3,
297            record.label
298        );
299    }
300}
Source

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?
examples/bench_evidence.rs (line 186)
131fn main() {
132    let args = parse_args();
133
134    eprintln!("loading shapes {}…", args.shapes.display());
135    let shapes_bytes = fs::read(&args.shapes).expect("cannot read --shapes");
136    let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
137    let parsed = shifty_parse::parse_loaded(&shapes);
138    let raw_schema = parsed.schema.clone();
139    // Inference runs against the normalized schema, exactly as the CLI does.
140    let inference_schema = shifty_opt::normalize(&raw_schema);
141    drop(shapes_bytes);
142
143    if args.header {
144        println!("{COLUMNS}");
145    }
146
147    for model in models_in(&args.models) {
148        let name = model
149            .file_name()
150            .and_then(|name| name.to_str())
151            .unwrap_or("?")
152            .to_string();
153        eprint!("{name}… ");
154
155        // ---- untimed setup: everything common to all workflows --------------
156        let data_bytes = fs::read(&model).expect("cannot read model");
157        let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse model");
158        drop(data_bytes);
159        let data_graph = if args.no_infer {
160            data.graph.clone()
161        } else {
162            shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
163                .unwrap_or_else(|error| panic!("{name}: inference failed: {error}"))
164                .graph
165        };
166        let data_triples = data_graph.len();
167
168        let prepared = PreparedEvidenceValidator::with_graphs(
169            &data_graph,
170            &shapes.graph,
171            &raw_schema,
172            ValidationGraphMode::Union,
173        )
174        .unwrap_or_else(|error| panic!("{name}: schema is non-stratifiable: {error}"));
175        let options = ValidationOptions::default();
176        let scan_options = ConformanceOptions::default();
177
178        // ---- timed: validation only -----------------------------------------
179        // One warm-up round, discarded: it faults in the caches all arms share.
180        // Its duration is only consulted when `--budget-ms` is in play.
181        let probe = Instant::now();
182        let _ = prepared.validate_conformance(&scan_options);
183        let _ = prepared.validate_canonical(&options);
184        let (_, warm_failures) = prepared.find_failures(&scan_options);
185        for pair in &warm_failures {
186            std::hint::black_box(prepared.explain_canonical(pair));
187        }
188        let probe_ms = probe.elapsed().as_secs_f64() * 1e3;
189        // Every model gets the same iteration count. `--budget-ms` only caps the
190        // count for a corpus slow enough that a fixed count is impractical; it
191        // is off by default, and the emitted `iters` column records what was used.
192        let iters = if args.budget_ms > 0.0 {
193            ((args.budget_ms / probe_ms.max(f64::MIN_POSITIVE)) as usize)
194                .clamp(args.min_iters.min(args.iters), args.iters)
195        } else {
196            args.iters
197        };
198
199        let mut conformance_samples = Vec::with_capacity(iters);
200        let mut evidence_samples = Vec::with_capacity(iters);
201        let mut discovery_samples = Vec::with_capacity(iters);
202        let mut explanation_samples = Vec::with_capacity(iters);
203        let mut on_demand_samples = Vec::with_capacity(iters);
204        let mut last_run = None;
205        let mut last_conformance = None;
206        let mut last_on_demand_conformance = None;
207        let mut last_failures = Vec::new();
208        let mut last_explanations = Vec::new();
209
210        // Rotate A/B/C so no arm systematically inherits the cache, allocator,
211        // scheduler, or thermal state left by another arm.
212        for iteration in 0..iters {
213            for arm in ARM_ORDERS[iteration % ARM_ORDERS.len()] {
214                match arm {
215                    Arm::Conformance => {
216                        let start = Instant::now();
217                        let conformance_run = prepared.validate_conformance(&scan_options);
218                        conformance_samples.push(start.elapsed());
219                        std::hint::black_box(conformance_run.selected_pairs);
220                        last_conformance = Some(conformance_run);
221                    }
222                    Arm::FullEvidence => {
223                        let start = Instant::now();
224                        let run = prepared.validate_canonical(&options);
225                        evidence_samples.push(start.elapsed());
226                        std::hint::black_box(run.statements.len());
227                        last_run = Some(run);
228                    }
229                    Arm::OnDemand => {
230                        let start = Instant::now();
231                        let (on_demand_conformance, failures) =
232                            prepared.find_failures(&scan_options);
233                        let discovery = start.elapsed();
234
235                        let start = Instant::now();
236                        let explanations: Vec<_> = failures
237                            .iter()
238                            .flat_map(|pair| prepared.explain_canonical(pair))
239                            .collect();
240                        let explanation = start.elapsed();
241
242                        discovery_samples.push(discovery);
243                        explanation_samples.push(explanation);
244                        on_demand_samples.push(discovery + explanation);
245                        std::hint::black_box(explanations.len());
246                        last_on_demand_conformance = Some(on_demand_conformance);
247                        last_failures = failures;
248                        last_explanations = explanations;
249                    }
250                }
251            }
252        }
253
254        // ---- untimed: shape of the evidence produced -------------------------
255        // Reuse the final timed results rather than paying for another run.
256        let conformance = last_conformance.expect("at least one iteration runs");
257        let on_demand_conformance =
258            last_on_demand_conformance.expect("at least one iteration runs");
259        let run = last_run.expect("at least one iteration runs");
260        let authored_pairs: usize = run
261            .statements
262            .iter()
263            .map(|statement| statement.selected_foci.len())
264            .sum();
265        let authored_fail_pairs: usize = run
266            .statements
267            .iter()
268            .flat_map(|statement| &statement.selected_foci)
269            .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
270            .count();
271
272        // The three interfaces must describe the same normalized selection and
273        // polarity partition. Compare exact failed identities, not just the
274        // report-wide conformance bit.
275        assert_eq!(
276            conformance, on_demand_conformance,
277            "{name}: on-demand counts"
278        );
279        assert_eq!(
280            conformance.conforms, run.conforms,
281            "{name}: conformance bit"
282        );
283        let full_selected: HashSet<_> = run
284            .statements
285            .iter()
286            .flat_map(|statement| {
287                statement.selected_foci.iter().filter_map(|focus| {
288                    statement
289                        .normalized_statement_id
290                        .map(|id| (id, focus.focus.clone()))
291                })
292            })
293            .collect();
294        let full_failures: HashSet<_> = run
295            .statements
296            .iter()
297            .flat_map(|statement| {
298                statement
299                    .selected_foci
300                    .iter()
301                    .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
302                    .filter_map(|focus| {
303                        statement
304                            .normalized_statement_id
305                            .map(|id| (id, focus.focus.clone()))
306                    })
307            })
308            .collect();
309        let discovered_failures: HashSet<_> = last_failures
310            .iter()
311            .map(|pair| (pair.normalized_statement(), pair.focus().clone()))
312            .collect();
313        assert_eq!(
314            full_selected.len(),
315            conformance.selected_pairs,
316            "{name}: selected pairs"
317        );
318        assert_eq!(
319            full_failures.len(),
320            conformance.failed,
321            "{name}: failed pairs"
322        );
323        assert_eq!(
324            full_selected.len() - full_failures.len(),
325            conformance.passed,
326            "{name}: passed pairs"
327        );
328        assert_eq!(
329            full_failures, discovered_failures,
330            "{name}: failed identities"
331        );
332
333        let full_by_authored: HashMap<_, _> = run
334            .statements
335            .iter()
336            .flat_map(|statement| {
337                statement.selected_foci.iter().map(|focus| {
338                    (
339                        (statement.source_statement_id, focus.focus.clone()),
340                        &focus.evidence,
341                    )
342                })
343            })
344            .collect();
345        let mut explained_foci = 0;
346        for statement in &last_explanations {
347            for focus in &statement.selected_foci {
348                explained_foci += 1;
349                assert_eq!(
350                    focus.status(),
351                    shifty_engine::EvaluationStatus::Fail,
352                    "{name}: on-demand explanation polarity"
353                );
354                let key = (statement.source_statement_id, focus.focus.clone());
355                assert_eq!(
356                    &focus.evidence,
357                    *full_by_authored
358                        .get(&key)
359                        .unwrap_or_else(|| panic!("{name}: unexplained full-run pair {key:?}")),
360                    "{name}: full and on-demand evidence differ for {key:?}"
361                );
362            }
363        }
364        assert_eq!(
365            explained_foci, authored_fail_pairs,
366            "{name}: authored explanations"
367        );
368
369        let evidence_nodes = run.walk().len();
370        let evidence_bytes: usize = run
371            .statements
372            .iter()
373            .flat_map(|statement| &statement.selected_foci)
374            .map(|focus| focus.evidence.to_json().expect("evidence serializes").len())
375            .sum();
376        let full_json = run.to_json().expect("evidence run serializes");
377        let full_run_bytes_with_catalog = full_json.len();
378        let mut full_value_no_catalog =
379            serde_json::to_value(&run).expect("evidence run converts to JSON");
380        full_value_no_catalog
381            .as_object_mut()
382            .expect("evidence run is a JSON object")
383            .remove("constraints");
384        let full_run_bytes_no_catalog = serde_json::to_string(&full_value_no_catalog)
385            .expect("catalog-free evidence run serializes")
386            .len();
387
388        // Compare complete runs with the same catalog policy on each side, and
389        // verify both encodings reconstruct the exact typed result.
390        let encoded_with_catalog =
391            shifty_engine::compact(&run, true).expect("compact encoding succeeds");
392        let compact_run_bytes_with_catalog = serde_json::to_string(&encoded_with_catalog)
393            .expect("compact encoding serializes")
394            .len();
395        assert_eq!(
396            shifty_engine::expand(&encoded_with_catalog).expect("compact run expands"),
397            run,
398            "{name}: compact run with catalog is not lossless"
399        );
400        let encoded_no_catalog =
401            shifty_engine::compact(&run, false).expect("compact encoding succeeds");
402        let compact_run_bytes_no_catalog = serde_json::to_string(&encoded_no_catalog)
403            .expect("catalog-free compact encoding serializes")
404            .len();
405        let catalog = serde_json::to_value(&run.constraints).expect("catalog serializes");
406        assert_eq!(
407            shifty_engine::expand_with_catalog(&encoded_no_catalog, catalog)
408                .expect("catalog-free compact run expands"),
409            run,
410            "{name}: compact run without catalog is not lossless"
411        );
412        // Sharing is reported over the evidence alone. The node and term tables
413        // of `encoded` also hold catalog entries, which would understate the
414        // redundancy by adding distinct entries with no evidence occurrences.
415        let sharing = shifty_engine::sharing(&run).expect("sharing measures");
416        let results = shifty_engine::result_sharing(&run);
417
418        // The two passes reach the same judgment nodes by routes that share no
419        // code: `sharing` interns serialized JSON, `result_sharing` walks the
420        // typed evidence. Agreement on both counts is what makes either number
421        // reportable; a mismatch means the node families are misclassified or a
422        // traversal is missing a variant.
423        assert_eq!(
424            sharing.result_occurrences + sharing.support_occurrences,
425            sharing.node_occurrences,
426            "{name}: node families do not partition occurrences"
427        );
428        assert_eq!(
429            sharing.distinct_results + sharing.distinct_support,
430            sharing.distinct_nodes,
431            "{name}: node families do not partition the table"
432        );
433        assert_eq!(
434            sharing.result_occurrences, evidence_nodes,
435            "{name}: interned judgments disagree with the typed walk"
436        );
437        assert_eq!(
438            results.occurrences, evidence_nodes,
439            "{name}: typed sharing pass disagrees with the typed walk"
440        );
441        assert_eq!(
442            results.distinct_payloads, sharing.distinct_results,
443            "{name}: distinct judgments disagree between the two passes"
444        );
445        // Structural equality implies key equality, never the reverse.
446        assert!(
447            results.distinct_keys <= results.distinct_payloads,
448            "{name}: more keys than payloads"
449        );
450        // Requests are the normalized selection the conformance pass counts;
451        // authored records are the source-preserving view over them.
452        assert_eq!(
453            results.normalized_requests, conformance.selected_pairs,
454            "{name}: requests disagree with the conformance selection"
455        );
456        assert_eq!(
457            results.authored_records, authored_pairs,
458            "{name}: authored records disagree with the run"
459        );
460
461        // Serialization is measured separately so its allocation traffic does
462        // not perturb the next validation arm.
463        let mut serialize_samples = Vec::with_capacity(iters);
464        for _ in 0..iters {
465            let start = Instant::now();
466            let json = run.to_json().expect("evidence run serializes");
467            serialize_samples.push(start.elapsed());
468            std::hint::black_box(json.len());
469        }
470
471        // Each ratio compares the corresponding raw samples from one rotated
472        // A/B/C round. The CSV retains those samples for independent checking.
473        let full_ratios: Vec<f64> = conformance_samples
474            .iter()
475            .zip(evidence_samples.iter())
476            .map(|(conformance, evidence)| {
477                evidence.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
478            })
479            .collect();
480        let on_demand_ratios: Vec<f64> = conformance_samples
481            .iter()
482            .zip(on_demand_samples.iter())
483            .map(|(conformance, on_demand)| {
484                on_demand.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
485            })
486            .collect();
487
488        let conformance_ms = summarize_durations(&conformance_samples);
489        let evidence_ms = summarize_durations(&evidence_samples);
490        let discovery_ms = summarize_durations(&discovery_samples);
491        let explanation_ms = summarize_durations(&explanation_samples);
492        let on_demand_ms = summarize_durations(&on_demand_samples);
493        let serialize_ms = summarize_durations(&serialize_samples);
494        let full_ratio = summarize_values(&full_ratios);
495        let on_demand_ratio = summarize_values(&on_demand_ratios);
496        let evaluated_pairs = conformance.selected_pairs.max(1) as f64;
497        let authored_pairs_denominator = authored_pairs.max(1) as f64;
498
499        println!(
500            "{suite},{name},{data_triples},{iters},\
501{c_med:.4},{c_mad:.4},{e_med:.4},{e_mad:.4},{full_ratio:.4},{full_ratio_mad:.4},\
502{find_med:.4},{find_mad:.4},{explain_med:.4},{explain_mad:.4},\
503{ondemand_med:.4},{ondemand_mad:.4},{ondemand_ratio:.4},{ondemand_ratio_mad:.4},\
504{serialize_med:.4},{serialize_mad:.4},\
505{conforms_c},{conforms_e},{conforms_o},\
506{pairs_eval},{pass_pairs},{fail_pairs},{fail_fraction:.6},\
507{authored_pairs},{authored_pass},{authored_fail},\
508{statements},\
509{evidence_nodes},{nodes_per_authored_pair:.3},\
510{evidence_bytes},{bytes_per_authored_pair:.1},\
511{full_with_catalog},{full_no_catalog},{compact_with_catalog},{compact_no_catalog},\
512{node_occurrences},{distinct_nodes},{node_redundancy:.3},\
513{result_occurrences},{distinct_results},{result_redundancy:.3},\
514{support_occurrences},{distinct_support},{support_redundancy:.3},{support_share:.4},\
515{term_occurrences},{distinct_terms},{term_redundancy:.3},\
516{normalized_requests},{duplicate_records},{divergent_duplicates},\
517{canonical_occurrences},{shared_payloads},{shared_payload_fraction:.4},\
518{shared_canonical_occurrences},{shared_occurrence_fraction:.4},\
519{request_reaches},{requests_per_payload:.3},{max_payload_requests},\
520{distinct_keys},{key_redundancy:.3},{multi_occurrence_keys},\
521{divergent_keys},{divergence_fraction:.4},{divergent_occurrences},\
522{distinct_payloads_per_key},{keys_over_payload_cap},{both_polarity_addresses},\
523{conformance_raw},{evidence_raw},{discovery_raw},{explanation_raw},{ondemand_raw}",
524            suite = args.suite,
525            node_occurrences = sharing.node_occurrences,
526            distinct_nodes = sharing.distinct_nodes,
527            node_redundancy = sharing.node_redundancy(),
528            result_occurrences = sharing.result_occurrences,
529            distinct_results = sharing.distinct_results,
530            result_redundancy = sharing.result_redundancy(),
531            support_occurrences = sharing.support_occurrences,
532            distinct_support = sharing.distinct_support,
533            support_redundancy = sharing.support_redundancy(),
534            support_share = sharing.support_share(),
535            term_occurrences = sharing.term_occurrences,
536            distinct_terms = sharing.distinct_terms,
537            term_redundancy = sharing.term_redundancy(),
538            normalized_requests = results.normalized_requests,
539            duplicate_records = results.duplicate_records,
540            divergent_duplicates = results.divergent_duplicates,
541            canonical_occurrences = results.canonical_occurrences,
542            shared_payloads = results.shared_payloads,
543            shared_payload_fraction = results.shared_payload_fraction(),
544            shared_canonical_occurrences = results.shared_canonical_occurrences,
545            shared_occurrence_fraction = results.shared_occurrence_fraction(),
546            request_reaches = results.request_reaches,
547            requests_per_payload = results.requests_per_payload(),
548            max_payload_requests = results.max_payload_requests,
549            distinct_keys = results.distinct_keys,
550            key_redundancy = results.key_redundancy(),
551            multi_occurrence_keys = results.multi_occurrence_keys,
552            divergent_keys = results.divergent_keys,
553            divergence_fraction = results.divergence_fraction(),
554            divergent_occurrences = results.divergent_occurrences,
555            distinct_payloads_per_key = results.distinct_payloads_per_key,
556            keys_over_payload_cap = results.keys_over_payload_cap,
557            both_polarity_addresses = results.both_polarity_addresses,
558            c_med = conformance_ms.median,
559            c_mad = conformance_ms.mad,
560            e_med = evidence_ms.median,
561            e_mad = evidence_ms.mad,
562            full_ratio = full_ratio.median,
563            full_ratio_mad = full_ratio.mad,
564            find_med = discovery_ms.median,
565            find_mad = discovery_ms.mad,
566            explain_med = explanation_ms.median,
567            explain_mad = explanation_ms.mad,
568            ondemand_med = on_demand_ms.median,
569            ondemand_mad = on_demand_ms.mad,
570            ondemand_ratio = on_demand_ratio.median,
571            ondemand_ratio_mad = on_demand_ratio.mad,
572            serialize_med = serialize_ms.median,
573            serialize_mad = serialize_ms.mad,
574            conforms_c = conformance.conforms,
575            conforms_e = run.conforms,
576            conforms_o = on_demand_conformance.conforms,
577            pairs_eval = conformance.selected_pairs,
578            pass_pairs = conformance.passed,
579            fail_pairs = conformance.failed,
580            fail_fraction = conformance.failed as f64 / evaluated_pairs,
581            authored_pass = authored_pairs - authored_fail_pairs,
582            authored_fail = authored_fail_pairs,
583            statements = run.statements.len(),
584            nodes_per_authored_pair = evidence_nodes as f64 / authored_pairs_denominator,
585            bytes_per_authored_pair = evidence_bytes as f64 / authored_pairs_denominator,
586            full_with_catalog = full_run_bytes_with_catalog,
587            full_no_catalog = full_run_bytes_no_catalog,
588            compact_with_catalog = compact_run_bytes_with_catalog,
589            compact_no_catalog = compact_run_bytes_no_catalog,
590            conformance_raw = samples_string(&conformance_samples),
591            evidence_raw = samples_string(&evidence_samples),
592            discovery_raw = samples_string(&discovery_samples),
593            explanation_raw = samples_string(&explanation_samples),
594            ondemand_raw = samples_string(&on_demand_samples),
595        );
596
597        eprintln!(
598            "median: {:.1}ms conformance, {:.1}ms full ({:.2}x), {:.1}ms on-demand ({:.2}x); {} failures / {} pairs; {iters} rounds",
599            conformance_ms.median,
600            evidence_ms.median,
601            full_ratio.median,
602            on_demand_ms.median,
603            on_demand_ratio.median,
604            conformance.failed,
605            conformance.selected_pairs,
606        );
607    }
608}
Source

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.

Source

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).

Source

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.

Source

pub fn validate(&self, options: &ValidationOptions) -> EvidenceRun

Validate the prepared snapshot and return its complete coverage horizon.

Examples found in repository?
examples/probe_evidence_cost.rs (line 99)
27fn main() {
28    let mut shapes_path = PathBuf::new();
29    let mut data_path = PathBuf::new();
30    let mut no_infer = false;
31    let mut top = 15usize;
32    let mut dump_json: Option<PathBuf> = None;
33    let mut argv = std::env::args().skip(1);
34    while let Some(flag) = argv.next() {
35        let mut value = || {
36            argv.next()
37                .unwrap_or_else(|| panic!("{flag} needs a value"))
38        };
39        match flag.as_str() {
40            "--shapes" => shapes_path = PathBuf::from(value()),
41            "--data" => data_path = PathBuf::from(value()),
42            "--no-infer" => no_infer = true,
43            "--top" => top = value().parse().expect("--top must be an integer"),
44            "--dump-json" => dump_json = Some(PathBuf::from(value())),
45            other => panic!("unknown flag {other}"),
46        }
47    }
48    assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49    assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51    let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52    let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53    let parsed = shifty_parse::parse_loaded(&shapes);
54    let raw_schema = parsed.schema.clone();
55    let inference_schema = shifty_opt::normalize(&raw_schema);
56
57    let data_bytes = fs::read(&data_path).expect("cannot read --data");
58    let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59    let data_graph = if no_infer {
60        data.graph.clone()
61    } else {
62        shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63            .expect("inference")
64            .graph
65    };
66
67    let prepared = PreparedEvidenceValidator::with_graphs(
68        &data_graph,
69        &shapes.graph,
70        &raw_schema,
71        ValidationGraphMode::Union,
72    )
73    .expect("stratifiable");
74    let options = ValidationOptions::default();
75    let scan_options = ConformanceOptions::default();
76
77    println!("model: {}", data_path.display());
78    println!("normalized arena nodes: {}", prepared.schema().arena.len());
79    println!(
80        "normalized statements:  {}",
81        prepared.schema().statements.len()
82    );
83    println!();
84
85    // ---- arm A: conformance only ------------------------------------------
86    profile::enable();
87    let _ = profile::take_evidence_visits();
88    let start = Instant::now();
89    let conformance = prepared.validate_conformance(&scan_options);
90    let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91    let conformance_visits = profile::take_evidence_visits();
92    let conformance_probes = profile::take_path_probes();
93    let conformance_cache = profile::take().expect("profiling enabled");
94    let conformance_cache = conformance_cache.shape_cache().clone();
95
96    // ---- arm B: dual evidence ----------------------------------------------
97    profile::enable();
98    let start = Instant::now();
99    let run = prepared.validate(&options);
100    let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101    let evidence_visits = profile::take_evidence_visits();
102    let evidence_probes = profile::take_path_probes();
103    let collector = profile::take().expect("profiling enabled");
104    let evidence_cache = collector.shape_cache().clone();
105
106    // ---- arm C: on-demand ---------------------------------------------------
107    // What a caller pays who wants explanations only for what failed: one
108    // conformance pass that also records the failing pairs, then evidence
109    // materialized for those pairs alone.
110    profile::enable();
111    let start = Instant::now();
112    let (_, failures) = prepared.find_failures(&scan_options);
113    let find_ms = start.elapsed().as_secs_f64() * 1e3;
114    let start = Instant::now();
115    let explained: usize = failures
116        .iter()
117        .map(|pair| prepared.explain(pair).len())
118        .sum();
119    let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120    let _ = profile::take();
121    let _ = profile::take_evidence_visits();
122    let _ = profile::take_path_probes();
123
124    let pairs = conformance.selected_pairs.max(1) as f64;
125    let retained = run.walk().len();
126
127    // ---- compact encoding --------------------------------------------------
128    let full_json = run.to_json().expect("evidence run serializes");
129    // Split so the encoding's own cost is separable from the intermediate
130    // `Value` tree it currently has to be handed: compaction is worth doing
131    // inline only if the total stays under the validation it describes.
132    let to_value_start = Instant::now();
133    let run_value = serde_json::to_value(&run).expect("run serializes");
134    let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135    let intern_start = Instant::now();
136    let interned = shifty_engine::compact_value(run_value, true);
137    let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138    let emit_start = Instant::now();
139    let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140    let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141    let compact_ms = to_value_ms + intern_ms + emit_ms;
142    drop(interned);
143    let compact_no_catalog =
144        shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145    println!();
146    println!(
147        "serialized full:           {:>10.2} MB  ({:.0} bytes/pair)",
148        full_json.len() as f64 / 1e6,
149        full_json.len() as f64 / pairs
150    );
151    println!(
152        "compact (with catalog):    {:>10.2} MB  ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153         = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154        compact_json.len() as f64 / 1e6,
155        compact_json.len() as f64 / pairs,
156        100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157    );
158    println!(
159        "compact (catalog elided):  {:>10.2} MB  ({:.0} bytes/pair, {:.1}% smaller)",
160        compact_no_catalog.len() as f64 / 1e6,
161        compact_no_catalog.len() as f64 / pairs,
162        100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163    );
164
165    if let Some(path) = &dump_json {
166        fs::write(path, run.to_json().expect("evidence run serializes"))
167            .expect("cannot write --dump-json");
168        eprintln!("wrote {}", path.display());
169    }
170
171    println!("                        conformance        evidence");
172    println!("  wall ms               {conformance_ms:>11.1}  {evidence_ms:>14.1}");
173    println!(
174        "  memo lookups          {:>11}  {:>14}",
175        conformance_cache.hits + conformance_cache.misses,
176        evidence_cache.hits + evidence_cache.misses
177    );
178    println!(
179        "  memo hits             {:>11}  {:>14}",
180        conformance_cache.hits, evidence_cache.hits
181    );
182    println!(
183        "  memo misses           {:>11}  {:>14}",
184        conformance_cache.misses, evidence_cache.misses
185    );
186    println!(
187        "  memo entries (peak)   {:>11}  {:>14}",
188        conformance_cache.peak_entries, evidence_cache.peak_entries
189    );
190    println!(
191        "  back-edges            {:>11}  {:>14}",
192        conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193    );
194    println!(
195        "  uncacheable results   {:>11}  {:>14}",
196        conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197    );
198    println!("  evidence visits       {conformance_visits:>11}  {evidence_visits:>14}");
199    println!("  path_support probes   {conformance_probes:>11}  {evidence_probes:>14}");
200    println!();
201    println!(
202        "path probes per visit:     {:.1}",
203        evidence_probes as f64 / evidence_visits.max(1) as f64
204    );
205
206    println!("selected pairs:            {}", conformance.selected_pairs);
207    println!("retained evidence nodes:   {retained}");
208    println!(
209        "visited / retained:        {:.1}x",
210        evidence_visits as f64 / retained.max(1) as f64
211    );
212    println!(
213        "visits per pair:           {:.0}",
214        evidence_visits as f64 / pairs
215    );
216    println!(
217        "distinct (shape,node) that a cache could collapse to at most: {}",
218        evidence_cache.peak_entries
219    );
220    println!(
221        "visits / distinct pairs:   {:.1}x",
222        evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223    );
224    println!();
225    println!(
226        "per-visit cost:            {:.2} us",
227        evidence_ms * 1e3 / evidence_visits.max(1) as f64
228    );
229    println!(
230        "per-memo-lookup cost:      {:.2} us",
231        conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232    );
233
234    // ---- attribution: which statements own the time ------------------------
235    let (selection, materialization): (Vec<_>, Vec<_>) = collector
236        .shape_records()
237        .iter()
238        .partition(|record| record.label.starts_with("select:"));
239    let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240    let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242    println!();
243    println!(
244        "target selection total:    {:.1} ms over {} statement(s)",
245        selection_us as f64 / 1e3,
246        selection.len()
247    );
248    println!(
249        "materialization total:     {:.1} ms over {} statement(s)",
250        materialization_us as f64 / 1e3,
251        materialization.len()
252    );
253    println!();
254    println!(
255        "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256        failures.len(),
257        conformance.selected_pairs,
258        explained
259    );
260    println!(
261        "  find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms  \
262         ({:.2}x the full evidence run, {:.2}x conformance alone)",
263        find_ms + explain_ms,
264        (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265        (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266    );
267
268    let mut ranked = materialization;
269    ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270    println!();
271    println!("top statements by evidence-materialization time:");
272    println!(
273        "  {:>8}  {:>7}  {:>6}  {:>10}  {:>12}  {:>10}  shape",
274        "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275    );
276    for record in ranked.iter().take(top) {
277        println!(
278            "  {:>8.1}  {:>6.1}%  {:>6}  {:>10.1}  {:>12}  {:>10.0}  {}",
279            record.total_us as f64 / 1e3,
280            100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281            record.invocations,
282            record.total_us as f64 / record.invocations.max(1) as f64,
283            record.visits,
284            record.visits as f64 / record.invocations.max(1) as f64,
285            record.label,
286        );
287    }
288
289    let mut ranked_selection = selection;
290    ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291    println!();
292    println!("top statements by target-selection time:");
293    for record in ranked_selection.iter().take(5) {
294        println!(
295            "  {:>8.1} ms  {}",
296            record.total_us as f64 / 1e3,
297            record.label
298        );
299    }
300}
Source

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?
examples/bench_evidence.rs (line 183)
131fn main() {
132    let args = parse_args();
133
134    eprintln!("loading shapes {}…", args.shapes.display());
135    let shapes_bytes = fs::read(&args.shapes).expect("cannot read --shapes");
136    let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
137    let parsed = shifty_parse::parse_loaded(&shapes);
138    let raw_schema = parsed.schema.clone();
139    // Inference runs against the normalized schema, exactly as the CLI does.
140    let inference_schema = shifty_opt::normalize(&raw_schema);
141    drop(shapes_bytes);
142
143    if args.header {
144        println!("{COLUMNS}");
145    }
146
147    for model in models_in(&args.models) {
148        let name = model
149            .file_name()
150            .and_then(|name| name.to_str())
151            .unwrap_or("?")
152            .to_string();
153        eprint!("{name}… ");
154
155        // ---- untimed setup: everything common to all workflows --------------
156        let data_bytes = fs::read(&model).expect("cannot read model");
157        let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse model");
158        drop(data_bytes);
159        let data_graph = if args.no_infer {
160            data.graph.clone()
161        } else {
162            shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
163                .unwrap_or_else(|error| panic!("{name}: inference failed: {error}"))
164                .graph
165        };
166        let data_triples = data_graph.len();
167
168        let prepared = PreparedEvidenceValidator::with_graphs(
169            &data_graph,
170            &shapes.graph,
171            &raw_schema,
172            ValidationGraphMode::Union,
173        )
174        .unwrap_or_else(|error| panic!("{name}: schema is non-stratifiable: {error}"));
175        let options = ValidationOptions::default();
176        let scan_options = ConformanceOptions::default();
177
178        // ---- timed: validation only -----------------------------------------
179        // One warm-up round, discarded: it faults in the caches all arms share.
180        // Its duration is only consulted when `--budget-ms` is in play.
181        let probe = Instant::now();
182        let _ = prepared.validate_conformance(&scan_options);
183        let _ = prepared.validate_canonical(&options);
184        let (_, warm_failures) = prepared.find_failures(&scan_options);
185        for pair in &warm_failures {
186            std::hint::black_box(prepared.explain_canonical(pair));
187        }
188        let probe_ms = probe.elapsed().as_secs_f64() * 1e3;
189        // Every model gets the same iteration count. `--budget-ms` only caps the
190        // count for a corpus slow enough that a fixed count is impractical; it
191        // is off by default, and the emitted `iters` column records what was used.
192        let iters = if args.budget_ms > 0.0 {
193            ((args.budget_ms / probe_ms.max(f64::MIN_POSITIVE)) as usize)
194                .clamp(args.min_iters.min(args.iters), args.iters)
195        } else {
196            args.iters
197        };
198
199        let mut conformance_samples = Vec::with_capacity(iters);
200        let mut evidence_samples = Vec::with_capacity(iters);
201        let mut discovery_samples = Vec::with_capacity(iters);
202        let mut explanation_samples = Vec::with_capacity(iters);
203        let mut on_demand_samples = Vec::with_capacity(iters);
204        let mut last_run = None;
205        let mut last_conformance = None;
206        let mut last_on_demand_conformance = None;
207        let mut last_failures = Vec::new();
208        let mut last_explanations = Vec::new();
209
210        // Rotate A/B/C so no arm systematically inherits the cache, allocator,
211        // scheduler, or thermal state left by another arm.
212        for iteration in 0..iters {
213            for arm in ARM_ORDERS[iteration % ARM_ORDERS.len()] {
214                match arm {
215                    Arm::Conformance => {
216                        let start = Instant::now();
217                        let conformance_run = prepared.validate_conformance(&scan_options);
218                        conformance_samples.push(start.elapsed());
219                        std::hint::black_box(conformance_run.selected_pairs);
220                        last_conformance = Some(conformance_run);
221                    }
222                    Arm::FullEvidence => {
223                        let start = Instant::now();
224                        let run = prepared.validate_canonical(&options);
225                        evidence_samples.push(start.elapsed());
226                        std::hint::black_box(run.statements.len());
227                        last_run = Some(run);
228                    }
229                    Arm::OnDemand => {
230                        let start = Instant::now();
231                        let (on_demand_conformance, failures) =
232                            prepared.find_failures(&scan_options);
233                        let discovery = start.elapsed();
234
235                        let start = Instant::now();
236                        let explanations: Vec<_> = failures
237                            .iter()
238                            .flat_map(|pair| prepared.explain_canonical(pair))
239                            .collect();
240                        let explanation = start.elapsed();
241
242                        discovery_samples.push(discovery);
243                        explanation_samples.push(explanation);
244                        on_demand_samples.push(discovery + explanation);
245                        std::hint::black_box(explanations.len());
246                        last_on_demand_conformance = Some(on_demand_conformance);
247                        last_failures = failures;
248                        last_explanations = explanations;
249                    }
250                }
251            }
252        }
253
254        // ---- untimed: shape of the evidence produced -------------------------
255        // Reuse the final timed results rather than paying for another run.
256        let conformance = last_conformance.expect("at least one iteration runs");
257        let on_demand_conformance =
258            last_on_demand_conformance.expect("at least one iteration runs");
259        let run = last_run.expect("at least one iteration runs");
260        let authored_pairs: usize = run
261            .statements
262            .iter()
263            .map(|statement| statement.selected_foci.len())
264            .sum();
265        let authored_fail_pairs: usize = run
266            .statements
267            .iter()
268            .flat_map(|statement| &statement.selected_foci)
269            .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
270            .count();
271
272        // The three interfaces must describe the same normalized selection and
273        // polarity partition. Compare exact failed identities, not just the
274        // report-wide conformance bit.
275        assert_eq!(
276            conformance, on_demand_conformance,
277            "{name}: on-demand counts"
278        );
279        assert_eq!(
280            conformance.conforms, run.conforms,
281            "{name}: conformance bit"
282        );
283        let full_selected: HashSet<_> = run
284            .statements
285            .iter()
286            .flat_map(|statement| {
287                statement.selected_foci.iter().filter_map(|focus| {
288                    statement
289                        .normalized_statement_id
290                        .map(|id| (id, focus.focus.clone()))
291                })
292            })
293            .collect();
294        let full_failures: HashSet<_> = run
295            .statements
296            .iter()
297            .flat_map(|statement| {
298                statement
299                    .selected_foci
300                    .iter()
301                    .filter(|focus| focus.status() == shifty_engine::EvaluationStatus::Fail)
302                    .filter_map(|focus| {
303                        statement
304                            .normalized_statement_id
305                            .map(|id| (id, focus.focus.clone()))
306                    })
307            })
308            .collect();
309        let discovered_failures: HashSet<_> = last_failures
310            .iter()
311            .map(|pair| (pair.normalized_statement(), pair.focus().clone()))
312            .collect();
313        assert_eq!(
314            full_selected.len(),
315            conformance.selected_pairs,
316            "{name}: selected pairs"
317        );
318        assert_eq!(
319            full_failures.len(),
320            conformance.failed,
321            "{name}: failed pairs"
322        );
323        assert_eq!(
324            full_selected.len() - full_failures.len(),
325            conformance.passed,
326            "{name}: passed pairs"
327        );
328        assert_eq!(
329            full_failures, discovered_failures,
330            "{name}: failed identities"
331        );
332
333        let full_by_authored: HashMap<_, _> = run
334            .statements
335            .iter()
336            .flat_map(|statement| {
337                statement.selected_foci.iter().map(|focus| {
338                    (
339                        (statement.source_statement_id, focus.focus.clone()),
340                        &focus.evidence,
341                    )
342                })
343            })
344            .collect();
345        let mut explained_foci = 0;
346        for statement in &last_explanations {
347            for focus in &statement.selected_foci {
348                explained_foci += 1;
349                assert_eq!(
350                    focus.status(),
351                    shifty_engine::EvaluationStatus::Fail,
352                    "{name}: on-demand explanation polarity"
353                );
354                let key = (statement.source_statement_id, focus.focus.clone());
355                assert_eq!(
356                    &focus.evidence,
357                    *full_by_authored
358                        .get(&key)
359                        .unwrap_or_else(|| panic!("{name}: unexplained full-run pair {key:?}")),
360                    "{name}: full and on-demand evidence differ for {key:?}"
361                );
362            }
363        }
364        assert_eq!(
365            explained_foci, authored_fail_pairs,
366            "{name}: authored explanations"
367        );
368
369        let evidence_nodes = run.walk().len();
370        let evidence_bytes: usize = run
371            .statements
372            .iter()
373            .flat_map(|statement| &statement.selected_foci)
374            .map(|focus| focus.evidence.to_json().expect("evidence serializes").len())
375            .sum();
376        let full_json = run.to_json().expect("evidence run serializes");
377        let full_run_bytes_with_catalog = full_json.len();
378        let mut full_value_no_catalog =
379            serde_json::to_value(&run).expect("evidence run converts to JSON");
380        full_value_no_catalog
381            .as_object_mut()
382            .expect("evidence run is a JSON object")
383            .remove("constraints");
384        let full_run_bytes_no_catalog = serde_json::to_string(&full_value_no_catalog)
385            .expect("catalog-free evidence run serializes")
386            .len();
387
388        // Compare complete runs with the same catalog policy on each side, and
389        // verify both encodings reconstruct the exact typed result.
390        let encoded_with_catalog =
391            shifty_engine::compact(&run, true).expect("compact encoding succeeds");
392        let compact_run_bytes_with_catalog = serde_json::to_string(&encoded_with_catalog)
393            .expect("compact encoding serializes")
394            .len();
395        assert_eq!(
396            shifty_engine::expand(&encoded_with_catalog).expect("compact run expands"),
397            run,
398            "{name}: compact run with catalog is not lossless"
399        );
400        let encoded_no_catalog =
401            shifty_engine::compact(&run, false).expect("compact encoding succeeds");
402        let compact_run_bytes_no_catalog = serde_json::to_string(&encoded_no_catalog)
403            .expect("catalog-free compact encoding serializes")
404            .len();
405        let catalog = serde_json::to_value(&run.constraints).expect("catalog serializes");
406        assert_eq!(
407            shifty_engine::expand_with_catalog(&encoded_no_catalog, catalog)
408                .expect("catalog-free compact run expands"),
409            run,
410            "{name}: compact run without catalog is not lossless"
411        );
412        // Sharing is reported over the evidence alone. The node and term tables
413        // of `encoded` also hold catalog entries, which would understate the
414        // redundancy by adding distinct entries with no evidence occurrences.
415        let sharing = shifty_engine::sharing(&run).expect("sharing measures");
416        let results = shifty_engine::result_sharing(&run);
417
418        // The two passes reach the same judgment nodes by routes that share no
419        // code: `sharing` interns serialized JSON, `result_sharing` walks the
420        // typed evidence. Agreement on both counts is what makes either number
421        // reportable; a mismatch means the node families are misclassified or a
422        // traversal is missing a variant.
423        assert_eq!(
424            sharing.result_occurrences + sharing.support_occurrences,
425            sharing.node_occurrences,
426            "{name}: node families do not partition occurrences"
427        );
428        assert_eq!(
429            sharing.distinct_results + sharing.distinct_support,
430            sharing.distinct_nodes,
431            "{name}: node families do not partition the table"
432        );
433        assert_eq!(
434            sharing.result_occurrences, evidence_nodes,
435            "{name}: interned judgments disagree with the typed walk"
436        );
437        assert_eq!(
438            results.occurrences, evidence_nodes,
439            "{name}: typed sharing pass disagrees with the typed walk"
440        );
441        assert_eq!(
442            results.distinct_payloads, sharing.distinct_results,
443            "{name}: distinct judgments disagree between the two passes"
444        );
445        // Structural equality implies key equality, never the reverse.
446        assert!(
447            results.distinct_keys <= results.distinct_payloads,
448            "{name}: more keys than payloads"
449        );
450        // Requests are the normalized selection the conformance pass counts;
451        // authored records are the source-preserving view over them.
452        assert_eq!(
453            results.normalized_requests, conformance.selected_pairs,
454            "{name}: requests disagree with the conformance selection"
455        );
456        assert_eq!(
457            results.authored_records, authored_pairs,
458            "{name}: authored records disagree with the run"
459        );
460
461        // Serialization is measured separately so its allocation traffic does
462        // not perturb the next validation arm.
463        let mut serialize_samples = Vec::with_capacity(iters);
464        for _ in 0..iters {
465            let start = Instant::now();
466            let json = run.to_json().expect("evidence run serializes");
467            serialize_samples.push(start.elapsed());
468            std::hint::black_box(json.len());
469        }
470
471        // Each ratio compares the corresponding raw samples from one rotated
472        // A/B/C round. The CSV retains those samples for independent checking.
473        let full_ratios: Vec<f64> = conformance_samples
474            .iter()
475            .zip(evidence_samples.iter())
476            .map(|(conformance, evidence)| {
477                evidence.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
478            })
479            .collect();
480        let on_demand_ratios: Vec<f64> = conformance_samples
481            .iter()
482            .zip(on_demand_samples.iter())
483            .map(|(conformance, on_demand)| {
484                on_demand.as_secs_f64() / conformance.as_secs_f64().max(f64::MIN_POSITIVE)
485            })
486            .collect();
487
488        let conformance_ms = summarize_durations(&conformance_samples);
489        let evidence_ms = summarize_durations(&evidence_samples);
490        let discovery_ms = summarize_durations(&discovery_samples);
491        let explanation_ms = summarize_durations(&explanation_samples);
492        let on_demand_ms = summarize_durations(&on_demand_samples);
493        let serialize_ms = summarize_durations(&serialize_samples);
494        let full_ratio = summarize_values(&full_ratios);
495        let on_demand_ratio = summarize_values(&on_demand_ratios);
496        let evaluated_pairs = conformance.selected_pairs.max(1) as f64;
497        let authored_pairs_denominator = authored_pairs.max(1) as f64;
498
499        println!(
500            "{suite},{name},{data_triples},{iters},\
501{c_med:.4},{c_mad:.4},{e_med:.4},{e_mad:.4},{full_ratio:.4},{full_ratio_mad:.4},\
502{find_med:.4},{find_mad:.4},{explain_med:.4},{explain_mad:.4},\
503{ondemand_med:.4},{ondemand_mad:.4},{ondemand_ratio:.4},{ondemand_ratio_mad:.4},\
504{serialize_med:.4},{serialize_mad:.4},\
505{conforms_c},{conforms_e},{conforms_o},\
506{pairs_eval},{pass_pairs},{fail_pairs},{fail_fraction:.6},\
507{authored_pairs},{authored_pass},{authored_fail},\
508{statements},\
509{evidence_nodes},{nodes_per_authored_pair:.3},\
510{evidence_bytes},{bytes_per_authored_pair:.1},\
511{full_with_catalog},{full_no_catalog},{compact_with_catalog},{compact_no_catalog},\
512{node_occurrences},{distinct_nodes},{node_redundancy:.3},\
513{result_occurrences},{distinct_results},{result_redundancy:.3},\
514{support_occurrences},{distinct_support},{support_redundancy:.3},{support_share:.4},\
515{term_occurrences},{distinct_terms},{term_redundancy:.3},\
516{normalized_requests},{duplicate_records},{divergent_duplicates},\
517{canonical_occurrences},{shared_payloads},{shared_payload_fraction:.4},\
518{shared_canonical_occurrences},{shared_occurrence_fraction:.4},\
519{request_reaches},{requests_per_payload:.3},{max_payload_requests},\
520{distinct_keys},{key_redundancy:.3},{multi_occurrence_keys},\
521{divergent_keys},{divergence_fraction:.4},{divergent_occurrences},\
522{distinct_payloads_per_key},{keys_over_payload_cap},{both_polarity_addresses},\
523{conformance_raw},{evidence_raw},{discovery_raw},{explanation_raw},{ondemand_raw}",
524            suite = args.suite,
525            node_occurrences = sharing.node_occurrences,
526            distinct_nodes = sharing.distinct_nodes,
527            node_redundancy = sharing.node_redundancy(),
528            result_occurrences = sharing.result_occurrences,
529            distinct_results = sharing.distinct_results,
530            result_redundancy = sharing.result_redundancy(),
531            support_occurrences = sharing.support_occurrences,
532            distinct_support = sharing.distinct_support,
533            support_redundancy = sharing.support_redundancy(),
534            support_share = sharing.support_share(),
535            term_occurrences = sharing.term_occurrences,
536            distinct_terms = sharing.distinct_terms,
537            term_redundancy = sharing.term_redundancy(),
538            normalized_requests = results.normalized_requests,
539            duplicate_records = results.duplicate_records,
540            divergent_duplicates = results.divergent_duplicates,
541            canonical_occurrences = results.canonical_occurrences,
542            shared_payloads = results.shared_payloads,
543            shared_payload_fraction = results.shared_payload_fraction(),
544            shared_canonical_occurrences = results.shared_canonical_occurrences,
545            shared_occurrence_fraction = results.shared_occurrence_fraction(),
546            request_reaches = results.request_reaches,
547            requests_per_payload = results.requests_per_payload(),
548            max_payload_requests = results.max_payload_requests,
549            distinct_keys = results.distinct_keys,
550            key_redundancy = results.key_redundancy(),
551            multi_occurrence_keys = results.multi_occurrence_keys,
552            divergent_keys = results.divergent_keys,
553            divergence_fraction = results.divergence_fraction(),
554            divergent_occurrences = results.divergent_occurrences,
555            distinct_payloads_per_key = results.distinct_payloads_per_key,
556            keys_over_payload_cap = results.keys_over_payload_cap,
557            both_polarity_addresses = results.both_polarity_addresses,
558            c_med = conformance_ms.median,
559            c_mad = conformance_ms.mad,
560            e_med = evidence_ms.median,
561            e_mad = evidence_ms.mad,
562            full_ratio = full_ratio.median,
563            full_ratio_mad = full_ratio.mad,
564            find_med = discovery_ms.median,
565            find_mad = discovery_ms.mad,
566            explain_med = explanation_ms.median,
567            explain_mad = explanation_ms.mad,
568            ondemand_med = on_demand_ms.median,
569            ondemand_mad = on_demand_ms.mad,
570            ondemand_ratio = on_demand_ratio.median,
571            ondemand_ratio_mad = on_demand_ratio.mad,
572            serialize_med = serialize_ms.median,
573            serialize_mad = serialize_ms.mad,
574            conforms_c = conformance.conforms,
575            conforms_e = run.conforms,
576            conforms_o = on_demand_conformance.conforms,
577            pairs_eval = conformance.selected_pairs,
578            pass_pairs = conformance.passed,
579            fail_pairs = conformance.failed,
580            fail_fraction = conformance.failed as f64 / evaluated_pairs,
581            authored_pass = authored_pairs - authored_fail_pairs,
582            authored_fail = authored_fail_pairs,
583            statements = run.statements.len(),
584            nodes_per_authored_pair = evidence_nodes as f64 / authored_pairs_denominator,
585            bytes_per_authored_pair = evidence_bytes as f64 / authored_pairs_denominator,
586            full_with_catalog = full_run_bytes_with_catalog,
587            full_no_catalog = full_run_bytes_no_catalog,
588            compact_with_catalog = compact_run_bytes_with_catalog,
589            compact_no_catalog = compact_run_bytes_no_catalog,
590            conformance_raw = samples_string(&conformance_samples),
591            evidence_raw = samples_string(&evidence_samples),
592            discovery_raw = samples_string(&discovery_samples),
593            explanation_raw = samples_string(&explanation_samples),
594            ondemand_raw = samples_string(&on_demand_samples),
595        );
596
597        eprintln!(
598            "median: {:.1}ms conformance, {:.1}ms full ({:.2}x), {:.1}ms on-demand ({:.2}x); {} failures / {} pairs; {iters} rounds",
599            conformance_ms.median,
600            evidence_ms.median,
601            full_ratio.median,
602            on_demand_ms.median,
603            on_demand_ratio.median,
604            conformance.failed,
605            conformance.selected_pairs,
606        );
607    }
608}
Source

pub fn schema(&self) -> &Schema

The normalized schema whose ids appear in returned evidence.

Examples found in repository?
examples/probe_evidence_cost.rs (line 78)
27fn main() {
28    let mut shapes_path = PathBuf::new();
29    let mut data_path = PathBuf::new();
30    let mut no_infer = false;
31    let mut top = 15usize;
32    let mut dump_json: Option<PathBuf> = None;
33    let mut argv = std::env::args().skip(1);
34    while let Some(flag) = argv.next() {
35        let mut value = || {
36            argv.next()
37                .unwrap_or_else(|| panic!("{flag} needs a value"))
38        };
39        match flag.as_str() {
40            "--shapes" => shapes_path = PathBuf::from(value()),
41            "--data" => data_path = PathBuf::from(value()),
42            "--no-infer" => no_infer = true,
43            "--top" => top = value().parse().expect("--top must be an integer"),
44            "--dump-json" => dump_json = Some(PathBuf::from(value())),
45            other => panic!("unknown flag {other}"),
46        }
47    }
48    assert!(!shapes_path.as_os_str().is_empty(), "--shapes is required");
49    assert!(!data_path.as_os_str().is_empty(), "--data is required");
50
51    let shapes_bytes = fs::read(&shapes_path).expect("cannot read --shapes");
52    let shapes = shifty_parse::load_turtle(&shapes_bytes, None).expect("cannot parse --shapes");
53    let parsed = shifty_parse::parse_loaded(&shapes);
54    let raw_schema = parsed.schema.clone();
55    let inference_schema = shifty_opt::normalize(&raw_schema);
56
57    let data_bytes = fs::read(&data_path).expect("cannot read --data");
58    let data = shifty_parse::load_turtle(&data_bytes, None).expect("cannot parse --data");
59    let data_graph = if no_infer {
60        data.graph.clone()
61    } else {
62        shifty_engine::infer_graphs(&data.graph, &shapes.graph, &inference_schema)
63            .expect("inference")
64            .graph
65    };
66
67    let prepared = PreparedEvidenceValidator::with_graphs(
68        &data_graph,
69        &shapes.graph,
70        &raw_schema,
71        ValidationGraphMode::Union,
72    )
73    .expect("stratifiable");
74    let options = ValidationOptions::default();
75    let scan_options = ConformanceOptions::default();
76
77    println!("model: {}", data_path.display());
78    println!("normalized arena nodes: {}", prepared.schema().arena.len());
79    println!(
80        "normalized statements:  {}",
81        prepared.schema().statements.len()
82    );
83    println!();
84
85    // ---- arm A: conformance only ------------------------------------------
86    profile::enable();
87    let _ = profile::take_evidence_visits();
88    let start = Instant::now();
89    let conformance = prepared.validate_conformance(&scan_options);
90    let conformance_ms = start.elapsed().as_secs_f64() * 1e3;
91    let conformance_visits = profile::take_evidence_visits();
92    let conformance_probes = profile::take_path_probes();
93    let conformance_cache = profile::take().expect("profiling enabled");
94    let conformance_cache = conformance_cache.shape_cache().clone();
95
96    // ---- arm B: dual evidence ----------------------------------------------
97    profile::enable();
98    let start = Instant::now();
99    let run = prepared.validate(&options);
100    let evidence_ms = start.elapsed().as_secs_f64() * 1e3;
101    let evidence_visits = profile::take_evidence_visits();
102    let evidence_probes = profile::take_path_probes();
103    let collector = profile::take().expect("profiling enabled");
104    let evidence_cache = collector.shape_cache().clone();
105
106    // ---- arm C: on-demand ---------------------------------------------------
107    // What a caller pays who wants explanations only for what failed: one
108    // conformance pass that also records the failing pairs, then evidence
109    // materialized for those pairs alone.
110    profile::enable();
111    let start = Instant::now();
112    let (_, failures) = prepared.find_failures(&scan_options);
113    let find_ms = start.elapsed().as_secs_f64() * 1e3;
114    let start = Instant::now();
115    let explained: usize = failures
116        .iter()
117        .map(|pair| prepared.explain(pair).len())
118        .sum();
119    let explain_ms = start.elapsed().as_secs_f64() * 1e3;
120    let _ = profile::take();
121    let _ = profile::take_evidence_visits();
122    let _ = profile::take_path_probes();
123
124    let pairs = conformance.selected_pairs.max(1) as f64;
125    let retained = run.walk().len();
126
127    // ---- compact encoding --------------------------------------------------
128    let full_json = run.to_json().expect("evidence run serializes");
129    // Split so the encoding's own cost is separable from the intermediate
130    // `Value` tree it currently has to be handed: compaction is worth doing
131    // inline only if the total stays under the validation it describes.
132    let to_value_start = Instant::now();
133    let run_value = serde_json::to_value(&run).expect("run serializes");
134    let to_value_ms = to_value_start.elapsed().as_secs_f64() * 1e3;
135    let intern_start = Instant::now();
136    let interned = shifty_engine::compact_value(run_value, true);
137    let intern_ms = intern_start.elapsed().as_secs_f64() * 1e3;
138    let emit_start = Instant::now();
139    let compact_json = serde_json::to_string(&interned).expect("compact encoding serializes");
140    let emit_ms = emit_start.elapsed().as_secs_f64() * 1e3;
141    let compact_ms = to_value_ms + intern_ms + emit_ms;
142    drop(interned);
143    let compact_no_catalog =
144        shifty_engine::to_compact_json(&run, false).expect("compact encoding succeeds");
145    println!();
146    println!(
147        "serialized full:           {:>10.2} MB  ({:.0} bytes/pair)",
148        full_json.len() as f64 / 1e6,
149        full_json.len() as f64 / pairs
150    );
151    println!(
152        "compact (with catalog):    {:>10.2} MB  ({:.0} bytes/pair, {:.1}% smaller, {compact_ms:.0} ms \
153         = {to_value_ms:.0} to_value + {intern_ms:.0} intern + {emit_ms:.0} emit)",
154        compact_json.len() as f64 / 1e6,
155        compact_json.len() as f64 / pairs,
156        100.0 * (full_json.len() - compact_json.len()) as f64 / full_json.len() as f64
157    );
158    println!(
159        "compact (catalog elided):  {:>10.2} MB  ({:.0} bytes/pair, {:.1}% smaller)",
160        compact_no_catalog.len() as f64 / 1e6,
161        compact_no_catalog.len() as f64 / pairs,
162        100.0 * (full_json.len() - compact_no_catalog.len()) as f64 / full_json.len() as f64
163    );
164
165    if let Some(path) = &dump_json {
166        fs::write(path, run.to_json().expect("evidence run serializes"))
167            .expect("cannot write --dump-json");
168        eprintln!("wrote {}", path.display());
169    }
170
171    println!("                        conformance        evidence");
172    println!("  wall ms               {conformance_ms:>11.1}  {evidence_ms:>14.1}");
173    println!(
174        "  memo lookups          {:>11}  {:>14}",
175        conformance_cache.hits + conformance_cache.misses,
176        evidence_cache.hits + evidence_cache.misses
177    );
178    println!(
179        "  memo hits             {:>11}  {:>14}",
180        conformance_cache.hits, evidence_cache.hits
181    );
182    println!(
183        "  memo misses           {:>11}  {:>14}",
184        conformance_cache.misses, evidence_cache.misses
185    );
186    println!(
187        "  memo entries (peak)   {:>11}  {:>14}",
188        conformance_cache.peak_entries, evidence_cache.peak_entries
189    );
190    println!(
191        "  back-edges            {:>11}  {:>14}",
192        conformance_cache.recursion_back_edges, evidence_cache.recursion_back_edges
193    );
194    println!(
195        "  uncacheable results   {:>11}  {:>14}",
196        conformance_cache.non_cacheable_results, evidence_cache.non_cacheable_results
197    );
198    println!("  evidence visits       {conformance_visits:>11}  {evidence_visits:>14}");
199    println!("  path_support probes   {conformance_probes:>11}  {evidence_probes:>14}");
200    println!();
201    println!(
202        "path probes per visit:     {:.1}",
203        evidence_probes as f64 / evidence_visits.max(1) as f64
204    );
205
206    println!("selected pairs:            {}", conformance.selected_pairs);
207    println!("retained evidence nodes:   {retained}");
208    println!(
209        "visited / retained:        {:.1}x",
210        evidence_visits as f64 / retained.max(1) as f64
211    );
212    println!(
213        "visits per pair:           {:.0}",
214        evidence_visits as f64 / pairs
215    );
216    println!(
217        "distinct (shape,node) that a cache could collapse to at most: {}",
218        evidence_cache.peak_entries
219    );
220    println!(
221        "visits / distinct pairs:   {:.1}x",
222        evidence_visits as f64 / evidence_cache.peak_entries.max(1) as f64
223    );
224    println!();
225    println!(
226        "per-visit cost:            {:.2} us",
227        evidence_ms * 1e3 / evidence_visits.max(1) as f64
228    );
229    println!(
230        "per-memo-lookup cost:      {:.2} us",
231        conformance_ms * 1e3 / (conformance_cache.hits + conformance_cache.misses).max(1) as f64
232    );
233
234    // ---- attribution: which statements own the time ------------------------
235    let (selection, materialization): (Vec<_>, Vec<_>) = collector
236        .shape_records()
237        .iter()
238        .partition(|record| record.label.starts_with("select:"));
239    let selection_us: u64 = selection.iter().map(|record| record.total_us).sum();
240    let materialization_us: u64 = materialization.iter().map(|record| record.total_us).sum();
241
242    println!();
243    println!(
244        "target selection total:    {:.1} ms over {} statement(s)",
245        selection_us as f64 / 1e3,
246        selection.len()
247    );
248    println!(
249        "materialization total:     {:.1} ms over {} statement(s)",
250        materialization_us as f64 / 1e3,
251        materialization.len()
252    );
253    println!();
254    println!(
255        "on-demand: {} failing pair(s) of {} selected -> {} explanation(s)",
256        failures.len(),
257        conformance.selected_pairs,
258        explained
259    );
260    println!(
261        "  find_failures {find_ms:.1} ms + explain {explain_ms:.1} ms = {:.1} ms  \
262         ({:.2}x the full evidence run, {:.2}x conformance alone)",
263        find_ms + explain_ms,
264        (find_ms + explain_ms) / evidence_ms.max(f64::MIN_POSITIVE),
265        (find_ms + explain_ms) / conformance_ms.max(f64::MIN_POSITIVE)
266    );
267
268    let mut ranked = materialization;
269    ranked.sort_by_key(|record| std::cmp::Reverse(record.total_us));
270    println!();
271    println!("top statements by evidence-materialization time:");
272    println!(
273        "  {:>8}  {:>7}  {:>6}  {:>10}  {:>12}  {:>10}  shape",
274        "ms", "share", "foci", "us/focus", "visits", "visits/foci"
275    );
276    for record in ranked.iter().take(top) {
277        println!(
278            "  {:>8.1}  {:>6.1}%  {:>6}  {:>10.1}  {:>12}  {:>10.0}  {}",
279            record.total_us as f64 / 1e3,
280            100.0 * record.total_us as f64 / materialization_us.max(1) as f64,
281            record.invocations,
282            record.total_us as f64 / record.invocations.max(1) as f64,
283            record.visits,
284            record.visits as f64 / record.invocations.max(1) as f64,
285            record.label,
286        );
287    }
288
289    let mut ranked_selection = selection;
290    ranked_selection.sort_by_key(|record| std::cmp::Reverse(record.total_us));
291    println!();
292    println!("top statements by target-selection time:");
293    for record in ranked_selection.iter().take(5) {
294        println!(
295            "  {:>8.1} ms  {}",
296            record.total_us as f64 / 1e3,
297            record.label
298        );
299    }
300}

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V