Skip to main content

rivet/
log.rs

1//! Deferred-formatting logging: [`log!`](crate::log!) is safe to call from
2//! ISR context (it does no formatting, no allocation, and never blocks —
3//! it just pushes a `{level, task_id, timestamp, message}` frame into a
4//! ring buffer), and a drain task formats and writes frames to the
5//! console at its own pace, off the hot path.
6//!
7//! # Scope note (plan.md Phase 8, extended by Phase 16)
8//!
9//! The old plan (§6.5) called for interning format strings into a
10//! `.rivet_log_fmt` linker section, storing only a small integer index per
11//! frame, and decoding on the host from the ELF's debug info (a
12//! `rivet-decode` crate). This module takes a simpler route that still
13//! delivers the properties that actually matter (ISR-safe, O(1) in the
14//! hot path, deferred formatting, lock-free ring buffer, dropped-frame
15//! accounting): a frame stores the message as a plain `&'static str`
16//! pointer + length, plus (Phase 16) up to two [`LogArg`] values —
17//! **not** a full `format_args!`-style template (that needs the
18//! interned-format-string + host-decoder machinery the old plan
19//! described, still not attempted here). `log!("x={}", x)` covers the
20//! large majority of real call sites, which log one or two values
21//! alongside a fixed message; `write_frame` substitutes each `{}` in
22//! `msg` with the corresponding argument, formatted at drain time (off
23//! the hot path, same as everything else here).
24//!
25//! The ring buffer is Rivet's own SPSC [`crate::sync::Channel`] — but
26//! logging is inherently **multi**-producer (any task or ISR might log,
27//! on any hart), so every producer path goes through
28//! [`crate::critical::enter`] to serialize pushes into a single logical
29//! producer. Since plan.md Phase 19, `critical::enter` is a genuine
30//! cross-hart lock (not just a local interrupt mask), so this holds under
31//! real SMP too, not only the single-hart case.
32
33use crate::sync::{Channel, Once, Receiver, Sender};
34use crate::sync::atomic::{AtomicU32, Ordering};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Level {
38    Trace,
39    Debug,
40    Info,
41    Warn,
42    Error,
43}
44
45impl Level {
46    fn as_str(self) -> &'static str {
47        match self {
48            Level::Trace => "TRACE",
49            Level::Debug => "DEBUG",
50            Level::Info => "INFO",
51            Level::Warn => "WARN",
52            Level::Error => "ERROR",
53        }
54    }
55}
56
57/// A single interpolated argument (plan.md Phase 16): a small closed set
58/// covering the large majority of real log call sites, not a general
59/// `Display`/`Debug` payload (which would need real formatting work done
60/// eagerly, defeating the point of deferring it to drain time).
61#[derive(Clone, Copy)]
62pub enum LogArg {
63    /// No argument in this slot (fewer than 2 given to `log!`).
64    None,
65    U32(u32),
66    I32(i32),
67    F32(f32),
68    Str(&'static str),
69}
70
71impl From<u32> for LogArg {
72    fn from(v: u32) -> Self {
73        LogArg::U32(v)
74    }
75}
76impl From<i32> for LogArg {
77    fn from(v: i32) -> Self {
78        LogArg::I32(v)
79    }
80}
81impl From<f32> for LogArg {
82    fn from(v: f32) -> Self {
83        LogArg::F32(v)
84    }
85}
86impl From<&'static str> for LogArg {
87    fn from(v: &'static str) -> Self {
88        LogArg::Str(v)
89    }
90}
91
92#[derive(Clone, Copy)]
93pub struct LogFrame {
94    pub level: Level,
95    /// Preemptive task id, or `None` if logged from a context with no
96    /// current task (e.g. before the scheduler starts).
97    pub task_id: Option<u16>,
98    pub timestamp_us: u32,
99    pub msg: &'static str,
100    /// Up to two interpolated arguments, substituted in order for each
101    /// `{}` in `msg` at drain time. `LogArg::None` in a slot the message
102    /// doesn't reference is simply unused.
103    pub args: [LogArg; 2],
104}
105
106/// Ring capacity. Deliberately not wired into `RIVET_*` build-time config
107/// (plan.md §4.1) yet — a fixed default is the honest starting point for a
108/// feature this new; making it configurable is a small follow-up once
109/// there's a real workload to size it against.
110const CAPACITY: usize = 16;
111
112#[cfg(not(loom))]
113static CHANNEL: Channel<LogFrame, CAPACITY> = Channel::new();
114#[cfg(loom)]
115loom::lazy_static! {
116    static ref CHANNEL: Channel<LogFrame, CAPACITY> = Channel::new();
117}
118
119#[cfg(not(loom))]
120static SENDER: Once<Sender<'static, LogFrame, CAPACITY>> = Once::new();
121#[cfg(loom)]
122loom::lazy_static! {
123    static ref SENDER: Once<Sender<'static, LogFrame, CAPACITY>> = Once::new();
124}
125
126#[cfg(not(loom))]
127static RECEIVER: Once<Receiver<'static, LogFrame, CAPACITY>> = Once::new();
128#[cfg(loom)]
129loom::lazy_static! {
130    static ref RECEIVER: Once<Receiver<'static, LogFrame, CAPACITY>> = Once::new();
131}
132#[cfg(not(loom))]
133static DROPPED: AtomicU32 = AtomicU32::new(0);
134#[cfg(loom)]
135loom::lazy_static! {
136    static ref DROPPED: AtomicU32 = AtomicU32::new(0);
137}
138
139/// Called once from [`crate::init`]. Splitting the channel here (rather
140/// than lazily on first use) means the first `log!` call anywhere is
141/// never the one paying for initialization, and keeps `push`'s hot path
142/// to just a critical-section-guarded `try_send`.
143pub(crate) fn init() {
144    if let Some((tx, rx)) = CHANNEL.split() {
145        let _ = SENDER.set(tx);
146        let _ = RECEIVER.set(rx);
147    }
148}
149
150/// Push a frame. ISR-safe: no allocation, no unbounded loops, never
151/// blocks — a full ring drops the frame and counts it (see
152/// [`dropped_frames`]) rather than backing up whatever called this.
153#[doc(hidden)]
154pub fn push(level: Level, msg: &'static str, arg0: LogArg, arg1: LogArg) {
155    let task_id = crate::preempt::sched::current().map(|id| id as u16);
156    let timestamp_us = crate::port::board::now_us() as u32;
157    let frame = LogFrame {
158        level,
159        task_id,
160        timestamp_us,
161        msg,
162        args: [arg0, arg1],
163    };
164    let sent = match SENDER.get() {
165        // SAFETY-relevant, not memory-safety: `try_send` requires a
166        // single logical producer; the critical section serializes
167        // however many concurrent callers (tasks and/or ISRs) there are
168        // into one.
169        Some(tx) => crate::critical::enter(|| tx.try_send(frame)).is_ok(),
170        None => false,
171    };
172    if !sent {
173        DROPPED.fetch_add(1, Ordering::Relaxed);
174    }
175}
176
177/// Number of frames dropped so far because the ring was full (or logging
178/// hadn't been initialized yet — i.e. called before [`crate::init`]).
179pub fn dropped_frames() -> usize {
180    DROPPED.load(Ordering::Relaxed) as usize
181}
182
183fn write_frame(frame: &LogFrame) {
184    crate::console::write_str("[");
185    crate::console::write_str(frame.level.as_str());
186    crate::console::write_str("] t=");
187    write_dec(frame.timestamp_us as usize);
188    if let Some(id) = frame.task_id {
189        crate::console::write_str(" task=");
190        write_dec(id as usize);
191    }
192    crate::console::write_str(" ");
193    write_interpolated(frame.msg, &frame.args);
194    crate::console::write_str("\n");
195}
196
197/// Write `msg`, substituting each `{}` (in order) with the corresponding
198/// entry of `args`. Extra `{}` beyond the two argument slots are written
199/// through literally, since there's nothing to fill them with — no
200/// silent truncation of the message.
201fn write_interpolated(msg: &str, args: &[LogArg; 2]) {
202    let mut rest = msg;
203    let mut arg_idx = 0usize;
204    while let Some(pos) = rest.find("{}") {
205        crate::console::write_str(&rest[..pos]);
206        match args.get(arg_idx) {
207            Some(LogArg::None) | None => crate::console::write_str("{}"),
208            Some(arg) => write_arg(arg),
209        }
210        arg_idx += 1;
211        rest = &rest[pos + 2..];
212    }
213    crate::console::write_str(rest);
214}
215
216fn write_arg(arg: &LogArg) {
217    match *arg {
218        LogArg::None => {}
219        LogArg::U32(v) => crate::console::_print(core::format_args!("{v}")),
220        LogArg::I32(v) => crate::console::_print(core::format_args!("{v}")),
221        LogArg::F32(v) => crate::console::_print(core::format_args!("{v}")),
222        LogArg::Str(s) => crate::console::write_str(s),
223    }
224}
225
226fn write_dec(mut n: usize) {
227    if n == 0 {
228        crate::console::write_str("0");
229        return;
230    }
231    let mut digits = [0u8; 20];
232    let mut i = 0;
233    while n > 0 {
234        digits[i] = b'0' + (n % 10) as u8;
235        n /= 10;
236        i += 1;
237    }
238    let mut buf = [0u8; 20];
239    for j in 0..i {
240        buf[j] = digits[i - 1 - j];
241    }
242    if let Ok(s) = core::str::from_utf8(&buf[..i]) {
243        crate::console::write_str(s);
244    }
245}
246
247/// Drain and format one pending frame. Returns `false` if the ring was
248/// empty. Call this in a loop from a low-priority task to flush the log
249/// (see [`drain_forever`] for a ready-made one).
250pub fn drain_one() -> bool {
251    match RECEIVER.get().and_then(|rx| rx.try_recv()) {
252        Some(frame) => {
253            write_frame(&frame);
254            true
255        }
256        None => false,
257    }
258}
259
260/// A ready-made drain loop: `.await`s new frames and writes them to the
261/// console as they arrive. Spawn this as a low-priority `#[rivet::task]`
262/// if you want logging without writing your own drain loop.
263///
264/// ```ignore
265/// #[rivet::task(priority = 0)]
266/// async fn log_drain() {
267///     rivet::log::drain_forever().await;
268/// }
269/// ```
270pub async fn drain_forever() -> ! {
271    let rx = loop {
272        if let Some(rx) = RECEIVER.get() {
273            break rx;
274        }
275        // Logging hasn't been initialized yet (called before rivet::init)
276        // — extremely unlikely given normal boot order, but don't spin
277        // hot if it happens.
278        crate::time::Sleep::<1000>::new().await;
279    };
280    loop {
281        let frame = rx.recv().await;
282        write_frame(&frame);
283    }
284}
285
286/// Log a message at the given level. ISR-safe: pushing a frame does no
287/// formatting and never blocks (see the module docs for why arguments are
288/// a small closed set — `u32`/`i32`/`f32`/`&'static str` — rather than a
289/// full `format_args!`-style template). Up to two `{}` placeholders in
290/// `$msg` are substituted, in order, at drain time:
291///
292/// ```ignore
293/// rivet::log!(Level::Info, "task {} spawned", id);
294/// rivet::log!(Level::Warn, "retry {}/{}", attempt, max);
295/// ```
296#[macro_export]
297macro_rules! log {
298    ($level:expr, $msg:expr) => {
299        $crate::log::push(
300            $level,
301            $msg,
302            $crate::log::LogArg::None,
303            $crate::log::LogArg::None,
304        )
305    };
306    ($level:expr, $msg:expr, $a:expr) => {
307        $crate::log::push(
308            $level,
309            $msg,
310            $crate::log::LogArg::from($a),
311            $crate::log::LogArg::None,
312        )
313    };
314    ($level:expr, $msg:expr, $a:expr, $b:expr) => {
315        $crate::log::push(
316            $level,
317            $msg,
318            $crate::log::LogArg::from($a),
319            $crate::log::LogArg::from($b),
320        )
321    };
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn log_arg_from_conversions() {
330        assert!(matches!(LogArg::from(5u32), LogArg::U32(5)));
331        assert!(matches!(LogArg::from(-3i32), LogArg::I32(-3)));
332        assert!(matches!(LogArg::from("hi"), LogArg::Str("hi")));
333        match LogArg::from(1.5f32) {
334            LogArg::F32(v) => assert!((v - 1.5).abs() < f32::EPSILON),
335            _ => panic!("expected F32"),
336        }
337    }
338
339    /// Pure substitution logic — doesn't touch the ring or console (the
340    /// host `port::host` console write is a no-op, so a real end-to-end
341    /// check of what actually got written needs the QEMU suite;
342    /// `examples/*/src/bin/report_test.rs`'s `hello from A, i={0..4}`
343    /// golden lines are that check). This test isolates `write_interpolated`'s
344    /// placeholder-counting/fallback behavior instead, by checking it
345    /// against a fake sink is impossible without one — so it only
346    /// checks it doesn't panic across the boundary cases (0, 1, 2, and
347    /// more `{}` than args).
348    #[test]
349    fn write_interpolated_boundary_cases_do_not_panic() {
350        write_interpolated("no placeholders", &[LogArg::None, LogArg::None]);
351        write_interpolated("one {}", &[LogArg::U32(1), LogArg::None]);
352        write_interpolated("two {} and {}", &[LogArg::U32(1), LogArg::Str("x")]);
353        write_interpolated("three {} {} {}", &[LogArg::U32(1), LogArg::U32(2)]);
354    }
355}