mntime_lib/
app.rs

1// Copyright © ArkBig
2//! This file provides application flow.
3
4use std::{cell::RefCell, collections::HashMap, io::IsTerminal as _, rc::Rc};
5use strum::IntoEnumIterator as _;
6
7/// The application is started and terminated.
8///
9/// Runs on 3 threads, including itself.
10/// Spawn two threads for updating and drawing the application.
11/// - main thread (this): Input monitoring.
12/// - updating thread: Business logic processing and updating data for drawing.
13/// - drawing thread: Output process.
14pub fn run() -> proc_exit::ExitResult {
15    let cli_args = crate::cli_args::parse();
16
17    let _cli_finalizer = initialize_cli();
18
19    // for updating thread
20    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    // for drawing thread
24    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        // Input monitoring.
52        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                        // Cancellation.
59                        (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        // Terminated.
70        draw_tx.send(DrawMsg::Quit).unwrap();
71        drawing_thread.join().unwrap();
72        ret = updating_thread.join().unwrap();
73    });
74
75    // Exit Code
76    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
97/// Initialize cli environment.
98///
99/// This returns a CliFinalizer that implements Drop, so please be good.
100fn initialize_cli() -> Option<CliFinalizer> {
101    if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() {
102        // Automatic finalizer setup
103        let default_panic_hook = std::panic::take_hook();
104        std::panic::set_hook(Box::new(move |panic_info| {
105            // stdout is disrupted, so the finalize first.
106            finalize_cli();
107            default_panic_hook(panic_info);
108        }));
109        let _cli_finalizer = CliFinalizer;
110
111        crossterm::terminal::enable_raw_mode().unwrap();
112        //crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture).unwrap();
113
114        Some(_cli_finalizer)
115    } else {
116        None
117    }
118}
119
120/// Finalize cli environment.
121///
122/// It is set by initialize_cli() to be called automatically.
123///
124/// It can be called in duplicate, and even if some errors occur,
125/// all termination processing is performed anyway.
126fn finalize_cli() {
127    if std::io::stdout().is_terminal() && std::io::stderr().is_terminal() {
128        // if let Err(err) = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture)
129        // {
130        //     eprintln!("[ERROR] {}", err);
131        // }
132
133        if let Err(err) = crossterm::terminal::disable_raw_mode() {
134            eprintln!("[ERROR] {}", err);
135        }
136        // Instead of resetting the cursor position.
137        println!();
138    }
139}
140
141//=============================================================================
142// Updating
143//=============================================================================
144
145/// Messages received by updating thread.
146enum UpdateMsg {
147    Quit,
148}
149
150/// Data model to be updated in the updating thread and viewed in the drawing thread.
151#[derive(Default)]
152struct SharedViewModel {
153    current_run: u16,
154    current_max: u16,
155    current_reports: Vec<HashMap<crate::cmd::MeasItem, f64>>,
156}
157
158/// Updating thread job
159fn 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    // Checking available
167    let time_commands = prepare_time_commands(&rx, tick_rate, cli_args);
168    if time_commands.is_none() {
169        // quit
170        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    // Benchmarking
199    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
276/// Checks and returns the time command to be used.
277///
278/// The default is to try to run BSD and GNU alternately.
279/// If neither of those is available, use built-in.
280fn 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
354/// Check if the specified time command is available.
355fn 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
383//=============================================================================
384// Drawing
385//=============================================================================
386
387/// Messages received by drawing thread.
388enum DrawMsg {
389    Quit,
390    Warn(String),
391    PrintH(String),
392    StartMeasure,
393    ReportMeasure(Vec<HashMap<crate::cmd::MeasItem, f64>>),
394}
395
396// Drawing thread state.
397#[derive(Default, Debug)]
398struct DrawState {
399    measuring: bool,
400    throbber: throbber_widgets_tui::ThrobberState,
401}
402
403// Drawing thread job
404fn 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
475/// Draw loop.
476fn 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 // Actual time not divided by loops.
545                )
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                // Required.
638            }
639            _ => {
640                // Skip if can't measure.
641                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}