diff --git a/apps/mofa-asr/src/screen/log_panel.rs b/apps/mofa-asr/src/screen/log_panel.rs index 3f4d7616..9140b997 100644 --- a/apps/mofa-asr/src/screen/log_panel.rs +++ b/apps/mofa-asr/src/screen/log_panel.rs @@ -215,11 +215,10 @@ impl MoFaASRScreen { /// Add a log entry (throttled) pub(super) fn add_log(&mut self, cx: &mut Cx, entry: &str) { - self.log_entries.push(entry.to_string()); + self.log_entries.push_back(entry.to_string()); - if self.log_entries.len() > MAX_LOG_ENTRIES { - let excess = self.log_entries.len() - MAX_LOG_ENTRIES; - self.log_entries.drain(0..excess); + while self.log_entries.len() > MAX_LOG_ENTRIES { + self.log_entries.pop_front(); } self.mark_log_dirty(cx); @@ -233,12 +232,11 @@ impl MoFaASRScreen { } for log_msg in logs { - self.log_entries.push(log_msg.format()); + self.log_entries.push_back(log_msg.format()); } - if self.log_entries.len() > MAX_LOG_ENTRIES { - let excess = self.log_entries.len() - MAX_LOG_ENTRIES; - self.log_entries.drain(0..excess); + while self.log_entries.len() > MAX_LOG_ENTRIES { + self.log_entries.pop_front(); } self.mark_log_dirty(cx); diff --git a/apps/mofa-asr/src/screen/mod.rs b/apps/mofa-asr/src/screen/mod.rs index 1e2072a4..8f20963d 100644 --- a/apps/mofa-asr/src/screen/mod.rs +++ b/apps/mofa-asr/src/screen/mod.rs @@ -13,6 +13,7 @@ use mofa_ui::{LedMeterWidgetExt, MicButtonWidgetExt, AecButtonWidgetExt}; use mofa_settings::data::Preferences; use crate::dora_integration::{AsrEngineId, DoraIntegration, DoraEvent}; use std::collections::HashMap; +use std::collections::VecDeque; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use moly_kit::prelude::*; @@ -139,7 +140,7 @@ pub struct MoFaASRScreen { #[rust] log_node_filter: usize, #[rust] - log_entries: Vec, + log_entries: VecDeque, #[rust] log_display_dirty: bool, #[rust] diff --git a/apps/mofa-debate/src/screen/audio_controls.rs b/apps/mofa-debate/src/screen/audio_controls.rs index 3f10f205..1fb2b4d1 100644 --- a/apps/mofa-debate/src/screen/audio_controls.rs +++ b/apps/mofa-debate/src/screen/audio_controls.rs @@ -121,10 +121,10 @@ impl MoFaDebateScreen { /// Initialize log entries with a startup message pub(super) fn init_demo_logs(&mut self, cx: &mut Cx) { // Start with empty logs - real logs will come from log_bridge - self.log_entries = vec![ + self.log_entries = std::collections::VecDeque::from(vec![ "[INFO] [App] MoFA FM initialized".to_string(), "[INFO] [App] System log ready - Rust logs will appear here".to_string(), - ]; + ]); // Update the log display self.update_log_display(cx); diff --git a/apps/mofa-debate/src/screen/log_panel.rs b/apps/mofa-debate/src/screen/log_panel.rs index 18e691ca..d3a28924 100644 --- a/apps/mofa-debate/src/screen/log_panel.rs +++ b/apps/mofa-debate/src/screen/log_panel.rs @@ -7,6 +7,8 @@ use makepad_widgets::*; use super::MoFaDebateScreen; +const MAX_LOG_ENTRIES: usize = 5000; + impl MoFaDebateScreen { /// Toggle log panel visibility pub(super) fn toggle_log_panel(&mut self, cx: &mut Cx) { @@ -221,7 +223,12 @@ impl MoFaDebateScreen { /// Add a log entry pub(super) fn add_log(&mut self, cx: &mut Cx, entry: &str) { - self.log_entries.push(entry.to_string()); + self.log_entries.push_back(entry.to_string()); + + while self.log_entries.len() > MAX_LOG_ENTRIES { + self.log_entries.pop_front(); + } + self.update_log_display(cx); } @@ -233,7 +240,11 @@ impl MoFaDebateScreen { } for log_msg in logs { - self.log_entries.push(log_msg.format()); + self.log_entries.push_back(log_msg.format()); + } + + while self.log_entries.len() > MAX_LOG_ENTRIES { + self.log_entries.pop_front(); } // Only update display if we got new logs diff --git a/apps/mofa-debate/src/screen/mod.rs b/apps/mofa-debate/src/screen/mod.rs index 8474dc6b..89d8b35d 100644 --- a/apps/mofa-debate/src/screen/mod.rs +++ b/apps/mofa-debate/src/screen/mod.rs @@ -25,6 +25,7 @@ use mofa_ui::{ }; use mofa_widgets::participant_panel::ParticipantPanelWidgetExt; use mofa_widgets::{StateChangeListener, TimerControl}; +use std::collections::VecDeque; use std::path::PathBuf; /// Register live design for this module @@ -82,7 +83,7 @@ pub struct MoFaDebateScreen { #[rust] log_node_filter: usize, // 0=ALL, 1=ASR, 2=TTS, 3=LLM, 4=Bridge, 5=Monitor, 6=App #[rust] - log_entries: Vec, // Raw log entries for filtering + log_entries: VecDeque, // Dropdown width caching for popup menu sync #[rust] diff --git a/apps/mofa-fm/src/screen/audio_controls.rs b/apps/mofa-fm/src/screen/audio_controls.rs index 739e89bc..d4dbbd04 100644 --- a/apps/mofa-fm/src/screen/audio_controls.rs +++ b/apps/mofa-fm/src/screen/audio_controls.rs @@ -109,10 +109,10 @@ impl MoFaFMScreen { /// Initialize log entries with a startup message pub(super) fn init_demo_logs(&mut self, cx: &mut Cx) { // Start with empty logs - real logs will come from log_bridge - self.log_entries = vec![ + self.log_entries = std::collections::VecDeque::from(vec![ "[INFO] [App] MoFA FM initialized".to_string(), "[INFO] [App] System log ready - Rust logs will appear here".to_string(), - ]; + ]); // Update the log display self.update_log_display(cx); diff --git a/apps/mofa-fm/src/screen/log_panel.rs b/apps/mofa-fm/src/screen/log_panel.rs index 2bb21f3b..234abe78 100644 --- a/apps/mofa-fm/src/screen/log_panel.rs +++ b/apps/mofa-fm/src/screen/log_panel.rs @@ -254,12 +254,10 @@ impl MoFaFMScreen { /// Add a log entry (throttled - doesn't immediately update display) pub(super) fn add_log(&mut self, cx: &mut Cx, entry: &str) { - self.log_entries.push(entry.to_string()); + self.log_entries.push_back(entry.to_string()); - // Prune oldest entries if over limit - if self.log_entries.len() > MAX_LOG_ENTRIES { - let excess = self.log_entries.len() - MAX_LOG_ENTRIES; - self.log_entries.drain(0..excess); + while self.log_entries.len() > MAX_LOG_ENTRIES { + self.log_entries.pop_front(); } // Mark dirty for throttled update (don't update immediately) @@ -274,13 +272,11 @@ impl MoFaFMScreen { } for log_msg in logs { - self.log_entries.push(log_msg.format()); + self.log_entries.push_back(log_msg.format()); } - // Prune oldest entries if over limit - if self.log_entries.len() > MAX_LOG_ENTRIES { - let excess = self.log_entries.len() - MAX_LOG_ENTRIES; - self.log_entries.drain(0..excess); + while self.log_entries.len() > MAX_LOG_ENTRIES { + self.log_entries.pop_front(); } // Mark dirty for throttled update (don't update immediately) @@ -295,3 +291,97 @@ impl MoFaFMScreen { self.update_log_display_now(cx); } } + +#[cfg(test)] +mod tests { + use std::collections::VecDeque; + + const MAX_LOG_ENTRIES: usize = super::MAX_LOG_ENTRIES; + + fn push_and_prune(buf: &mut VecDeque, entry: String) { + buf.push_back(entry); + while buf.len() > MAX_LOG_ENTRIES { + buf.pop_front(); + } + } + + #[test] + fn stays_within_capacity() { + let mut buf = VecDeque::new(); + for i in 0..(MAX_LOG_ENTRIES + 500) { + push_and_prune(&mut buf, format!("log {i}")); + } + assert_eq!(buf.len(), MAX_LOG_ENTRIES); + } + + #[test] + fn oldest_entries_evicted_first() { + let mut buf = VecDeque::new(); + for i in 0..(MAX_LOG_ENTRIES + 3) { + push_and_prune(&mut buf, format!("log {i}")); + } + // first 3 entries should be evicted + assert_eq!(buf.front().unwrap(), "log 3"); + assert_eq!(buf.back().unwrap(), &format!("log {}", MAX_LOG_ENTRIES + 2)); + } + + #[test] + fn below_cap_no_eviction() { + let mut buf = VecDeque::new(); + for i in 0..100 { + push_and_prune(&mut buf, format!("log {i}")); + } + assert_eq!(buf.len(), 100); + assert_eq!(buf.front().unwrap(), "log 0"); + assert_eq!(buf.back().unwrap(), "log 99"); + } + + #[test] + fn exactly_at_cap() { + let mut buf = VecDeque::new(); + for i in 0..MAX_LOG_ENTRIES { + push_and_prune(&mut buf, format!("log {i}")); + } + assert_eq!(buf.len(), MAX_LOG_ENTRIES); + assert_eq!(buf.front().unwrap(), "log 0"); + // One more triggers first eviction + push_and_prune(&mut buf, "overflow".to_string()); + assert_eq!(buf.len(), MAX_LOG_ENTRIES); + assert_eq!(buf.front().unwrap(), "log 1"); + assert_eq!(buf.back().unwrap(), "overflow"); + } + + #[test] + fn clear_resets_buffer() { + let mut buf = VecDeque::new(); + for i in 0..100 { + push_and_prune(&mut buf, format!("log {i}")); + } + buf.clear(); + assert_eq!(buf.len(), 0); + assert!(buf.is_empty()); + // Can continue adding after clear + push_and_prune(&mut buf, "after clear".to_string()); + assert_eq!(buf.len(), 1); + assert_eq!(buf.front().unwrap(), "after clear"); + } + + #[test] + fn batch_insert_pruning() { + // poll_rust_logs + let mut buf = VecDeque::new(); + for i in 0..MAX_LOG_ENTRIES { + buf.push_back(format!("log {i}")); + } + // Batch of 10 + for i in 0..10 { + buf.push_back(format!("batch {i}")); + } + while buf.len() > MAX_LOG_ENTRIES { + buf.pop_front(); + } + assert_eq!(buf.len(), MAX_LOG_ENTRIES); + assert_eq!(buf.front().unwrap(), "log 10"); + assert_eq!(buf.back().unwrap(), "batch 9"); + } +} diff --git a/apps/mofa-fm/src/screen/mod.rs b/apps/mofa-fm/src/screen/mod.rs index d225cdb6..4bbabae7 100644 --- a/apps/mofa-fm/src/screen/mod.rs +++ b/apps/mofa-fm/src/screen/mod.rs @@ -23,6 +23,7 @@ use crate::dora_integration::{DoraIntegration, DoraCommand}; use mofa_widgets::participant_panel::ParticipantPanelWidgetExt; use mofa_widgets::{StateChangeListener, TimerControl}; use mofa_ui::{LedMeterWidgetExt, MicButtonWidgetExt, AecButtonWidgetExt}; +use std::collections::VecDeque; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -92,7 +93,7 @@ pub struct MoFaFMScreen { #[rust] log_node_filter: usize, // 0=ALL, 1=ASR, 2=TTS, 3=LLM, 4=Bridge, 5=Monitor, 6=App #[rust] - log_entries: Vec, // Raw log entries for filtering + log_entries: VecDeque, #[rust] log_display_dirty: bool, // Flag to track if log display needs update #[rust]