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?)
/// 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<String, UserStats>,
/// 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()
});
}

View File

@ -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<String, UserStats>,
}
@ -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<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::{
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(100)].as_ref()
[
Constraint::Percentage(41),
Constraint::Percentage(27),
Constraint::Percentage(32),
]
.as_ref()
}
} else {
[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<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, 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,
);
}
_ => {}
}
if self.show_logs {
self.draw_logs(f, app, body[1]);
self.draw_overall_generic_text(f, app, bottom_layout);
}
fn draw_client_ui<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, 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<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect)
where
fn draw_item_geometry_text<B>(
&mut self,
f: &mut Frame<B>,
app: &Arc<RwLock<TuiContext>>,
area: Rect,
is_overall: bool,
) 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!(
"{}",
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()
.execs_per_sec_timed
.series
.back()
.map_or(0, |x| x.item)
))),
.clients
.get(&self.clients_idx)
.unwrap()
.item_geometry
.clone()
};
let items = vec![
Row::new(vec![
Cell::from(Span::raw("pending")),
Cell::from(Span::raw(format!("{}", item_geometry.pending))),
]),
Row::new(vec![
Cell::from(Span::raw("pend fav")),
Cell::from(Span::raw(format!("{}", item_geometry.pend_fav))),
]),
Row::new(vec![
Cell::from(Span::raw("own finds")),
Cell::from(Span::raw(format!("{}", item_geometry.own_finds))),
]),
Row::new(vec![
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<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)
.block(
Block::default()
@ -386,68 +763,16 @@ 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![];
{
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(client.exec_sec.clone())),
]));
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")]
fn draw_introspection_text<B>(
&mut self,
f: &mut Frame<B>,
app: &Arc<RwLock<TuiContext>>,
area: Rect,
) where
B: Backend,
{
let mut items = vec![];
{
@ -499,10 +824,8 @@ impl TuiUI {
.borders(Borders::ALL),
)
.widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
f.render_widget(table, client_chunks[1]);
f.render_widget(table, area);
}
}
#[allow(clippy::unused_self)]
fn draw_logs<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect)
where