Skip to main content

EvaluationSession

Struct EvaluationSession 

Source
pub struct EvaluationSession { /* private fields */ }

Implementations§

Source§

impl EvaluationSession

Source

pub fn internal_term(&self, term: &Term) -> Term

Resolve a public RDF term to the indexed identity used by evidence constraints. This is a constant-time lookup and leaves other terms alone.

Source

pub fn prepared_evidence(&self) -> &PreparedEvidenceValidator

Borrow the prepared evidence view for existing shape-map projections.

Source

pub fn validate(&self, options: &FindingOptions) -> ValidationOutcome

Examples found in repository?
examples/bench_many_sessions.rs (line 54)
27fn main() {
28    let args: Vec<_> = env::args().skip(1).collect();
29    assert!(
30        args.len() == 3 || (args.len() == 4 && args[3] == "--infer"),
31        "usage: bench_many_sessions SHAPES DATA COUNT [--infer]"
32    );
33    let count: usize = args[2].parse().expect("COUNT must be an integer");
34    assert!(count > 0, "COUNT must be positive");
35    let options = SessionOptions {
36        inference: args.len() == 4,
37        ..SessionOptions::default()
38    };
39    let findings = FindingOptions::default();
40
41    let started = Instant::now();
42    let compiled = CompiledShapes::compile(load(&args[0])).expect("compilable shapes");
43    let compile_ms = started.elapsed().as_secs_f64() * 1000.0;
44    let data = load(&args[1]).graph;
45    let compiled_rss_kib = rss_kib();
46
47    let mut sessions = Vec::with_capacity(count);
48    let started = Instant::now();
49    let mut first_rss_kib = None;
50    for index in 0..count {
51        let session = compiled
52            .session(SessionData::Separate(data.clone()), options)
53            .expect("session");
54        black_box(session.validate(&findings));
55        sessions.push(session);
56        if index == 0 {
57            first_rss_kib = rss_kib();
58        }
59    }
60    let sessions_ms = started.elapsed().as_secs_f64() * 1000.0;
61    let final_rss_kib = rss_kib();
62    black_box(&sessions);
63    println!(
64        "shapes={},data={},count={count},infer={},compile_ms={compile_ms:.2},sessions_ms={sessions_ms:.2},compiled_rss_kib={compiled_rss_kib:?},first_rss_kib={first_rss_kib:?},final_rss_kib={final_rss_kib:?}",
65        args[0], args[1], options.inference,
66    );
67}
More examples
Hide additional examples
examples/bench_sessions.rs (line 71)
33fn main() {
34    let args: Vec<_> = env::args().skip(1).collect();
35    assert!(
36        args.len() == 3 || (args.len() == 4 && args[3] == "--infer"),
37        "usage: bench_sessions SHAPES DATA_A DATA_B [--infer]"
38    );
39    let options = SessionOptions {
40        inference: args.len() == 4,
41        ..SessionOptions::default()
42    };
43    let findings = FindingOptions::default();
44
45    let start = Instant::now();
46    let shapes = load(&args[0]);
47    let load_shapes_ms = start.elapsed().as_secs_f64() * 1000.0;
48    let start = Instant::now();
49    let compiled = CompiledShapes::compile(shapes).expect("compilable shapes");
50    let compile_ms = start.elapsed().as_secs_f64() * 1000.0;
51    let compiled_rss_kib = rss_kib();
52
53    let data_a = load(&args[1]).graph;
54    let data_b = load(&args[2]).graph;
55    let delta = GraphDelta {
56        delete: data_a
57            .iter()
58            .next()
59            .map(|triple| triple.into_owned())
60            .into_iter()
61            .collect(),
62        ..GraphDelta::default()
63    };
64
65    let start = Instant::now();
66    let first = compiled
67        .session(SessionData::Separate(data_a), options)
68        .expect("first session");
69    let first_session_ms = start.elapsed().as_secs_f64() * 1000.0;
70    let start = Instant::now();
71    black_box(first.validate(&findings));
72    let first_validate_ms = start.elapsed().as_secs_f64() * 1000.0;
73    let first_rss_kib = rss_kib();
74
75    let start = Instant::now();
76    for _ in 0..3 {
77        black_box(first.validate(&findings));
78    }
79    let repeated_validate_ms = start.elapsed().as_secs_f64() * 1000.0 / 3.0;
80
81    let start = Instant::now();
82    let second = compiled
83        .session(SessionData::Separate(data_b), options)
84        .expect("second session");
85    let second_session_ms = start.elapsed().as_secs_f64() * 1000.0;
86    let start = Instant::now();
87    black_box(second.validate(&findings));
88    let second_validate_ms = start.elapsed().as_secs_f64() * 1000.0;
89    let second_rss_kib = rss_kib();
90
91    let start = Instant::now();
92    let edited = first.with_delta(&delta).expect("edited session");
93    let edit_session_ms = start.elapsed().as_secs_f64() * 1000.0;
94    let start = Instant::now();
95    black_box(edited.validate(&findings));
96    let edit_validate_ms = start.elapsed().as_secs_f64() * 1000.0;
97    let edited_rss_kib = rss_kib();
98
99    println!(
100        "shapes={},data_a={},data_b={},infer={},load_shapes_ms={load_shapes_ms:.2},compile_ms={compile_ms:.2},first_session_ms={first_session_ms:.2},first_validate_ms={first_validate_ms:.2},repeated_validate_ms={repeated_validate_ms:.2},second_session_ms={second_session_ms:.2},second_validate_ms={second_validate_ms:.2},edit_session_ms={edit_session_ms:.2},edit_validate_ms={edit_validate_ms:.2},compiled_rss_kib={compiled_rss_kib:?},first_rss_kib={first_rss_kib:?},second_rss_kib={second_rss_kib:?},edited_rss_kib={edited_rss_kib:?}",
101        args[0], args[1], args[2], options.inference,
102    );
103}
Source

