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