From a0bcdfa00578dc1c64883786ba607c5fea13cc40 Mon Sep 17 00:00:00 2001 From: ToSeven <397341575@qq.com> Date: Mon, 4 Sep 2023 05:52:48 +0800 Subject: [PATCH] implement the AFL-Style Tui (#1432) * implement an AFL-Style TUI * improve the tui/mod.rs according to the reviews * fixing fmt manually --------- Co-authored-by: toseven Co-authored-by: Dominik Maier --- libafl/src/monitors/mod.rs | 11 +- libafl/src/monitors/tui/mod.rs | 193 ++++++++++- libafl/src/monitors/tui/ui.rs | 609 +++++++++++++++++++++++++-------- 3 files changed, 666 insertions(+), 147 deletions(-) diff --git a/libafl/src/monitors/mod.rs b/libafl/src/monitors/mod.rs index 95a0f8027a..a488572232 100644 --- a/libafl/src/monitors/mod.rs +++ b/libafl/src/monitors/mod.rs @@ -87,12 +87,16 @@ pub struct ClientStats { // monitor (maybe we need a separated struct?) /// The corpus size for this client pub corpus_size: u64, + /// The time for the last update of the corpus size + pub last_corpus_time: Duration, /// The total executions for this client pub executions: u64, /// The number of executions of the previous state in case a client decrease the number of execution (e.g when restarting without saving the state) pub prev_state_executions: u64, /// The size of the objectives corpus for this client pub objective_size: u64, + /// The time for the last update of the objective size + pub last_objective_time: Duration, /// The last reported executions for this client #[cfg(feature = "afl_exec_sec")] pub last_window_executions: u64, @@ -101,6 +105,8 @@ pub struct ClientStats { pub last_execs_per_sec: f64, /// The last time we got this information pub last_window_time: Duration, + /// the start time of the client + pub start_time: Duration, /// User-defined monitor pub user_monitor: HashMap, /// Client performance statistics @@ -140,6 +146,7 @@ impl ClientStats { /// We got a new information about corpus size for this client, insert them. pub fn update_corpus_size(&mut self, corpus_size: u64) { self.corpus_size = corpus_size; + self.last_corpus_time = current_time(); } /// We got a new information about objective corpus size for this client, insert them. @@ -206,8 +213,9 @@ impl ClientStats { self.user_monitor.insert(name, value); } + #[must_use] /// Get a user-defined stat using the name - pub fn get_user_stats(&mut self, name: &str) -> Option<&UserStats> { + pub fn get_user_stats(&self, name: &str) -> Option<&UserStats> { self.user_monitor.get(name) } @@ -275,6 +283,7 @@ pub trait Monitor { for _ in client_stat_count..(client_id.0 + 1) as usize { self.client_stats_mut().push(ClientStats { last_window_time: current_time(), + start_time: current_time(), ..ClientStats::default() }); } diff --git a/libafl/src/monitors/tui/mod.rs b/libafl/src/monitors/tui/mod.rs index 5045645e3b..6e058e8a4b 100644 --- a/libafl/src/monitors/tui/mod.rs +++ b/libafl/src/monitors/tui/mod.rs @@ -1,6 +1,6 @@ //! Monitor based on ratatui -use alloc::boxed::Box; +use alloc::{boxed::Box, string::ToString}; use std::{ collections::VecDeque, fmt::Write, @@ -22,6 +22,7 @@ use crossterm::{ use hashbrown::HashMap; use libafl_bolts::{current_time, format_duration_hms, ClientId}; use ratatui::{backend::CrosstermBackend, Terminal}; +use serde_json::{self, Value}; #[cfg(feature = "introspection")] use super::{ClientPerfMonitor, PerfFeature}; @@ -162,14 +163,53 @@ impl PerfTuiContext { } } +#[derive(Debug, Default, Clone)] +pub struct ProcessTiming { + pub client_start_time: Duration, + pub exec_speed: String, + pub last_new_entry: Duration, + pub last_saved_solution: Duration, +} + +impl ProcessTiming { + fn new() -> Self { + Self { + exec_speed: "0".to_string(), + ..Default::default() + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct ItemGeometry { + pub pending: u64, + pub pend_fav: u64, + pub own_finds: u64, + pub imported: u64, + pub stability: String, +} + +impl ItemGeometry { + fn new() -> Self { + Self { + stability: "0%".to_string(), + ..Default::default() + } + } +} + #[derive(Debug, Default, Clone)] pub struct ClientTuiContext { pub corpus: u64, pub objectives: u64, pub executions: u64, /// Float value formatted as String - pub exec_sec: String, + pub map_density: String, + pub cycles_done: u64, + + pub process_timing: ProcessTiming, + pub item_geometry: ItemGeometry, pub user_stats: HashMap, } @@ -178,7 +218,47 @@ impl ClientTuiContext { self.corpus = client.corpus_size; self.objectives = client.objective_size; self.executions = client.executions; - self.exec_sec = exec_sec; + self.process_timing.client_start_time = client.start_time; + self.process_timing.last_new_entry = if client.last_corpus_time > client.start_time { + client.last_corpus_time - client.start_time + } else { + Duration::default() + }; + + self.process_timing.last_saved_solution = if client.last_objective_time > client.start_time + { + client.last_objective_time - client.start_time + } else { + Duration::default() + }; + + self.process_timing.exec_speed = exec_sec; + + self.map_density = client + .get_user_stats("edges") + .map_or("0%".to_string(), ToString::to_string); + + let default_json = serde_json::json!({ + "pending": 0, + "pend_fav": 0, + "imported": 0, + "own_finds": 0, + }); + let afl_stats = client + .get_user_stats("AflStats") + .map_or(default_json.to_string(), ToString::to_string); + + let afl_stats_json: Value = + serde_json::from_str(afl_stats.as_str()).unwrap_or(default_json); + self.item_geometry.pending = afl_stats_json["pending"].as_u64().unwrap_or_default(); + self.item_geometry.pend_fav = afl_stats_json["pend_fav"].as_u64().unwrap_or_default(); + self.item_geometry.imported = afl_stats_json["imported"].as_u64().unwrap_or_default(); + self.item_geometry.own_finds = afl_stats_json["own_finds"].as_u64().unwrap_or_default(); + + let stability = client + .get_user_stats("stability") + .map_or("0%".to_string(), ToString::to_string); + self.item_geometry.stability = stability; for (key, val) in &client.user_monitor { self.user_stats.insert(key.clone(), val.clone()); @@ -205,6 +285,14 @@ pub struct TuiContext { pub clients_num: usize, pub total_execs: u64, pub start_time: Duration, + + pub total_map_density: String, + pub total_solutions: u64, + pub total_cycles_done: u64, + pub total_corpus_count: u64, + + pub total_process_timing: ProcessTiming, + pub total_item_geometry: ItemGeometry, } impl TuiContext { @@ -226,6 +314,13 @@ impl TuiContext { clients_num: 0, total_execs: 0, start_time, + + total_map_density: "0%".to_string(), + total_solutions: 0, + total_cycles_done: 0, + total_corpus_count: 0, + total_item_geometry: ItemGeometry::new(), + total_process_timing: ProcessTiming::new(), } } } @@ -264,14 +359,21 @@ impl Monitor for TuiMonitor { let execsec = self.execs_per_sec() as u64; let totalexec = self.total_execs(); let run_time = cur_time - self.start_time; + let total_process_timing = self.process_timing(); let mut ctx = self.context.write().unwrap(); + ctx.total_process_timing = total_process_timing; ctx.corpus_size_timed.add(run_time, self.corpus_size()); ctx.objective_size_timed .add(run_time, self.objective_size()); ctx.execs_per_sec_timed.add(run_time, execsec); ctx.total_execs = totalexec; ctx.clients_num = self.client_stats.len(); + ctx.total_map_density = self.map_density(); + ctx.total_solutions = self.objective_size(); + ctx.total_cycles_done = 0; + ctx.total_corpus_count = self.corpus_size(); + ctx.total_item_geometry = self.item_geometry(); } let client = self.client_stats_mut_for(sender_id); @@ -339,6 +441,91 @@ impl TuiMonitor { client_stats: vec![], } } + + fn map_density(&self) -> String { + if self.client_stats.len() < 2 { + return "0%".to_string(); + } + let mut max_map_density = self + .client_stats() + .get(1) + .unwrap() + .get_user_stats("edges") + .map_or("0%".to_string(), ToString::to_string); + + for client in self.client_stats().iter().skip(2) { + let client_map_density = client + .get_user_stats("edges") + .map_or(String::new(), ToString::to_string); + if client_map_density > max_map_density { + max_map_density = client_map_density; + } + } + max_map_density + } + + fn item_geometry(&self) -> ItemGeometry { + let mut total_item_geometry = ItemGeometry::new(); + if self.client_stats.len() < 2 { + return total_item_geometry; + } + let mut ratio_a: u64 = 0; + let mut ratio_b: u64 = 0; + for client in self.client_stats().iter().skip(1) { + let afl_stats = client + .get_user_stats("AflStats") + .map_or("None".to_string(), ToString::to_string); + let stability = client + .get_user_stats("stability") + .map_or(&UserStats::Ratio(0, 100), |x| x); + + if afl_stats != "None" { + let default_json = serde_json::json!({ + "pending": 0, + "pend_fav": 0, + "imported": 0, + "own_finds": 0, + }); + let afl_stats_json: Value = + serde_json::from_str(afl_stats.as_str()).unwrap_or(default_json); + total_item_geometry.pending += + afl_stats_json["pending"].as_u64().unwrap_or_default(); + total_item_geometry.pend_fav += + afl_stats_json["pend_fav"].as_u64().unwrap_or_default(); + total_item_geometry.own_finds += + afl_stats_json["own_finds"].as_u64().unwrap_or_default(); + total_item_geometry.imported += + afl_stats_json["imported"].as_u64().unwrap_or_default(); + } + + if let UserStats::Ratio(a, b) = stability { + ratio_a += a; + ratio_b += b; + } + } + total_item_geometry.stability = format!("{}%", ratio_a * 100 / ratio_b); + total_item_geometry + } + + fn process_timing(&mut self) -> ProcessTiming { + let mut total_process_timing = ProcessTiming::new(); + total_process_timing.exec_speed = self.execs_per_sec_pretty(); + if self.client_stats.len() > 1 { + let mut new_path_time = Duration::default(); + let mut new_objectives_time = Duration::default(); + for client in self.client_stats().iter().skip(1) { + new_path_time = client.last_corpus_time.max(new_path_time); + new_objectives_time = client.last_objective_time.max(new_objectives_time); + } + if new_path_time > self.start_time { + total_process_timing.last_new_entry = new_path_time - self.start_time; + } + if new_objectives_time > self.start_time { + total_process_timing.last_saved_solution = new_objectives_time - self.start_time; + } + } + total_process_timing + } } fn run_tui_thread(context: Arc>, tick_rate: Duration, tui_ui: TuiUI) { diff --git a/libafl/src/monitors/tui/ui.rs b/libafl/src/monitors/tui/ui.rs index 0fb0f38ec9..4f1a2c8530 100644 --- a/libafl/src/monitors/tui/ui.rs +++ b/libafl/src/monitors/tui/ui.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{string::ToString, vec::Vec}; use std::{ cmp::{max, min}, sync::{Arc, RwLock}, @@ -16,7 +16,10 @@ use ratatui::{ Frame, }; -use super::{current_time, format_duration_hms, Duration, String, TimedStats, TuiContext}; +use super::{ + current_time, format_duration_hms, Duration, ItemGeometry, ProcessTiming, String, TimedStats, + TuiContext, +}; #[derive(Default, Debug)] pub struct TuiUI { @@ -99,21 +102,59 @@ impl TuiUI { let body = Layout::default() .constraints(if self.show_logs { - [Constraint::Percentage(50), Constraint::Percentage(50)].as_ref() + if cfg!(feature = "introspection") { + [ + Constraint::Percentage(41), + Constraint::Percentage(44), + Constraint::Percentage(15), + ] + .as_ref() + } else { + [ + Constraint::Percentage(41), + Constraint::Percentage(27), + Constraint::Percentage(32), + ] + .as_ref() + } } else { - [Constraint::Percentage(100)].as_ref() + [Constraint::Percentage(50), Constraint::Percentage(50)].as_ref() }) .split(f.size()); + let top_body = body[0]; + let mid_body = body[1]; + self.draw_overall_ui(f, app, top_body); + self.draw_client_ui(f, app, mid_body); + + if self.show_logs { + let bottom_body = body[2]; + self.draw_logs(f, app, bottom_body); + } + } + + #[allow(clippy::too_many_lines)] + fn draw_overall_ui(&mut self, f: &mut Frame, app: &Arc>, area: Rect) + where + B: Backend, + { let top_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(body[0]); + .direction(Direction::Vertical) + .constraints([Constraint::Length(16), Constraint::Min(0)].as_ref()) + .split(area); + let bottom_layout = top_layout[1]; - let left_layout = Layout::default() - .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + let left_top_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(40), Constraint::Percentage(60)].as_ref()) .split(top_layout[0]); + let right_top_layout = left_top_layout[1]; + + let title_layout = Layout::default() + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(left_top_layout[0]); + let mut status_bar: String = self.title.clone(); status_bar = status_bar + " (" + self.version.as_str() + ")"; @@ -126,14 +167,24 @@ impl TuiUI { let block = Block::default().borders(Borders::ALL); let paragraph = Paragraph::new(text) .block(block) - .alignment(Alignment::Center); //.wrap(Wrap { trim: true }); - f.render_widget(paragraph, left_layout[0]); + .alignment(Alignment::Center); + f.render_widget(paragraph, title_layout[0]); - self.draw_text(f, app, left_layout[1]); + let process_timting_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(6), Constraint::Min(0)].as_ref()) + .split(title_layout[1]); + self.draw_process_timing_text(f, app, process_timting_layout[0], true); - let right_layout = Layout::default() + let path_geometry_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(7), Constraint::Min(0)].as_ref()) + .split(process_timting_layout[1]); + self.draw_item_geometry_text(f, app, path_geometry_layout[0], true); + + let title_chart_layout = Layout::default() .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) - .split(top_layout[1]); + .split(right_top_layout); let titles = vec![ Line::from(Span::styled( "speed", @@ -144,7 +195,7 @@ impl TuiUI { Style::default().fg(Color::LightGreen), )), Line::from(Span::styled( - "objectives", + "objectives (`g` switch)", Style::default().fg(Color::LightGreen), )), ]; @@ -152,7 +203,7 @@ impl TuiUI { .block( Block::default() .title(Span::styled( - "charts (`g` switch)", + "charts", Style::default() .fg(Color::LightCyan) .add_modifier(Modifier::BOLD), @@ -161,7 +212,9 @@ impl TuiUI { ) .highlight_style(Style::default().fg(Color::LightYellow)) .select(self.charts_tab_idx); - f.render_widget(tabs, right_layout[0]); + f.render_widget(tabs, title_chart_layout[0]); + + let chart_layout = title_chart_layout[1]; match self.charts_tab_idx { 0 => { @@ -170,7 +223,7 @@ impl TuiUI { "speed chart", "exec/sec", f, - right_layout[1], + chart_layout, &ctx.execs_per_sec_timed, ); } @@ -180,7 +233,7 @@ impl TuiUI { "corpus chart", "corpus size", f, - right_layout[1], + chart_layout, &ctx.corpus_size_timed, ); } @@ -190,16 +243,60 @@ impl TuiUI { "corpus chart", "objectives", f, - right_layout[1], + chart_layout, &ctx.objective_size_timed, ); } _ => {} } + self.draw_overall_generic_text(f, app, bottom_layout); + } - if self.show_logs { - self.draw_logs(f, app, body[1]); + fn draw_client_ui(&mut self, f: &mut Frame, app: &Arc>, area: Rect) + where + B: Backend, + { + let client_block = Block::default() + .title(Span::styled( + format!("client #{} (l/r arrows to switch)", self.clients_idx), + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL); + let client_area = client_block.inner(area); + f.render_widget(client_block, area); + + #[cfg(feature = "introspection")] + { + let introspection_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(11), Constraint::Min(0)].as_ref()) + .split(client_area)[1]; + self.draw_introspection_text(f, app, introspection_layout); } + + let left_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(client_area); + let right_layout = left_layout[1]; + + let left_top_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(6), Constraint::Length(0)].as_ref()) + .split(left_layout[0]); + let left_bottom_layout = left_top_layout[1]; + self.draw_process_timing_text(f, app, left_top_layout[0], false); + self.draw_client_generic_text(f, app, left_bottom_layout); + + let right_top_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(7), Constraint::Length(0)].as_ref()) + .split(right_layout); + let right_bottom_layout = right_top_layout[1]; + self.draw_item_geometry_text(f, app, right_top_layout[0], false); + self.draw_client_results_text(f, app, right_bottom_layout); } #[allow(clippy::too_many_lines, clippy::cast_precision_loss)] @@ -329,37 +426,49 @@ impl TuiUI { f.render_widget(chart, area); } - #[allow(clippy::too_many_lines)] - fn draw_text(&mut self, f: &mut Frame, app: &Arc>, area: Rect) - where + fn draw_item_geometry_text( + &mut self, + f: &mut Frame, + app: &Arc>, + area: Rect, + is_overall: bool, + ) where B: Backend, { + let item_geometry: ItemGeometry = if is_overall { + app.read().unwrap().total_item_geometry.clone() + } else if self.clients < 2 { + ItemGeometry::new() + } else { + app.read() + .unwrap() + .clients + .get(&self.clients_idx) + .unwrap() + .item_geometry + .clone() + }; + let items = vec![ Row::new(vec![ - Cell::from(Span::raw("run time")), - Cell::from(Span::raw(format_duration_hms( - &(current_time() - app.read().unwrap().start_time), - ))), + Cell::from(Span::raw("pending")), + Cell::from(Span::raw(format!("{}", item_geometry.pending))), ]), Row::new(vec![ - Cell::from(Span::raw("clients")), - Cell::from(Span::raw(format!("{}", self.clients))), + Cell::from(Span::raw("pend fav")), + Cell::from(Span::raw(format!("{}", item_geometry.pend_fav))), ]), Row::new(vec![ - Cell::from(Span::raw("executions")), - Cell::from(Span::raw(format!("{}", app.read().unwrap().total_execs))), + Cell::from(Span::raw("own finds")), + Cell::from(Span::raw(format!("{}", item_geometry.own_finds))), ]), Row::new(vec![ - Cell::from(Span::raw("exec/sec")), - Cell::from(Span::raw(format!( - "{}", - app.read() - .unwrap() - .execs_per_sec_timed - .series - .back() - .map_or(0, |x| x.item) - ))), + Cell::from(Span::raw("imported")), + Cell::from(Span::raw(format!("{}", item_geometry.imported))), + ]), + Row::new(vec![ + Cell::from(Span::raw("stability")), + Cell::from(Span::raw(item_geometry.stability)), ]), ]; @@ -373,6 +482,274 @@ impl TuiUI { ) .split(area); + let table = Table::new(items) + .block( + Block::default() + .title(Span::styled( + "item geometry", + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); + f.render_widget(table, chunks[0]); + } + + fn draw_process_timing_text( + &mut self, + f: &mut Frame, + app: &Arc>, + area: Rect, + is_overall: bool, + ) where + B: Backend, + { + let tup: (Duration, ProcessTiming) = if is_overall { + let tui_context = app.read().unwrap(); + ( + tui_context.start_time, + tui_context.total_process_timing.clone(), + ) + } else if self.clients < 2 { + (current_time(), ProcessTiming::new()) + } else { + let client = app + .read() + .unwrap() + .clients + .get(&self.clients_idx) + .unwrap() + .clone(); + ( + client.process_timing.client_start_time, + client.process_timing, + ) + }; + let items = vec![ + Row::new(vec![ + Cell::from(Span::raw("run time")), + Cell::from(Span::raw(format_duration_hms(&(current_time() - tup.0)))), + ]), + Row::new(vec![ + Cell::from(Span::raw("exec speed")), + Cell::from(Span::raw(tup.1.exec_speed)), + ]), + Row::new(vec![ + Cell::from(Span::raw("last new entry")), + Cell::from(Span::raw(format_duration_hms(&(tup.1.last_new_entry)))), + ]), + Row::new(vec![ + Cell::from(Span::raw("last solution")), + Cell::from(Span::raw(format_duration_hms(&(tup.1.last_saved_solution)))), + ]), + ]; + + let chunks = Layout::default() + .constraints( + [ + Constraint::Length(2 + items.len() as u16), + Constraint::Min(0), + ] + .as_ref(), + ) + .split(area); + + let table = Table::new(items) + .block( + Block::default() + .title(Span::styled( + "process timing", + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); + f.render_widget(table, chunks[0]); + } + + fn draw_overall_generic_text( + &mut self, + f: &mut Frame, + app: &Arc>, + area: Rect, + ) where + B: Backend, + { + let items = vec![ + Row::new(vec![ + Cell::from(Span::raw("clients")), + Cell::from(Span::raw(format!("{}", self.clients))), + Cell::from(Span::raw("total execs")), + Cell::from(Span::raw(format!("{}", app.read().unwrap().total_execs))), + Cell::from(Span::raw("map density")), + Cell::from(Span::raw(app.read().unwrap().total_map_density.to_string())), + ]), + Row::new(vec![ + Cell::from(Span::raw("solutions")), + Cell::from(Span::raw(format!( + "{}", + app.read().unwrap().total_solutions + ))), + Cell::from(Span::raw("cycle done")), + Cell::from(Span::raw(format!( + "{}", + app.read().unwrap().total_cycles_done + ))), + Cell::from(Span::raw("corpus count")), + Cell::from(Span::raw(format!( + "{}", + app.read().unwrap().total_corpus_count + ))), + ]), + ]; + + let chunks = Layout::default() + .constraints( + [ + Constraint::Length(2 + items.len() as u16), + Constraint::Min(0), + ] + .as_ref(), + ) + .split(area); + + let table = Table::new(items) + .block( + Block::default() + .title(Span::styled( + "generic", + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .widths(&[ + Constraint::Percentage(15), + Constraint::Percentage(16), + Constraint::Percentage(15), + Constraint::Percentage(16), + Constraint::Percentage(15), + Constraint::Percentage(27), + ]); + f.render_widget(table, chunks[0]); + } + + fn draw_client_results_text( + &mut self, + f: &mut Frame, + app: &Arc>, + area: Rect, + ) where + B: Backend, + { + let items = vec![ + Row::new(vec![ + Cell::from(Span::raw("cycles done")), + Cell::from(Span::raw(format!( + "{}", + app.read() + .unwrap() + .clients + .get(&self.clients_idx) + .map_or(0, |x| x.cycles_done) + ))), + ]), + Row::new(vec![ + Cell::from(Span::raw("solutions")), + Cell::from(Span::raw(format!( + "{}", + app.read() + .unwrap() + .clients + .get(&self.clients_idx) + .map_or(0, |x| x.objectives) + ))), + ]), + ]; + + let chunks = Layout::default() + .constraints( + [ + Constraint::Length(2 + items.len() as u16), + Constraint::Min(0), + ] + .as_ref(), + ) + .split(area); + + let table = Table::new(items) + .block( + Block::default() + .title(Span::styled( + "overall results", + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); + f.render_widget(table, chunks[0]); + } + + fn draw_client_generic_text( + &mut self, + f: &mut Frame, + app: &Arc>, + area: Rect, + ) where + B: Backend, + { + let items = vec![ + Row::new(vec![ + Cell::from(Span::raw("corpus count")), + Cell::from(Span::raw(format!( + "{}", + app.read() + .unwrap() + .clients + .get(&self.clients_idx) + .map_or(0, |x| x.corpus) + ))), + ]), + Row::new(vec![ + Cell::from(Span::raw("total execs")), + Cell::from(Span::raw(format!( + "{}", + app.read() + .unwrap() + .clients + .get(&self.clients_idx) + .map_or(0, |x| x.executions) + ))), + ]), + Row::new(vec![ + Cell::from(Span::raw("map density")), + Cell::from(Span::raw( + app.read() + .unwrap() + .clients + .get(&self.clients_idx) + .map_or("0%".to_string(), |x| x.map_density.to_string()), + )), + ]), + ]; + + let chunks = Layout::default() + .constraints( + [ + Constraint::Length(2 + items.len() as u16), + Constraint::Min(0), + ] + .as_ref(), + ) + .split(area); + let table = Table::new(items) .block( Block::default() @@ -386,123 +763,69 @@ impl TuiUI { ) .widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); f.render_widget(table, chunks[0]); + } - let client_block = Block::default() - .title(Span::styled( - format!("client #{} (l/r arrows to switch)", self.clients_idx), - Style::default() - .fg(Color::LightCyan) - .add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL); - let client_area = client_block.inner(chunks[1]); - f.render_widget(client_block, chunks[1]); - - let mut client_items = vec![]; + #[cfg(feature = "introspection")] + fn draw_introspection_text( + &mut self, + f: &mut Frame, + app: &Arc>, + area: Rect, + ) where + B: Backend, + { + let mut items = vec![]; { let ctx = app.read().unwrap(); - if let Some(client) = ctx.clients.get(&self.clients_idx) { - client_items.push(Row::new(vec![ - Cell::from(Span::raw("executions")), - Cell::from(Span::raw(format!("{}", client.executions))), + if let Some(client) = ctx.introspection.get(&self.clients_idx) { + items.push(Row::new(vec![ + Cell::from(Span::raw("scheduler")), + Cell::from(Span::raw(format!("{:.2}%", client.scheduler * 100.0))), ])); - client_items.push(Row::new(vec![ - Cell::from(Span::raw("exec/sec")), - Cell::from(Span::raw(client.exec_sec.clone())), + items.push(Row::new(vec![ + Cell::from(Span::raw("manager")), + Cell::from(Span::raw(format!("{:.2}%", client.manager * 100.0))), ])); - client_items.push(Row::new(vec![ - Cell::from(Span::raw("corpus")), - Cell::from(Span::raw(format!("{}", client.corpus))), - ])); - client_items.push(Row::new(vec![ - Cell::from(Span::raw("objectives")), - Cell::from(Span::raw(format!("{}", client.objectives))), - ])); - for (key, val) in &client.user_stats { - client_items.push(Row::new(vec![ - Cell::from(Span::raw(key.clone())), - Cell::from(Span::raw(format!("{}", val.clone()))), - ])); - } - }; - } - - #[cfg(feature = "introspection")] - let client_chunks = Layout::default() - .constraints( - [ - Constraint::Length(client_items.len() as u16), - Constraint::Min(4), - ] - .as_ref(), - ) - .split(client_area); - #[cfg(not(feature = "introspection"))] - let client_chunks = Layout::default() - .constraints([Constraint::Percentage(100)].as_ref()) - .split(client_area); - - let table = Table::new(client_items) - .block(Block::default()) - .widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); - f.render_widget(table, client_chunks[0]); - - #[cfg(feature = "introspection")] - { - let mut items = vec![]; - { - let ctx = app.read().unwrap(); - if let Some(client) = ctx.introspection.get(&self.clients_idx) { + for i in 0..client.stages.len() { items.push(Row::new(vec![ - Cell::from(Span::raw("scheduler")), - Cell::from(Span::raw(format!("{:.2}%", client.scheduler * 100.0))), + Cell::from(Span::raw(format!("stage {i}"))), + Cell::from(Span::raw("")), ])); - items.push(Row::new(vec![ - Cell::from(Span::raw("manager")), - Cell::from(Span::raw(format!("{:.2}%", client.manager * 100.0))), - ])); - for i in 0..client.stages.len() { - items.push(Row::new(vec![ - Cell::from(Span::raw(format!("stage {i}"))), - Cell::from(Span::raw("")), - ])); - for (key, val) in &client.stages[i] { - items.push(Row::new(vec![ - Cell::from(Span::raw(key.clone())), - Cell::from(Span::raw(format!("{:.2}%", val * 100.0))), - ])); - } - } - for (key, val) in &client.feedbacks { + for (key, val) in &client.stages[i] { items.push(Row::new(vec![ Cell::from(Span::raw(key.clone())), Cell::from(Span::raw(format!("{:.2}%", val * 100.0))), ])); } + } + for (key, val) in &client.feedbacks { items.push(Row::new(vec![ - Cell::from(Span::raw("not measured")), - Cell::from(Span::raw(format!("{:.2}%", client.unmeasured * 100.0))), + Cell::from(Span::raw(key.clone())), + Cell::from(Span::raw(format!("{:.2}%", val * 100.0))), ])); - }; - } - - let table = Table::new(items) - .block( - Block::default() - .title(Span::styled( - "introspection", - Style::default() - .fg(Color::LightCyan) - .add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL), - ) - .widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); - f.render_widget(table, client_chunks[1]); + } + items.push(Row::new(vec![ + Cell::from(Span::raw("not measured")), + Cell::from(Span::raw(format!("{:.2}%", client.unmeasured * 100.0))), + ])); + }; } - } + let table = Table::new(items) + .block( + Block::default() + .title(Span::styled( + "introspection", + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); + f.render_widget(table, area); + } #[allow(clippy::unused_self)] fn draw_logs(&mut self, f: &mut Frame, app: &Arc>, area: Rect) where