From cc0880e78485db19e1287df5c5c5bb82aa73c074 Mon Sep 17 00:00:00 2001 From: Andrea Fioraldi Date: Thu, 20 Jan 2022 23:55:48 +0100 Subject: [PATCH] Monitor with UI based on tui-rs (#480) * first working version * full gui * remove warnings * remove errors in release * allow missing_docs in tui * tui_monitor flag * working graphs * disable tui on windows * clippy * clippy * tui module only under std * use tui from git * fmt * tui from crates --- fuzzers/libfuzzer_libpng_launcher/src/lib.rs | 4 +- libafl/Cargo.toml | 5 +- libafl/src/monitors/mod.rs | 5 + libafl/src/monitors/tui/mod.rs | 415 +++++++++++++++ libafl/src/monitors/tui/ui.rs | 507 +++++++++++++++++++ 5 files changed, 934 insertions(+), 2 deletions(-) create mode 100644 libafl/src/monitors/tui/mod.rs create mode 100644 libafl/src/monitors/tui/ui.rs diff --git a/fuzzers/libfuzzer_libpng_launcher/src/lib.rs b/fuzzers/libfuzzer_libpng_launcher/src/lib.rs index 2a507d1491..5c3e9f6e77 100644 --- a/fuzzers/libfuzzer_libpng_launcher/src/lib.rs +++ b/fuzzers/libfuzzer_libpng_launcher/src/lib.rs @@ -30,6 +30,7 @@ use libafl::{ feedbacks::{CrashFeedback, MapFeedbackState, MaxMapFeedback, TimeFeedback, TimeoutFeedback}, fuzzer::{Fuzzer, StdFuzzer}, inputs::{BytesInput, HasTargetBytes}, + monitors::tui::TuiMonitor, monitors::MultiMonitor, mutators::scheduled::{havoc_mutations, tokens_mutations, StdScheduledMutator}, mutators::token_mutations::Tokens, @@ -141,7 +142,8 @@ pub fn libafl_main() { let shmem_provider = StdShMemProvider::new().expect("Failed to init shared memory"); - let monitor = MultiMonitor::new(|s| println!("{}", s)); + //let monitor = MultiMonitor::new(|s| println!("{}", s)); + let monitor = TuiMonitor::new("Test fuzzer on libpng".into(), true); let mut run_client = |state: Option>, mut restarting_mgr, _core_id| { // Create an observation channel using the coverage map diff --git a/libafl/Cargo.toml b/libafl/Cargo.toml index ce6c9b0d2b..8f8b39887d 100644 --- a/libafl/Cargo.toml +++ b/libafl/Cargo.toml @@ -12,12 +12,13 @@ edition = "2021" [features] default = ["std", "derive", "llmp_compression", "rand_trait", "fork"] -std = ["serde_json", "serde_json/std", "hostname", "core_affinity", "nix", "serde/std", "bincode", "wait-timeout", "regex", "build_id", "uuid"] # print, env, launcher ... support +std = ["serde_json", "serde_json/std", "hostname", "core_affinity", "nix", "serde/std", "bincode", "wait-timeout", "regex", "build_id", "uuid", "tui_monitor"] # print, env, launcher ... support derive = ["libafl_derive"] # provide derive(SerdeAny) macro. fork = [] # uses the fork() syscall to spawn children, instead of launching a new command, if supported by the OS (has no effect on Windows, no_std). rand_trait = ["rand_core"] # If set, libafl's rand implementations will implement `rand::Rng` introspection = [] # Include performance statistics of the fuzzing pipeline concolic_mutation = ["z3"] # include a simple concolic mutator based on z3 +tui_monitor = ["tui", "crossterm"] # enable TuiMonitor with crossterm # features hiding dependencies licensed under GPL gpl = [] # features hiding dependencies licensed under AGPL @@ -70,6 +71,8 @@ regex = { version = "1", optional = true } build_id = { version = "0.2.1", git = "https://github.com/domenukk/build_id", rev = "6a61943", optional = true } uuid = { version = "0.8.2", optional = true, features = ["serde", "v4"] } libm = "0.2.1" +tui = { version = "0.16", default-features = false, features = ['crossterm'], optional = true } +crossterm = { version = "0.20", optional = true } wait-timeout = { version = "0.2", optional = true } # used by CommandExecutor to wait for child process diff --git a/libafl/src/monitors/mod.rs b/libafl/src/monitors/mod.rs index 028fcbadd8..a71467a40f 100644 --- a/libafl/src/monitors/mod.rs +++ b/libafl/src/monitors/mod.rs @@ -3,6 +3,10 @@ pub mod multi; pub use multi::MultiMonitor; +#[cfg(all(feature = "tui_monitor", feature = "std"))] +#[allow(missing_docs)] +pub mod tui; + use alloc::{string::String, vec::Vec}; #[cfg(feature = "introspection")] @@ -17,6 +21,7 @@ use crate::bolts::{current_time, format_duration_hms}; const CLIENT_STATS_TIME_WINDOW_SECS: u64 = 5; // 5 seconds /// User-defined stat types +/// TODO define aggregation function (avg, median, max, ...) #[derive(Serialize, Deserialize, Debug, Clone)] pub enum UserStats { /// A numerical value diff --git a/libafl/src/monitors/tui/mod.rs b/libafl/src/monitors/tui/mod.rs new file mode 100644 index 0000000000..c88e80f00c --- /dev/null +++ b/libafl/src/monitors/tui/mod.rs @@ -0,0 +1,415 @@ +//! Monitor based on tui-rs + +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use hashbrown::HashMap; +use tui::{backend::CrosstermBackend, Terminal}; + +use std::{ + collections::VecDeque, + io::{self, BufRead}, + string::String, + sync::{Arc, RwLock}, + thread, + time::{Duration, Instant}, + vec::Vec, +}; + +#[cfg(feature = "introspection")] +use super::{ClientPerfMonitor, PerfFeature}; + +use crate::{ + bolts::{current_time, format_duration_hms}, + monitors::{ClientStats, Monitor, UserStats}, +}; + +mod ui; +use ui::TuiUI; + +const DEFAULT_TIME_WINDOW: u64 = 60 * 10; // 10 min +const DEFAULT_LOGS_NUMBER: usize = 128; + +#[derive(Debug, Copy, Clone)] +pub struct TimedStat { + pub time: Duration, + pub item: u64, +} + +#[derive(Debug, Clone)] +pub struct TimedStats { + pub series: VecDeque, + pub window: Duration, +} + +impl TimedStats { + #[must_use] + pub fn new(window: Duration) -> Self { + Self { + series: VecDeque::new(), + window, + } + } + + pub fn add(&mut self, time: Duration, item: u64) { + if self.series.is_empty() || self.series.back().unwrap().item != item { + if self.series.front().is_some() + && time - self.series.front().unwrap().time > self.window + { + self.series.pop_front(); + } + self.series.push_back(TimedStat { time, item }); + } + } + + pub fn add_now(&mut self, item: u64) { + if self.series.is_empty() || self.series[self.series.len() - 1].item != item { + let time = current_time(); + if self.series.front().is_some() + && time - self.series.front().unwrap().time > self.window + { + self.series.pop_front(); + } + self.series.push_back(TimedStat { time, item }); + } + } + + pub fn update_window(&mut self, window: Duration) { + self.window = window; + while !self.series.is_empty() + && self.series.back().unwrap().time - self.series.front().unwrap().time > window + { + self.series.pop_front(); + } + } +} + +#[cfg(feature = "introspection")] +#[derive(Debug, Default, Clone)] +pub struct PerfTuiContext { + pub scheduler: f64, + pub manager: f64, + pub unmeasured: f64, + pub stages: Vec>, + pub feedbacks: Vec<(String, f64)>, +} + +#[cfg(feature = "introspection")] +impl PerfTuiContext { + #[allow(clippy::cast_precision_loss)] + pub fn grab_data(&mut self, m: &ClientPerfMonitor) { + // Calculate the elapsed time from the monitor + let elapsed: f64 = m.elapsed_cycles() as f64; + + // Calculate the percentages for each benchmark + self.scheduler = m.scheduler_cycles() as f64 / elapsed; + self.manager = m.manager_cycles() as f64 / elapsed; + + // Calculate the remaining percentage that has not been benchmarked + let mut other_percent = 1.0; + other_percent -= self.scheduler; + other_percent -= self.manager; + + self.stages.clear(); + + // Calculate each stage + // Make sure we only iterate over used stages + for (_stage_index, features) in m.used_stages() { + let mut features_percentages = vec![]; + + for (feature_index, feature) in features.iter().enumerate() { + // Calculate this current stage's percentage + let feature_percent = *feature as f64 / elapsed; + + // Ignore this feature if it isn't used + if feature_percent == 0.0 { + continue; + } + + // Update the other percent by removing this current percent + other_percent -= feature_percent; + + // Get the actual feature from the feature index for printing its name + let feature: PerfFeature = feature_index.into(); + features_percentages.push((format!("{:?}", feature), feature_percent)); + } + + self.stages.push(features_percentages); + } + + self.feedbacks.clear(); + + for (feedback_name, feedback_time) in m.feedbacks() { + // Calculate this current stage's percentage + let feedback_percent = *feedback_time as f64 / elapsed; + + // Ignore this feedback if it isn't used + if feedback_percent == 0.0 { + continue; + } + + // Update the other percent by removing this current percent + other_percent -= feedback_percent; + + self.feedbacks + .push((feedback_name.clone(), feedback_percent)); + } + + self.unmeasured = other_percent; + } +} + +#[derive(Debug, Default, Clone)] +pub struct ClientTuiContext { + pub corpus: u64, + pub objectives: u64, + pub executions: u64, + pub exec_sec: u64, + + pub user_stats: HashMap, +} + +impl ClientTuiContext { + pub fn grab_data(&mut self, client: &ClientStats, exec_sec: u64) { + self.corpus = client.corpus_size; + self.objectives = client.objective_size; + self.executions = client.executions; + self.exec_sec = exec_sec; + + for (key, val) in &client.user_monitor { + self.user_stats.insert(key.clone(), val.clone()); + } + } +} + +#[derive(Debug, Clone)] +pub struct TuiContext { + pub graphs: Vec, + + // TODO update the window using the UI key press events (+/-) + pub corpus_size_timed: TimedStats, + pub objective_size_timed: TimedStats, + pub execs_per_sec_timed: TimedStats, + + #[cfg(feature = "introspection")] + pub introspection: HashMap, + + pub clients: HashMap, + + pub client_logs: VecDeque, + + pub clients_num: usize, + pub total_execs: u64, + pub start_time: Duration, +} + +impl TuiContext { + /// Create a new TUI context + #[must_use] + pub fn new(start_time: Duration) -> Self { + Self { + graphs: vec!["corpus".into(), "objectives".into(), "exec/sec".into()], + corpus_size_timed: TimedStats::new(Duration::from_secs(DEFAULT_TIME_WINDOW)), + objective_size_timed: TimedStats::new(Duration::from_secs(DEFAULT_TIME_WINDOW)), + execs_per_sec_timed: TimedStats::new(Duration::from_secs(DEFAULT_TIME_WINDOW)), + + #[cfg(feature = "introspection")] + introspection: HashMap::default(), + clients: HashMap::default(), + + client_logs: VecDeque::with_capacity(DEFAULT_LOGS_NUMBER), + + clients_num: 0, + total_execs: 0, + start_time, + } + } +} + +/// Tracking monitor during fuzzing and display with tui-rs. +#[derive(Debug, Clone)] +pub struct TuiMonitor { + pub(crate) context: Arc>, + + start_time: Duration, + client_stats: Vec, +} + +impl Monitor for TuiMonitor { + /// the client monitor, mutable + fn client_stats_mut(&mut self) -> &mut Vec { + &mut self.client_stats + } + + /// the client monitor + fn client_stats(&self) -> &[ClientStats] { + &self.client_stats + } + + /// Time this fuzzing run stated + fn start_time(&mut self) -> Duration { + self.start_time + } + + fn display(&mut self, event_msg: String, sender_id: u32) { + let cur_time = current_time(); + + { + let execsec = self.execs_per_sec(); + let totalexec = self.total_execs(); + let run_time = cur_time - self.start_time; + + let mut ctx = self.context.write().unwrap(); + 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(); + } + + let client = self.client_stats_mut_for(sender_id); + let exec_sec = client.execs_per_sec(cur_time); + + let sender = format!("#{}", sender_id); + let pad = if event_msg.len() + sender.len() < 13 { + " ".repeat(13 - event_msg.len() - sender.len()) + } else { + String::new() + }; + let head = format!("{}{} {}", event_msg, pad, sender); + let mut fmt = format!( + "[{}] corpus: {}, objectives: {}, executions: {}, exec/sec: {}", + head, client.corpus_size, client.objective_size, client.executions, exec_sec + ); + for (key, val) in &client.user_monitor { + fmt += &format!(", {}: {}", key, val); + } + + { + let client = &self.client_stats()[sender_id as usize]; + let mut ctx = self.context.write().unwrap(); + ctx.clients + .entry(sender_id as usize) + .or_default() + .grab_data(client, exec_sec); + while ctx.client_logs.len() >= DEFAULT_LOGS_NUMBER { + ctx.client_logs.pop_front(); + } + ctx.client_logs.push_back(fmt); + } + + #[cfg(feature = "introspection")] + { + // Print the client performance monitor. Skip the Client 0 which is the broker + for (i, client) in self.client_stats.iter().skip(1).enumerate() { + self.context + .write() + .unwrap() + .introspection + .entry(i + 1) + .or_default() + .grab_data(&client.introspection_monitor); + } + } + } +} + +impl TuiMonitor { + /// Creates the monitor + #[must_use] + pub fn new(title: String, enhanced_graphics: bool) -> Self { + Self::with_time(title, enhanced_graphics, current_time()) + } + + /// Creates the monitor with a given `start_time`. + #[must_use] + pub fn with_time(title: String, enhanced_graphics: bool, start_time: Duration) -> Self { + let context = Arc::new(RwLock::new(TuiContext::new(start_time))); + run_tui_thread( + context.clone(), + Duration::from_millis(250), + title, + enhanced_graphics, + ); + Self { + context, + start_time, + client_stats: vec![], + } + } +} + +fn run_tui_thread( + context: Arc>, + tick_rate: Duration, + title: String, + enhanced_graphics: bool, +) { + thread::spawn(move || -> io::Result<()> { + // setup terminal + let mut stdout = io::stdout(); + enable_raw_mode()?; + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + let mut ui = TuiUI::new(title, enhanced_graphics); + + let mut last_tick = Instant::now(); + let mut cnt = 0; + loop { + // to avoid initial ui glitches + if cnt < 8 { + drop(terminal.clear()); + cnt += 1; + } + terminal.draw(|f| ui.draw(f, &context))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char(c) => ui.on_key(c), + KeyCode::Left => ui.on_left(), + //KeyCode::Up => ui.on_up(), + KeyCode::Right => ui.on_right(), + //KeyCode::Down => ui.on_down(), + _ => {} + } + } + } + if last_tick.elapsed() >= tick_rate { + //context.on_tick(); + last_tick = Instant::now(); + } + if ui.should_quit { + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + println!("\nPress Control-C to stop the fuzzers, otherwise press Enter to resume the visualization\n"); + + let mut line = String::new(); + io::stdin().lock().read_line(&mut line)?; + + // setup terminal + let mut stdout = io::stdout(); + enable_raw_mode()?; + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + + cnt = 0; + ui.should_quit = false; + } + } + }); +} diff --git a/libafl/src/monitors/tui/ui.rs b/libafl/src/monitors/tui/ui.rs new file mode 100644 index 0000000000..d315f5a411 --- /dev/null +++ b/libafl/src/monitors/tui/ui.rs @@ -0,0 +1,507 @@ +use super::{current_time, format_duration_hms, Duration, String, TimedStats, TuiContext}; + +use tui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + symbols, + text::{Span, Spans}, + widgets::{ + Axis, Block, Borders, Cell, Chart, Dataset, List, ListItem, Paragraph, Row, Table, Tabs, + }, + Frame, +}; + +use std::{ + cmp::{max, min}, + sync::{Arc, RwLock}, +}; + +#[derive(Default)] +pub struct TuiUI { + title: String, + enhanced_graphics: bool, + show_logs: bool, + clients_idx: usize, + clients: usize, + charts_tab_idx: usize, + graph_data: Vec<(f64, f64)>, + + pub should_quit: bool, +} + +impl TuiUI { + pub fn new(title: String, enhanced_graphics: bool) -> Self { + Self { + title, + enhanced_graphics, + show_logs: true, + clients_idx: 1, + ..TuiUI::default() + } + } + + pub fn on_key(&mut self, c: char) { + match c { + 'q' => { + self.should_quit = true; + } + 'g' => { + self.charts_tab_idx = (self.charts_tab_idx + 1) % 3; + } + 't' => { + self.show_logs = !self.show_logs; + } + _ => {} + } + } + + //pub fn on_up(&mut self) {} + + //pub fn on_down(&mut self) {} + + pub fn on_right(&mut self) { + // never 0 + self.clients_idx = 1 + self.clients_idx % (self.clients - 1); + } + + pub fn on_left(&mut self) { + // never 0 + if self.clients_idx == 1 { + self.clients_idx = self.clients - 1; + } else { + self.clients_idx = 1 + (self.clients_idx - 2) % (self.clients - 1); + } + } + + pub fn draw(&mut self, f: &mut Frame, app: &Arc>) + where + B: Backend, + { + self.clients = app.read().unwrap().clients_num; + + let body = Layout::default() + .constraints(if self.show_logs { + [Constraint::Percentage(50), Constraint::Percentage(50)].as_ref() + } else { + [Constraint::Percentage(100)].as_ref() + }) + .split(f.size()); + + let top_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(body[0]); + + let left_layout = Layout::default() + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(top_layout[0]); + + let text = vec![Spans::from(Span::styled( + &self.title, + Style::default() + .fg(Color::LightMagenta) + .add_modifier(Modifier::BOLD), + ))]; + 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]); + + self.draw_text(f, app, left_layout[1]); + + let right_layout = Layout::default() + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(top_layout[1]); + let titles = vec![ + Spans::from(Span::styled( + "speed", + Style::default().fg(Color::LightGreen), + )), + Spans::from(Span::styled( + "corpus", + Style::default().fg(Color::LightGreen), + )), + Spans::from(Span::styled( + "objectives", + Style::default().fg(Color::LightGreen), + )), + ]; + let tabs = Tabs::new(titles) + .block( + Block::default() + .title(Span::styled( + "charts (`g` switch)", + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .highlight_style(Style::default().fg(Color::LightYellow)) + .select(self.charts_tab_idx); + f.render_widget(tabs, right_layout[0]); + + match self.charts_tab_idx { + 0 => { + let ctx = app.read().unwrap(); + self.draw_time_chart( + "speed chart", + "exec/sec", + f, + right_layout[1], + &ctx.execs_per_sec_timed, + ); + } + 1 => { + let ctx = app.read().unwrap(); + self.draw_time_chart( + "corpus chart", + "corpus size", + f, + right_layout[1], + &ctx.corpus_size_timed, + ); + } + 2 => { + let ctx = app.read().unwrap(); + self.draw_time_chart( + "corpus chart", + "objectives", + f, + right_layout[1], + &ctx.objective_size_timed, + ); + } + _ => {} + } + + if self.show_logs { + self.draw_logs(f, app, body[1]); + } + } + + #[allow(clippy::too_many_lines, clippy::cast_precision_loss)] + fn draw_time_chart( + &mut self, + title: &str, + y_name: &str, + f: &mut Frame, + area: Rect, + stats: &TimedStats, + ) where + B: Backend, + { + if stats.series.is_empty() { + return; + } + let start = stats.series.front().unwrap().time; + let end = stats.series.back().unwrap().time; + let min_lbl_x = format_duration_hms(&start); + let med_lbl_x = format_duration_hms(&((end - start) / 2)); + let max_lbl_x = format_duration_hms(&end); + + let x_labels = vec![ + Span::styled(min_lbl_x, Style::default().add_modifier(Modifier::BOLD)), + Span::raw(med_lbl_x), + Span::styled(max_lbl_x, Style::default().add_modifier(Modifier::BOLD)), + ]; + + let max_x = u64::from(area.width); + let window = end - start; + let time_unit = if max_x > window.as_secs() { + 0 // millis / 10 + } else if max_x > window.as_secs() * 60 { + 1 // secs + } else { + 2 // min + }; + let convert_time = |d: &Duration| -> u64 { + if time_unit == 0 { + (d.as_millis() / 10) as u64 + } else if time_unit == 1 { + d.as_secs() + } else { + d.as_secs() * 60 + } + }; + let window_unit = convert_time(&window); + if window_unit == 0 { + return; + } + + let to_x = |d: &Duration| (convert_time(d) - convert_time(&start)) * max_x / window_unit; + + self.graph_data.clear(); + + let mut max_y = u64::MIN; + let mut min_y = u64::MAX; + let mut prev = (0, 0); + for ts in &stats.series { + let x = to_x(&ts.time); + if x > prev.0 + 1 && x < max_x { + for v in (prev.0 + 1)..x { + self.graph_data.push((v as f64, prev.1 as f64)); + } + } + prev = (x, ts.item); + self.graph_data.push((x as f64, ts.item as f64)); + max_y = max(ts.item, max_y); + min_y = min(ts.item, min_y); + } + if max_x > prev.0 + 1 { + for v in (prev.0 + 1)..max_x { + self.graph_data.push((v as f64, prev.1 as f64)); + } + } + + //println!("max_x: {}, len: {}", max_x, self.graph_data.len()); + + let datasets = vec![Dataset::default() + //.name("data") + .marker(if self.enhanced_graphics { + symbols::Marker::Braille + } else { + symbols::Marker::Dot + }) + .style( + Style::default() + .fg(Color::LightYellow) + .add_modifier(Modifier::BOLD), + ) + .data(&self.graph_data)]; + let chart = Chart::new(datasets) + .block( + Block::default() + .title(Span::styled( + title, + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .x_axis( + Axis::default() + .title("time") + .style(Style::default().fg(Color::Gray)) + .bounds([0.0, max_x as f64]) + .labels(x_labels), + ) + .y_axis( + Axis::default() + .title(y_name) + .style(Style::default().fg(Color::Gray)) + .bounds([min_y as f64, max_y as f64]) + .labels(vec![ + Span::styled( + format!("{}", min_y), + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(format!("{}", (max_y - min_y) / 2)), + Span::styled( + format!("{}", max_y), + Style::default().add_modifier(Modifier::BOLD), + ), + ]), + ); + f.render_widget(chart, area); + } + + #[allow(clippy::too_many_lines)] + fn draw_text(&mut self, f: &mut Frame, app: &Arc>, area: Rect) + where + B: Backend, + { + 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), + ))), + ]), + Row::new(vec![ + Cell::from(Span::raw("clients")), + Cell::from(Span::raw(format!("{}", self.clients))), + ]), + Row::new(vec![ + Cell::from(Span::raw("executions")), + Cell::from(Span::raw(format!("{}", app.read().unwrap().total_execs))), + ]), + 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) + ))), + ]), + ]; + + let chunks = Layout::default() + .constraints( + [ + Constraint::Length(2 + items.len() as u16), + Constraint::Min(8), + ] + .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::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![]; + { + 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))), + ])); + client_items.push(Row::new(vec![ + Cell::from(Span::raw("exec/sec")), + Cell::from(Span::raw(format!("{}", client.exec_sec))), + ])); + 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) { + items.push(Row::new(vec![ + Cell::from(Span::raw("scheduler")), + Cell::from(Span::raw(format!("{:.2}%", client.scheduler * 100.0))), + ])); + 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 { + items.push(Row::new(vec![ + Cell::from(Span::raw(key.clone())), + Cell::from(Span::raw(format!("{:.2}%", val * 100.0))), + ])); + } + 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, client_chunks[1]); + } + } + + #[allow(clippy::unused_self)] + fn draw_logs(&mut self, f: &mut Frame, app: &Arc>, area: Rect) + where + B: Backend, + { + let app = app.read().unwrap(); + let logs: Vec = app + .client_logs + .iter() + .map(|msg| ListItem::new(Span::raw(msg))) + .collect(); + let logs = List::new(logs).block( + Block::default().borders(Borders::ALL).title(Span::styled( + "clients logs (`t` to show/hide)", + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )), + ); + f.render_widget(logs, area); + } +}