shifty-engine 0.5.1

SHACL validation and SHACL-AF inference execution over the IR
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
//! Opt-in execution telemetry (doc ยง269). Collected via a thread-local so no
//! validation API signatures change. Enable with `enable()`, consume with
//! `take()`.
//!
//! Shape-cache counters are accumulated locally by each evaluator and published
//! once when it is dropped, avoiding a thread-local operation per lookup.

use std::cell::{Cell, RefCell};
use web_time::Instant;

/// Per-query performance record.
#[derive(Debug, Clone)]
pub struct QueryRecord {
    /// Stable fingerprint derived from the canonical query text (first 160 chars).
    pub fingerprint: String,
    /// Whether the native executor or Spareval fallback handled this query.
    pub executor: ExecutorKind,
    /// How many execution batches or fallback probes invoked this query.
    pub invocations: u64,
    /// Total wall-clock execution time across all invocations, in microseconds.
    pub total_exec_us: u64,
}

/// Per-shape or per-rule wall-clock record. One entry per distinct label
/// (shape IRI, `@N` slot id, or `rule[N]`).
#[derive(Debug, Clone)]
pub struct ShapeRecord {
    /// Shape IRI (named shapes), `@N` arena slot (blank-node shapes), or
    /// `rule[N]` (inference rules).
    pub label: String,
    /// Number of evaluation calls (one per focus node for validation, one per
    /// rule firing for inference).
    pub invocations: u64,
    /// Total wall-clock time across all invocations, in microseconds.
    pub total_us: u64,
    /// Evidence-materialization node visits attributed to this label, when the
    /// caller reported them. Zero for conformance-only and inference records.
    pub visits: u64,
}

/// Aggregate shape-cache telemetry for one profiling session.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ShapeCacheRecord {
    /// Number of per-snapshot evaluators that reported cache activity.
    pub evaluators: u64,
    /// Lookups served from an existing memo entry.
    pub hits: u64,
    /// Lookups without an existing memo entry, including recursion back-edges.
    pub misses: u64,
    /// Completed results admitted to the memo.
    pub insertions: u64,
    /// Lookups that encountered the same `(ShapeId, Term)` on the active stack.
    pub recursion_back_edges: u64,
    /// Completed results not admitted because they depended on a back-edge.
    pub non_cacheable_results: u64,
    /// Largest final entry count reported by any one evaluator.
    pub peak_entries: usize,
    /// Approximate maximum bytes retained by any one evaluator.
    ///
    /// Includes hash-table bucket storage and owned RDF-term string payloads,
    /// but not allocator metadata.
    pub estimated_peak_bytes: usize,
}

/// Storage work counted only while profiling is enabled. Counts are structural
/// evidence; wall-clock costs are reported separately.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StorageRecord {
    /// Time spent compiling data-independent read/write demand.
    pub access_catalog_us: u64,
    pub store_builds: u64,
    pub graph_union_builds: u64,
    pub graph_union_rows: u64,
    pub graph_projection_builds: u64,
    pub graph_projection_rows: u64,
    pub source_builds: u64,
    pub source_rows: u64,
    pub dataset_builds: u64,
    pub encoded_rows: u64,
    pub committed_rows: u64,
    pub commit_batches: u64,
    pub commit_us: u64,
    pub index_builds: u64,
    pub index_bytes: u64,
    pub index_declines: u64,
    /// Allocated PSO pair-buffer capacity, excluding map nodes and dictionaries.
    pub source_primary_bytes: u64,
    pub session_primary_bytes: u64,
    /// Time spent encoding the lazily shared source storage.
    pub source_encode_us: u64,
    /// Time spent encoding session datasets from data graphs.
    pub session_encode_us: u64,
}

/// One optional-index admission decision. The base PSO index is mandatory and
/// therefore does not appear here.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexRecord {
    pub scope: &'static str,
    pub kind: &'static str,
    pub predicate: Option<String>,
    pub reason: &'static str,
    pub rows: usize,
    pub estimated_bytes: usize,
    pub actual_bytes: usize,
    pub budget_bytes: usize,
    pub build_us: u64,
    pub accepted: bool,
}

/// Candidate rows visited by one scan pattern. The mask uses S=1, P=2, O=4.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ScanRecord {
    pub calls: u64,
    pub candidate_rows: u64,
}

