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 <Byone.heng@gmail.com>
Co-authored-by: Dominik Maier <domenukk@gmail.com>
This commit is contained in:
ToSeven 2023-09-04 05:52:48 +08:00 committed by GitHub
parent 1b6ef52a4e
commit a0bcdfa005
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 666 additions and 147 deletions

View File

@ -87,12 +87,16 @@ pub struct ClientStats {
// monitor (maybe we need a separated struct?) // monitor (maybe we need a separated struct?)
/// The corpus size for this client /// The corpus size for this client
pub corpus_size: u64, 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 /// The total executions for this client
pub executions: u64, 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) /// 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, pub prev_state_executions: u64,
/// The size of the objectives corpus for this client /// The size of the objectives corpus for this client
pub objective_size: u64, 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 /// The last reported executions for this client
#[cfg(feature = "afl_exec_sec")] #[cfg(feature = "afl_exec_sec")]
pub last_window_executions: u64, pub last_window_executions: u64,
@ -101,6 +105,8 @@ pub struct ClientStats {
pub last_execs_per_sec: f64, pub last_execs_per_sec: f64,
/// The last time we got this information /// The last time we got this information
pub last_window_time: Duration, pub last_window_time: Duration,
/// the start time of the client
pub start_time: Duration,
/// User-defined monitor /// User-defined monitor
pub user_monitor: HashMap<String, UserStats>, pub user_monitor: HashMap<String, UserStats>,
/// Client performance statistics /// Client performance statistics
@ -140,6 +146,7 @@ impl ClientStats {
/// We got a new information about corpus size for this client, insert them. /// We got a new information about corpus size for this client, insert them.
pub fn update_corpus_size(&mut self, corpus_size: u64) { pub fn update_corpus_size(&mut self, corpus_size: u64) {
self.corpus_size = corpus_size; 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. /// 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); self.user_monitor.insert(name, value);
} }
#[must_use]
/// Get a user-defined stat using the name /// 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) self.user_monitor.get(name)
} }
@ -275,6 +283,7 @@ pub trait Monitor {
for _ in client_stat_count..(client_id.0 + 1) as usize { for _ in client_stat_count..(client_id.0 + 1) as usize {
self.client_stats_mut().push(ClientStats { self.client_stats_mut().push(ClientStats {
last_window_time: current_time(), last_window_time: current_time(),
start_time: current_time(),
..ClientStats::default() ..ClientStats::default()
}); });
} }

View File

@ -1,6 +1,6 @@
//! Monitor based on ratatui //! Monitor based on ratatui
use alloc::boxed::Box; use alloc::{boxed::Box, string::ToString};
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
fmt::Write, fmt::Write,
@ -22,6 +22,7 @@ use crossterm::{
use hashbrown::HashMap; use hashbrown::HashMap;
use libafl_bolts::{current_time, format_duration_hms, ClientId}; use libafl_bolts::{current_time, format_duration_hms, ClientId};
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use serde_json::{self, Value};
#[cfg(feature = "introspection")] #[cfg(feature = "introspection")]
use super::{ClientPerfMonitor, PerfFeature}; 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)] #[derive(Debug, Default, Clone)]
pub struct ClientTuiContext { pub struct ClientTuiContext {
pub corpus: u64, pub corpus: u64,
pub objectives: u64, pub objectives: u64,
pub executions: u64, pub executions: u64,
/// Float value formatted as String /// 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<String, UserStats>, pub user_stats: HashMap<String, UserStats>,
} }
@ -178,7 +218,47 @@ impl ClientTuiContext {
self.corpus = client.corpus_size; self.corpus = client.corpus_size;
self.objectives = client.objective_size; self.objectives = client.objective_size;
self.executions = client.executions; 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 { for (key, val) in &client.user_monitor {
self.user_stats.insert(key.clone(), val.clone()); self.user_stats.insert(key.clone(), val.clone());
@ -205,6 +285,14 @@ pub struct TuiContext {
pub clients_num: usize, pub clients_num: usize,
pub total_execs: u64, pub total_execs: u64,
pub start_time: Duration, 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 { impl TuiContext {
@ -226,6 +314,13 @@ impl TuiContext {
clients_num: 0, clients_num: 0,
total_execs: 0, total_execs: 0,
start_time, 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 execsec = self.execs_per_sec() as u64;
let totalexec = self.total_execs(); let totalexec = self.total_execs();
let run_time = cur_time - self.start_time; let run_time = cur_time - self.start_time;
let total_process_timing = self.process_timing();
let mut ctx = self.context.write().unwrap(); 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.corpus_size_timed.add(run_time, self.corpus_size());
ctx.objective_size_timed ctx.objective_size_timed
.add(run_time, self.objective_size()); .add(run_time, self.objective_size());
ctx.execs_per_sec_timed.add(run_time, execsec); ctx.execs_per_sec_timed.add(run_time, execsec);
ctx.total_execs = totalexec; ctx.total_execs = totalexec;
ctx.clients_num = self.client_stats.len(); 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); let client = self.client_stats_mut_for(sender_id);
@ -339,6 +441,91 @@ impl TuiMonitor {
client_stats: vec![], 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<RwLock<TuiContext>>, tick_rate: Duration, tui_ui: TuiUI) { fn run_tui_thread(context: Arc<RwLock<TuiContext>>, tick_rate: Duration, tui_ui: TuiUI) {

View File

@ -1,4 +1,4 @@
use alloc::vec::Vec; use alloc::{string::ToString, vec::Vec};
use std::{ use std::{
cmp::{max, min}, cmp::{max, min},
sync::{Arc, RwLock}, sync::{Arc, RwLock},
@ -16,7 +16,10 @@ use ratatui::{
Frame, 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)] #[derive(Default, Debug)]
pub struct TuiUI { pub struct TuiUI {
@ -99,21 +102,59 @@ impl TuiUI {
let body = Layout::default() let body = Layout::default()
.constraints(if self.show_logs { .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 { } else {
[Constraint::Percentage(100)].as_ref() [Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()
}) })
.split(f.size()); .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<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect)
where
B: Backend,
{
let top_layout = Layout::default() let top_layout = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Length(16), Constraint::Min(0)].as_ref())
.split(body[0]); .split(area);
let bottom_layout = top_layout[1];
let left_layout = Layout::default() let left_top_layout = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)].as_ref())
.split(top_layout[0]); .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(); let mut status_bar: String = self.title.clone();
status_bar = status_bar + " (" + self.version.as_str() + ")"; status_bar = status_bar + " (" + self.version.as_str() + ")";
@ -126,14 +167,24 @@ impl TuiUI {
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
let paragraph = Paragraph::new(text) let paragraph = Paragraph::new(text)
.block(block) .block(block)
.alignment(Alignment::Center); //.wrap(Wrap { trim: true }); .alignment(Alignment::Center);
f.render_widget(paragraph, left_layout[0]); 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()) .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(top_layout[1]); .split(right_top_layout);
let titles = vec![ let titles = vec![
Line::from(Span::styled( Line::from(Span::styled(
"speed", "speed",
@ -144,7 +195,7 @@ impl TuiUI {
Style::default().fg(Color::LightGreen), Style::default().fg(Color::LightGreen),
)), )),
Line::from(Span::styled( Line::from(Span::styled(
"objectives", "objectives (`g` switch)",
Style::default().fg(Color::LightGreen), Style::default().fg(Color::LightGreen),
)), )),
]; ];
@ -152,7 +203,7 @@ impl TuiUI {
.block( .block(
Block::default() Block::default()
.title(Span::styled( .title(Span::styled(
"charts (`g` switch)", "charts",
Style::default() Style::default()
.fg(Color::LightCyan) .fg(Color::LightCyan)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
@ -161,7 +212,9 @@ impl TuiUI {
) )
.highlight_style(Style::default().fg(Color::LightYellow)) .highlight_style(Style::default().fg(Color::LightYellow))
.select(self.charts_tab_idx); .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 { match self.charts_tab_idx {
0 => { 0 => {
@ -170,7 +223,7 @@ impl TuiUI {
"speed chart", "speed chart",
"exec/sec", "exec/sec",
f, f,
right_layout[1], chart_layout,
&ctx.execs_per_sec_timed, &ctx.execs_per_sec_timed,
); );
} }
@ -180,7 +233,7 @@ impl TuiUI {
"corpus chart", "corpus chart",
"corpus size", "corpus size",
f, f,
right_layout[1], chart_layout,
&ctx.corpus_size_timed, &ctx.corpus_size_timed,
); );
} }
@ -190,16 +243,60 @@ impl TuiUI {
"corpus chart", "corpus chart",
"objectives", "objectives",
f, f,
right_layout[1], chart_layout,
&ctx.objective_size_timed, &ctx.objective_size_timed,
); );
} }
_ => {} _ => {}
} }
self.draw_overall_generic_text(f, app, bottom_layout);
}
if self.show_logs { fn draw_client_ui<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect)
self.draw_logs(f, app, body[1]); 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)] #[allow(clippy::too_many_lines, clippy::cast_precision_loss)]
@ -329,37 +426,49 @@ impl TuiUI {
f.render_widget(chart, area); f.render_widget(chart, area);
} }
#[allow(clippy::too_many_lines)] fn draw_item_geometry_text<B>(
fn draw_text<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect) &mut self,
where f: &mut Frame<B>,
app: &Arc<RwLock<TuiContext>>,
area: Rect,
is_overall: bool,
) where
B: Backend, 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![ let items = vec![
Row::new(vec![ Row::new(vec![
Cell::from(Span::raw("run time")), Cell::from(Span::raw("pending")),
Cell::from(Span::raw(format_duration_hms( Cell::from(Span::raw(format!("{}", item_geometry.pending))),
&(current_time() - app.read().unwrap().start_time),
))),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(Span::raw("clients")), Cell::from(Span::raw("pend fav")),
Cell::from(Span::raw(format!("{}", self.clients))), Cell::from(Span::raw(format!("{}", item_geometry.pend_fav))),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(Span::raw("executions")), Cell::from(Span::raw("own finds")),
Cell::from(Span::raw(format!("{}", app.read().unwrap().total_execs))), Cell::from(Span::raw(format!("{}", item_geometry.own_finds))),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(Span::raw("exec/sec")), Cell::from(Span::raw("imported")),
Cell::from(Span::raw(format!( Cell::from(Span::raw(format!("{}", item_geometry.imported))),
"{}", ]),
app.read() Row::new(vec![
.unwrap() Cell::from(Span::raw("stability")),
.execs_per_sec_timed Cell::from(Span::raw(item_geometry.stability)),
.series
.back()
.map_or(0, |x| x.item)
))),
]), ]),
]; ];
@ -373,6 +482,274 @@ impl TuiUI {
) )
.split(area); .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<B>(
&mut self,
f: &mut Frame<B>,
app: &Arc<RwLock<TuiContext>>,
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<B>(
&mut self,
f: &mut Frame<B>,
app: &Arc<RwLock<TuiContext>>,
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<B>(
&mut self,
f: &mut Frame<B>,
app: &Arc<RwLock<TuiContext>>,
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<B>(
&mut self,
f: &mut Frame<B>,
app: &Arc<RwLock<TuiContext>>,
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) let table = Table::new(items)
.block( .block(
Block::default() Block::default()
@ -386,123 +763,69 @@ impl TuiUI {
) )
.widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); .widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
f.render_widget(table, chunks[0]); f.render_widget(table, chunks[0]);
}
let client_block = Block::default() #[cfg(feature = "introspection")]
.title(Span::styled( fn draw_introspection_text<B>(
format!("client #{} (l/r arrows to switch)", self.clients_idx), &mut self,
Style::default() f: &mut Frame<B>,
.fg(Color::LightCyan) app: &Arc<RwLock<TuiContext>>,
.add_modifier(Modifier::BOLD), area: Rect,
)) ) where
.borders(Borders::ALL); B: Backend,
let client_area = client_block.inner(chunks[1]); {
f.render_widget(client_block, chunks[1]); let mut items = vec![];
let mut client_items = vec![];
{ {
let ctx = app.read().unwrap(); let ctx = app.read().unwrap();
if let Some(client) = ctx.clients.get(&self.clients_idx) { if let Some(client) = ctx.introspection.get(&self.clients_idx) {
client_items.push(Row::new(vec![ items.push(Row::new(vec![
Cell::from(Span::raw("executions")), Cell::from(Span::raw("scheduler")),
Cell::from(Span::raw(format!("{}", client.executions))), Cell::from(Span::raw(format!("{:.2}%", client.scheduler * 100.0))),
])); ]));
client_items.push(Row::new(vec![ items.push(Row::new(vec![
Cell::from(Span::raw("exec/sec")), Cell::from(Span::raw("manager")),
Cell::from(Span::raw(client.exec_sec.clone())), Cell::from(Span::raw(format!("{:.2}%", client.manager * 100.0))),
])); ]));
client_items.push(Row::new(vec![ for i in 0..client.stages.len() {
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![ items.push(Row::new(vec![
Cell::from(Span::raw("scheduler")), Cell::from(Span::raw(format!("stage {i}"))),
Cell::from(Span::raw(format!("{:.2}%", client.scheduler * 100.0))), 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] { 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![ items.push(Row::new(vec![
Cell::from(Span::raw(key.clone())), Cell::from(Span::raw(key.clone())),
Cell::from(Span::raw(format!("{:.2}%", val * 100.0))), Cell::from(Span::raw(format!("{:.2}%", val * 100.0))),
])); ]));
} }
}
for (key, val) in &client.feedbacks {
items.push(Row::new(vec![ items.push(Row::new(vec![
Cell::from(Span::raw("not measured")), Cell::from(Span::raw(key.clone())),
Cell::from(Span::raw(format!("{:.2}%", client.unmeasured * 100.0))), Cell::from(Span::raw(format!("{:.2}%", val * 100.0))),
])); ]));
}; }
} items.push(Row::new(vec![
Cell::from(Span::raw("not measured")),
let table = Table::new(items) Cell::from(Span::raw(format!("{:.2}%", client.unmeasured * 100.0))),
.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]);
} }
}
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)] #[allow(clippy::unused_self)]
fn draw_logs<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect) fn draw_logs<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect)
where where