rosace-trace 0.1.0

Event bus, ring buffer, and flight-recorder logging framework for ROSACE
Documentation
use std::time::Duration;
use web_time::Instant;

/// Unique identifier for a component instance in the tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ComponentId(pub u64);

/// Unique identifier for an atom instance.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AtomId(pub u64);

/// Unique identifier for a network request.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RequestId(pub u64);

/// Severity of a user-facing log record (`info!`/`warn!`/… macros). Ordered
/// most-severe → least, so a max-level filter is a simple `<=` on the `u8`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum LogLevel {
    Error = 0,
    Warn = 1,
    Info = 2,
    Debug = 3,
    Trace = 4,
}

impl LogLevel {
    /// Uppercase 5-char label used in console/DevTools output.
    pub fn label(self) -> &'static str {
        match self {
            LogLevel::Error => "ERROR",
            LogLevel::Warn => "WARN ",
            LogLevel::Info => "INFO ",
            LogLevel::Debug => "DEBUG",
            LogLevel::Trace => "TRACE",
        }
    }

    /// ANSI color code for a colored terminal sink (bright red/yellow/… by level).
    pub fn ansi(self) -> &'static str {
        match self {
            LogLevel::Error => "\x1b[1;31m", // bold red
            LogLevel::Warn => "\x1b[33m",    // yellow
            LogLevel::Info => "\x1b[32m",    // green
            LogLevel::Debug => "\x1b[36m",   // cyan
            LogLevel::Trace => "\x1b[2;37m", // dim grey
        }
    }
}

/// Source location captured at a trace emit site.
#[derive(Debug, Clone)]
pub struct Location {
    pub file: &'static str,
    pub line: u32,
}

/// 2D size in logical pixels.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Size {
    pub width: f32,
    pub height: f32,
}

/// 2D point in logical pixels.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
    pub x: f32,
    pub y: f32,
}

/// Axis-aligned rectangle in logical pixels.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
    pub origin: Point,
    pub size: Size,
}

/// Simplified layout constraints carried in trace events.
///
/// `None` on a max field means the axis is unbounded (scroll axis or top-level).
#[derive(Debug, Clone)]
pub struct TraceConstraints {
    pub min_width: f32,
    pub max_width: Option<f32>,
    pub min_height: f32,
    pub max_height: Option<f32>,
}

/// A snapshot of an atom's value for trace events.
#[derive(Debug, Clone)]
pub enum TraceValue {
    /// Value formatted via its `Debug` impl.
    Debug(String),
    /// Value type does not implement `Debug`.
    Opaque,
}

/// Why a component was scheduled for rebuild by the refresh engine.
#[derive(Debug, Clone)]
pub enum RebuildCause {
    /// A subscribed atom changed.
    AtomChanged(AtomId),
    /// Parent component was rebuilt, child must follow.
    ParentRebuilt,
    /// Component's own props changed.
    PropsChanged,
    /// Manually triggered rebuild.
    Manual,
}

/// A navigation route (opaque string for tracing; typed routes live in rosace-nav).
#[derive(Debug, Clone)]
pub struct Route(pub String);

/// A navigation transition name.
#[derive(Debug, Clone)]
pub struct Transition(pub String);

/// HTTP method for request tracing.
#[derive(Debug, Clone)]
pub enum Method {
    Get,
    Post,
    Put,
    Delete,
    Patch,
    Other(String),
}

/// Input gesture kind.
#[derive(Debug, Clone)]
pub enum GestureKind {
    Tap,
    LongPress,
    Drag,
    Swipe,
    Pinch,
    Scroll,
}

/// Unified event type emitted by all ROSACE systems.
///
/// All emit sites are gated behind `#[cfg(debug_assertions)]` via the `trace!()`
/// macro — zero cost in production builds.
#[derive(Debug, Clone)]
pub enum RosaceTrace {
    /// A component was added to the tree.
    ComponentMount {
        id: ComponentId,
        name: &'static str,
        location: Location,
    },
    /// A component was removed from the tree.
    ComponentUnmount {
        id: ComponentId,
        name: &'static str,
    },
    /// A component was rebuilt by the refresh engine.
    ComponentRebuild {
        id: ComponentId,
        cause: RebuildCause,
        duration: Duration,
    },
    /// An atom value was read; the reading component auto-subscribed.
    AtomRead {
        atom: AtomId,
        component: ComponentId,
    },
    /// An atom value was written.
    AtomWrite {
        atom: AtomId,
        old: TraceValue,
        new: TraceValue,
        by: ComponentId,
        location: Location,
    },
    /// Layout measurement pass started for a component.
    LayoutStart {
        component: ComponentId,
        constraints: TraceConstraints,
    },
    /// Layout measurement pass completed for a component.
    LayoutEnd {
        component: ComponentId,
        size: Size,
        duration: Duration,
    },
    /// A new frame render began.
    FrameStart {
        frame: u64,
        timestamp: Instant,
    },
    /// A frame render completed.
    FrameEnd {
        frame: u64,
        duration: Duration,
        /// True if this frame exceeded the 16.67ms (60fps) or 8.33ms (120fps) budget.
        dropped: bool,
    },
    /// A dirty screen region was repainted.
    PaintRegion {
        rect: Rect,
    },
    /// The active route changed.
    RouteChange {
        from: Option<Route>,
        to: Route,
        transition: Transition,
    },
    /// A network request was initiated.
    RequestStart {
        id: RequestId,
        url: String,
        method: Method,
        component: ComponentId,
    },
    /// A network request completed.
    RequestEnd {
        id: RequestId,
        status: u16,
        duration: Duration,
        cached: bool,
        size: usize,
    },
    /// An FFI call returned successfully.
    FfiCall {
        fn_name: &'static str,
        duration: Duration,
    },
    /// An FFI call returned an error.
    FfiError {
        fn_name: &'static str,
        error: String,
    },
    /// A gesture was received and dispatched to a handler.
    GestureReceived {
        kind: GestureKind,
        handler: ComponentId,
    },
    /// A shader pipeline was registered (D109). Emitted at `register_shader`
    /// time — before compilation, which happens when the platform drains the
    /// queue into the compositor (eager, never lazy-on-first-paint).
    ShaderRegister {
        pipeline: u64,
        wgsl_len: usize,
    },
    /// A user-facing log record from the `info!`/`warn!`/`error!`/`debug!`/
    /// `log!` macros. Unlike the structured events above (debug-only), logs
    /// flow in release too, subject to the global level filter — so the same
    /// interceptor bus carries framework traces AND app logs to every sink
    /// (colored console, DevTools panel, a browser-tools socket, third parties).
    Log {
        level: LogLevel,
        /// The emitting module path (`module_path!()`).
        target: &'static str,
        message: String,
        timestamp: Instant,
    },
}