/// Aggregate bounded reachability-cache telemetry for one profiling session.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ReachCacheRecord {
    pub hits: u64,
    pub misses: u64,
    pub insertions: u64,
    pub declined_ids: u64,
}

/// One evaluator's cache counters, merged into [`ShapeCacheRecord`] on drop.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct ShapeCacheSample {
    pub hits: u64,
    pub misses: u64,
    pub insertions: u64,
    pub recursion_back_edges: u64,
    pub non_cacheable_results: u64,
    pub entries: usize,
    pub estimated_bytes: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExecutorKind {
    /// Spareval fallback; carries the capability-analysis reason if known.
    Fallback {
        reason: Option<String>,
    },
    Native,
}

#[derive(Debug, Default)]
pub struct ProfileCollector {
    records: Vec<QueryRecord>,
    shape_records: Vec<ShapeRecord>,
    shape_cache: ShapeCacheRecord,
    storage: StorageRecord,
    indexes: Vec<IndexRecord>,
    scans: [[ScanRecord; 8]; 2],
    reach_cache: ReachCacheRecord,
}

impl ProfileCollector {
    pub fn new() -> Self {
        ProfileCollector::default()
    }

    pub fn record_invocation(&mut self, fingerprint: &str, exec_us: u64, executor: ExecutorKind) {
        if let Some(r) = self
            .records
            .iter_mut()
            .find(|r| r.fingerprint == fingerprint)
        {
            r.invocations += 1;
            r.total_exec_us += exec_us;
        } else {
            self.records.push(QueryRecord {
                fingerprint: fingerprint.to_string(),
                executor,
                invocations: 1,
                total_exec_us: exec_us,
            });
        }
    }

    pub fn record_shape_invocation(&mut self, label: &str, exec_us: u64) {
        self.record_shape_work(label, exec_us, 0);
    }

    /// Record one evaluation together with the evidence nodes it visited.
    pub fn record_shape_work(&mut self, label: &str, exec_us: u64, visits: u64) {
        if let Some(r) = self.shape_records.iter_mut().find(|r| r.label == label) {
            r.invocations += 1;
            r.total_us += exec_us;
            r.visits += visits;
        } else {
            self.shape_records.push(ShapeRecord {
                label: label.to_string(),
                invocations: 1,
                total_us: exec_us,
                visits,
            });
        }
    }

    pub(crate) fn record_shape_cache(&mut self, sample: ShapeCacheSample) {
        self.shape_cache.evaluators += 1;
        self.shape_cache.hits += sample.hits;
        self.shape_cache.misses += sample.misses;
        self.shape_cache.insertions += sample.insertions;
        self.shape_cache.recursion_back_edges += sample.recursion_back_edges;
        self.shape_cache.non_cacheable_results += sample.non_cacheable_results;
        self.shape_cache.peak_entries = self.shape_cache.peak_entries.max(sample.entries);
        self.shape_cache.estimated_peak_bytes = self
            .shape_cache
            .estimated_peak_bytes
            .max(sample.estimated_bytes);
    }

    pub fn records(&self) -> &[QueryRecord] {
        &self.records
    }

    pub fn shape_records(&self) -> &[ShapeRecord] {
        &self.shape_records
    }

    pub fn shape_cache(&self) -> &ShapeCacheRecord {
        &self.shape_cache
    }

    pub fn storage(&self) -> &StorageRecord {
        &self.storage
    }

    pub fn indexes(&self) -> &[IndexRecord] {
        &self.indexes
    }

    pub fn scans(&self) -> &[[ScanRecord; 8]; 2] {
        &self.scans
    }

    pub fn reach_cache(&self) -> &ReachCacheRecord {
        &self.reach_cache
    }

