Skip to main content

rosace_trace/subscribers/
console.rs

1use std::sync::atomic::{AtomicUsize, Ordering};
2
3use crate::bus::TraceSubscriber;
4use crate::event::{Method, RosaceTrace};
5
6/// Controls which event categories the `ConsoleSubscriber` prints.
7///
8/// Mirrors the `--trace=<category>` CLI flags from `rsc dev`.
9#[derive(Debug, Clone, PartialEq)]
10pub enum ConsoleFilter {
11    /// Print all events.
12    All,
13    /// Print only atom read/write events.
14    State,
15    /// Print only network request events.
16    Network,
17    /// Print only frame and layout timing events.
18    Performance,
19    /// Print only events for a specific component name.
20    Component(String),
21}
22
23/// Writes formatted trace events to stderr.
24///
25/// Output format mirrors the Phase 1 dev-tools terminal layout:
26/// ```text
27/// [MOUNT]   HomeScreen        src/screens/home.rs:12
28/// [ATOM]    FEED.set()        64 items
29/// [REBUILD] FeedList          cause: FEED  0.8ms
30/// [FRAME]   #847              2.1ms ✓
31/// [REQUEST] GET /api/feed     200  145ms  cache:miss
32/// ```
33pub struct ConsoleSubscriber {
34    filter: ConsoleFilter,
35    event_count: AtomicUsize,
36    /// When false (default), high-frequency events (AtomRead/FrameStart/…)
37    /// are never printed — the firehose that made the naive console
38    /// subscriber unusable (D123/O1). `Performance` mode still needs frame
39    /// events, so this is only enforced OUTSIDE that filter unless opted in.
40    include_high_frequency: bool,
41}
42
43impl ConsoleSubscriber {
44    /// Creates a new console subscriber printing all events.
45    pub fn new() -> Self {
46        Self {
47            filter: ConsoleFilter::All,
48            event_count: AtomicUsize::new(0),
49            include_high_frequency: false,
50        }
51    }
52
53    /// Creates a new console subscriber with the given filter.
54    pub fn with_filter(filter: ConsoleFilter) -> Self {
55        Self {
56            filter,
57            event_count: AtomicUsize::new(0),
58            include_high_frequency: false,
59        }
60    }
61
62    /// Opt into printing high-frequency events (AtomRead/FrameStart/…) —
63    /// off by default (D123/O1). Only enable for deep, short-lived
64    /// profiling; leaving it on floods the terminal every frame.
65    pub fn high_frequency(mut self, on: bool) -> Self {
66        self.include_high_frequency = on;
67        self
68    }
69
70    /// Returns the total number of events received (regardless of filter).
71    pub fn event_count(&self) -> usize {
72        self.event_count.load(Ordering::Relaxed)
73    }
74
75    fn should_print(&self, event: &RosaceTrace) -> bool {
76        // The governing rule (D123/O1): high-frequency events never reach a
77        // visible sink unless explicitly opted in — EXCEPT under the
78        // Performance filter, which exists specifically to watch frame
79        // timing and would be pointless without them.
80        if event.is_high_frequency()
81            && !self.include_high_frequency
82            && !matches!(self.filter, ConsoleFilter::Performance)
83        {
84            return false;
85        }
86        match &self.filter {
87            ConsoleFilter::All => true,
88            ConsoleFilter::State => {
89                matches!(event, RosaceTrace::AtomRead { .. } | RosaceTrace::AtomWrite { .. })
90            }
91            ConsoleFilter::Network => matches!(
92                event,
93                RosaceTrace::RequestStart { .. } | RosaceTrace::RequestEnd { .. }
94            ),
95            ConsoleFilter::Performance => matches!(
96                event,
97                RosaceTrace::FrameStart { .. }
98                    | RosaceTrace::FrameEnd { .. }
99                    | RosaceTrace::LayoutStart { .. }
100                    | RosaceTrace::LayoutEnd { .. }
101            ),
102            ConsoleFilter::Component(name) => match event {
103                RosaceTrace::ComponentMount { name: n, .. } => *n == name.as_str(),
104                RosaceTrace::ComponentUnmount { name: n, .. } => *n == name.as_str(),
105                // Rebuilds carry ComponentId, not name — include all for now.
106                RosaceTrace::ComponentRebuild { .. } => true,
107                _ => false,
108            },
109        }
110    }
111
112    pub fn format(event: &RosaceTrace) -> String {
113        match event {
114            RosaceTrace::ComponentMount { name, location, .. } => {
115                format!(
116                    "[MOUNT]   {:<20} {}:{}",
117                    name, location.file, location.line
118                )
119            }
120            RosaceTrace::ComponentUnmount { name, .. } => {
121                format!("[UNMOUNT] {}", name)
122            }
123            RosaceTrace::ComponentRebuild { cause, duration, .. } => {
124                format!(
125                    "[REBUILD] cause: {:?}  {:.1}ms",
126                    cause,
127                    duration.as_secs_f64() * 1000.0
128                )
129            }
130            RosaceTrace::AtomRead { atom, component } => {
131                format!("[ATOM]    read  atom:{} by component:{}", atom.0, component.0)
132            }
133            RosaceTrace::AtomWrite { atom, location, .. } => {
134                format!(
135                    "[ATOM]    write atom:{}  {}:{}",
136                    atom.0, location.file, location.line
137                )
138            }
139            RosaceTrace::LayoutStart { component, constraints } => {
140                format!(
141                    "[LAYOUT]  start component:{}  w:[{:.0}..{:?}] h:[{:.0}..{:?}]",
142                    component.0,
143                    constraints.min_width,
144                    constraints.max_width,
145                    constraints.min_height,
146                    constraints.max_height
147                )
148            }
149            RosaceTrace::LayoutEnd { component, size, duration } => {
150                format!(
151                    "[LAYOUT]  end   component:{}  {:.0}x{:.0}  {:.2}ms",
152                    component.0,
153                    size.width,
154                    size.height,
155                    duration.as_secs_f64() * 1000.0
156                )
157            }
158            RosaceTrace::FrameStart { frame, .. } => {
159                format!("[FRAME]   #{:<6} start", frame)
160            }
161            RosaceTrace::FrameEnd { frame, duration, dropped } => {
162                let budget = if *dropped { "✗ DROPPED" } else { "✓" };
163                format!(
164                    "[FRAME]   #{:<6} {:.2}ms {}",
165                    frame,
166                    duration.as_secs_f64() * 1000.0,
167                    budget
168                )
169            }
170            RosaceTrace::PaintRegion { rect } => {
171                format!(
172                    "[PAINT]   ({:.0},{:.0}) {:.0}x{:.0}",
173                    rect.origin.x, rect.origin.y, rect.size.width, rect.size.height
174                )
175            }
176            RosaceTrace::RouteChange { from, to, transition } => {
177                let from_str = from.as_ref().map(|r| r.0.as_str()).unwrap_or("(none)");
178                format!(
179                    "[ROUTE]   {} → {}  {}",
180                    from_str, to.0, transition.0
181                )
182            }
183            RosaceTrace::RequestStart { url, method, .. } => {
184                let m = match method {
185                    Method::Get => "GET",
186                    Method::Post => "POST",
187                    Method::Put => "PUT",
188                    Method::Delete => "DELETE",
189                    Method::Patch => "PATCH",
190                    Method::Other(s) => s.as_str(),
191                };
192                format!("[REQUEST] {} {}", m, url)
193            }
194            RosaceTrace::RequestEnd { id, status, duration, cached, size } => {
195                let cache = if *cached { "cache:hit" } else { "cache:miss" };
196                format!(
197                    "[REQUEST] id:{}  {}  {:.0}ms  {}  {}b",
198                    id.0,
199                    status,
200                    duration.as_secs_f64() * 1000.0,
201                    cache,
202                    size
203                )
204            }
205            RosaceTrace::FfiCall { fn_name, duration } => {
206                format!(
207                    "[FFI]     {}  {:.2}ms",
208                    fn_name,
209                    duration.as_secs_f64() * 1000.0
210                )
211            }
212            RosaceTrace::FfiError { fn_name, error } => {
213                format!("[FFI]     {} ERROR: {}", fn_name, error)
214            }
215            RosaceTrace::GestureReceived { kind, handler } => {
216                format!("[GESTURE] {:?}  handler:{}", kind, handler.0)
217            }
218            RosaceTrace::ShaderRegister { pipeline, wgsl_len } => {
219                format!("[SHADER]  register pipeline:{}  wgsl:{}b", pipeline, wgsl_len)
220            }
221            RosaceTrace::Log { level, target, message, .. } => {
222                format!("[{}] {} {}", level.label().trim(), target, message)
223            }
224        }
225    }
226}
227
228impl Default for ConsoleSubscriber {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234impl TraceSubscriber for ConsoleSubscriber {
235    fn on_trace(&self, event: &RosaceTrace) {
236        self.event_count.fetch_add(1, Ordering::Relaxed);
237        if self.should_print(event) {
238            eprintln!("{}", Self::format(event));
239        }
240    }
241}