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