    pub fn print_summary(&self) {
        if self.storage.store_builds + self.storage.dataset_builds + self.storage.source_builds > 0
        {
            println!(
                "profile: storage: {} Store build(s), {} source build(s) / {} row(s), {} dataset build(s), {} encoded row(s), {} committed row(s), {} index build(s) / {} byte(s), {} declined",
                self.storage.store_builds,
                self.storage.source_builds,
                self.storage.source_rows,
                self.storage.dataset_builds,
                self.storage.encoded_rows,
                self.storage.committed_rows,
                self.storage.index_builds,
                self.storage.index_bytes,
                self.storage.index_declines,
            );
            println!(
                "profile: storage time: {} ยตs access catalog, {} ยตs shared source encode, {} ยตs session encode, {} ยตs across {} commit batch(es)",
                self.storage.access_catalog_us,
                self.storage.source_encode_us,
                self.storage.session_encode_us,
                self.storage.commit_us,
                self.storage.commit_batches,
            );
            println!(
                "profile: primary pair buffers: {} source byte(s), {} session byte(s)",
                self.storage.source_primary_bytes, self.storage.session_primary_bytes,
            );
            println!(
                "profile: graph materialization: {} union build(s) / {} row(s), {} compatibility projection(s) / {} row(s)",
                self.storage.graph_union_builds,
                self.storage.graph_union_rows,
                self.storage.graph_projection_builds,
                self.storage.graph_projection_rows,
            );
        }
        for index in &self.indexes {
            let predicate = index
                .predicate
                .as_deref()
                .map_or(String::new(), |predicate| format!(" <{predicate}>"));
            println!(
                "profile: index: {} {}{}: {}, {} row(s), {} estimated byte(s), {} allocated byte(s), {} byte budget, {} ยตs build, {}",
                index.scope,
                index.kind,
                predicate,
                index.reason,
                index.rows,
                index.estimated_bytes,
                index.actual_bytes,
                index.budget_bytes,
                index.build_us,
                if index.accepted { "built" } else { "declined" },
            );
        }
        for (scope, patterns) in ["source", "session"].into_iter().zip(&self.scans) {
            for (mask, scan) in patterns.iter().enumerate() {
                if scan.calls == 0 {
                    continue;
                }
                let pattern = [
                    if mask & 1 != 0 { 'S' } else { '-' },
                    if mask & 2 != 0 { 'P' } else { '-' },
                    if mask & 4 != 0 { 'O' } else { '-' },
                ];
                println!(
                    "profile: scan: {scope} {}{}{}: {} call(s), {} candidate row(s)",
                    pattern[0], pattern[1], pattern[2], scan.calls, scan.candidate_rows,
                );
            }
        }
        if self.reach_cache.hits + self.reach_cache.misses > 0 {
            println!(
                "profile: reach cache: {} hit(s), {} miss(es), {} insertion(s), {} declined result id(s)",
                self.reach_cache.hits,
                self.reach_cache.misses,
                self.reach_cache.insertions,
                self.reach_cache.declined_ids,
            );
        }
        if !self.shape_records.is_empty() {
            println!(
                "profile: {} distinct shape(s)/rule(s)",
                self.shape_records.len()
            );
            let mut sorted = self.shape_records.to_vec();
            sorted.sort_by_key(|b| std::cmp::Reverse(b.total_us));
            for r in &sorted {
                let avg_us = r.total_us.checked_div(r.invocations).unwrap_or(0);
                println!(
                    "  {}: {} call(s), {}ยตs total, {}ยตs avg",
                    r.label, r.invocations, r.total_us, avg_us,
                );
            }
        }
        if self.shape_cache.evaluators > 0 {
            let lookups = self.shape_cache.hits + self.shape_cache.misses;
            let hit_rate = if lookups == 0 {
                0.0
            } else {
                self.shape_cache.hits as f64 * 100.0 / lookups as f64
            };
            println!(
                "profile: shape cache: {} evaluator(s), {} hit(s), {} miss(es), \
                 {hit_rate:.1}% hit rate",
                self.shape_cache.evaluators, self.shape_cache.hits, self.shape_cache.misses,
            );
            println!(
                "  {} insertion(s), {} recursion back-edge(s), {} non-cacheable result(s)",
                self.shape_cache.insertions,
                self.shape_cache.recursion_back_edges,
                self.shape_cache.non_cacheable_results,
            );
            println!(
                "  peak: {} entries, ~{} bytes",
                self.shape_cache.peak_entries, self.shape_cache.estimated_peak_bytes,
            );
        }
        if self.records.is_empty() {
            if self.shape_records.is_empty() && self.shape_cache.evaluators == 0 {
                println!("profile: no data collected");
            }
            return;
        }
        println!(
            "profile: {} distinct SPARQL query/queries",
            self.records.len()
        );
        let mut sorted = self.records.to_vec();
        sorted.sort_by_key(|b| std::cmp::Reverse(b.total_exec_us));
        for r in &sorted {
            let exec_str = match &r.executor {
                ExecutorKind::Fallback { reason: None } => "fallback".to_string(),
                ExecutorKind::Fallback { reason: Some(s) } => format!("fallback({s})"),
                ExecutorKind::Native => "native".to_string(),
            };
            let avg_us = r.total_exec_us.checked_div(r.invocations).unwrap_or(0);
            println!(
                "  [{exec_str}] {}: {} call(s), {}ยตs total, {}ยตs avg",
                r.fingerprint, r.invocations, r.total_exec_us, avg_us,
            );
        }
    }
}

