rosace_trace/subscribers/ring_buffer.rs
1use std::collections::VecDeque;
2use std::sync::{Arc, Mutex};
3// `web_time::Instant` (not `std::time`) — `std::time::Instant::now()` panics
4// on wasm ("time not implemented"), which aborted the whole app on the first
5// trace. `event.rs` already uses `web_time`; this subscriber was the one spot
6// still on `std`. Drop-in identical on native.
7use web_time::Instant;
8
9use crate::bus::TraceSubscriber;
10use crate::event::RosaceTrace;
11
12/// Predicate deciding whether an event is recorded. See
13/// [`RingBufferSubscriber::filtered`].
14type EventFilter = Arc<dyn Fn(&RosaceTrace) -> bool + Send + Sync>;
15
16/// Retains the last `capacity` trace events in a circular buffer.
17///
18/// Used for time-travel debugging — the dev tools can read the buffer to replay
19/// the sequence of events leading up to the current state or a crash.
20///
21/// Default capacity: 1000 events.
22pub struct RingBufferSubscriber {
23 buffer: Arc<Mutex<VecDeque<(Instant, RosaceTrace)>>>,
24 capacity: usize,
25 /// When set, only events for which this returns true are retained
26 /// (D123/O1 — the flight recorder excludes high-frequency events).
27 filter: Option<EventFilter>,
28}
29
30impl RingBufferSubscriber {
31 /// Creates a new ring buffer with the given capacity.
32 pub fn new(capacity: usize) -> Self {
33 Self {
34 buffer: Arc::new(Mutex::new(VecDeque::with_capacity(capacity))),
35 capacity,
36 filter: None,
37 }
38 }
39
40 /// A ring buffer that only records events passing `filter` — the basis
41 /// of the always-on flight recorder, which excludes high-frequency
42 /// events so it never becomes a per-frame firehose (D123/O1).
43 pub fn filtered(
44 capacity: usize,
45 filter: impl Fn(&RosaceTrace) -> bool + Send + Sync + 'static,
46 ) -> Self {
47 Self {
48 buffer: Arc::new(Mutex::new(VecDeque::with_capacity(capacity))),
49 capacity,
50 filter: Some(Arc::new(filter)),
51 }
52 }
53
54 /// Returns the number of events currently held in the buffer.
55 pub fn len(&self) -> usize {
56 self.buffer
57 .lock()
58 .expect("RingBufferSubscriber lock poisoned")
59 .len()
60 }
61
62 /// Returns true if the buffer contains no events.
63 pub fn is_empty(&self) -> bool {
64 self.len() == 0
65 }
66
67 /// Drains all buffered events into a `Vec`, oldest first.
68 pub fn drain(&self) -> Vec<RosaceTrace> {
69 self.buffer
70 .lock()
71 .expect("RingBufferSubscriber lock poisoned")
72 .drain(..)
73 .map(|(_, e)| e)
74 .collect()
75 }
76
77 /// Returns a snapshot of all buffered events, oldest first, without clearing.
78 pub fn snapshot(&self) -> Vec<RosaceTrace> {
79 self.buffer
80 .lock()
81 .expect("RingBufferSubscriber lock poisoned")
82 .iter()
83 .map(|(_, e)| e.clone())
84 .collect()
85 }
86
87 /// Like [`Self::snapshot`], but pairs each event with the `Instant` it
88 /// was recorded at (D123/O1 — the Perfetto/Chrome trace JSON export
89 /// needs a timestamp per event; the events themselves mostly don't
90 /// carry one).
91 pub fn snapshot_timestamped(&self) -> Vec<(Instant, RosaceTrace)> {
92 self.buffer
93 .lock()
94 .expect("RingBufferSubscriber lock poisoned")
95 .iter()
96 .cloned()
97 .collect()
98 }
99
100 /// Export the current buffer as a Chrome/Perfetto-loadable trace JSON
101 /// (Trace Event Format) — drop it in Perfetto UI's "Open trace file"
102 /// for a full flamegraph, for free (D123/O1).
103 pub fn export_perfetto_json(&self) -> String {
104 super::perfetto::to_chrome_trace_json(&self.snapshot_timestamped())
105 }
106}
107
108impl TraceSubscriber for RingBufferSubscriber {
109 fn on_trace(&self, event: &RosaceTrace) {
110 if let Some(f) = &self.filter {
111 if !f(event) {
112 return;
113 }
114 }
115 let mut buf = self
116 .buffer
117 .lock()
118 .expect("RingBufferSubscriber lock poisoned");
119 if buf.len() >= self.capacity {
120 buf.pop_front();
121 }
122 buf.push_back((Instant::now(), event.clone()));
123 }
124}