trustfall_rustdoc_adapter/adapter/tracer/
ptrace.rs

1use std::{
2    cell::RefCell, collections::BTreeMap, fmt::Debug, marker::PhantomData, rc::Rc, sync::Arc,
3    time::Duration,
4};
5
6use std::time::Instant;
7
8use trustfall::{
9    FieldValue,
10    provider::{
11        Adapter, AsVertex, ContextIterator, ContextOutcomeIterator, EdgeParameters, Eid,
12        ResolveEdgeInfo, ResolveInfo, VertexInfo, VertexIterator, Vid,
13    },
14};
15
16/// A simple histogram that stores 15 roughly exponentially increasing buckets of
17/// values, from 0 to 1 billion, followed by a final bucket to store numbers greater
18/// than 1 billion.
19#[derive(Clone)]
20pub struct ExpHistogram {
21    buckets: [u32; 16],
22}
23
24/// The largest value that will be accepted into each bucket of the histogram.
25pub const HIST_BOUNDARIES: [u64; 16] = [
26    100,
27    300,
28    1000,
29    3000,
30    10000,
31    30000,
32    100000,
33    300000,
34    1000000,
35    3000000,
36    10000000,
37    30000000,
38    100000000,
39    300000000,
40    1000000000,
41    u64::MAX,
42];
43
44impl ExpHistogram {
45    /// Create a new histogram.
46    pub fn new() -> ExpHistogram {
47        ExpHistogram { buckets: [0; 16] }
48    }
49
50    /// Add a value to the histogram.
51    pub fn add(&mut self, num: u64) {
52        for (i, lim) in HIST_BOUNDARIES.iter().enumerate() {
53            if num <= *lim {
54                self.buckets[i] = self.buckets[i].saturating_add(1);
55                break;
56            }
57        }
58    }
59
60    /// Returns the largest value that will be accepted into each bucket.
61    pub fn boundaries(&self) -> &'static [u64; 16] {
62        &HIST_BOUNDARIES
63    }
64
65    /// Returns the number of values stored in the histogram.
66    pub fn count(&self) -> u32 {
67        self.buckets()
68            .iter()
69            .copied()
70            .fold(0, |acc, num| acc.saturating_add(num))
71    }
72
73    /// Returns the count of each bucket
74    pub fn buckets(&self) -> &[u32; 16] {
75        &self.buckets
76    }
77}
78
79impl Debug for ExpHistogram {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(
82            f,
83            "ExpHistogram {{ buckets: {:?}, count: {:?} }}",
84            &self.buckets,
85            self.count()
86        )
87    }
88}
89
90impl Default for ExpHistogram {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96/// A summary of key timing statistics.
97#[derive(Debug, Clone)]
98pub struct Summary {
99    hist: ExpHistogram,
100    min: Duration,
101    max: Duration,
102    sum: Duration,
103}
104
105impl Summary {
106    // By initialising with a duration, we don't require min/max to be options.
107    pub fn new(duration: Duration) -> Summary {
108        let mut hist = ExpHistogram::new();
109        hist.add(duration.as_nanos() as u64);
110
111        Summary {
112            hist,
113            min: duration,
114            max: duration,
115            sum: duration,
116        }
117    }
118
119    /// Add a new time to the summary.
120    pub fn update(&mut self, duration: Duration) {
121        self.hist.add(duration.as_nanos() as u64);
122
123        self.min = self.min.min(duration);
124        self.max = self.max.max(duration);
125        self.sum = self.sum.saturating_add(duration);
126    }
127
128    /// Returns the number of items that have been processed.
129    pub fn count(&self) -> u32 {
130        self.hist.count()
131    }
132
133    /// Returns the total time
134    pub fn total(&self) -> Duration {
135        self.sum
136    }
137
138    /// Returns the fastest operation
139    pub fn min(&self) -> Duration {
140        self.min
141    }
142
143    /// Returns the slowest operation
144    pub fn max(&self) -> Duration {
145        self.max
146    }
147
148    /// Returns the exponential histogram with times in nanoseconds.
149    pub fn histogram(&self) -> &ExpHistogram {
150        &self.hist
151    }
152
153    /// Returns the mean duration.
154    pub fn mean(&self) -> Duration {
155        // This cannot panic since `count()` is always at least 1.
156        self.sum / self.count()
157    }
158}
159
160/// Records and stores operations performed by the adapter.
161///
162/// This struct is intended for use inside of a [`TracingAdapter`].
163/// Operations must be recorded sequentially in chronological order.
164/// Recording out-of-order operations will lead to invalid state.
165#[derive(Debug, Clone)]
166pub struct Tracer {
167    pub calls: BTreeMap<FunctionCall, Summary>,
168
169    // When we measure the time of an iterator, we also measure the time spent
170    // evaluating its inputs. We must therefore subtract out this time when
171    // we record the time each operation takes.
172    last_input_duration: Option<Duration>,
173}
174
175impl Tracer {
176    /// Construct a new tracer.
177    pub fn new() -> Self {
178        Self {
179            calls: BTreeMap::new(),
180            last_input_duration: None,
181        }
182    }
183
184    /// Record an operation.
185    pub fn record_time(&mut self, call_id: &FunctionCall, duration: Duration) {
186        if let Some(summary) = self.calls.get_mut(call_id) {
187            summary.update(duration);
188        } else {
189            self.calls.insert(call_id.clone(), Summary::new(duration));
190        }
191    }
192
193    /// Set the duration of the last input.
194    pub fn record_last_input_duration(&mut self, duration: Duration) {
195        self.last_input_duration = Some(duration);
196    }
197
198    /// Get the duration of the last input. Panics if the duration is `None`.
199    pub fn get_last_input_duration(&self) -> Duration {
200        self.last_input_duration.unwrap()
201    }
202}
203
204impl Default for Tracer {
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
211pub enum FunctionCall {
212    ResolveProperty(Vid, Arc<str>, Arc<str>), // vertex ID + type name + name of the property
213    ResolveNeighbors(Vid, Arc<str>, Eid),     // vertex ID + type name + edge ID
214    ResolveNeighborsInner(Vid, Arc<str>, Eid), // same as ResolveNeighbors
215    ResolveCoercion(Vid, Arc<str>, Arc<str>), // vertex ID + current type + coerced-to type
216}
217
218struct PerfSpanIter<I, T, F>
219where
220    I: Iterator<Item = T>,
221    F: Fn(T, Duration) -> T,
222{
223    inner: I,
224    post_action: F,
225}
226
227impl<I, T, F> Iterator for PerfSpanIter<I, T, F>
228where
229    I: Iterator<Item = T>,
230    F: Fn(T, Duration) -> T,
231{
232    type Item = T;
233
234    fn next(&mut self) -> Option<Self::Item> {
235        let start = Instant::now();
236        let item = self.inner.next();
237        let time = start.elapsed();
238        match item {
239            Some(item) => Some((self.post_action)(item, time)),
240            None => None,
241        }
242    }
243
244    fn size_hint(&self) -> (usize, Option<usize>) {
245        self.inner.size_hint()
246    }
247}
248
249fn make_iter_with_perf_span<I, T, F>(inner: I, post_action: F) -> PerfSpanIter<I, T, F>
250where
251    I: Iterator<Item = T>,
252    F: Fn(T, Duration) -> T,
253{
254    PerfSpanIter { inner, post_action }
255}
256
257/// Traces an inner adapter. Constructed with [`TracingAdapter::new()`]. Each
258/// tracer is only valid for one query. When switching between queries, if you
259/// use the same adapter, ensure you first call [`TracingAdapter::finish()`],
260/// otherwise the new operations will be traced with the old.
261#[derive(Debug, Clone)]
262pub struct TracingAdapter<'vertex, AdapterT>
263where
264    AdapterT: Adapter<'vertex>,
265    AdapterT::Vertex: Debug + Clone + 'vertex,
266{
267    pub tracer: Rc<RefCell<Tracer>>,
268    inner: AdapterT,
269    _phantom: PhantomData<&'vertex ()>,
270}
271
272impl<'vertex, AdapterT> TracingAdapter<'vertex, AdapterT>
273where
274    AdapterT: Adapter<'vertex>,
275    AdapterT::Vertex: Debug + Clone + 'vertex,
276{
277    pub fn new(adapter: AdapterT) -> Self {
278        Self {
279            tracer: Rc::new(RefCell::new(Tracer::new())),
280            inner: adapter,
281            _phantom: PhantomData,
282        }
283    }
284
285    /// Finalise the trace and return it.
286    pub fn finish(&self) -> Tracer {
287        // Ensure nothing is reading the trace i.e. we can safely stop interpreting.
288        let trace_ref = self.tracer.borrow_mut();
289        let new_trace = Tracer::new();
290        drop(trace_ref);
291        self.tracer.replace(new_trace)
292    }
293}
294
295// For all resolutions we want to know:
296// 1. What are we resolving?
297// 2. How long did the resolution take?
298// 3. How many times was a specific resolution resolved?
299//     e.g. resolve_property(Vid(1), "Trait", "name"))
300//
301// We are not collecting:
302// (1) Number of times a function is called, and time spent constructing
303//     the iterators.
304//
305//     The number of times any given resolution function is called is
306//     an implementation detail of trustfall, and since most time is spent
307//     resolving iterators, tracking it isn't valuable.
308//
309// (2) Whether or not the returned iterator is empty.
310impl<'vertex, AdapterT> Adapter<'vertex> for TracingAdapter<'vertex, AdapterT>
311where
312    AdapterT: Adapter<'vertex> + 'vertex,
313    AdapterT::Vertex: Debug + Clone + 'vertex,
314{
315    type Vertex = AdapterT::Vertex;
316
317    fn resolve_starting_vertices(
318        &self,
319        edge_name: &Arc<str>,
320        parameters: &EdgeParameters,
321        resolve_info: &ResolveInfo,
322    ) -> VertexIterator<'vertex, Self::Vertex> {
323        // We don't care about the first resolution since it's normally quick
324        // and it only occurs once.
325        self.inner
326            .resolve_starting_vertices(edge_name, parameters, resolve_info)
327    }
328
329    fn resolve_property<V: AsVertex<Self::Vertex> + 'vertex>(
330        &self,
331        contexts: ContextIterator<'vertex, V>,
332        type_name: &Arc<str>,
333        property_name: &Arc<str>,
334        resolve_info: &ResolveInfo,
335    ) -> ContextOutcomeIterator<'vertex, V, FieldValue> {
336        let call_id = FunctionCall::ResolveProperty(
337            resolve_info.vid(),
338            type_name.clone(),
339            property_name.clone(),
340        );
341
342        let tracer_ref = self.tracer.clone();
343
344        let wrapped_contexts = Box::new(make_iter_with_perf_span(
345            contexts,
346            move |context, duration| {
347                tracer_ref.borrow_mut().record_last_input_duration(duration);
348                context
349            },
350        ));
351
352        let inner_iter =
353            self.inner
354                .resolve_property(wrapped_contexts, type_name, property_name, resolve_info);
355
356        let tracer_ref_2 = self.tracer.clone();
357
358        Box::new(make_iter_with_perf_span(
359            inner_iter,
360            move |(context, value), duration| {
361                let input_duration = tracer_ref_2.borrow().get_last_input_duration();
362                tracer_ref_2
363                    .borrow_mut()
364                    .record_time(&call_id, duration - input_duration);
365                (context, value)
366            },
367        ))
368    }
369
370    fn resolve_neighbors<V: AsVertex<Self::Vertex> + 'vertex>(
371        &self,
372        contexts: ContextIterator<'vertex, V>,
373        type_name: &Arc<str>,
374        edge_name: &Arc<str>,
375        parameters: &EdgeParameters,
376        resolve_info: &ResolveEdgeInfo,
377    ) -> ContextOutcomeIterator<'vertex, V, VertexIterator<'vertex, Self::Vertex>> {
378        // Inner and outer call times are often quite different, so they need to
379        // be tracked separately.
380        let call_id = FunctionCall::ResolveNeighbors(
381            resolve_info.origin_vid(),
382            type_name.clone(),
383            resolve_info.eid(),
384        );
385        let call_id_inner = FunctionCall::ResolveNeighborsInner(
386            resolve_info.origin_vid(),
387            type_name.clone(),
388            resolve_info.eid(),
389        );
390
391        let tracer_ref = self.tracer.clone();
392
393        let wrapped_contexts = Box::new(make_iter_with_perf_span(
394            contexts,
395            move |context, duration| {
396                tracer_ref.borrow_mut().record_last_input_duration(duration);
397                context
398            },
399        ));
400
401        let inner_iter = self.inner.resolve_neighbors(
402            wrapped_contexts,
403            type_name,
404            edge_name,
405            parameters,
406            resolve_info,
407        );
408
409        let tracer_ref_2 = self.tracer.clone();
410
411        Box::new(make_iter_with_perf_span(
412            inner_iter,
413            move |(context, neighbor_iter), duration| {
414                let input_duration = tracer_ref_2.borrow().get_last_input_duration();
415                tracer_ref_2
416                    .borrow_mut()
417                    .record_time(&call_id, duration - input_duration);
418
419                let tracer_ref_3 = tracer_ref_2.clone();
420
421                let value = call_id_inner.clone();
422
423                // We do not subtract the input duration for the inner iterator
424                // because there is no input.
425                let tapped_neighbor_iter = Box::new(make_iter_with_perf_span(
426                    neighbor_iter,
427                    move |vertex, duration| {
428                        tracer_ref_3.borrow_mut().record_time(&value, duration);
429                        vertex
430                    },
431                ));
432
433                (context, tapped_neighbor_iter)
434            },
435        ))
436    }
437
438    fn resolve_coercion<V: AsVertex<Self::Vertex> + 'vertex>(
439        &self,
440        contexts: ContextIterator<'vertex, V>,
441        type_name: &Arc<str>,
442        coerce_to_type: &Arc<str>,
443        resolve_info: &ResolveInfo,
444    ) -> ContextOutcomeIterator<'vertex, V, bool> {
445        let call_id = FunctionCall::ResolveCoercion(
446            resolve_info.vid(),
447            type_name.clone(),
448            coerce_to_type.clone(),
449        );
450
451        let tracer_ref = self.tracer.clone();
452
453        let wrapped_contexts = Box::new(make_iter_with_perf_span(
454            contexts,
455            move |context, duration| {
456                tracer_ref.borrow_mut().record_last_input_duration(duration);
457                context
458            },
459        ));
460
461        let inner_iter =
462            self.inner
463                .resolve_coercion(wrapped_contexts, type_name, coerce_to_type, resolve_info);
464
465        let tracer_ref_2 = self.tracer.clone();
466
467        Box::new(make_iter_with_perf_span(
468            inner_iter,
469            move |(context, can_coerce), duration| {
470                let input_duration = tracer_ref_2.borrow().get_last_input_duration();
471                tracer_ref_2
472                    .borrow_mut()
473                    .record_time(&call_id, duration - input_duration);
474                (context, can_coerce)
475            },
476        ))
477    }
478}