1use crate::build_trace::Phase;
19#[cfg(not(target_arch = "wasm32"))]
20use std::sync::atomic::{AtomicU64, AtomicU8, AtomicUsize, Ordering};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum Unit {
25 #[default]
27 None,
28 Bytes,
30 Statements,
32 Terms,
34 Records,
36 Queries,
38}
39
40impl Unit {
41 #[cfg(not(target_arch = "wasm32"))]
42 const ALL: [Unit; 6] = [
43 Unit::None,
44 Unit::Bytes,
45 Unit::Statements,
46 Unit::Terms,
47 Unit::Records,
48 Unit::Queries,
49 ];
50
51 pub fn as_str(self) -> &'static str {
53 match self {
54 Unit::None => "none",
55 Unit::Bytes => "bytes",
56 Unit::Statements => "statements",
57 Unit::Terms => "terms",
58 Unit::Records => "records",
59 Unit::Queries => "queries",
60 }
61 }
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Eq)]
66pub struct Snapshot {
67 pub phase: Option<Phase>,
69 pub seq: u64,
71 pub begun: Vec<(Phase, std::time::Instant)>,
75 pub done: u64,
77 pub total: Option<u64>,
79 pub unit: Unit,
81 pub statements: u64,
83 pub terms: u64,
85 pub chunks: u64,
87 pub spill_dir: Option<std::path::PathBuf>,
89}
90
91impl Snapshot {
92 pub fn has_begun(&self, phase: Phase) -> bool {
94 self.begun.iter().any(|(p, _)| *p == phase)
95 }
96}
97
98#[cfg(not(target_arch = "wasm32"))]
100fn phase_index(phase: Phase) -> usize {
101 Phase::ALL.iter().position(|p| *p == phase).unwrap_or(0)
102}
103
104#[cfg(not(target_arch = "wasm32"))]
105mod state {
106 use super::*;
107 pub(super) static LISTENERS: AtomicUsize = AtomicUsize::new(0);
108 pub(super) static PHASE: AtomicU8 = AtomicU8::new(0);
110 pub(super) static SEQ: AtomicU64 = AtomicU64::new(0);
111 pub(super) static BEGUN: std::sync::Mutex<Vec<(Phase, std::time::Instant)>> =
112 std::sync::Mutex::new(Vec::new());
113 pub(super) static DONE: AtomicU64 = AtomicU64::new(0);
114 pub(super) static TOTAL: AtomicU64 = AtomicU64::new(u64::MAX);
116 pub(super) static UNIT: AtomicU8 = AtomicU8::new(0);
117 pub(super) static STATEMENTS: AtomicU64 = AtomicU64::new(0);
118 pub(super) static TERMS: AtomicU64 = AtomicU64::new(0);
119 pub(super) static CHUNKS: AtomicU64 = AtomicU64::new(0);
120 pub(super) static SPILL_DIR: std::sync::Mutex<Option<std::path::PathBuf>> =
121 std::sync::Mutex::new(None);
122}
123
124pub struct Listener {
126 _private: (),
127}
128
129pub fn listen() -> Listener {
132 #[cfg(not(target_arch = "wasm32"))]
133 {
134 use state::*;
135 PHASE.store(0, Ordering::Relaxed);
136 DONE.store(0, Ordering::Relaxed);
137 TOTAL.store(u64::MAX, Ordering::Relaxed);
138 UNIT.store(0, Ordering::Relaxed);
139 if let Ok(mut b) = BEGUN.lock() {
140 b.clear();
141 }
142 for c in [&STATEMENTS, &TERMS, &CHUNKS] {
143 c.store(0, Ordering::Relaxed);
144 }
145 if let Ok(mut d) = SPILL_DIR.lock() {
146 *d = None;
147 }
148 SEQ.store(0, Ordering::Release);
149 LISTENERS.fetch_add(1, Ordering::AcqRel);
150 #[cfg(test)]
151 THIS_THREAD_LISTENS.with(|c| c.set(true));
152 }
153 Listener { _private: () }
154}
155
156impl Drop for Listener {
157 fn drop(&mut self) {
158 #[cfg(not(target_arch = "wasm32"))]
159 state::LISTENERS.fetch_sub(1, Ordering::AcqRel);
160 #[cfg(test)]
161 THIS_THREAD_LISTENS.with(|c| c.set(false));
162 }
163}
164
165#[cfg(test)]
166thread_local! {
167 static THIS_THREAD_LISTENS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
168}
169
170#[inline]
172pub fn listening() -> bool {
173 #[cfg(not(target_arch = "wasm32"))]
174 {
175 #[cfg(test)]
180 if !THIS_THREAD_LISTENS.with(|c| c.get()) {
181 return false;
182 }
183 state::LISTENERS.load(Ordering::Relaxed) > 0
184 }
185 #[cfg(target_arch = "wasm32")]
186 false
187}
188
189pub fn begin(phase: Phase, total: Option<u64>, unit: Unit) {
192 #[cfg(not(target_arch = "wasm32"))]
193 if listening() {
194 use state::*;
195 let idx = phase_index(phase);
196 DONE.store(0, Ordering::Relaxed);
197 TOTAL.store(total.unwrap_or(u64::MAX), Ordering::Relaxed);
198 UNIT.store(unit as u8, Ordering::Relaxed);
199 PHASE.store(idx as u8 + 1, Ordering::Relaxed);
200 if let Ok(mut b) = BEGUN.lock() {
201 b.push((phase, std::time::Instant::now()));
202 }
203 SEQ.fetch_add(1, Ordering::Release);
204 }
205 #[cfg(target_arch = "wasm32")]
206 let _ = (phase, total, unit);
207}
208
209pub fn set_total(total: Option<u64>, unit: Unit) {
211 #[cfg(not(target_arch = "wasm32"))]
212 if listening() {
213 state::TOTAL.store(total.unwrap_or(u64::MAX), Ordering::Relaxed);
214 state::UNIT.store(unit as u8, Ordering::Relaxed);
215 }
216 #[cfg(target_arch = "wasm32")]
217 let _ = (total, unit);
218}
219
220#[inline]
222pub fn advance(n: u64) {
223 #[cfg(not(target_arch = "wasm32"))]
224 if listening() {
225 state::DONE.fetch_add(n, Ordering::Relaxed);
226 }
227 #[cfg(target_arch = "wasm32")]
228 let _ = n;
229}
230
231pub fn set_done(n: u64) {
233 #[cfg(not(target_arch = "wasm32"))]
234 if listening() {
235 state::DONE.store(n, Ordering::Relaxed);
236 }
237 #[cfg(target_arch = "wasm32")]
238 let _ = n;
239}
240
241#[derive(Debug, Clone, Copy)]
243pub enum Stat {
244 Statements,
246 Terms,
248 Chunks,
250}
251
252#[inline]
254pub fn set_stat(stat: Stat, v: u64) {
255 #[cfg(not(target_arch = "wasm32"))]
256 if listening() {
257 match stat {
258 Stat::Statements => &state::STATEMENTS,
259 Stat::Terms => &state::TERMS,
260 Stat::Chunks => &state::CHUNKS,
261 }
262 .store(v, Ordering::Relaxed);
263 }
264 #[cfg(target_arch = "wasm32")]
265 let _ = (stat, v);
266}
267
268pub fn set_spill_dir(dir: Option<&std::path::Path>) {
271 #[cfg(not(target_arch = "wasm32"))]
272 if listening() {
273 if let Ok(mut d) = state::SPILL_DIR.lock() {
274 *d = dir.map(std::path::Path::to_path_buf);
275 }
276 }
277 #[cfg(target_arch = "wasm32")]
278 let _ = dir;
279}
280
281pub fn snapshot() -> Snapshot {
284 #[cfg(not(target_arch = "wasm32"))]
285 {
286 use state::*;
287 loop {
288 let seq = SEQ.load(Ordering::Acquire);
289 let phase = PHASE.load(Ordering::Relaxed);
290 let done = DONE.load(Ordering::Relaxed);
291 let total = TOTAL.load(Ordering::Relaxed);
292 let unit = UNIT.load(Ordering::Relaxed);
293 let begun = BEGUN.lock().map(|b| b.clone()).unwrap_or_default();
294 if SEQ.load(Ordering::Acquire) != seq {
295 continue;
296 }
297 return Snapshot {
298 phase: (phase as usize)
299 .checked_sub(1)
300 .and_then(|i| Phase::ALL.get(i).copied()),
301 seq,
302 begun,
303 done,
304 total: (total != u64::MAX).then_some(total),
305 unit: Unit::ALL.get(unit as usize).copied().unwrap_or_default(),
306 statements: STATEMENTS.load(Ordering::Relaxed),
307 terms: TERMS.load(Ordering::Relaxed),
308 chunks: CHUNKS.load(Ordering::Relaxed),
309 spill_dir: SPILL_DIR.lock().ok().and_then(|d| d.clone()),
310 };
311 }
312 }
313 #[cfg(target_arch = "wasm32")]
314 Snapshot::default()
315}
316
317#[derive(Debug, Default)]
321pub struct Ticker {
322 pending: u64,
323}
324
325impl Ticker {
326 pub const EVERY: u64 = 1 << 16;
328
329 pub fn new() -> Self {
331 Ticker { pending: 0 }
332 }
333
334 #[inline]
336 pub fn tick(&mut self, n: u64) {
337 #[cfg(not(target_arch = "wasm32"))]
338 {
339 self.pending += n;
340 if self.pending >= Self::EVERY {
341 self.flush();
342 }
343 }
344 #[cfg(target_arch = "wasm32")]
345 let _ = n;
346 }
347
348 pub fn flush(&mut self) {
350 if self.pending > 0 {
351 advance(self.pending);
352 self.pending = 0;
353 }
354 }
355}
356
357impl Drop for Ticker {
358 fn drop(&mut self) {
359 self.flush();
360 }
361}
362
363#[cfg(all(test, not(target_arch = "wasm32")))]
364mod tests {
365 use super::*;
366
367 #[test]
371 fn publishes_only_while_listened_to() {
372 begin(Phase::Parse, Some(10), Unit::Bytes);
373 advance(5);
374 assert_eq!(snapshot().phase, None, "nothing before a listener");
375 {
376 let mut t = Ticker::new();
377 t.tick(3);
378 }
379 let l = listen();
380 assert!(listening());
381 begin(Phase::ParseChunk, None, Unit::Bytes);
382 set_total(Some(100), Unit::Bytes);
383 advance(40);
384 set_stat(Stat::Statements, 7);
385 let s = snapshot();
386 assert_eq!(s.phase, Some(Phase::ParseChunk));
387 assert_eq!((s.done, s.total, s.unit), (40, Some(100), Unit::Bytes));
388 assert_eq!(s.statements, 7);
389 assert!(s.has_begun(Phase::ParseChunk) && !s.has_begun(Phase::Parse));
390 let seq = s.seq;
391 begin(Phase::Dictionary, Some(1), Unit::Terms);
392 {
393 let mut t = Ticker::new();
394 for _ in 0..(Ticker::EVERY + 5) {
395 t.tick(1);
396 }
397 assert_eq!(snapshot().done, Ticker::EVERY, "published once per EVERY");
398 }
399 let s = snapshot();
400 assert_eq!(s.done, Ticker::EVERY + 5, "the rest on drop");
401 assert_eq!(s.seq, seq + 1);
402 assert_eq!(s.unit.as_str(), "terms");
403 drop(l);
404 assert!(!listening());
405 begin(Phase::Write, None, Unit::None);
406 assert_eq!(
407 snapshot().phase,
408 Some(Phase::Dictionary),
409 "frozen once dropped"
410 );
411 }
412}