Skip to main content

query_lang/
stats.rs

1//! Cumulative counters describing how the engine spent its work.
2
3use core::fmt;
4
5/// A snapshot of how a [`Database`](crate::Database) resolved its queries.
6///
7/// Incremental compilation is only worth its complexity if it actually avoids
8/// work, and the only way to know it does is to count. Every derived query
9/// resolution takes exactly one of three paths, and `Stats` counts each:
10///
11/// - **`computed`** — the query ran its [`compute`](crate::System::compute)
12///   function. This is the expensive path: a cache miss, or a dependency that
13///   genuinely changed and forced a recomputation.
14/// - **`validated`** — the query was stale (something changed since it was last
15///   checked) but re-examining its dependencies proved none of them actually
16///   changed its inputs, so the cached value was reused without recomputing.
17///   This is *early cutoff*, the property that makes the engine fast: a change
18///   that does not alter a query's inputs does not recompute it.
19/// - **`hits`** — the query was already verified at the current revision and
20///   returned immediately, without even checking dependencies.
21///
22/// The counters are cumulative over the life of the database and only ever
23/// increase. Snapshot them with [`Database::stats`](crate::Database::stats)
24/// before and after an operation to measure exactly what that operation cost.
25///
26/// # Examples
27///
28/// After a run, most re-queries of an unchanged graph are hits, and a targeted
29/// input change recomputes only what depends on it:
30///
31/// ```
32/// use query_lang::{Database, System, QueryError};
33///
34/// struct Doubler;
35/// impl System for Doubler {
36///     type Key = u32;
37///     type Value = u32;
38///     fn compute(&self, db: &Database<Self>, key: &u32) -> Result<u32, QueryError> {
39///         Ok(db.get(&(key + 100))? * 2) // reads input (key + 100), doubles it
40///     }
41/// }
42///
43/// let mut db = Database::new(Doubler);
44/// db.set(101, 5);
45///
46/// assert_eq!(db.get(&1)?, 10);
47/// assert_eq!(db.stats().computed, 1); // first resolution computes
48///
49/// assert_eq!(db.get(&1)?, 10);
50/// assert_eq!(db.stats().hits, 1);     // second is a free hit
51/// # Ok::<(), QueryError>(())
52/// ```
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize))]
55pub struct Stats {
56    /// The number of times a query ran its `compute` function (a real cache
57    /// miss or a forced recomputation).
58    pub computed: u64,
59    /// The number of times a stale query was revalidated and its cached value
60    /// reused because no dependency actually changed its inputs (early cutoff).
61    pub validated: u64,
62    /// The number of times a query was already current and returned immediately.
63    pub hits: u64,
64}
65
66impl Stats {
67    /// The total number of derived-query resolutions across all three paths.
68    ///
69    /// This counts work the engine was *asked* to do; the ratio of
70    /// [`computed`](Self::computed) to this total is how much of that work
71    /// actually cost a recomputation.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use query_lang::Stats;
77    ///
78    /// let s = Stats { computed: 2, validated: 3, hits: 5 };
79    /// assert_eq!(s.total(), 10);
80    /// ```
81    #[must_use]
82    #[inline]
83    pub const fn total(self) -> u64 {
84        self.computed
85            .saturating_add(self.validated)
86            .saturating_add(self.hits)
87    }
88}
89
90impl fmt::Display for Stats {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(
93            f,
94            "computed={}, validated={}, hits={}",
95            self.computed, self.validated, self.hits
96        )
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use alloc::string::ToString;
104
105    #[test]
106    fn test_default_is_all_zero() {
107        let s = Stats::default();
108        assert_eq!(s.computed, 0);
109        assert_eq!(s.validated, 0);
110        assert_eq!(s.hits, 0);
111        assert_eq!(s.total(), 0);
112    }
113
114    #[test]
115    fn test_total_sums_all_paths() {
116        let s = Stats {
117            computed: 1,
118            validated: 2,
119            hits: 4,
120        };
121        assert_eq!(s.total(), 7);
122    }
123
124    #[test]
125    fn test_display_lists_counters() {
126        let s = Stats {
127            computed: 1,
128            validated: 2,
129            hits: 3,
130        };
131        assert_eq!(s.to_string(), "computed=1, validated=2, hits=3");
132    }
133}