thread_local! {
    static PROFILER: RefCell<Option<ProfileCollector>> = const { RefCell::new(None) };
}

/// Enable profiling for the current thread. Resets any previous collector.
pub fn enable() {
    PROFILER.with(|p| *p.borrow_mut() = Some(ProfileCollector::new()));
}

/// Disable profiling and return the collected data, if any.
pub fn take() -> Option<ProfileCollector> {
    PROFILER.with(|p| p.borrow_mut().take())
}

/// Whether telemetry is enabled for the current thread.
pub(crate) fn is_enabled() -> bool {
    PROFILER.with(|p| p.borrow().is_some())
}

/// Record one query invocation. No-op when profiling is disabled.
pub fn record(fingerprint: &str, exec_us: u64, executor: ExecutorKind) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.record_invocation(fingerprint, exec_us, executor);
        }
    });
}

/// Record one shape/rule evaluation. No-op when profiling is disabled.
pub fn record_shape(label: &str, exec_us: u64) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.record_shape_invocation(label, exec_us);
        }
    });
}

pub(crate) fn record_store_build() {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.store_builds += 1;
        }
    });
}

pub(crate) fn record_graph_union(rows: usize) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.graph_union_builds += 1;
            col.storage.graph_union_rows += rows as u64;
        }
    });
}

pub(crate) fn record_graph_projection(rows: usize) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.graph_projection_builds += 1;
            col.storage.graph_projection_rows += rows as u64;
        }
    });
}

pub(crate) fn record_source_build(rows: usize) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.source_builds += 1;
            col.storage.source_rows += rows as u64;
        }
    });
}

pub(crate) fn record_source_encode_time(us: u64) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.source_encode_us += us;
        }
    });
}

pub(crate) fn record_access_catalog_time(us: u64) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.access_catalog_us = col.storage.access_catalog_us.saturating_add(us);
        }
    });
}

pub(crate) fn record_dataset_build(rows: usize) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.dataset_builds += 1;
            col.storage.encoded_rows += rows as u64;
        }
    });
}

pub(crate) fn record_session_encode_time(us: u64) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.session_encode_us += us;
        }
    });
}

pub(crate) fn record_dataset_commit(rows: usize, us: u64) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.committed_rows += rows as u64;
            col.storage.commit_batches += 1;
            col.storage.commit_us += us;
        }
    });
}

pub(crate) fn record_index_decision(record: IndexRecord) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            if record.accepted {
                col.storage.index_builds += 1;
                col.storage.index_bytes += record.actual_bytes as u64;
            } else {
                col.storage.index_declines += 1;
            }
            col.indexes.push(record);
        }
    });
}

pub(crate) fn record_primary_index_bytes(scope: &'static str, old: usize, new: usize) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            let bytes = if scope == "source" {
                &mut col.storage.source_primary_bytes
            } else {
                &mut col.storage.session_primary_bytes
            };
            *bytes = bytes.saturating_sub(old as u64) + new as u64;
        }
    });
}

pub(crate) fn observe_shared_source_primary_bytes(bytes: usize) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.storage.source_primary_bytes = col.storage.source_primary_bytes.max(bytes as u64);
        }
    });
}

pub(crate) fn record_scan(scope: &'static str, mask: usize, candidates: usize) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            let scope = usize::from(scope == "session");
            let scan = &mut col.scans[scope][mask];
            scan.calls += 1;
            scan.candidate_rows += candidates as u64;
        }
    });
}