pub fn report(&self, options: &FindingOptions) -> ValidationReport

Source

pub fn property_witnesses( &self, key_path: Option<&Path>, options: &FindingOptions, ) -> Vec<PropertyWitness>

Source

pub fn evidence(&self, options: &EvidenceOptions) -> EvidenceRun

Source

pub fn evidence_with_validation( &self, validation: &ValidationOptions, include_progress: bool, ) -> EvidenceRun

Source

pub fn conformance(&self, options: &ConformanceOptions) -> ConformanceRun

Source

pub fn find_failures( &self, options: &ConformanceOptions, ) -> (ConformanceRun, Vec<SelectedPair>)

Source

pub fn explain( &self, pair: &SelectedPair, ) -> Result<Vec<StatementEvaluation>, EvaluationError>

Source

pub fn explain_canonical( &self, pair: &SelectedPair, ) -> Result<Vec<StatementEvaluation>, EvaluationError>

Source

pub fn with_delta(&self, delta: &GraphDelta) -> Result<Self, SessionError>

Examples found in repository?
examples/bench_sessions.rs (line 92)
33fn main() {
34    let args: Vec<_> = env::args().skip(1).collect();
35    assert!(
36        args.len() == 3 || (args.len() == 4 && args[3] == "--infer"),
37        "usage: bench_sessions SHAPES DATA_A DATA_B [--infer]"
38    );
39    let options = SessionOptions {
40        inference: args.len() == 4,
41        ..SessionOptions::default()
42    };
43    let findings = FindingOptions::default();
44
45    let start = Instant::now();
46    let shapes = load(&args[0]);
47    let load_shapes_ms = start.elapsed().as_secs_f64() * 1000.0;
48    let start = Instant::now();
49    let compiled = CompiledShapes::compile(shapes).expect("compilable shapes");
50    let compile_ms = start.elapsed().as_secs_f64() * 1000.0;
51    let compiled_rss_kib = rss_kib();
52
53    let data_a = load(&args[1]).graph;
54    let data_b = load(&args[2]).graph;
55    let delta = GraphDelta {
56        delete: data_a
57            .iter()
58            .next()
59            .map(|triple| triple.into_owned())
60            .into_iter()
61            .collect(),
62        ..GraphDelta::default()
63    };
64
65    let start = Instant::now();
66    let first = compiled
67        .session(SessionData::Separate(data_a), options)
68        .expect("first session");
69    let first_session_ms = start.elapsed().as_secs_f64() * 1000.0;
70    let start = Instant::now();
71    black_box(first.validate(&findings));
72    let first_validate_ms = start.elapsed().as_secs_f64() * 1000.0;
73    let first_rss_kib = rss_kib();
74
75    let start = Instant::now();
76    for _ in 0..3 {
77        black_box(first.validate(&findings));
78    }
79    let repeated_validate_ms = start.elapsed().as_secs_f64() * 1000.0 / 3.0;
80
81    let start = Instant::now();
82    let second = compiled
83        .session(SessionData::Separate(data_b), options)
84        .expect("second session");
85    let second_session_ms = start.elapsed().as_secs_f64() * 1000.0;
86    let start = Instant::now();
87    black_box(second.validate(&findings));
88    let second_validate_ms = start.elapsed().as_secs_f64() * 1000.0;
89    let second_rss_kib = rss_kib();
90
91    let start = Instant::now();
92    let edited = first.with_delta(&delta).expect("edited session");
93    let edit_session_ms = start.elapsed().as_secs_f64() * 1000.0;
94    let start = Instant::now();
95    black_box(edited.validate(&findings));
96    let edit_validate_ms = start.elapsed().as_secs_f64() * 1000.0;
97    let edited_rss_kib = rss_kib();
98
99    println!(
100        "shapes={},data_a={},data_b={},infer={},load_shapes_ms={load_shapes_ms:.2},compile_ms={compile_ms:.2},first_session_ms={first_session_ms:.2},first_validate_ms={first_validate_ms:.2},repeated_validate_ms={repeated_validate_ms:.2},second_session_ms={second_session_ms:.2},second_validate_ms={second_validate_ms:.2},edit_session_ms={edit_session_ms:.2},edit_validate_ms={edit_validate_ms:.2},compiled_rss_kib={compiled_rss_kib:?},first_rss_kib={first_rss_kib:?},second_rss_kib={second_rss_kib:?},edited_rss_kib={edited_rss_kib:?}",
101        args[0], args[1], args[2], options.inference,
102    );
103}
Source

