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

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, !>

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