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]
57 pub fn new(level: LogLevel, text: impl Into<String>) -> Self {
58 Self { level, time: None, text: text.into() }
59 }
60
61 #[must_use]
63 pub fn time(mut self, time: impl Into<String>) -> Self {
64 self.time = Some(time.into());
65 self
66 }
67
68 #[must_use]
70 pub fn level(&self) -> LogLevel {
71 self.level
72 }
73
74 #[must_use]
76 pub fn timestamp(&self) -> Option<&str> {
77 self.time.as_deref()
78 }
79
80 #[must_use]
82 pub fn text(&self) -> &str {
83 &self.text
84 }
85}
86
87#[derive(Debug, Clone)]
94pub struct LogBuffer {
95 id: u64,
96 capacity: usize,
97 chunks: VecDeque<Arc<Vec<LogLine>>>,
98 skip: usize,
100 len: usize,
101 dropped: u64,
103}
104
105impl LogBuffer {
106 #[must_use]
108 pub fn new(capacity: usize) -> Self {
109 Self {
110 id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
111 capacity: capacity.max(1),
112 chunks: VecDeque::new(),
113 skip: 0,
114 len: 0,
115 dropped: 0,
116 }
117 }
118
119 pub fn push(&mut self, line: LogLine) {
121 match self.chunks.back_mut() {
122 Some(last) if last.len() < CHUNK => Arc::make_mut(last).push(line),
123 _ => {
124 let mut chunk = Vec::with_capacity(CHUNK);
125 chunk.push(line);
126 self.chunks.push_back(Arc::new(chunk));
127 }
128 }
129 self.len += 1;
130 while self.len > self.capacity {
131 self.skip += 1;
132 self.len -= 1;
133 self.dropped += 1;
134 if self.chunks.front().is_some_and(|first| self.skip >= first.len()) {
135 self.chunks.pop_front();
136 self.skip = 0;
137 }
138 }
139 }
140
141 pub fn clear(&mut self) {
143 self.dropped += self.len as u64;
144 self.chunks.clear();
145 self.skip = 0;
146 self.len = 0;
147 }
148
149 #[must_use]
151 pub fn len(&self) -> usize {
152 self.len
153 }
154
155 #[must_use]
157 pub fn is_empty(&self) -> bool {
158 self.len == 0
159 }
160
161 #[must_use]
163 pub fn capacity(&self) -> usize {
164 self.capacity
165 }
166
167 #[must_use]
169 pub fn get(&self, index: usize) -> Option<&LogLine> {
170 if index >= self.len {
171 return None;
172 }
173 let first = self.chunks.front()?.len() - self.skip;
174 if index < first {
175 return self.chunks[0].get(self.skip + index);
176 }
177 let rest = index - first;
178 self.chunks.get(1 + rest / CHUNK)?.get(rest % CHUNK)
179 }
180
181 pub fn iter(&self) -> impl Iterator<Item = &LogLine> {
183 self.chunks.iter().enumerate().flat_map(|(i, chunk)| chunk[if i == 0 { self.skip } else { 0 }..].iter())
184 }
185
186 pub(crate) fn id(&self) -> u64 {
187 self.id
188 }
189
190 pub(crate) fn first_number(&self) -> u64 {
192 self.dropped
193 }
194
195 pub(crate) fn by_number(&self, number: u64) -> Option<&LogLine> {
197 let index = usize::try_from(number.checked_sub(self.dropped)?).ok()?;
198 self.get(index)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn line(n: usize) -> LogLine {
207 LogLine::new(LogLevel::Info, format!("line {n}"))
208 }
209
210 #[test]
211 fn drops_oldest_lines_when_full_and_keeps_numbers() {
212 let mut buffer = LogBuffer::new(600);
213 for n in 0..1000 {
214 buffer.push(line(n));
215 }
216 assert_eq!(buffer.len(), 600);
217 assert_eq!(buffer.get(0).map(LogLine::text), Some("line 400"));
218 assert_eq!(buffer.get(599).map(LogLine::text), Some("line 999"));
219 assert_eq!(buffer.get(600), None);
220 assert_eq!(buffer.first_number(), 400);
221 assert_eq!(buffer.by_number(512).map(LogLine::text), Some("line 512"));
222 assert_eq!(buffer.by_number(12), None);
223 assert_eq!(buffer.iter().count(), 600);
224 assert_eq!(buffer.iter().nth(300).map(LogLine::text), Some("line 700"));
225 buffer.clear();
226 assert!(buffer.is_empty());
227 assert_eq!(buffer.first_number(), 1000);
228 }
229
230 #[test]
231 fn clones_share_lines_until_one_changes() {
232 let mut buffer = LogBuffer::new(10_000);
233 for n in 0..CHUNK * 3 {
234 buffer.push(line(n));
235 }
236 let shared = buffer.clone();
237 buffer.push(line(9999));
238 assert_eq!(shared.len(), CHUNK * 3);
239 assert_eq!(buffer.len(), CHUNK * 3 + 1);
240 assert!(Arc::ptr_eq(&shared.chunks[0], &buffer.chunks[0]), "full chunks stay shared");
241 assert_eq!(shared.id(), buffer.id());
242 }
243}