reovim_server/session/ring_buffer.rs
1//! Per-client debug ring buffer for event tracking.
2//!
3//! This module provides a fixed-capacity ring buffer (8 KB default) that captures
4//! client-specific events like key presses, commands, mode changes, and errors.
5//!
6//! # Architecture
7//!
8//! ```text
9//! Client (Owner)
10//! ├── EditingState (mode, cursor, selection)
11//! └── ClientRingBuffer (events for this client)
12//! ├── KeyPress events
13//! ├── CommandExecuted events
14//! ├── ModeChanged events
15//! └── Error events
16//! ```
17//!
18//! # Thread Safety
19//!
20//! Uses `parking_lot::RwLock` for fast, non-poisoning concurrent access.
21//!
22//! # Usage
23//!
24//! ```ignore
25//! use reovim_server::session::ClientRingBuffer;
26//!
27//! let buffer = ClientRingBuffer::new();
28//! buffer.log_event(ClientEventType::KeyPress, "pressed 'j'");
29//! buffer.log_event(ClientEventType::ModeChanged, "normal -> insert");
30//!
31//! // Get recent events
32//! let recent = buffer.tail(10);
33//!
34//! // Dump for debugging
35//! let dump = buffer.dump();
36//! ```
37
38use std::{fmt::Write, time::Instant};
39
40use parking_lot::RwLock;
41
42// =============================================================================
43// Constants
44// =============================================================================
45
46/// Default capacity for client ring buffer (8 KB).
47pub const DEFAULT_CLIENT_CAPACITY: usize = 8 * 1024;
48
49/// Maximum event details length before truncation (1 KB).
50pub const MAX_DETAILS_LEN: usize = 1024;
51
52// =============================================================================
53// Event Types
54// =============================================================================
55
56/// Type of client event.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ClientEventType {
59 /// Key press received.
60 KeyPress,
61 /// Command executed.
62 CommandExecuted,
63 /// Mode transition.
64 ModeChanged,
65 /// Cursor/selection/state change.
66 StateChanged,
67 /// Error encountered.
68 Error,
69 /// Warning.
70 Warning,
71 /// Informational message.
72 Info,
73}
74
75impl ClientEventType {
76 /// Returns the string representation.
77 #[must_use]
78 pub const fn as_str(&self) -> &'static str {
79 match self {
80 Self::KeyPress => "KEY",
81 Self::CommandExecuted => "CMD",
82 Self::ModeChanged => "MODE",
83 Self::StateChanged => "STATE",
84 Self::Error => "ERROR",
85 Self::Warning => "WARN",
86 Self::Info => "INFO",
87 }
88 }
89}
90
91impl std::fmt::Display for ClientEventType {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 write!(f, "{}", self.as_str())
94 }
95}
96
97// =============================================================================
98// Log Entry
99// =============================================================================
100
101/// A single log entry in the client ring buffer.
102#[derive(Debug, Clone)]
103pub struct ClientLogEntry {
104 /// Per-client sequence number.
105 pub seq: u64,
106 /// Timestamp in microseconds since client connected.
107 pub timestamp_us: u64,
108 /// Event type.
109 pub event_type: ClientEventType,
110 /// Event details (truncated to `MAX_DETAILS_LEN`).
111 pub details: String,
112}
113
114impl ClientLogEntry {
115 /// Estimates the memory size of this entry in bytes.
116 #[allow(clippy::missing_const_for_fn)]
117 fn size_bytes(&self) -> usize {
118 // Fixed fields: seq(8) + timestamp_us(8) + event_type(1) = 17
119 // Plus details heap allocation
120 17 + self.details.capacity()
121 }
122}
123
124// =============================================================================
125// Buffer Statistics
126// =============================================================================
127
128/// Statistics about the client ring buffer.
129#[derive(Debug, Clone, Copy)]
130pub struct ClientBufferStats {
131 /// Maximum capacity in bytes.
132 pub capacity_bytes: usize,
133 /// Current usage in bytes.
134 pub bytes_used: usize,
135 /// Number of entries currently in the buffer.
136 pub entry_count: usize,
137 /// Total number of entries ever logged.
138 pub total_logged: u64,
139 /// Number of entries dropped due to overflow.
140 pub dropped: u64,
141}
142
143// =============================================================================
144// Ring Buffer Inner
145// =============================================================================
146
147/// Inner state of the client ring buffer, protected by `RwLock`.
148#[derive(Debug)]
149struct ClientRingBufferInner {
150 /// Log entries in insertion order.
151 entries: Vec<ClientLogEntry>,
152 /// Total entries logged (monotonic, never wraps).
153 total_logged: u64,
154 /// Current byte usage.
155 bytes_used: usize,
156 /// Maximum capacity in bytes.
157 capacity_bytes: usize,
158 /// Client connection time for timestamps.
159 start_time: Instant,
160}
161
162impl ClientRingBufferInner {
163 /// Creates a new ring buffer inner with the given capacity.
164 fn new(capacity_bytes: usize) -> Self {
165 Self {
166 entries: Vec::new(),
167 total_logged: 0,
168 bytes_used: 0,
169 capacity_bytes,
170 start_time: Instant::now(),
171 }
172 }
173
174 /// Pushes a new entry, evicting old entries if necessary.
175 fn push(&mut self, event_type: ClientEventType, details: String) {
176 // Truncate details if too long
177 let details = if details.len() > MAX_DETAILS_LEN {
178 let mut truncated = details[..MAX_DETAILS_LEN].to_string();
179 truncated.push_str("...");
180 truncated
181 } else {
182 details
183 };
184
185 let entry = ClientLogEntry {
186 seq: self.total_logged,
187 #[allow(clippy::cast_possible_truncation)]
188 timestamp_us: self.start_time.elapsed().as_micros() as u64,
189 event_type,
190 details,
191 };
192
193 let entry_size = entry.size_bytes();
194 self.total_logged += 1;
195
196 // Evict old entries until we have room
197 while self.bytes_used + entry_size > self.capacity_bytes && !self.entries.is_empty() {
198 let removed = self.entries.remove(0);
199 self.bytes_used = self.bytes_used.saturating_sub(removed.size_bytes());
200 }
201
202 // Add new entry
203 self.entries.push(entry);
204 self.bytes_used += entry_size;
205 }
206
207 /// Returns the N most recent entries (newest first).
208 fn tail(&self, n: usize) -> Vec<ClientLogEntry> {
209 let count = n.min(self.entries.len());
210 self.entries.iter().rev().take(count).cloned().collect()
211 }
212
213 /// Returns all entries in order (oldest to newest).
214 fn entries(&self) -> Vec<ClientLogEntry> {
215 self.entries.clone()
216 }
217
218 /// Formats all entries as a string for crash dumps.
219 fn dump(&self) -> String {
220 let mut output = String::new();
221 output.push_str("=== Client Ring Buffer Dump ===\n");
222 let _ = writeln!(
223 output,
224 "Entries: {} | Bytes: {}/{} | Total logged: {}",
225 self.entries.len(),
226 self.bytes_used,
227 self.capacity_bytes,
228 self.total_logged
229 );
230 output.push_str("---\n");
231
232 for entry in &self.entries {
233 let _ = writeln!(
234 output,
235 "[{:>10}us] {:5} {}",
236 entry.timestamp_us,
237 entry.event_type.as_str(),
238 entry.details
239 );
240 }
241
242 output.push_str("=== End Dump ===\n");
243 output
244 }
245
246 /// Returns buffer statistics.
247 #[allow(clippy::missing_const_for_fn)]
248 fn stats(&self) -> ClientBufferStats {
249 let dropped = if self.total_logged > self.entries.len() as u64 {
250 self.total_logged - self.entries.len() as u64
251 } else {
252 0
253 };
254
255 ClientBufferStats {
256 capacity_bytes: self.capacity_bytes,
257 bytes_used: self.bytes_used,
258 entry_count: self.entries.len(),
259 total_logged: self.total_logged,
260 dropped,
261 }
262 }
263}
264
265// =============================================================================
266// Client Ring Buffer
267// =============================================================================
268
269/// Per-client debug ring buffer.
270///
271/// A fixed-capacity (8 KB default) ring buffer that captures client-specific
272/// events for debugging. When the buffer is full, oldest entries are discarded.
273///
274/// # Thread Safety
275///
276/// All operations are thread-safe. Multiple readers can access the buffer
277/// simultaneously; writers are exclusive.
278pub struct ClientRingBuffer {
279 inner: RwLock<ClientRingBufferInner>,
280}
281
282impl ClientRingBuffer {
283 /// Creates a new ring buffer with default capacity (8 KB).
284 #[must_use]
285 pub fn new() -> Self {
286 Self::with_capacity(DEFAULT_CLIENT_CAPACITY)
287 }
288
289 /// Creates a new ring buffer with the specified capacity in bytes.
290 #[must_use]
291 pub fn with_capacity(capacity_bytes: usize) -> Self {
292 Self {
293 inner: RwLock::new(ClientRingBufferInner::new(capacity_bytes)),
294 }
295 }
296
297 /// Logs an event to the buffer.
298 ///
299 /// If the buffer would exceed capacity, oldest entries are discarded.
300 pub fn log_event(&self, event_type: ClientEventType, details: impl Into<String>) {
301 // Use try_write to avoid blocking during panic
302 if let Some(mut inner) = self.inner.try_write() {
303 inner.push(event_type, details.into());
304 }
305 // If we can't get the lock, skip this entry (panic safety)
306 }
307
308 /// Logs a key press event.
309 pub fn log_key(&self, key: &str) {
310 self.log_event(ClientEventType::KeyPress, key);
311 }
312
313 /// Logs a command execution event.
314 pub fn log_command(&self, command: &str) {
315 self.log_event(ClientEventType::CommandExecuted, command);
316 }
317
318 /// Logs a mode change event.
319 pub fn log_mode_change(&self, from: &str, to: &str) {
320 self.log_event(ClientEventType::ModeChanged, format!("{from} -> {to}"));
321 }
322
323 /// Logs a state change event.
324 pub fn log_state_change(&self, description: &str) {
325 self.log_event(ClientEventType::StateChanged, description);
326 }
327
328 /// Logs an error event.
329 pub fn log_error(&self, error: &str) {
330 self.log_event(ClientEventType::Error, error);
331 }
332
333 /// Returns the N most recent entries (newest first).
334 #[must_use]
335 pub fn tail(&self, n: usize) -> Vec<ClientLogEntry> {
336 self.inner.read().tail(n)
337 }
338
339 /// Returns all entries in order (oldest to newest).
340 #[must_use]
341 pub fn entries(&self) -> Vec<ClientLogEntry> {
342 self.inner.read().entries()
343 }
344
345 /// Formats all entries as a string for crash dumps.
346 ///
347 /// Blocks until the lock is acquired.
348 #[must_use]
349 pub fn dump(&self) -> String {
350 self.inner.read().dump()
351 }
352
353 /// Attempts to format all entries without blocking.
354 ///
355 /// Returns `None` if the lock cannot be acquired immediately.
356 /// Use this in panic handlers to avoid deadlocks.
357 #[must_use]
358 pub fn try_dump(&self) -> Option<String> {
359 self.inner.try_read().map(|inner| inner.dump())
360 }
361
362 /// Returns buffer statistics.
363 #[must_use]
364 pub fn stats(&self) -> ClientBufferStats {
365 self.inner.read().stats()
366 }
367
368 /// Returns the current byte usage.
369 #[must_use]
370 pub fn bytes_used(&self) -> usize {
371 self.inner.read().bytes_used
372 }
373
374 /// Clears all entries from the buffer.
375 pub fn clear(&self) {
376 if let Some(mut inner) = self.inner.try_write() {
377 inner.entries.clear();
378 inner.bytes_used = 0;
379 }
380 }
381}
382
383impl Default for ClientRingBuffer {
384 fn default() -> Self {
385 Self::new()
386 }
387}
388
389impl std::fmt::Debug for ClientRingBuffer {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 let stats = self.stats();
392 f.debug_struct("ClientRingBuffer")
393 .field("capacity_bytes", &stats.capacity_bytes)
394 .field("bytes_used", &stats.bytes_used)
395 .field("entry_count", &stats.entry_count)
396 .field("total_logged", &stats.total_logged)
397 .finish()
398 }
399}
400
401impl Clone for ClientRingBuffer {
402 fn clone(&self) -> Self {
403 let inner = self.inner.read();
404 let capacity = inner.capacity_bytes;
405 let entries = inner.entries.clone();
406 let total_logged = inner.total_logged;
407 let bytes_used = inner.bytes_used;
408 let start_time = inner.start_time;
409 drop(inner);
410
411 let mut new_inner = ClientRingBufferInner::new(capacity);
412 new_inner.entries = entries;
413 new_inner.total_logged = total_logged;
414 new_inner.bytes_used = bytes_used;
415 new_inner.start_time = start_time;
416 Self {
417 inner: RwLock::new(new_inner),
418 }
419 }
420}
421
422// =============================================================================
423// Tests
424// =============================================================================
425
426#[cfg(test)]
427#[path = "ring_buffer_tests.rs"]
428mod tests;