pub(crate) fn record_reach_cache_lookup(hit: bool) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            if hit {
                col.reach_cache.hits += 1;
            } else {
                col.reach_cache.misses += 1;
            }
        }
    });
}

pub(crate) fn record_reach_cache_admission(inserted: bool, result_ids: usize) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            if inserted {
                col.reach_cache.insertions += 1;
            } else {
                col.reach_cache.declined_ids += result_ids as u64;
            }
        }
    });
}

/// Record one evaluation plus its evidence-node visits. No-op when profiling
/// is disabled.
pub(crate) fn record_shape_work(label: &str, exec_us: u64, visits: u64) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.record_shape_work(label, exec_us, visits);
        }
    });
}

thread_local! {
    static EVIDENCE_VISITS: Cell<u64> = const { Cell::new(0) };
}

/// One evidence-materialization node visit.
///
/// Unlike the counters above this is always on: it is a `Cell` increment beside
/// a function that already clones a `Shape`, and it is the only way to see how
/// far the shared shape DAG expands into a per-pair evidence tree. Visits count
/// nodes *entered*, including those later pruned from the retained evidence.
pub(crate) fn record_evidence_visit() {
    EVIDENCE_VISITS.with(|visits| visits.set(visits.get().wrapping_add(1)));
}

/// Read and reset the evidence-visit counter for this thread.
pub fn take_evidence_visits() -> u64 {
    EVIDENCE_VISITS.with(|visits| visits.replace(0))
}

/// Read the evidence-visit counter without resetting it, for deltas around a
/// region of work.
pub fn evidence_visits() -> u64 {
    EVIDENCE_VISITS.with(Cell::get)
}

thread_local! {
    static PATH_PROBES: Cell<u64> = const { Cell::new(0) };
}

/// One `path_support` probe. Each probe re-derives a full successor set from
/// the backend to answer a single membership question, so probe count is the
/// multiplier on path work that evidence pays and conformance does not.
pub(crate) fn record_path_probe() {
    PATH_PROBES.with(|probes| probes.set(probes.get().wrapping_add(1)));
}

/// Read and reset the path-probe counter for this thread.
pub fn take_path_probes() -> u64 {
    PATH_PROBES.with(|probes| probes.replace(0))
}

/// Merge one evaluator's shape-cache telemetry. No-op when profiling is
/// disabled.
pub(crate) fn record_shape_cache(sample: ShapeCacheSample) {
    PROFILER.with(|p| {
        if let Some(col) = p.borrow_mut().as_mut() {
            col.record_shape_cache(sample);
        }
    });
}

/// Helper: measure `f` and record the result under `fingerprint`. Returns the
/// value produced by `f`.
pub fn timed<T>(fingerprint: &str, f: impl FnOnce() -> T) -> T {
    let start = Instant::now();
    let result = f();
    let us = start.elapsed().as_micros() as u64;
    record(fingerprint, us, ExecutorKind::Fallback { reason: None });
    result
}

/// Derive a short fingerprint from a canonical query string.
pub fn fingerprint(query: &str) -> String {
    let trimmed = query.trim();
    let preview: String = trimmed.chars().take(160).collect();
    // Replace newlines/runs of whitespace with a single space for readability.
    preview.split_whitespace().collect::<Vec<_>>().join(" ")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn aggregates_shape_cache_samples() {
        let mut collector = ProfileCollector::new();
        collector.record_shape_cache(ShapeCacheSample {
            hits: 3,
            misses: 5,
            insertions: 4,
            recursion_back_edges: 1,
            non_cacheable_results: 2,
            entries: 4,
            estimated_bytes: 400,
        });
        collector.record_shape_cache(ShapeCacheSample {
            hits: 7,
            misses: 2,
            insertions: 2,
            recursion_back_edges: 0,
            non_cacheable_results: 0,
            entries: 2,
            estimated_bytes: 250,
        });

        assert_eq!(
            collector.shape_cache(),
            &ShapeCacheRecord {
                evaluators: 2,
                hits: 10,
                misses: 7,
                insertions: 6,
                recursion_back_edges: 1,
                non_cacheable_results: 2,
                peak_entries: 4,
                estimated_peak_bytes: 400,
            }
        );
    }
}