#![allow(dead_code)]
use std::{collections::HashMap, sync::OnceLock, time::Instant};
use std::fmt::Write;
use parking_lot::RwLock;
use reovim_kernel::api::v1::{Level, Record};
pub const DEFAULT_CAPACITY: usize = 64 * 1024;
pub const MAX_MESSAGE_LEN: usize = 8 * 1024;
const MAX_INTERN_ENTRIES: usize = u16::MAX as usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AlreadyInitialized;
impl std::fmt::Display for AlreadyInitialized {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "debug ring buffer already initialized")
}
}
impl std::error::Error for AlreadyInitialized {}
#[derive(Debug, Default)]
struct InternTable {
strings: Vec<String>,
map: HashMap<String, u16>,
}
impl InternTable {
fn new() -> Self {
Self::default()
}
fn intern(&mut self, s: &str) -> u16 {
if let Some(&id) = self.map.get(s) {
return id;
}
if self.strings.len() >= MAX_INTERN_ENTRIES {
return 0;
}
#[allow(clippy::cast_possible_truncation)]
let id = self.strings.len() as u16;
self.strings.push(s.to_string());
self.map.insert(s.to_string(), id);
id
}
fn get(&self, id: u16) -> &str {
self.strings.get(id as usize).map_or("", String::as_str)
}
#[allow(clippy::missing_const_for_fn)]
fn len(&self) -> usize {
self.strings.len()
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub seq: u64,
pub timestamp_us: u64,
pub level: Level,
target_id: u16,
file_id: u16,
pub line: u32,
pub message: String,
}
impl LogEntry {
#[allow(clippy::missing_const_for_fn)]
fn size_bytes(&self) -> usize {
25 + self.message.capacity()
}
}
#[derive(Debug, Clone)]
pub struct LogEntryView {
pub seq: u64,
pub timestamp_us: u64,
pub level: Level,
pub target: String,
pub file: String,
pub line: u32,
pub message: String,
}
#[derive(Debug, Clone, Copy)]
pub struct BufferStats {
pub capacity_bytes: usize,
pub bytes_used: usize,
pub entry_count: usize,
pub total_logged: u64,
pub dropped: u64,
pub interned_targets: usize,
pub interned_files: usize,
}
#[derive(Debug)]
struct RingBufferInner {
entries: Vec<LogEntry>,
total_logged: u64,
bytes_used: usize,
capacity_bytes: usize,
targets: InternTable,
files: InternTable,
start_time: Instant,
}
impl RingBufferInner {
fn new(capacity_bytes: usize) -> Self {
Self {
entries: Vec::new(),
total_logged: 0,
bytes_used: 0,
capacity_bytes,
targets: InternTable::new(),
files: InternTable::new(),
start_time: Instant::now(),
}
}
fn push(&mut self, record: &Record) {
let target_id = self.targets.intern(record.module_path());
let file_id = self.files.intern(record.file());
let message = if record.message().len() > MAX_MESSAGE_LEN {
let mut truncated = record.message()[..MAX_MESSAGE_LEN].to_string();
truncated.push_str("...[truncated]");
truncated
} else {
record.message().to_string()
};
let entry = LogEntry {
seq: self.total_logged,
#[allow(clippy::cast_possible_truncation)]
timestamp_us: self.start_time.elapsed().as_micros() as u64,
level: record.level(),
target_id,
file_id,
line: record.line(),
message,
};
let entry_size = entry.size_bytes();
self.total_logged += 1;
while self.bytes_used + entry_size > self.capacity_bytes && !self.entries.is_empty() {
self.bytes_used = self.bytes_used.saturating_sub(self.entries[0].size_bytes());
self.entries.remove(0);
}
self.entries.push(entry);
self.bytes_used += entry_size;
}
fn tail(&self, n: usize) -> Vec<LogEntryView> {
let count = n.min(self.entries.len());
let mut result = Vec::with_capacity(count);
for entry in self.entries.iter().rev().take(count) {
result.push(LogEntryView {
seq: entry.seq,
timestamp_us: entry.timestamp_us,
level: entry.level,
target: self.targets.get(entry.target_id).to_string(),
file: self.files.get(entry.file_id).to_string(),
line: entry.line,
message: entry.message.clone(),
});
}
result
}
fn entries(&self) -> Vec<LogEntryView> {
self.entries
.iter()
.map(|entry| LogEntryView {
seq: entry.seq,
timestamp_us: entry.timestamp_us,
level: entry.level,
target: self.targets.get(entry.target_id).to_string(),
file: self.files.get(entry.file_id).to_string(),
line: entry.line,
message: entry.message.clone(),
})
.collect()
}
fn dump(&self) -> String {
let mut output = String::new();
output.push_str("=== Debug Ring Buffer Dump ===\n");
let _ = writeln!(
output,
"Entries: {} | Bytes: {}/{} | Total logged: {}",
self.entries.len(),
self.bytes_used,
self.capacity_bytes,
self.total_logged
);
output.push_str("---\n");
for entry in &self.entries {
let target = self.targets.get(entry.target_id);
let file = self.files.get(entry.file_id);
let _ = writeln!(
output,
"[{:>10}us] {:5} {}:{} ({}) {}",
entry.timestamp_us,
entry.level.as_str(),
file,
entry.line,
target,
entry.message
);
}
output.push_str("=== End Dump ===\n");
output
}
fn stats(&self) -> BufferStats {
let dropped = if self.total_logged > self.entries.len() as u64 {
self.total_logged - self.entries.len() as u64
} else {
0
};
BufferStats {
capacity_bytes: self.capacity_bytes,
bytes_used: self.bytes_used,
entry_count: self.entries.len(),
total_logged: self.total_logged,
dropped,
interned_targets: self.targets.len(),
interned_files: self.files.len(),
}
}
}
pub struct DebugRingBuffer {
inner: RwLock<RingBufferInner>,
}
impl DebugRingBuffer {
#[must_use]
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
#[must_use]
pub fn with_capacity(capacity_bytes: usize) -> Self {
Self {
inner: RwLock::new(RingBufferInner::new(capacity_bytes)),
}
}
pub fn push(&self, record: &Record) {
if let Some(mut inner) = self.inner.try_write() {
inner.push(record);
}
}
#[must_use]
pub fn tail(&self, n: usize) -> Vec<LogEntryView> {
self.inner.read().tail(n)
}
#[must_use]
pub fn entries(&self) -> Vec<LogEntryView> {
self.inner.read().entries()
}
#[must_use]
pub fn dump(&self) -> String {
self.inner.read().dump()
}
#[must_use]
pub fn try_dump(&self) -> Option<String> {
self.inner.try_read().map(|inner| inner.dump())
}
#[must_use]
pub fn stats(&self) -> BufferStats {
self.inner.read().stats()
}
#[must_use]
pub fn bytes_used(&self) -> usize {
self.inner.read().bytes_used
}
}
impl Default for DebugRingBuffer {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for DebugRingBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let stats = self.stats();
f.debug_struct("DebugRingBuffer")
.field("capacity_bytes", &stats.capacity_bytes)
.field("bytes_used", &stats.bytes_used)
.field("entry_count", &stats.entry_count)
.field("total_logged", &stats.total_logged)
.finish()
}
}
static DEBUG_RING: OnceLock<DebugRingBuffer> = OnceLock::new();
pub fn init_debug_ring() -> Result<(), AlreadyInitialized> {
init_debug_ring_with_capacity(DEFAULT_CAPACITY)
}
pub fn init_debug_ring_with_capacity(capacity_bytes: usize) -> Result<(), AlreadyInitialized> {
DEBUG_RING
.set(DebugRingBuffer::with_capacity(capacity_bytes))
.map_err(|_| AlreadyInitialized)
}
#[must_use]
pub fn debug_ring() -> &'static DebugRingBuffer {
DEBUG_RING
.get()
.expect("debug ring buffer not initialized - call init_debug_ring() first")
}
#[must_use]
pub fn try_debug_ring() -> Option<&'static DebugRingBuffer> {
DEBUG_RING.get()
}
#[cfg(test)]
#[path = "ring_buffer_tests.rs"]
mod tests;