pub fn with_delta_and_inference( &self, delta: &GraphDelta, inference: bool, ) -> Result<Self, SessionError>

Revalidate a delta with a different inference setting. Enabling rules patches asserted data and derives again; disabling them patches the already evaluated graph so previously inferred triples remain present.

Source

pub fn data(&self) -> &Graph

Examples found in repository?
examples/probe_evidence_cost.rs (line 70)
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 169)
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_shared(&self) -> Arc<Graph> ⓘ

Share the evaluated data graph without copying it.

Source

pub fn public_shapes_graph_if_relabelled(&self) -> Option<Graph>

A source graph with only colliding blank nodes relabeled for combining with the public data graph. No copy is made when scopes do not collide.

Source

pub fn public_schema(&self, schema: Arc<Schema>) -> Arc<Schema> ⓘ

Relabel shape constants only when a split graph has a blank-node collision; otherwise preserve the caller’s shared schema allocation.

Source

pub fn asserted_shared(&self) -> Arc<Graph> ⓘ

Share the asserted data graph used by with_delta.

Source

pub fn inferred(&self) -> &[Triple]

Source

pub fn inferred_count(&self) -> usize

Count inferred triples without materializing their public RDF terms.

Source

pub fn inferred_for_write_back(&self) -> Vec<Triple>

Inferred triples with input data blank-node labels restored and shapes nodes kept distinct, for adding the delta to a caller-owned data graph.

Source

pub fn diagnostics(&self) -> &[ExecutionDiagnostic]

Source

pub fn gate(&self, delta: &GraphDelta) -> Result<RepairOutcome, SessionError>

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