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:
parent
ab7d16347f
commit
cc0880e784
@ -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<StdState<_, _, _, _, _>>, mut restarting_mgr, _core_id| {
|
||||
// Create an observation channel using the coverage map
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
415
libafl/src/monitors/tui/mod.rs
Normal file
415
libafl/src/monitors/tui/mod.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
507
libafl/src/monitors/tui/ui.rs
Normal file
507
libafl/src/monitors/tui/ui.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user