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
This commit is contained in:
Andrea Fioraldi 2022-01-20 23:55:48 +01:00 committed by GitHub
parent ab7d16347f
commit cc0880e784
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 934 additions and 2 deletions

View File

@ -30,6 +30,7 @@ use libafl::{
feedbacks::{CrashFeedback, MapFeedbackState, MaxMapFeedback, TimeFeedback, TimeoutFeedback}, feedbacks::{CrashFeedback, MapFeedbackState, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
fuzzer::{Fuzzer, StdFuzzer}, fuzzer::{Fuzzer, StdFuzzer},
inputs::{BytesInput, HasTargetBytes}, inputs::{BytesInput, HasTargetBytes},
monitors::tui::TuiMonitor,
monitors::MultiMonitor, monitors::MultiMonitor,
mutators::scheduled::{havoc_mutations, tokens_mutations, StdScheduledMutator}, mutators::scheduled::{havoc_mutations, tokens_mutations, StdScheduledMutator},
mutators::token_mutations::Tokens, mutators::token_mutations::Tokens,
@ -141,7 +142,8 @@ pub fn libafl_main() {
let shmem_provider = StdShMemProvider::new().expect("Failed to init shared memory"); 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<StdState<_, _, _, _, _>>, mut restarting_mgr, _core_id| { let mut run_client = |state: Option<StdState<_, _, _, _, _>>, mut restarting_mgr, _core_id| {
// Create an observation channel using the coverage map // Create an observation channel using the coverage map

View File

@ -12,12 +12,13 @@ edition = "2021"
[features] [features]
default = ["std", "derive", "llmp_compression", "rand_trait", "fork"] 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. 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). 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` rand_trait = ["rand_core"] # If set, libafl's rand implementations will implement `rand::Rng`
introspection = [] # Include performance statistics of the fuzzing pipeline introspection = [] # Include performance statistics of the fuzzing pipeline
concolic_mutation = ["z3"] # include a simple concolic mutator based on z3 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 # features hiding dependencies licensed under GPL
gpl = [] gpl = []
# features hiding dependencies licensed under AGPL # 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 } 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"] } uuid = { version = "0.8.2", optional = true, features = ["serde", "v4"] }
libm = "0.2.1" 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 wait-timeout = { version = "0.2", optional = true } # used by CommandExecutor to wait for child process

View File

@ -3,6 +3,10 @@
pub mod multi; pub mod multi;
pub use multi::MultiMonitor; pub use multi::MultiMonitor;
#[cfg(all(feature = "tui_monitor", feature = "std"))]
#[allow(missing_docs)]
pub mod tui;
use alloc::{string::String, vec::Vec}; use alloc::{string::String, vec::Vec};
#[cfg(feature = "introspection")] #[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 const CLIENT_STATS_TIME_WINDOW_SECS: u64 = 5; // 5 seconds
/// User-defined stat types /// User-defined stat types
/// TODO define aggregation function (avg, median, max, ...)
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum UserStats { pub enum UserStats {
/// A numerical value /// A numerical value

View File

@ -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<TimedStat>,
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<Vec<(String, f64)>>,
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<String, UserStats>,
}
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<String>,
// 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<usize, PerfTuiContext>,
pub clients: HashMap<usize, ClientTuiContext>,
pub client_logs: VecDeque<String>,
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<RwLock<TuiContext>>,
start_time: Duration,
client_stats: Vec<ClientStats>,
}
impl Monitor for TuiMonitor {
/// the client monitor, mutable
fn client_stats_mut(&mut self) -> &mut Vec<ClientStats> {
&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<RwLock<TuiContext>>,
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;
}
}
});
}

View File

@ -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<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>)
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<B>(
&mut self,
title: &str,
y_name: &str,
f: &mut Frame<B>,
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<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("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<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect)
where
B: Backend,
{
let app = app.read().unwrap();
let logs: Vec<ListItem> = 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);
}
}