concinnity_engine/crash/
ring.rs1use 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 self.lines.lock().unwrap_or_else(|p| p.into_inner())
34 }
35
36 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 pub(crate) fn snapshot(&self) -> Vec<String> {
64 self.lock().iter().cloned().collect()
65 }
66
67 #[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
83struct 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
107struct 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
126pub 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 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}