1use std::{cell::RefCell, collections::HashMap, io::IsTerminal as _, rc::Rc};
5use strum::IntoEnumIterator as _;
6
7pub fn run() -> proc_exit::ExitResult {
15 let cli_args = crate::cli_args::parse();
16
17 let _cli_finalizer = initialize_cli();
18
19 let (update_tx, update_rx) = std::sync::mpsc::channel();
21 let update_tick_rate = std::time::Duration::from_millis(50);
22 let model = std::sync::Arc::new(std::sync::RwLock::new(SharedViewModel::default()));
23 let (draw_tx, draw_rx) = std::sync::mpsc::channel();
25 let draw_tick_rate = std::time::Duration::from_millis(100);
26 let backend = ratatui::backend::CrosstermBackend::new(std::io::stdout());
27 let mut terminal = crate::terminal::Wrapper::new(backend);
28
29 let mut ret = (proc_exit::Code::SUCCESS, None);
30 std::thread::scope(|s| {
31 let draw_tx_clone = draw_tx.clone();
32 let updating_thread = s.spawn(|| {
33 run_app(
34 update_rx,
35 update_tick_rate,
36 draw_tx_clone,
37 model.clone(),
38 &cli_args,
39 )
40 });
41 let drawing_thread = s.spawn(|| {
42 view_app(
43 draw_rx,
44 draw_tick_rate,
45 model.clone(),
46 &cli_args,
47 &mut terminal,
48 )
49 });
50
51 let is_in_tty = std::io::stdin().is_terminal();
53 while !updating_thread.is_finished() {
54 if is_in_tty && crossterm::event::poll(update_tick_rate).unwrap() {
55 if let crossterm::event::Event::Key(key) = crossterm::event::read().unwrap() {
56 use crossterm::event::{KeyCode, KeyModifiers};
57 match (key.code, key.modifiers) {
58 (KeyCode::Char('c'), KeyModifiers::CONTROL)
60 | (KeyCode::Char('q'), KeyModifiers::NONE) => {
61 update_tx.send(UpdateMsg::Quit).unwrap()
62 }
63 _ => {}
64 }
65 }
66 }
67 }
68
69 draw_tx.send(DrawMsg::Quit).unwrap();
71 drawing_thread.join().unwrap();
72 ret = updating_thread.join().unwrap();
73 });
74
75 let exit_code = ret.0;
77 let exit_msg = ret.1;
78 if exit_code == proc_exit::Code::SUCCESS && exit_msg.is_none() {
79 Ok(())
80 } else {
81 let res = proc_exit::Exit::new(exit_code);
82 if let Some(msg) = exit_msg {
83 Err(res.with_message(msg))
84 } else {
85 Err(res)
86 }
87 }
88}
89
90struct CliFinalizer;
91impl Drop for CliFinalizer {
92 fn drop(&mut self) {
93 finalize_cli();
94 }
95}
96
97fn initialize_cli() -> Option<CliFinalizer> {
101 if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() {
102 let default_panic_hook = std::panic::take_hook();
104 std::panic::set_hook(Box::new(move |panic_info| {
105 finalize_cli();
107 default_panic_hook(panic_info);
108 }));
109 let _cli_finalizer = CliFinalizer;
110
111 crossterm::terminal::enable_raw_mode().unwrap();
112 Some(_cli_finalizer)
115 } else {
116 None
117 }
118}
119
120fn finalize_cli() {
127 if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() {
128 if let Err(err) = crossterm::terminal::disable_raw_mode() {
134 eprintln!("[ERROR] {}", err);
135 }
136 println!();
138 }
139}
140
141enum UpdateMsg {
147 Quit,
148}
149
150#[derive(Default)]
152struct SharedViewModel {
153 current_run: u16,
154 current_max: u16,
155 current_reports: Vec<HashMap<crate::cmd::MeasItem, f64>>,
156}
157
158fn run_app(
160 rx: std::sync::mpsc::Receiver<UpdateMsg>,
161 tick_rate: std::time::Duration,
162 draw_tx: std::sync::mpsc::Sender<DrawMsg>,
163 model: std::sync::Arc<std::sync::RwLock<SharedViewModel>>,
164 cli_args: &crate::cli_args::CliArgs,
165) -> (proc_exit::Code, Option<String>) {
166 let time_commands = prepare_time_commands(&rx, tick_rate, cli_args);
168 if time_commands.is_none() {
169 return (proc_exit::Code::FAILURE, None);
171 }
172 let time_commands = time_commands.unwrap();
173 if time_commands.is_empty() {
174 return (
175 proc_exit::Code::FAILURE,
176 Some(String::from(
177 "time command not found. Install the BSD or GNU version or both.",
178 )),
179 );
180 }
181 if !cli_args.use_builtin_only {
182 if !cli_args.no_bsd
183 && !time_commands
184 .iter()
185 .any(|x| x.borrow().cmd_type == crate::cmd::CmdType::Bsd)
186 {
187 draw_tx.send(DrawMsg::Warn("The bsd time command not found. Please install or specify `--no-bsd` to turn off this warning.".to_string())).unwrap();
188 }
189 if !cli_args.no_gnu
190 && !time_commands
191 .iter()
192 .any(|x| x.borrow().cmd_type == crate::cmd::CmdType::Gnu)
193 {
194 draw_tx.send(DrawMsg::Warn("The gnu time command not found. Please install or specify `--no-gnu=` to turn off this warning.".to_string())).unwrap();
195 }
196 }
197
198 let mut last_tick = std::time::Instant::now();
200 for (target_index, target) in cli_args.normalized_commands().iter().enumerate() {
201 draw_tx
202 .send(DrawMsg::PrintH(format!(
203 "Benchmark #{}> {}",
204 target_index + 1,
205 target
206 )))
207 .unwrap();
208 {
209 let mut m = model.write().unwrap();
210 m.current_reports = Vec::new();
211 m.current_max = cli_args.runs;
212 draw_tx.send(DrawMsg::StartMeasure).unwrap();
213 }
214 for n in 0..cli_args.runs {
215 model.write().unwrap().current_run = n;
216 let time_cmd = Rc::clone(&time_commands[(n as usize) % time_commands.len()]);
217 let mut running = false;
218 loop {
219 if running {
220 if (*time_cmd).borrow_mut().is_finished() {
221 model
222 .write()
223 .unwrap()
224 .current_reports
225 .push((*time_cmd).borrow_mut().get_report().unwrap().clone());
226 break;
227 }
228 } else {
229 let time_cmd_result = if cli_args.loops <= 1 {
230 (*time_cmd).borrow_mut().execute(target.as_str())
231 } else {
232 (*time_cmd).borrow_mut().execute(
233 format!(
234 "sh -c 'for i in {} ;do {};done'",
235 vec!["0"; cli_args.loops as usize].join(" "),
236 target
237 )
238 .as_str(),
239 )
240 };
241 if let Err(err) = time_cmd_result {
242 return (proc_exit::Code::FAILURE, Some(format!("{:}", err)));
243 }
244 running = true;
245 }
246 if wait_recv_quit(&rx, tick_rate, last_tick) {
247 if running {
248 (*time_cmd).borrow_mut().kill().unwrap();
249 }
250 return (proc_exit::Code::FAILURE, None);
251 }
252 last_tick = std::time::Instant::now();
253 }
254 }
255 draw_tx
256 .send(DrawMsg::ReportMeasure(
257 model.read().unwrap().current_reports.clone(),
258 ))
259 .unwrap();
260 }
261 (proc_exit::Code::SUCCESS, None)
262}
263
264fn wait_recv_quit(
265 rx: &std::sync::mpsc::Receiver<UpdateMsg>,
266 tick_rate: std::time::Duration,
267 last_tick: std::time::Instant,
268) -> bool {
269 let timeout = tick_rate
270 .checked_sub(last_tick.elapsed())
271 .unwrap_or_else(|| std::time::Duration::from_secs(0));
272 let msg = rx.recv_timeout(timeout);
273 matches!(msg, Ok(UpdateMsg::Quit))
274}
275
276fn prepare_time_commands(
281 rx: &std::sync::mpsc::Receiver<UpdateMsg>,
282 tick_rate: std::time::Duration,
283 cli_args: &crate::cli_args::CliArgs,
284) -> Option<Vec<Rc<RefCell<crate::cmd::TimeCmd>>>> {
285 let mut commands = Vec::<_>::new();
286 if !cli_args.use_builtin_only {
287 if !cli_args.no_bsd {
288 let mut fallback_sh = false;
289 loop {
290 let mut cmd = crate::cmd::try_new_bsd_time(cli_args, fallback_sh);
291 match command_available(rx, tick_rate, &mut cmd) {
292 None => return None,
293 Some(available) => {
294 if available {
295 commands.push(Rc::new(RefCell::new(cmd.unwrap())));
296 break;
297 } else if fallback_sh {
298 break;
299 } else {
300 fallback_sh = true;
301 }
302 }
303 }
304 }
305 }
306 if !cli_args.no_gnu {
307 let mut fallback_sh = false;
308 let mut fallback_time = false;
309 loop {
310 let mut cmd = crate::cmd::try_new_gnu_time(cli_args, fallback_sh, fallback_time);
311 match command_available(rx, tick_rate, &mut cmd) {
312 None => return None,
313 Some(available) => {
314 if available {
315 commands.push(Rc::new(RefCell::new(cmd.unwrap())));
316 break;
317 } else if fallback_sh && fallback_time {
318 break;
319 } else if fallback_sh {
320 fallback_time = true;
321 } else if fallback_time {
322 fallback_sh = true;
323 fallback_time = false;
324 } else {
325 fallback_time = true;
326 }
327 }
328 }
329 }
330 }
331 }
332 if commands.is_empty() {
333 let mut fallback_sh = false;
334 loop {
335 let mut cmd = crate::cmd::try_new_builtin_time(cli_args, fallback_sh);
336 match command_available(rx, tick_rate, &mut cmd) {
337 None => return None,
338 Some(available) => {
339 if available {
340 commands.push(Rc::new(RefCell::new(cmd.unwrap())));
341 break;
342 } else if fallback_sh {
343 break;
344 } else {
345 fallback_sh = true;
346 }
347 }
348 }
349 }
350 }
351 Some(commands)
352}
353
354fn command_available(
356 rx: &std::sync::mpsc::Receiver<UpdateMsg>,
357 tick_rate: std::time::Duration,
358 command: &mut anyhow::Result<crate::cmd::TimeCmd>,
359) -> Option<bool> {
360 if command.is_err() {
361 return Some(false);
362 }
363 let mut last_tick = std::time::Instant::now();
364 let cmd = command.as_mut().unwrap();
365 loop {
366 match cmd.ready_status() {
367 crate::cmd::ReadyStatus::Checking => {}
368 crate::cmd::ReadyStatus::Ready => {
369 return Some(true);
370 }
371 crate::cmd::ReadyStatus::Error => {
372 return Some(false);
373 }
374 }
375
376 if wait_recv_quit(rx, tick_rate, last_tick) {
377 return None;
378 }
379 last_tick = std::time::Instant::now();
380 }
381}
382
383enum DrawMsg {
389 Quit,
390 Warn(String),
391 PrintH(String),
392 StartMeasure,
393 ReportMeasure(Vec<HashMap<crate::cmd::MeasItem, f64>>),
394}
395
396#[derive(Default, Debug)]
398struct DrawState {
399 measuring: bool,
400 throbber: throbber_widgets_tui::ThrobberState,
401}
402
403fn view_app<B>(
405 rx: std::sync::mpsc::Receiver<DrawMsg>,
406 tick_rate: std::time::Duration,
407 model: std::sync::Arc<std::sync::RwLock<SharedViewModel>>,
408 cli_args: &crate::cli_args::CliArgs,
409 terminal: &mut crate::terminal::Wrapper<B>,
410) where
411 B: ratatui::backend::Backend,
412{
413 let mut draw_state = DrawState::default();
414
415 let mut last_tick = std::time::Instant::now();
416 loop {
417 let timeout = tick_rate
418 .checked_sub(last_tick.elapsed())
419 .unwrap_or_else(|| std::time::Duration::from_secs(0));
420 let msg = rx.recv_timeout(timeout);
421 match msg {
422 Ok(DrawMsg::Quit) => {
423 return;
424 }
425 Ok(DrawMsg::Warn(text)) => {
426 terminal.clear_after();
427 terminal.queue_attribute_err(crossterm::style::Attribute::Bold);
428 terminal.queue_fg_err(crossterm::style::Color::Yellow);
429 terminal
430 .queue_print_err(crossterm::style::Print(format!("[WARNING]: {0}\r\n", text)));
431 terminal.flush_err(true);
432 }
433 Ok(DrawMsg::PrintH(text)) => {
434 terminal.clear_after();
435 static CONTINUE_TIME: std::sync::atomic::AtomicBool =
436 std::sync::atomic::AtomicBool::new(false);
437 if CONTINUE_TIME.load(std::sync::atomic::Ordering::Relaxed) {
438 terminal.queue_print(crossterm::style::Print("\r\n"));
439 }
440 terminal.queue_attribute(crossterm::style::Attribute::Bold);
441 terminal.queue_fg(crossterm::style::Color::Cyan);
442 terminal.queue_print(crossterm::style::Print(text + "\r\n"));
443 terminal.flush(true);
444 CONTINUE_TIME.store(true, std::sync::atomic::Ordering::Relaxed);
445 }
446 Ok(DrawMsg::StartMeasure) => {
447 draw_state.measuring = true;
448 }
449 Ok(DrawMsg::ReportMeasure(reports)) => {
450 draw_state.measuring = false;
451 terminal.clear_after();
452 print_reports(terminal, reports.as_ref(), cli_args.loops);
453 }
454 _ => {}
455 }
456
457 if last_tick.elapsed() >= tick_rate {
458 let mut cur_y = terminal.get_cursor().1;
459 terminal.draw_if_tty(|f| {
460 ui(
461 f,
462 model.read().as_ref().unwrap(),
463 &mut draw_state,
464 &mut cur_y,
465 cli_args.loops,
466 )
467 });
468 last_tick = std::time::Instant::now();
469 terminal.set_cursor(0, cur_y);
470 draw_state.throbber.calc_next();
471 }
472 }
473}
474
475fn ui(
477 f: &mut ratatui::Frame,
478 model: &SharedViewModel,
479 state: &mut DrawState,
480 cur_y: &mut u16,
481 loops: u16,
482) {
483 let mut _offset_y = 0;
484 if state.measuring {
485 _offset_y += draw_progress(f, model, state, cur_y, _offset_y, loops);
486 _offset_y += draw_summary_report(f, model, state, cur_y, _offset_y, loops);
487 }
488}
489
490fn draw_progress(
491 f: &mut ratatui::Frame,
492 model: &SharedViewModel,
493 state: &mut DrawState,
494 cur_y: &mut u16,
495 offset_y: u16,
496 loops: u16,
497) -> u16 {
498 let size = f.size();
499 let height = 1;
500 if size.height < offset_y + height {
501 return 0;
502 }
503 while size.height < *cur_y + offset_y + height {
504 println!();
505 *cur_y -= 1;
506 }
507
508 let rect = ratatui::layout::Rect::new(0, *cur_y + offset_y, size.width, height);
509 let chunks = ratatui::layout::Layout::default()
510 .direction(ratatui::layout::Direction::Horizontal)
511 .constraints(
512 [
513 ratatui::layout::Constraint::Min(10),
514 ratatui::layout::Constraint::Percentage(100),
515 ]
516 .as_ref(),
517 )
518 .split(rect);
519
520 let throbber = throbber_widgets_tui::Throbber::default()
521 .label(format!("{:>3}/{:<3}", model.current_run, model.current_max))
522 .style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan))
523 .throbber_set(throbber_widgets_tui::CLOCK)
524 .use_type(throbber_widgets_tui::WhichUse::Spin);
525 f.render_stateful_widget(throbber, chunks[0], &mut state.throbber);
526
527 let label = if model.current_reports.is_empty() {
528 String::from("Measuring...")
529 } else {
530 let samples: Vec<_> = model
531 .current_reports
532 .iter()
533 .filter_map(|x| x.get(&crate::cmd::MeasItem::Real))
534 .copied()
535 .collect();
536 let stats = crate::stats::Stats::new(&samples);
537 if 0.0 < stats.mean {
538 format!(
539 "Mean {}, so about {} left",
540 crate::cmd::meas_item_unit_value(&crate::cmd::MeasItem::Real, stats.mean, loops),
541 crate::cmd::meas_item_unit_value(
542 &crate::cmd::MeasItem::Real,
543 stats.mean * ((model.current_max - model.current_run) as f64),
544 1 )
546 )
547 } else {
548 String::from("Measuring...")
549 }
550 };
551 let gauge = ratatui::widgets::Gauge::default()
552 .gauge_style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan))
553 .ratio(model.current_run as f64 / model.current_max as f64)
554 .label(label);
555 f.render_widget(gauge, chunks[1]);
556
557 height
558}
559
560fn draw_summary_report(
561 f: &mut ratatui::Frame,
562 model: &SharedViewModel,
563 _state: &mut DrawState,
564 cur_y: &mut u16,
565 offset_y: u16,
566 loops: u16,
567) -> u16 {
568 use crate::cmd::{meas_item_unit_value, MeasItem};
569
570 let size = f.size();
571 let height = 1;
572 if size.height < offset_y + height {
573 return 0;
574 }
575 while size.height < *cur_y + offset_y + height {
576 println!();
577 *cur_y -= 1;
578 }
579
580 let rect = ratatui::layout::Rect::new(0, *cur_y + offset_y, size.width, height);
581 let chunks = ratatui::layout::Layout::default()
582 .direction(ratatui::layout::Direction::Horizontal)
583 .constraints(
584 [
585 ratatui::layout::Constraint::Percentage(33),
586 ratatui::layout::Constraint::Percentage(33),
587 ratatui::layout::Constraint::Percentage(33),
588 ]
589 .as_ref(),
590 )
591 .split(rect);
592
593 for (index, item) in [MeasItem::Real, MeasItem::User, MeasItem::Sys]
594 .iter()
595 .enumerate()
596 {
597 let samples: Vec<_> = model
598 .current_reports
599 .iter()
600 .filter_map(|x| x.get(item))
601 .copied()
602 .collect();
603 let stats = crate::stats::Stats::new(&samples);
604 let text = ratatui::widgets::Paragraph::new(ratatui::text::Line::from(format!(
605 "{} {} ± {}",
606 item.as_ref(),
607 meas_item_unit_value(item, stats.mean, loops),
608 meas_item_unit_value(item, stats.stdev, loops),
609 )));
610 f.render_widget(text, chunks[index]);
611 }
612
613 height
614}
615
616fn print_reports<B>(
617 terminal: &mut crate::terminal::Wrapper<B>,
618 reports: &[HashMap<crate::cmd::MeasItem, f64>],
619 loops: u16,
620) where
621 B: ratatui::backend::Backend,
622{
623 use crate::cmd::{meas_item_name, meas_item_name_max_width, meas_item_unit_value};
624
625 const MEAN_WIDTH: usize = 13;
626
627 let mut lines = Vec::new();
628 let mut exist_error = false;
629 for item in crate::cmd::MeasItem::iter() {
630 let samples: Vec<_> = reports
631 .iter()
632 .filter_map(|x| x.get(&item))
633 .copied()
634 .collect();
635 match item {
636 crate::cmd::MeasItem::Real | crate::cmd::MeasItem::User | crate::cmd::MeasItem::Sys => {
637 }
639 _ => {
640 if !samples.iter().any(|&x| x.to_bits() != 0) {
642 continue;
643 }
644 if samples.is_empty() {
645 continue;
646 }
647 }
648 }
649 if item == crate::cmd::MeasItem::ExitStatus {
650 exist_error = true;
651 print_exit_status(terminal, &samples, loops);
652 continue;
653 }
654 let stats = crate::stats::Stats::new(&samples);
655 lines.push(format!(
656 "{:name_width$}:{:>mean_width$} ± {} ({:.1} %) [{} ≦ {} ≦ {}] / {}",
657 meas_item_name(&item, loops),
658 meas_item_unit_value(&item, stats.mean, loops),
659 meas_item_unit_value(&item, stats.stdev, loops),
660 stats.calc_cv() * 100.0,
661 meas_item_unit_value(&item, stats.min(), loops),
662 meas_item_unit_value(&item, stats.median(), loops),
663 meas_item_unit_value(&item, stats.max(), loops),
664 stats.count(),
665 name_width = meas_item_name_max_width(loops),
666 mean_width = MEAN_WIDTH,
667 ));
668 if stats.has_outlier() {
669 lines.push(format!(
670 "{:^name_width$}:{:>mean_width$} ± {} ({:.1} %) [{} ≦ {} ≦ {}] / {}(-{})",
671 "└─Excluding Outlier",
672 meas_item_unit_value(&item, stats.mean_excluding_outlier, loops),
673 meas_item_unit_value(&item, stats.stdev_excluding_outlier, loops),
674 stats.calc_cv_excluding_outlier() * 100.0,
675 meas_item_unit_value(&item, stats.min_excluding_outlier(), loops),
676 meas_item_unit_value(&item, stats.median_excluding_outlier(), loops),
677 meas_item_unit_value(&item, stats.max_excluding_outlier(), loops),
678 stats.count_excluding_outlier(),
679 stats.outlier_count,
680 name_width = meas_item_name_max_width(loops),
681 mean_width = MEAN_WIDTH,
682 ));
683 }
684 }
685
686 if exist_error {
687 terminal.queue_fg(crossterm::style::Color::Red);
688 } else {
689 terminal.queue_fg(crossterm::style::Color::Green);
690 }
691 terminal.queue_print(crossterm::style::Print(format!(
692 "{:^name_width$}:{:>mean_width$} ± σ (Coefficient of variation %) [Min ≦ Median ≦ Max] / Valid count\r\n",
693 "LEGEND",
694 "Mean",
695 name_width = meas_item_name_max_width(loops),
696 mean_width = MEAN_WIDTH,
697 )));
698 terminal.queue_attribute(crossterm::style::Attribute::Reset);
699
700 terminal.queue_print(crossterm::style::Print(lines.join("\r\n") + "\r\n"));
701 terminal.flush(true);
702}
703
704fn print_exit_status<B>(terminal: &mut crate::terminal::Wrapper<B>, samples: &[f64], loops: u16)
705where
706 B: ratatui::backend::Backend,
707{
708 use crate::cmd::{meas_item_name, meas_item_name_max_width};
709
710 let mut histogram = samples.iter().fold(HashMap::<i32, i16>::new(), |mut s, x| {
711 let code = x.floor() as i32;
712 if let std::collections::hash_map::Entry::Vacant(e) = s.entry(code) {
713 e.insert(1);
714 } else {
715 *s.get_mut(&code).unwrap() += 1;
716 }
717 s
718 });
719 let success = *histogram.get(&0).unwrap_or(&0);
720 if histogram.get(&0).is_some() {
721 histogram.remove(&0);
722 }
723 let failure = samples.len() - success as usize;
724 let mut failure_codes = histogram.iter().collect::<Vec<_>>();
725 failure_codes.sort_by(|a, b| a.0.cmp(b.0));
726 terminal.queue_fg(crossterm::style::Color::Red);
727 terminal.queue_print(crossterm::style::Print(format!(
728 "{:>name_width$}: ",
729 meas_item_name(&crate::cmd::MeasItem::ExitStatus, loops),
730 name_width = meas_item_name_max_width(loops)
731 )));
732 terminal.queue_fg(crossterm::style::Color::Green);
733 terminal.queue_print(crossterm::style::Print(format!(
734 "Success {} times. ",
735 success
736 )));
737 terminal.queue_fg(crossterm::style::Color::Red);
738 terminal.queue_print(crossterm::style::Print(format!(
739 "Failure {} times. [(code× times) {}]\r\n",
740 failure,
741 failure_codes
742 .iter()
743 .map(|x| format!("{}× {}", x.0, x.1))
744 .collect::<Vec<_>>()
745 .join(", ")
746 )));
747 terminal.queue_attribute(crossterm::style::Attribute::Reset);
748 terminal.flush(true);
749}