refactor: reduce lines of log cloned

Instead of cloning every single line of logs, now we only clone those logs that are visible +- a padding.

Containers with hunders or thousands of lines of logs can see a huge reducing in CPU and memory usage
This commit is contained in:
Jack Wills
2025-06-16 20:54:40 +00:00
parent e7114d2f5e
commit ecefa302b9
3 changed files with 82 additions and 7 deletions
+18 -3
View File
@@ -586,10 +586,25 @@ impl Logs {
} }
} }
pub fn to_vec(&self) -> Vec<ListItem<'static>> { /// Get the logs vec, but instead of cloning to whole vec, only clone items with x of the currently selected index
self.logs.items.clone() /// Where x is the abs different of the index plus the panel height & a padding
/// The rest can be just empty list items
/// TODO test me, pass in 1000 lines of "something", expect response to be different!
pub fn to_vec(&self, height: usize, padding: usize) -> Vec<ListItem<'static>> {
let current_index = self.logs.state.selected().unwrap_or_default();
self.logs
.items
.iter()
.enumerate()
.map(|(index, item)| {
if current_index.abs_diff(index) <= height + padding {
item.clone()
} else {
ListItem::from("")
}
})
.collect()
} }
/// The rest of the methods are basically forwarding from the underlying StatefulList /// The rest of the methods are basically forwarding from the underlying StatefulList
pub fn get_state_title(&self) -> String { pub fn get_state_title(&self) -> String {
self.logs.get_state_title() self.logs.get_state_title()
+61 -3
View File
@@ -677,12 +677,12 @@ impl AppData {
} }
/// Get mutable Vec of current containers logs /// Get mutable Vec of current containers logs
pub fn get_logs(&self) -> Vec<ListItem<'static>> { pub fn get_logs(&self, height: u16, padding: usize) -> Vec<ListItem<'static>> {
self.containers self.containers
.state .state
.selected() .selected()
.and_then(|i| self.containers.items.get(i)) .and_then(|i| self.containers.items.get(i))
.map_or(vec![], |i| i.logs.to_vec()) .map_or(vec![], |i| i.logs.to_vec(height.into(), padding))
} }
/// Get mutable Option of the currently selected container Logs state /// Get mutable Option of the currently selected container Logs state
@@ -1953,7 +1953,7 @@ mod tests {
assert_eq!(result.as_ref().unwrap().selected(), Some(2)); assert_eq!(result.as_ref().unwrap().selected(), Some(2));
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_logs(); let result = app_data.get_logs(4, 1);
assert_eq!(result.len(), 3); assert_eq!(result.len(), 3);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
@@ -2324,4 +2324,62 @@ mod tests {
let result = app_data.get_log_state(); let result = app_data.get_log_state();
assert!(result.is_none()); assert!(result.is_none());
} }
// *************** //
// Get logs method //
// *************** //
#[test]
/// get_logs() returns vec of item, but the items are empty unless they are in the *visible" zone, based on height, index, and padding
fn test_app_data_update_get_logs() {
let (ids, containers) = gen_containers();
let mut app_data = gen_appdata(&containers);
app_data.containers_start();
let logs = (0..=999).map(|i| format!("{i} {i}")).collect::<Vec<_>>();
app_data.update_log_by_id(logs, &ids[0]);
let result = app_data.get_logs(10, 10);
for (index, item) in result.iter().enumerate() {
if index < 979 {
assert_eq!(item, &ListItem::new(""));
} else {
assert_eq!(item, &ListItem::new(format!("{index}")));
}
}
let result = app_data.get_logs(100, 20);
for (index, item) in result.iter().enumerate() {
if index < 879 {
assert_eq!(item, &ListItem::new(""));
} else {
assert_eq!(item, &ListItem::new(format!("{index}")));
}
}
app_data.log_start();
let result = app_data.get_logs(10, 10);
for (index, item) in result.iter().enumerate() {
if index > 20 {
assert_eq!(item, &ListItem::new(""));
} else {
assert_eq!(item, &ListItem::new(format!("{index}")));
}
}
for _ in 0..=500 {
app_data.log_next();
}
let result = app_data.get_logs(10, 10);
for (index, item) in result.iter().enumerate() {
if (481..=521).contains(&index) {
assert_eq!(item, &ListItem::new(format!("{index}")));
} else {
assert_eq!(item, &ListItem::new(""));
}
}
}
} }
+3 -1
View File
@@ -39,7 +39,8 @@ pub fn draw(
} }
f.render_widget(paragraph, area); f.render_widget(paragraph, area);
} else { } else {
let logs = app_data.lock().get_logs(); let padding = usize::from(area.height / 5);
let logs = app_data.lock().get_logs(area.height, padding);
if logs.is_empty() { if logs.is_empty() {
let mut paragraph = Paragraph::new("no logs found") let mut paragraph = Paragraph::new("no logs found")
.block(block) .block(block)
@@ -52,6 +53,7 @@ pub fn draw(
let items = List::new(logs) let items = List::new(logs)
.block(block) .block(block)
.highlight_symbol(RIGHT_ARROW) .highlight_symbol(RIGHT_ARROW)
.scroll_padding(padding)
.highlight_style(Style::default().add_modifier(Modifier::BOLD)); .highlight_style(Style::default().add_modifier(Modifier::BOLD));
// This should always return Some, as logs is not empty // This should always return Some, as logs is not empty
if let Some(log_state) = app_data.lock().get_log_state() { if let Some(log_state) = app_data.lock().get_log_state() {