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}