1use std::cell::{Cell, RefCell};
9use web_time::Instant;
10
11#[derive(Debug, Clone)]
13pub struct QueryRecord {
14 pub fingerprint: String,
16 pub executor: ExecutorKind,
18 pub invocations: u64,
20 pub total_exec_us: u64,
22}
23
24#[derive(Debug, Clone)]
27pub struct ShapeRecord {
28 pub label: String,
31 pub invocations: u64,
34 pub total_us: u64,
36 pub visits: u64,
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
43pub struct ShapeCacheRecord {
44 pub evaluators: u64,
46 pub hits: u64,
48 pub misses: u64,
50 pub insertions: u64,
52 pub recursion_back_edges: u64,
54 pub non_cacheable_results: u64,
56 pub peak_entries: usize,
58 pub estimated_peak_bytes: usize,
63}
64
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
67pub(crate) struct ShapeCacheSample {
68 pub hits: u64,
69 pub misses: u64,
70 pub insertions: u64,
71 pub recursion_back_edges: u64,
72 pub non_cacheable_results: u64,
73 pub entries: usize,
74 pub estimated_bytes: usize,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum ExecutorKind {
79 Fallback {
81 reason: Option<String>,
82 },
83 Native,
84}
85
86#[derive(Debug, Default)]
87pub struct ProfileCollector {
88 records: Vec<QueryRecord>,
89 shape_records: Vec<ShapeRecord>,
90 shape_cache: ShapeCacheRecord,
91}
92
93impl ProfileCollector {
94 pub fn new() -> Self {
95 ProfileCollector::default()
96 }
97
98 pub fn record_invocation(&mut self, fingerprint: &str, exec_us: u64, executor: ExecutorKind) {
99 if let Some(r) = self
100 .records
101 .iter_mut()
102 .find(|r| r.fingerprint == fingerprint)
103 {
104 r.invocations += 1;
105 r.total_exec_us += exec_us;
106 } else {
107 self.records.push(QueryRecord {
108 fingerprint: fingerprint.to_string(),
109 executor,
110 invocations: 1,
111 total_exec_us: exec_us,
112 });
113 }
114 }
115
116 pub fn record_shape_invocation(&mut self, label: &str, exec_us: u64) {
117 self.record_shape_work(label, exec_us, 0);
118 }
119
120 pub fn record_shape_work(&mut self, label: &str, exec_us: u64, visits: u64) {
122 if let Some(r) = self.shape_records.iter_mut().find(|r| r.label == label) {
123 r.invocations += 1;
124 r.total_us += exec_us;
125 r.visits += visits;
126 } else {
127 self.shape_records.push(ShapeRecord {
128 label: label.to_string(),
129 invocations: 1,
130 total_us: exec_us,
131 visits,
132 });
133 }
134 }
135
136 pub(crate) fn record_shape_cache(&mut self, sample: ShapeCacheSample) {
137 self.shape_cache.evaluators += 1;
138 self.shape_cache.hits += sample.hits;
139 self.shape_cache.misses += sample.misses;
140 self.shape_cache.insertions += sample.insertions;
141 self.shape_cache.recursion_back_edges += sample.recursion_back_edges;
142 self.shape_cache.non_cacheable_results += sample.non_cacheable_results;
143 self.shape_cache.peak_entries = self.shape_cache.peak_entries.max(sample.entries);
144 self.shape_cache.estimated_peak_bytes = self
145 .shape_cache
146 .estimated_peak_bytes
147 .max(sample.estimated_bytes);
148 }
149
150 pub fn records(&self) -> &[QueryRecord] {
151 &self.records
152 }
153
154 pub fn shape_records(&self) -> &[ShapeRecord] {
155 &self.shape_records
156 }
157
158 pub fn shape_cache(&self) -> &ShapeCacheRecord {
159 &self.shape_cache
160 }
161
162 pub fn print_summary(&self) {
163 if !self.shape_records.is_empty() {
164 println!(
165 "profile: {} distinct shape(s)/rule(s)",
166 self.shape_records.len()
167 );
168 let mut sorted = self.shape_records.to_vec();
169 sorted.sort_by_key(|b| std::cmp::Reverse(b.total_us));
170 for r in &sorted {
171 let avg_us = r.total_us.checked_div(r.invocations).unwrap_or(0);
172 println!(
173 " {}: {} call(s), {}µs total, {}µs avg",
174 r.label, r.invocations, r.total_us, avg_us,
175 );
176 }
177 }
178 if self.shape_cache.evaluators > 0 {
179 let lookups = self.shape_cache.hits + self.shape_cache.misses;
180 let hit_rate = if lookups == 0 {
181 0.0
182 } else {
183 self.shape_cache.hits as f64 * 100.0 / lookups as f64
184 };
185 println!(
186 "profile: shape cache: {} evaluator(s), {} hit(s), {} miss(es), \
187 {hit_rate:.1}% hit rate",
188 self.shape_cache.evaluators, self.shape_cache.hits, self.shape_cache.misses,
189 );
190 println!(
191 " {} insertion(s), {} recursion back-edge(s), {} non-cacheable result(s)",
192 self.shape_cache.insertions,
193 self.shape_cache.recursion_back_edges,
194 self.shape_cache.non_cacheable_results,
195 );
196 println!(
197 " peak: {} entries, ~{} bytes",
198 self.shape_cache.peak_entries, self.shape_cache.estimated_peak_bytes,
199 );
200 }
201 if self.records.is_empty() {
202 if self.shape_records.is_empty() && self.shape_cache.evaluators == 0 {
203 println!("profile: no data collected");
204 }
205 return;
206 }
207 println!(
208 "profile: {} distinct SPARQL query/queries",
209 self.records.len()
210 );
211 let mut sorted = self.records.to_vec();
212 sorted.sort_by_key(|b| std::cmp::Reverse(b.total_exec_us));
213 for r in &sorted {
214 let exec_str = match &r.executor {
215 ExecutorKind::Fallback { reason: None } => "fallback".to_string(),
216 ExecutorKind::Fallback { reason: Some(s) } => format!("fallback({s})"),
217 ExecutorKind::Native => "native".to_string(),
218 };
219 let avg_us = r.total_exec_us.checked_div(r.invocations).unwrap_or(0);
220 println!(
221 " [{exec_str}] {}: {} call(s), {}µs total, {}µs avg",
222 r.fingerprint, r.invocations, r.total_exec_us, avg_us,
223 );
224 }
225 }
226}
227
228thread_local! {
229 static PROFILER: RefCell<Option<ProfileCollector>> = const { RefCell::new(None) };
230}
231
232pub fn enable() {
234 PROFILER.with(|p| *p.borrow_mut() = Some(ProfileCollector::new()));
235}
236
237pub fn take() -> Option<ProfileCollector> {
239 PROFILER.with(|p| p.borrow_mut().take())
240}
241
242pub(crate) fn is_enabled() -> bool {
244 PROFILER.with(|p| p.borrow().is_some())
245}
246
247pub fn record(fingerprint: &str, exec_us: u64, executor: ExecutorKind) {
249 PROFILER.with(|p| {
250 if let Some(col) = p.borrow_mut().as_mut() {
251 col.record_invocation(fingerprint, exec_us, executor);
252 }
253 });
254}
255
256pub fn record_shape(label: &str, exec_us: u64) {
258 PROFILER.with(|p| {
259 if let Some(col) = p.borrow_mut().as_mut() {
260 col.record_shape_invocation(label, exec_us);
261 }
262 });
263}
264
265pub(crate) fn record_shape_work(label: &str, exec_us: u64, visits: u64) {
268 PROFILER.with(|p| {
269 if let Some(col) = p.borrow_mut().as_mut() {
270 col.record_shape_work(label, exec_us, visits);
271 }
272 });
273}
274
275thread_local! {
276 static EVIDENCE_VISITS: Cell<u64> = const { Cell::new(0) };
277}
278
279pub(crate) fn record_evidence_visit() {
286 EVIDENCE_VISITS.with(|visits| visits.set(visits.get().wrapping_add(1)));
287}
288
289pub fn take_evidence_visits() -> u64 {
291 EVIDENCE_VISITS.with(|visits| visits.replace(0))
292}
293
294pub fn evidence_visits() -> u64 {
297 EVIDENCE_VISITS.with(Cell::get)
298}
299
300thread_local! {
301 static PATH_PROBES: Cell<u64> = const { Cell::new(0) };
302}
303
304pub(crate) fn record_path_probe() {
308 PATH_PROBES.with(|probes| probes.set(probes.get().wrapping_add(1)));
309}
310
311pub fn take_path_probes() -> u64 {
313 PATH_PROBES.with(|probes| probes.replace(0))
314}
315
316pub(crate) fn record_shape_cache(sample: ShapeCacheSample) {
319 PROFILER.with(|p| {
320 if let Some(col) = p.borrow_mut().as_mut() {
321 col.record_shape_cache(sample);
322 }
323 });
324}
325
326pub fn timed<T>(fingerprint: &str, f: impl FnOnce() -> T) -> T {
329 let start = Instant::now();
330 let result = f();
331 let us = start.elapsed().as_micros() as u64;
332 record(fingerprint, us, ExecutorKind::Fallback { reason: None });
333 result
334}
335
336pub fn fingerprint(query: &str) -> String {
338 let trimmed = query.trim();
339 let preview: String = trimmed.chars().take(60).collect();
340 preview.split_whitespace().collect::<Vec<_>>().join(" ")
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn aggregates_shape_cache_samples() {
350 let mut collector = ProfileCollector::new();
351 collector.record_shape_cache(ShapeCacheSample {
352 hits: 3,
353 misses: 5,
354 insertions: 4,
355 recursion_back_edges: 1,
356 non_cacheable_results: 2,
357 entries: 4,
358 estimated_bytes: 400,
359 });
360 collector.record_shape_cache(ShapeCacheSample {
361 hits: 7,
362 misses: 2,
363 insertions: 2,
364 recursion_back_edges: 0,
365 non_cacheable_results: 0,
366 entries: 2,
367 estimated_bytes: 250,
368 });
369
370 assert_eq!(
371 collector.shape_cache(),
372 &ShapeCacheRecord {
373 evaluators: 2,
374 hits: 10,
375 misses: 7,
376 insertions: 6,
377 recursion_back_edges: 1,
378 non_cacheable_results: 2,
379 peak_entries: 4,
380 estimated_peak_bytes: 400,
381 }
382 );
383 }
384}