Skip to main content

concinnity_engine/crash/
ring.rs

1// src/crash/ring.rs
2//
3// Bounded in-memory capture of recent tracing events for crash reports. A
4// fixed ring of recycled line buffers behind a mutex: the write path formats
5// into an existing buffer with byte-capped output, so steady state allocates
6// nothing and the lock is held only while one line is formatted. Purely
7// passive: it sees only events other code already emits.
8
9use std::collections::VecDeque;
10use std::fmt::Write as _;
11use std::sync::{Mutex, MutexGuard, OnceLock};
12use std::time::Instant;
13
14pub(crate) const RING_CAPACITY: usize = 256;
15pub(crate) const MAX_LINE_BYTES: usize = 256;
16
17pub(crate) struct LogRing {
18    lines: Mutex<VecDeque<String>>,
19    started: Instant,
20}
21
22impl LogRing {
23    pub(crate) fn new() -> Self {
24        Self {
25            lines: Mutex::new(VecDeque::with_capacity(RING_CAPACITY)),
26            started: Instant::now(),
27        }
28    }
29
30    fn lock(&self) -> MutexGuard<'_, VecDeque<String>> {
31        // A panic while the lock was held leaves at worst a garbled line;
32        // recent logs are still worth reporting.
33        self.lines.lock().unwrap_or_else(|p| p.into_inner())
34    }
35
36    // Append one line, evicting the oldest at capacity and recycling its
37    // buffer. `fill` receives a cleared buffer wrapped in a byte cap.
38    fn push_with(&self, fill: impl FnOnce(&mut BoundedLine<'_>)) {
39        let mut lines = self.lock();
40        let mut buf = if lines.len() == RING_CAPACITY {
41            lines.pop_front().unwrap_or_default()
42        } else {
43            String::with_capacity(MAX_LINE_BYTES)
44        };
45        buf.clear();
46        fill(&mut BoundedLine(&mut buf));
47        lines.push_back(buf);
48    }
49
50    pub(crate) fn push_event(&self, level: &tracing::Level, target: &str, event: &tracing::Event) {
51        let elapsed = self.started.elapsed().as_secs_f64();
52        self.push_with(|line| {
53            let _ = write!(line, "+{elapsed:.3}s {level} {target}: ");
54            event.record(&mut LineVisitor {
55                line,
56                seen_any: false,
57            });
58        });
59    }
60
61    // Oldest-first copy of the ring. Allocates; called only when a report is
62    // being written.
63    pub(crate) fn snapshot(&self) -> Vec<String> {
64        self.lock().iter().cloned().collect()
65    }
66
67    // Snapshot that gives up instead of blocking, for use where the crashing
68    // thread may already hold the lock.
69    #[cfg(any(target_os = "macos", target_os = "windows"))]
70    pub(crate) fn try_snapshot(&self) -> Vec<String> {
71        match self.lines.try_lock() {
72            Ok(lines) => lines.iter().cloned().collect(),
73            Err(_) => Vec::new(),
74        }
75    }
76}
77
78pub(crate) fn global() -> &'static LogRing {
79    static RING: OnceLock<LogRing> = OnceLock::new();
80    RING.get_or_init(LogRing::new)
81}
82
83// Byte-capped sink for one ring line: appends stop (on a char boundary) once
84// the line reaches `MAX_LINE_BYTES`, so no event can grow a buffer past its
85// preallocated capacity.
86struct BoundedLine<'a>(&'a mut String);
87
88impl std::fmt::Write for BoundedLine<'_> {
89    fn write_str(&mut self, s: &str) -> std::fmt::Result {
90        let remaining = MAX_LINE_BYTES.saturating_sub(self.0.len());
91        if remaining == 0 {
92            return Ok(());
93        }
94        if s.len() <= remaining {
95            self.0.push_str(s);
96        } else {
97            let mut end = remaining;
98            while !s.is_char_boundary(end) {
99                end -= 1;
100            }
101            self.0.push_str(&s[..end]);
102        }
103        Ok(())
104    }
105}
106
107// Formats an event's fields into the line: the `message` field verbatim,
108// every other field as ` key=value`.
109struct LineVisitor<'a, 'b> {
110    line: &'a mut BoundedLine<'b>,
111    seen_any: bool,
112}
113
114impl tracing::field::Visit for LineVisitor<'_, '_> {
115    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
116        if field.name() == "message" {
117            let _ = write!(self.line, "{value:?}");
118        } else {
119            let sep = if self.seen_any { " " } else { "" };
120            let _ = write!(self.line, "{sep}{}={value:?}", field.name());
121        }
122        self.seen_any = true;
123    }
124}
125
126/// A `tracing` layer that keeps the most recent INFO-and-above log lines in a
127/// bounded in-memory ring for inclusion in crash reports. Passive and cheap:
128/// lines are formatted into recycled fixed-size buffers under a short lock,
129/// and nothing is emitted per frame by the layer itself.
130pub struct RingLayer;
131
132impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for RingLayer {
133    fn on_event(
134        &self,
135        event: &tracing::Event<'_>,
136        _ctx: tracing_subscriber::layer::Context<'_, S>,
137    ) {
138        let meta = event.metadata();
139        if *meta.level() > tracing::Level::INFO {
140            return;
141        }
142        global().push_event(meta.level(), meta.target(), event);
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn push_line(ring: &LogRing, text: &str) {
151        ring.push_with(|line| {
152            let _ = write!(line, "{text}");
153        });
154    }
155
156    #[test]
157    fn ring_keeps_the_newest_lines_in_order() {
158        let ring = LogRing::new();
159        for i in 0..RING_CAPACITY + 40 {
160            push_line(&ring, &format!("line {i}"));
161        }
162        let snap = ring.snapshot();
163        assert_eq!(snap.len(), RING_CAPACITY);
164        assert_eq!(snap.first().unwrap(), "line 40");
165        assert_eq!(
166            snap.last().unwrap(),
167            &format!("line {}", RING_CAPACITY + 39)
168        );
169    }
170
171    #[test]
172    fn lines_cap_at_the_byte_limit_on_char_boundaries() {
173        let ring = LogRing::new();
174        push_line(&ring, &"\u{e9}".repeat(MAX_LINE_BYTES));
175        let snap = ring.snapshot();
176        assert_eq!(snap.len(), 1);
177        assert!(snap[0].len() <= MAX_LINE_BYTES);
178        assert!(snap[0].chars().all(|c| c == '\u{e9}'));
179    }
180
181    #[test]
182    fn concurrent_writers_never_lose_the_ring() {
183        let ring = std::sync::Arc::new(LogRing::new());
184        let threads: Vec<_> = (0..8)
185            .map(|t| {
186                let ring = ring.clone();
187                std::thread::spawn(move || {
188                    for i in 0..200 {
189                        push_line(&ring, &format!("t{t} line {i}"));
190                    }
191                })
192            })
193            .collect();
194        for t in threads {
195            t.join().unwrap();
196        }
197        let snap = ring.snapshot();
198        assert_eq!(snap.len(), RING_CAPACITY);
199        // Every retained line is complete, none were torn by contention.
200        assert!(
201            snap.iter()
202                .all(|l| l.starts_with('t') && l.contains(" line "))
203        );
204    }
205
206    #[test]
207    fn layer_mirrors_info_events_into_the_global_ring() {
208        use tracing_subscriber::layer::SubscriberExt;
209        let subscriber = tracing_subscriber::registry().with(RingLayer);
210        tracing::subscriber::with_default(subscriber, || {
211            tracing::info!(answer = 42, "ring capture probe");
212            tracing::debug!("below the ring threshold");
213        });
214        let snap = global().snapshot();
215        let probe = snap.iter().find(|l| l.contains("ring capture probe"));
216        let probe = probe.expect("INFO event captured");
217        assert!(probe.contains("answer=42"));
218        assert!(probe.contains("INFO"));
219        assert!(!snap.iter().any(|l| l.contains("below the ring threshold")));
220    }
221}