Skip to main content

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;