qframe/widgets/
log_buffer.rs1use std::collections::VecDeque;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8const CHUNK: usize = 256;
10
11static NEXT_ID: AtomicU64 = AtomicU64::new(1);
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum LogLevel {
17 Trace,
19 Debug,
21 Info,
23 Warn,
25 Error,
27}
28
29impl LogLevel {
30 pub const ALL: [Self; 5] = [Self::Trace, Self::Debug, Self::Info, Self::Warn, Self::Error];
32
33 #[must_use]
35 pub fn name(self) -> &'static str {
36 match self {
37 Self::Trace => "trace",
38 Self::Debug => "debug",
39 Self::Info => "info",
40 Self::Warn => "warn",
41 Self::Error => "error",
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct LogLine {
49 level: LogLevel,
50 time: Option<String>,
51 text: String,
52}
53
54impl LogLine {
55 #[must_use]
61 pub fn new(level: LogLevel, text: impl Into<String>) -> Self {
62 let text = text.into();
63 let text = match crate::text::printable(&text) {
64 std::borrow::Cow::Borrowed(_) => text,
65 std::borrow::Cow::Owned(clean) => clean,
66 };
67 Self { level, time: None, text }
68 }
69
70 #[must_use]
72 pub fn time(mut self, time: impl Into<String>) -> Self {
73 self.time = Some(time.into());
74 self
75 }
76
77 #[must_use]
79 pub fn level(&self) -> LogLevel {
80 self.level
81 }
82
83 #[must_use]
85 pub fn timestamp(&self) -> Option<&str> {
86 self.time.as_deref()
87 }
88
89 #[must_use]
91 pub fn text(&self) -> &str {
92 &self.text
93 }
94}
95
96#[derive(Debug, Clone)]
103pub struct LogBuffer {
104 id: u64,
105 capacity: usize,
106 chunks: VecDeque<Arc<Vec<LogLine>>>,
107 skip: usize,
109 len: usize,
110 dropped: u64,
112}
113
114impl LogBuffer {
115 #[must_use]
117 pub fn new(capacity: usize) -> Self {
118 Self {
119 id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
120 capacity: capacity.max(1),
121 chunks: VecDeque::new(),
122 skip: 0,
123 len: 0,
124 dropped: 0,
125 }
126 }
127
128 pub fn push(&mut self, line: LogLine) {
130 match self.chunks.back_mut() {
131 Some(last) if last.len() < CHUNK => Arc::make_mut(last).push(line),
132 _ => {
133 let mut chunk = Vec::with_capacity(CHUNK);
134 chunk.push(line);
135 self.chunks.push_back(Arc::new(chunk));
136 }
137 }
138 self.len += 1;
139 while self.len > self.capacity {
140 self.skip += 1;
141 self.len -= 1;
142 self.dropped += 1;
143 if self.chunks.front().is_some_and(|first| self.skip >= first.len()) {
144 self.chunks.pop_front();
145 self.skip = 0;
146 }
147 }
148 }
149
150 pub fn clear(&mut self) {
152 self.dropped += self.len as u64;
153 self.chunks.clear();
154 self.skip = 0;
155 self.len = 0;
156 }
157
158 #[must_use]
160 pub fn len(&self) -> usize {
161 self.len
162 }
163
164 #[must_use]
166 pub fn is_empty(&self) -> bool {
167 self.len == 0
168 }
169
170 #[must_use]
172 pub fn capacity(&self) -> usize {
173 self.capacity
174 }
175
176 #[must_use]
178 pub fn get(&self, index: usize) -> Option<&LogLine> {
179 if index >= self.len {
180 return None;
181 }
182 let first = self.chunks.front()?.len() - self.skip;
183 if index < first {
184 return self.chunks[0].get(self.skip + index);
185 }
186 let rest = index - first;
187 self.chunks.get(1 + rest / CHUNK)?.get(rest % CHUNK)
188 }
189
190 pub fn iter(&self) -> impl Iterator<Item = &LogLine> {
192 self.chunks.iter().enumerate().flat_map(|(i, chunk)| chunk[if i == 0 { self.skip } else { 0 }..].iter())
193 }
194
195 pub(crate) fn id(&self) -> u64 {
196 self.id
197 }
198
199 pub(crate) fn first_number(&self) -> u64 {
201 self.dropped
202 }
203
204 pub(crate) fn by_number(&self, number: u64) -> Option<&LogLine> {
206 let index = usize::try_from(number.checked_sub(self.dropped)?).ok()?;
207 self.get(index)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 fn line(n: usize) -> LogLine {
216 LogLine::new(LogLevel::Info, format!("line {n}"))
217 }
218
219 #[test]
220 fn drops_oldest_lines_when_full_and_keeps_numbers() {
221 let mut buffer = LogBuffer::new(600);
222 for n in 0..1000 {
223 buffer.push(line(n));
224 }
225 assert_eq!(buffer.len(), 600);
226 assert_eq!(buffer.get(0).map(LogLine::text), Some("line 400"));
227 assert_eq!(buffer.get(599).map(LogLine::text), Some("line 999"));
228 assert_eq!(buffer.get(600), None);
229 assert_eq!(buffer.first_number(), 400);
230 assert_eq!(buffer.by_number(512).map(LogLine::text), Some("line 512"));
231 assert_eq!(buffer.by_number(12), None);
232 assert_eq!(buffer.iter().count(), 600);
233 assert_eq!(buffer.iter().nth(300).map(LogLine::text), Some("line 700"));
234 buffer.clear();
235 assert!(buffer.is_empty());
236 assert_eq!(buffer.first_number(), 1000);
237 }
238
239 #[test]
240 fn clones_share_lines_until_one_changes() {
241 let mut buffer = LogBuffer::new(10_000);
242 for n in 0..CHUNK * 3 {
243 buffer.push(line(n));
244 }
245 let shared = buffer.clone();
246 buffer.push(line(9999));
247 assert_eq!(shared.len(), CHUNK * 3);
248 assert_eq!(buffer.len(), CHUNK * 3 + 1);
249 assert!(Arc::ptr_eq(&shared.chunks[0], &buffer.chunks[0]), "full chunks stay shared");
250 assert_eq!(shared.id(), buffer.id());
251 }
252}