Skip to main content

rosace_trace/
event.rs

1use std::time::Duration;
2use web_time::Instant;
3
4/// Unique identifier for a component instance in the tree.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub struct ComponentId(pub u64);
7
8/// Unique identifier for an atom instance.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct AtomId(pub u64);
11
12/// Unique identifier for a network request.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct RequestId(pub u64);
15
16/// Severity of a user-facing log record (`info!`/`warn!`/… macros). Ordered
17/// most-severe → least, so a max-level filter is a simple `<=` on the `u8`.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19#[repr(u8)]
20pub enum LogLevel {
21    Error = 0,
22    Warn = 1,
23    Info = 2,
24    Debug = 3,
25    Trace = 4,
26}
27
28impl LogLevel {
29    /// Uppercase 5-char label used in console/DevTools output.
30    pub fn label(self) -> &'static str {
31        match self {
32            LogLevel::Error => "ERROR",
33            LogLevel::Warn => "WARN ",
34            LogLevel::Info => "INFO ",
35            LogLevel::Debug => "DEBUG",
36            LogLevel::Trace => "TRACE",
37        }
38    }
39
40    /// ANSI color code for a colored terminal sink (bright red/yellow/… by level).
41    pub fn ansi(self) -> &'static str {
42        match self {
43            LogLevel::Error => "\x1b[1;31m", // bold red
44            LogLevel::Warn => "\x1b[33m",    // yellow
45            LogLevel::Info => "\x1b[32m",    // green
46            LogLevel::Debug => "\x1b[36m",   // cyan
47            LogLevel::Trace => "\x1b[2;37m", // dim grey
48        }
49    }
50}
51
52/// Source location captured at a trace emit site.
53#[derive(Debug, Clone)]
54pub struct Location {
55    pub file: &'static str,
56    pub line: u32,
57}
58
59/// 2D size in logical pixels.
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct Size {
62    pub width: f32,
63    pub height: f32,
64}
65
66/// 2D point in logical pixels.
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct Point {
69    pub x: f32,
70    pub y: f32,
71}
72
73/// Axis-aligned rectangle in logical pixels.
74#[derive(Debug, Clone, Copy, PartialEq)]
75pub struct Rect {
76    pub origin: Point,
77    pub size: Size,
78}
79
80/// Simplified layout constraints carried in trace events.
81///
82/// `None` on a max field means the axis is unbounded (scroll axis or top-level).
83#[derive(Debug, Clone)]
84pub struct TraceConstraints {
85    pub min_width: f32,
86    pub max_width: Option<f32>,
87    pub min_height: f32,
88    pub max_height: Option<f32>,
89}
90
91/// A snapshot of an atom's value for trace events.
92#[derive(Debug, Clone)]
93pub enum TraceValue {
94    /// Value formatted via its `Debug` impl.
95    Debug(String),
96    /// Value type does not implement `Debug`.
97    Opaque,
98}
99
100/// Why a component was scheduled for rebuild by the refresh engine.
101#[derive(Debug, Clone)]
102pub enum RebuildCause {
103    /// A subscribed atom changed.
104    AtomChanged(AtomId),
105    /// Parent component was rebuilt, child must follow.
106    ParentRebuilt,
107    /// Component's own props changed.
108    PropsChanged,
109    /// Manually triggered rebuild.
110    Manual,
111}
112
113/// A navigation route (opaque string for tracing; typed routes live in rosace-nav).
114#[derive(Debug, Clone)]
115pub struct Route(pub String);
116
117/// A navigation transition name.
118#[derive(Debug, Clone)]
119pub struct Transition(pub String);
120
121/// HTTP method for request tracing.
122#[derive(Debug, Clone)]
123pub enum Method {
124    Get,
125    Post,
126    Put,
127    Delete,
128    Patch,
129    Other(String),
130}
131
132/// Input gesture kind.
133#[derive(Debug, Clone)]
134pub enum GestureKind {
135    Tap,
136    LongPress,
137    Drag,
138    Swipe,
139    Pinch,
140    Scroll,
141}
142
143/// Unified event type emitted by all ROSACE systems.
144///
145/// All emit sites are gated behind `#[cfg(debug_assertions)]` via the `trace!()`
146/// macro — zero cost in production builds.
147#[derive(Debug, Clone)]
148pub enum RosaceTrace {
149    /// A component was added to the tree.
150    ComponentMount {
151        id: ComponentId,
152        name: &'static str,
153        location: Location,
154    },
155    /// A component was removed from the tree.
156    ComponentUnmount {
157        id: ComponentId,
158        name: &'static str,
159    },
160    /// A component was rebuilt by the refresh engine.
161    ComponentRebuild {
162        id: ComponentId,
163        cause: RebuildCause,
164        duration: Duration,
165    },
166    /// An atom value was read; the reading component auto-subscribed.
167    AtomRead {
168        atom: AtomId,
169        component: ComponentId,
170    },
171    /// An atom value was written.
172    AtomWrite {
173        atom: AtomId,
174        old: TraceValue,
175        new: TraceValue,
176        by: ComponentId,
177        location: Location,
178    },
179    /// Layout measurement pass started for a component.
180    LayoutStart {
181        component: ComponentId,
182        constraints: TraceConstraints,
183    },
184    /// Layout measurement pass completed for a component.
185    LayoutEnd {
186        component: ComponentId,
187        size: Size,
188        duration: Duration,
189    },
190    /// A new frame render began.
191    FrameStart {
192        frame: u64,
193        timestamp: Instant,
194    },
195    /// A frame render completed.
196    FrameEnd {
197        frame: u64,
198        duration: Duration,
199        /// True if this frame exceeded the 16.67ms (60fps) or 8.33ms (120fps) budget.
200        dropped: bool,
201    },
202    /// A dirty screen region was repainted.
203    PaintRegion {
204        rect: Rect,
205    },
206    /// The active route changed.
207    RouteChange {
208        from: Option<Route>,
209        to: Route,
210        transition: Transition,
211    },
212    /// A network request was initiated.
213    RequestStart {
214        id: RequestId,
215        url: String,
216        method: Method,
217        component: ComponentId,
218    },
219    /// A network request completed.
220    RequestEnd {
221        id: RequestId,
222        status: u16,
223        duration: Duration,
224        cached: bool,
225        size: usize,
226    },
227    /// An FFI call returned successfully.
228    FfiCall {
229        fn_name: &'static str,
230        duration: Duration,
231    },
232    /// An FFI call returned an error.
233    FfiError {
234        fn_name: &'static str,
235        error: String,
236    },
237    /// A gesture was received and dispatched to a handler.
238    GestureReceived {
239        kind: GestureKind,
240        handler: ComponentId,
241    },
242    /// A shader pipeline was registered (D109). Emitted at `register_shader`
243    /// time — before compilation, which happens when the platform drains the
244    /// queue into the compositor (eager, never lazy-on-first-paint).
245    ShaderRegister {
246        pipeline: u64,
247        wgsl_len: usize,
248    },
249    /// A user-facing log record from the `info!`/`warn!`/`error!`/`debug!`/
250    /// `log!` macros. Unlike the structured events above (debug-only), logs
251    /// flow in release too, subject to the global level filter — so the same
252    /// interceptor bus carries framework traces AND app logs to every sink
253    /// (colored console, DevTools panel, a browser-tools socket, third parties).
254    Log {
255        level: LogLevel,
256        /// The emitting module path (`module_path!()`).
257        target: &'static str,
258        message: String,
259        timestamp: Instant,
260    },
261}
262
263/// Coarse grouping of trace events for filtered sinks (D123/O1). A DevTools
264/// panel or the console subscriber picks the categories it cares about
265/// instead of drowning in everything.
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
267pub enum TraceCategory {
268    /// Atom reads/writes — reactive state flow.
269    State,
270    /// Component mount/unmount/rebuild.
271    Lifecycle,
272    /// Layout passes.
273    Layout,
274    /// Frame boundaries (per-frame — high frequency).
275    Frame,
276    /// Paint regions (per-frame — high frequency).
277    Render,
278    /// Navigation / route changes.
279    Route,
280    /// HTTP / WebSocket request lifecycle.
281    Network,
282    /// FFI boundary crossings.
283    Ffi,
284    /// Gesture recognition.
285    Gesture,
286    /// GPU shader registration.
287    Shader,
288    /// User-facing log records (`info!`/`warn!`/…).
289    Log,
290}
291
292impl RosaceTrace {
293    /// The event's category — for filtered sinks (D123/O1).
294    pub fn category(&self) -> TraceCategory {
295        match self {
296            RosaceTrace::ComponentMount { .. }
297            | RosaceTrace::ComponentUnmount { .. }
298            | RosaceTrace::ComponentRebuild { .. } => TraceCategory::Lifecycle,
299            RosaceTrace::AtomRead { .. } | RosaceTrace::AtomWrite { .. } => TraceCategory::State,
300            RosaceTrace::LayoutStart { .. } | RosaceTrace::LayoutEnd { .. } => TraceCategory::Layout,
301            RosaceTrace::FrameStart { .. } | RosaceTrace::FrameEnd { .. } => TraceCategory::Frame,
302            RosaceTrace::PaintRegion { .. } => TraceCategory::Render,
303            RosaceTrace::RouteChange { .. } => TraceCategory::Route,
304            RosaceTrace::RequestStart { .. } | RosaceTrace::RequestEnd { .. } => TraceCategory::Network,
305            RosaceTrace::FfiCall { .. } | RosaceTrace::FfiError { .. } => TraceCategory::Ffi,
306            RosaceTrace::GestureReceived { .. } => TraceCategory::Gesture,
307            RosaceTrace::ShaderRegister { .. } => TraceCategory::Shader,
308            RosaceTrace::Log { .. } => TraceCategory::Log,
309        }
310    }
311
312    /// True for events that fire once (or more) EVERY frame — the ones that
313    /// turned a naive console subscriber into a per-frame firehose and hung
314    /// the app (D123/O1). No visible sink or the default flight recorder
315    /// should ever accept these; they exist for opt-in deep profiling only.
316    ///
317    /// `AtomRead` is included because it fires on every `atom.get()` during
318    /// paint — the single loudest event in the system.
319    pub fn is_high_frequency(&self) -> bool {
320        matches!(
321            self,
322            RosaceTrace::AtomRead { .. }
323                | RosaceTrace::FrameStart { .. }
324                | RosaceTrace::FrameEnd { .. }
325                | RosaceTrace::PaintRegion { .. }
326                | RosaceTrace::LayoutStart { .. }
327                | RosaceTrace::LayoutEnd { .. }
328        )
329    }
330}
331
332#[cfg(test)]
333mod category_tests {
334    use super::*;
335
336    #[test]
337    fn atom_read_is_high_frequency_state() {
338        let e = RosaceTrace::AtomRead {
339            atom: AtomId(1),
340            component: ComponentId(1),
341        };
342        assert_eq!(e.category(), TraceCategory::State);
343        assert!(e.is_high_frequency(), "AtomRead is the loudest event — must be high-frequency");
344    }
345
346    #[test]
347    fn atom_write_is_state_but_not_high_frequency() {
348        let e = RosaceTrace::AtomWrite {
349            atom: AtomId(1),
350            old: TraceValue::Opaque,
351            new: TraceValue::Opaque,
352            by: ComponentId(1),
353            location: crate::location!(),
354        };
355        assert_eq!(e.category(), TraceCategory::State);
356        assert!(!e.is_high_frequency(), "a state CHANGE is meaningful, not per-frame noise");
357    }
358
359    #[test]
360    fn frame_and_paint_events_are_high_frequency() {
361        let frame = RosaceTrace::FrameStart { frame: 0, timestamp: std::time::Instant::now() };
362        assert!(frame.is_high_frequency());
363        assert_eq!(frame.category(), TraceCategory::Frame);
364    }
365
366    #[test]
367    fn network_and_lifecycle_are_meaningful() {
368        let mount = RosaceTrace::ComponentMount {
369            id: ComponentId(1), name: "X", location: crate::location!(),
370        };
371        assert_eq!(mount.category(), TraceCategory::Lifecycle);
372        assert!(!mount.is_high_frequency());
373    }
374}