tinymist_analysis/
stats.rs

1//! Tinymist Analysis Statistics
2
3use std::fmt::Write;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::{Arc, LazyLock};
6
7use parking_lot::Mutex;
8use tinymist_std::hash::FxDashMap;
9use tinymist_std::time::Duration;
10use typst::syntax::FileId;
11
12/// Statistics about the allocation
13
14#[derive(Debug, Default)]
15pub struct AllocStats {
16    /// The number of allocated objects.
17    pub allocated: AtomicUsize,
18    /// The number of dropped objects.
19    pub dropped: AtomicUsize,
20}
21
22impl AllocStats {
23    /// increment the statistics.
24    pub fn increment(&self) {
25        self.allocated.fetch_add(1, Ordering::Relaxed);
26    }
27
28    /// decrement the statistics.
29    pub fn decrement(&self) {
30        self.dropped.fetch_add(1, Ordering::Relaxed);
31    }
32
33    /// Report the statistics of the allocation.
34    pub fn report() -> String {
35        let maps = crate::adt::interner::MAPS.lock().clone();
36        let mut data = Vec::new();
37        for (name, sz, map) in maps {
38            let allocated = map.allocated.load(std::sync::atomic::Ordering::Relaxed);
39            let dropped = map.dropped.load(std::sync::atomic::Ordering::Relaxed);
40            let alive = allocated.saturating_sub(dropped);
41            data.push((name, sz * alive, allocated, dropped, alive));
42        }
43
44        // sort by total
45        data.sort_by(|x, y| y.4.cmp(&x.4));
46
47        // format to html
48
49        let mut html = String::new();
50        html.push_str(r#"<div>
51<style>
52table.alloc-stats { width: 100%; border-collapse: collapse; }
53table.alloc-stats th, table.alloc-stats td { border: 1px solid black; padding: 8px; text-align: center; }
54table.alloc-stats th.name-column, table.alloc-stats td.name-column { text-align: left; }
55table.alloc-stats tr:nth-child(odd) { background-color: rgba(242, 242, 242, 0.8); }
56@media (prefers-color-scheme: dark) {
57    table.alloc-stats tr:nth-child(odd) { background-color: rgba(50, 50, 50, 0.8); }
58}
59</style>
60<table class="alloc-stats"><tr><th class="name-column">Name</th><th>Alive</th><th>Allocated</th><th>Dropped</th><th>Size</th></tr>"#);
61
62        for (name, sz, allocated, dropped, alive) in data {
63            html.push_str("<tr>");
64            html.push_str(&format!(r#"<td class="name-column">{name}</td>"#));
65            html.push_str(&format!("<td>{alive}</td>"));
66            html.push_str(&format!("<td>{allocated}</td>"));
67            html.push_str(&format!("<td>{dropped}</td>"));
68            html.push_str(&format!("<td>{}</td>", human_size(sz)));
69            html.push_str("</tr>");
70        }
71        html.push_str("</table>");
72        html.push_str("</div>");
73
74        html
75    }
76}
77
78/// The data of the query statistic.
79#[derive(Clone)]
80pub struct QueryStatBucketData {
81    pub(crate) query: u64,
82    pub(crate) missing: u64,
83    pub(crate) total: Duration,
84    pub(crate) min: Duration,
85    pub(crate) max: Duration,
86}
87
88impl Default for QueryStatBucketData {
89    fn default() -> Self {
90        Self {
91            query: 0,
92            missing: 0,
93            total: Duration::from_secs(0),
94            min: Duration::from_secs(u64::MAX),
95            max: Duration::from_secs(0),
96        }
97    }
98}
99
100/// Statistics about some query
101#[derive(Default, Clone)]
102pub struct QueryStatBucket {
103    /// The data of the query statistic.
104    pub data: Arc<Mutex<QueryStatBucketData>>,
105}
106
107impl QueryStatBucket {
108    /// Increment the query statistic.
109    pub fn increment(&self, elapsed: Duration) {
110        let mut data = self.data.lock();
111        data.query += 1;
112        data.total += elapsed;
113        data.min = data.min.min(elapsed);
114        data.max = data.max.max(elapsed);
115    }
116}
117
118/// A guard for the query statistic.
119pub struct QueryStatGuard {
120    /// The bucket of the query statistic for any file.
121    pub bucket_any: Option<QueryStatBucket>,
122    /// The bucket of the query statistic.
123    pub bucket: QueryStatBucket,
124    /// The start time of the query.
125    pub since: tinymist_std::time::Instant,
126}
127
128impl Drop for QueryStatGuard {
129    fn drop(&mut self) {
130        let elapsed = self.since.elapsed();
131        self.bucket.increment(elapsed);
132        if let Some(bucket) = self.bucket_any.as_ref() {
133            bucket.increment(elapsed);
134        }
135    }
136}
137
138impl QueryStatGuard {
139    /// Increment the missing count.
140    pub fn miss(&self) {
141        let mut data = self.bucket.data.lock();
142        data.missing += 1;
143    }
144}
145
146/// Statistics about the analyzers
147#[derive(Default)]
148pub struct AnalysisStats {
149    /// The query statistics.
150    pub query_stats: Arc<FxDashMap<Option<FileId>, FxDashMap<&'static str, QueryStatBucket>>>,
151}
152
153impl AnalysisStats {
154    /// Gets a statistic guard for a query.
155    pub fn stat(&self, id: Option<FileId>, name: &'static str) -> QueryStatGuard {
156        let stats = &self.query_stats;
157        let get = |v| stats.entry(v).or_default().entry(name).or_default().clone();
158        QueryStatGuard {
159            bucket_any: if id.is_some() { Some(get(None)) } else { None },
160            bucket: get(id),
161            since: tinymist_std::time::Instant::now(),
162        }
163    }
164
165    /// Reports the statistics of the analysis.
166    pub fn report(&self) -> String {
167        let stats = &self.query_stats;
168        let mut data = Vec::new();
169        for refs in stats.iter() {
170            let id = refs.key();
171            let queries = refs.value();
172            for refs2 in queries.iter() {
173                let query = refs2.key();
174                let bucket = refs2.value().data.lock().clone();
175                let name = match id {
176                    Some(id) => format!("{id:?}:{query}"),
177                    None => query.to_string(),
178                };
179                let name = name.replace('\\', "/");
180                data.push((name, bucket));
181            }
182        }
183
184        // sort by query duration
185        data.sort_by(|x, y| y.1.max.cmp(&x.1.max));
186
187        // format to html
188
189        let mut html = String::new();
190        html.push_str(r#"<div>
191<style>
192table.analysis-stats { width: 100%; border-collapse: collapse; }
193table.analysis-stats th, table.analysis-stats td { border: 1px solid black; padding: 8px; text-align: center; }
194table.analysis-stats th.name-column, table.analysis-stats td.name-column { text-align: left; }
195table.analysis-stats tr:nth-child(odd) { background-color: rgba(242, 242, 242, 0.8); }
196@media (prefers-color-scheme: dark) {
197    table.analysis-stats tr:nth-child(odd) { background-color: rgba(50, 50, 50, 0.8); }
198}
199</style>
200<table class="analysis-stats"><tr><th class="query-column">Name</th><th>Count</th><th>Missing</th><th>Total</th><th>Min</th><th>Max</th></tr>"#);
201
202        for (name, bucket) in data {
203            let _ = write!(
204                &mut html,
205                "<tr><td class=\"query-column\">{name}</td><td>{}</td><td>{}</td><td>{:?}</td><td>{:?}</td><td>{:?}</td></tr>",
206                bucket.query, bucket.missing, bucket.total, bucket.min, bucket.max
207            );
208        }
209        html.push_str("</table>");
210        html.push_str("</div>");
211
212        html
213    }
214}
215
216/// The global statistics about the analyzers.
217pub static GLOBAL_STATS: LazyLock<AnalysisStats> = LazyLock::new(AnalysisStats::default);
218
219fn human_size(size: usize) -> String {
220    let units = ["B", "KB", "MB", "GB", "TB"];
221    let mut unit = 0;
222    let mut size = size as f64;
223    while size >= 768.0 && unit < units.len() {
224        size /= 1024.0;
225        unit += 1;
226    }
227    format!("{:.2} {}", size, units[unit])
228}