Skip to main content

sharing

Function sharing 

Source
pub fn sharing(run: &EvidenceRun) -> Result<Sharing>
Expand description

Measure how much a run’s evidence repeats, without keeping the encoding.

Reported against the same predicates the encoder interns by, so the ratio cannot drift from what compaction actually collapses.

Examples found in repository?
examples/bench_evidence.rs (line 415)
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}