/// Coarse grouping of trace events for filtered sinks (D123/O1). A DevTools
/// panel or the console subscriber picks the categories it cares about
/// instead of drowning in everything.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TraceCategory {
    /// Atom reads/writes — reactive state flow.
    State,
    /// Component mount/unmount/rebuild.
    Lifecycle,
    /// Layout passes.
    Layout,
    /// Frame boundaries (per-frame — high frequency).
    Frame,
    /// Paint regions (per-frame — high frequency).
    Render,
    /// Navigation / route changes.
    Route,
    /// HTTP / WebSocket request lifecycle.
    Network,
    /// FFI boundary crossings.
    Ffi,
    /// Gesture recognition.
    Gesture,
    /// GPU shader registration.
    Shader,
    /// User-facing log records (`info!`/`warn!`/…).
    Log,
}

impl RosaceTrace {
    /// The event's category — for filtered sinks (D123/O1).
    pub fn category(&self) -> TraceCategory {
        match self {
            RosaceTrace::ComponentMount { .. }
            | RosaceTrace::ComponentUnmount { .. }
            | RosaceTrace::ComponentRebuild { .. } => TraceCategory::Lifecycle,
            RosaceTrace::AtomRead { .. } | RosaceTrace::AtomWrite { .. } => TraceCategory::State,
            RosaceTrace::LayoutStart { .. } | RosaceTrace::LayoutEnd { .. } => TraceCategory::Layout,
            RosaceTrace::FrameStart { .. } | RosaceTrace::FrameEnd { .. } => TraceCategory::Frame,
            RosaceTrace::PaintRegion { .. } => TraceCategory::Render,
            RosaceTrace::RouteChange { .. } => TraceCategory::Route,
            RosaceTrace::RequestStart { .. } | RosaceTrace::RequestEnd { .. } => TraceCategory::Network,
            RosaceTrace::FfiCall { .. } | RosaceTrace::FfiError { .. } => TraceCategory::Ffi,
            RosaceTrace::GestureReceived { .. } => TraceCategory::Gesture,
            RosaceTrace::ShaderRegister { .. } => TraceCategory::Shader,
            RosaceTrace::Log { .. } => TraceCategory::Log,
        }
    }

    /// True for events that fire once (or more) EVERY frame — the ones that
    /// turned a naive console subscriber into a per-frame firehose and hung
    /// the app (D123/O1). No visible sink or the default flight recorder
    /// should ever accept these; they exist for opt-in deep profiling only.
    ///
    /// `AtomRead` is included because it fires on every `atom.get()` during
    /// paint — the single loudest event in the system.
    pub fn is_high_frequency(&self) -> bool {
        matches!(
            self,
            RosaceTrace::AtomRead { .. }
                | RosaceTrace::FrameStart { .. }
                | RosaceTrace::FrameEnd { .. }
                | RosaceTrace::PaintRegion { .. }
                | RosaceTrace::LayoutStart { .. }
                | RosaceTrace::LayoutEnd { .. }
        )
    }
}

#[cfg(test)]
mod category_tests {
    use super::*;

    #[test]
    fn atom_read_is_high_frequency_state() {
        let e = RosaceTrace::AtomRead {
            atom: AtomId(1),
            component: ComponentId(1),
        };
        assert_eq!(e.category(), TraceCategory::State);
        assert!(e.is_high_frequency(), "AtomRead is the loudest event — must be high-frequency");
    }

    #[test]
    fn atom_write_is_state_but_not_high_frequency() {
        let e = RosaceTrace::AtomWrite {
            atom: AtomId(1),
            old: TraceValue::Opaque,
            new: TraceValue::Opaque,
            by: ComponentId(1),
            location: crate::location!(),
        };
        assert_eq!(e.category(), TraceCategory::State);
        assert!(!e.is_high_frequency(), "a state CHANGE is meaningful, not per-frame noise");
    }

    #[test]
    fn frame_and_paint_events_are_high_frequency() {
        let frame = RosaceTrace::FrameStart { frame: 0, timestamp: std::time::Instant::now() };
        assert!(frame.is_high_frequency());
        assert_eq!(frame.category(), TraceCategory::Frame);
    }

    #[test]
    fn network_and_lifecycle_are_meaningful() {
        let mount = RosaceTrace::ComponentMount {
            id: ComponentId(1), name: "X", location: crate::location!(),
        };
        assert_eq!(mount.category(), TraceCategory::Lifecycle);
        assert!(!mount.is_high_frequency());
    }
}