1use crate::logger::{EvaluationProgressLogger, ProgressSnapshot, TrainingProgressLogger};
2use crate::metric::{MetricAttributes, MetricDefinition, MetricId};
3use crate::renderer::tui::TuiSplit;
4use crate::renderer::{EvaluationName, MetricState, MetricsRenderer, MetricsRendererEvaluation};
5use crate::renderer::{MetricsRendererTraining, tui::NumericMetricsState};
6use crate::{Interrupter, LearnerSummary};
7use burn_core::data::dataloader::Progress;
8use ratatui::{
9 Terminal,
10 crossterm::{
11 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
12 execute,
13 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
14 },
15 prelude::*,
16};
17use std::collections::HashMap;
18use std::panic::{set_hook, take_hook};
19use std::sync::Once;
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::mpsc::{Receiver, Sender};
22use std::sync::{Arc, Mutex, mpsc};
23use std::thread::JoinHandle;
24use std::{
25 error::Error,
26 io::{self, Stdout},
27 time::{Duration, Instant},
28};
29
30use super::{
31 Callback, CallbackFn, ControlsView, MetricsView, PopupState, ProgressBarState, StatusState,
32 TextEventOutcome, TextMetricsState, TuiGroup, TuiTag,
33};
34
35pub(crate) type TerminalBackend = CrosstermBackend<Stdout>;
37pub(crate) type TerminalFrame<'a> = ratatui::Frame<'a>;
39
40type PanicHook = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + 'static + Sync + Send>;
41
42const MAX_REFRESH_RATE_MILLIS: u64 = 100;
43
44static USER_KILL_REQUESTED: AtomicBool = AtomicBool::new(false);
45static USER_KILL_NOTIFICATION: Once = Once::new();
47
48enum TuiRendererEvent {
49 MetricRegistration(MetricDefinition),
50 MetricsUpdate((TuiSplit, TuiGroup, MetricState)),
51 StatusUpdateTrain((TuiSplit, ProgressSnapshot)),
52 StatusUpdateTest(ProgressSnapshot),
53 ProcessEnd {
54 summary: Option<LearnerSummary>,
55 reset: bool,
57 },
58 CounterUpdate(String),
59 LabelUpdate(Option<String>),
60 SplitEnd,
61 ManualClose,
62 Close,
63 Persistent,
64}
65
66pub struct TuiMetricsRendererWrapper {
68 sender: mpsc::Sender<TuiRendererEvent>,
69 interrupter: Interrupter,
70 handle_join: Option<JoinHandle<()>>,
71 kill_signal: Arc<Mutex<Receiver<()>>>,
72 current_split: TuiSplit,
73 training_progress: ProgressSnapshot,
74 eval_progress: ProgressSnapshot,
75}
76
77impl TuiMetricsRendererWrapper {
78 pub fn new(interrupter: Interrupter, checkpoint: Option<usize>) -> Self {
80 let (sender, receiver) = mpsc::channel();
81 let (kill_signal_sender, kill_signal_receiver) = mpsc::channel();
82
83 let interrupter_clone = interrupter.clone();
84 let handle_join = std::thread::Builder::new()
85 .name("train-renderer".into())
86 .spawn(move || {
87 let mut renderer =
88 TuiMetricsRenderer::new(interrupter_clone, checkpoint, kill_signal_sender);
89
90 let tick_rate = Duration::from_millis(MAX_REFRESH_RATE_MILLIS);
91 loop {
92 let remaining_time = tick_rate.saturating_sub(renderer.last_update.elapsed());
93 match receiver.recv_timeout(remaining_time) {
94 Ok(event) => renderer.handle_event(event),
95 Err(mpsc::RecvTimeoutError::Timeout) => (),
96 Err(mpsc::RecvTimeoutError::Disconnected) => {
97 log::error!("Renderer thread disconnected.");
98 break;
99 }
100 }
101
102 if renderer.last_update.elapsed() >= tick_rate
104 && let Err(err) = renderer.render()
105 {
106 log::error!("Render error: {err}");
107 break;
108 }
109
110 if (renderer.manual_close && renderer.interrupter.should_stop())
111 || renderer.close
112 {
113 break;
114 }
115 }
116 })
117 .unwrap();
118
119 let init = Progress::new(0, 0, None);
120 Self {
121 sender,
122 interrupter,
123 handle_join: Some(handle_join),
124 kill_signal: Arc::new(Mutex::new(kill_signal_receiver)),
125 current_split: TuiSplit::Train,
126 training_progress: ProgressSnapshot::new(init.clone(), init.clone()),
127 eval_progress: ProgressSnapshot::new(init.clone(), init),
128 }
129 }
130
131 fn send_event(&self, event: TuiRendererEvent) {
132 if self.kill_signal.lock().unwrap().try_recv().is_ok()
133 || USER_KILL_REQUESTED.load(Ordering::Relaxed)
134 {
135 USER_KILL_REQUESTED.store(true, Ordering::Relaxed);
136 panic!("Killing training from user input.");
137 }
138
139 if let Err(e) = self.sender.send(event) {
140 if !USER_KILL_REQUESTED.load(Ordering::Relaxed) {
142 log::warn!("Failed to send TUI event: {e}");
143 }
144 }
145 }
146
147 pub fn persistent(self) -> Self {
149 self.send_event(TuiRendererEvent::Persistent);
150 self
151 }
152
153 fn join_render_thread(&mut self) {
159 if let Some(handle) = self.handle_join.take() {
160 let _ = handle.join();
161 }
162 }
163}
164
165struct TuiMetricsRenderer {
166 terminal: Terminal<TerminalBackend>,
167 last_update: std::time::Instant,
168 progress: ProgressBarState,
169 metric_definitions: HashMap<MetricId, MetricDefinition>,
170 metrics_numeric: NumericMetricsState,
171 metrics_text: TextMetricsState,
172 status: StatusState,
173 interrupter: Interrupter,
174 popup: PopupState,
175 previous_panic_hook: Option<Arc<PanicHook>>,
176 persistent: bool,
177 manual_close: bool,
178 close: bool,
179 summary: Option<LearnerSummary>,
180 kill_signal: Sender<()>,
181}
182
183impl MetricsRendererEvaluation for TuiMetricsRendererWrapper {
184 fn update_test(&mut self, name: EvaluationName, state: MetricState) {
185 self.send_event(TuiRendererEvent::MetricsUpdate((
186 TuiSplit::Test,
187 TuiGroup::Named(name.name),
188 state,
189 )));
190 }
191
192 fn on_test_end(&mut self, summary: Option<LearnerSummary>) -> Result<(), Box<dyn Error>> {
193 self.send_event(TuiRendererEvent::ProcessEnd {
195 summary,
196 reset: false,
197 });
198 Ok(())
199 }
200}
201
202impl MetricsRenderer for TuiMetricsRendererWrapper {
203 fn manual_close(&mut self) {
204 self.send_event(TuiRendererEvent::ManualClose);
205 self.join_render_thread();
206 }
207
208 fn register_metric(&mut self, definition: MetricDefinition) {
209 self.send_event(TuiRendererEvent::MetricRegistration(definition));
210 }
211}
212
213impl MetricsRendererTraining for TuiMetricsRendererWrapper {
214 fn update_train(&mut self, state: MetricState) {
215 self.send_event(TuiRendererEvent::MetricsUpdate((
216 TuiSplit::Train,
217 TuiGroup::Default,
218 state,
219 )));
220 }
221
222 fn update_valid(&mut self, state: MetricState) {
223 self.send_event(TuiRendererEvent::MetricsUpdate((
224 TuiSplit::Valid,
225 TuiGroup::Default,
226 state,
227 )));
228 }
229
230 fn on_train_end(&mut self, summary: Option<LearnerSummary>) -> Result<(), Box<dyn Error>> {
231 self.interrupter.reset();
233 self.send_event(TuiRendererEvent::ProcessEnd {
235 summary,
236 reset: true,
237 });
238 Ok(())
239 }
240}
241
242impl TrainingProgressLogger for TuiMetricsRendererWrapper {
243 fn start(
244 &mut self,
245 total_epochs: usize,
246 starting_epoch: usize,
247 total_items: Option<usize>,
248 label: Option<&str>,
249 ) {
250 self.send_event(TuiRendererEvent::LabelUpdate(label.map(str::to_string)));
251 self.training_progress.global =
252 Progress::new(starting_epoch, total_epochs, Some("epochs".to_string()));
253 if let Some(items) = total_items {
254 self.training_progress.split = Progress::new(0, items, Some("items".to_string()));
255 }
256 }
257
258 fn update_epoch(&mut self, epoch: usize) {
259 let total = self.training_progress.global.items_total;
260 let unit = self.training_progress.global.unit.clone();
261 self.training_progress.global = Progress::new(epoch + 1, total, unit);
262 }
263
264 fn start_split(&mut self, split: &str, total_items: usize) {
265 self.training_progress.split = Progress::new(0, total_items, Some("items".to_string()));
266 self.current_split = if split == "train" {
267 TuiSplit::Train
268 } else {
269 TuiSplit::Valid
270 };
271 }
272
273 fn update_split(&mut self, items_processed: usize) {
274 let total = self.training_progress.split.items_total;
275 let unit = self.training_progress.split.unit.clone();
276 self.training_progress.split = Progress::new(items_processed, total, unit);
277 if self.training_progress.global.items_total == 0 {
278 self.training_progress.global = self.training_progress.split.clone();
279 }
280 self.send_event(TuiRendererEvent::StatusUpdateTrain((
281 self.current_split,
282 self.training_progress.clone(),
283 )));
284 }
285
286 fn end_split(&mut self) {
287 self.send_event(TuiRendererEvent::SplitEnd);
288 self.current_split = TuiSplit::Train;
289 }
290
291 fn end(&mut self) {}
292
293 fn log_event_training(&mut self, event: String) {
294 self.send_event(TuiRendererEvent::CounterUpdate(event));
295 }
296}
297
298impl EvaluationProgressLogger for TuiMetricsRendererWrapper {
299 fn start_global_progress(&mut self, total_tests: usize) {
300 self.eval_progress.global = Progress::new(0, total_tests, Some("tests".to_string()));
301 }
302
303 fn start_test(&mut self, _name: &str, total_items: usize) {
304 let current = self.eval_progress.global.items_processed + 1;
305 let total = self.eval_progress.global.items_total;
306 self.eval_progress.global = Progress::new(current, total, Some("tests".to_string()));
307 self.eval_progress.split = Progress::new(0, total_items, Some("items".to_string()));
308 }
309
310 fn update_test_progress(&mut self, items_processed: usize) {
311 let total = self.eval_progress.split.items_total;
312 let unit = self.eval_progress.split.unit.clone();
313 self.eval_progress.split = Progress::new(items_processed, total, unit);
314 self.send_event(TuiRendererEvent::StatusUpdateTest(
315 self.eval_progress.clone(),
316 ));
317 }
318
319 fn end_test(&mut self) {
320 self.send_event(TuiRendererEvent::SplitEnd);
321 }
322
323 fn end_global_progress(&mut self) {}
324
325 fn log_event_evaluation(&mut self, event: String) {
326 self.send_event(TuiRendererEvent::CounterUpdate(event));
327 }
328}
329
330impl Drop for TuiMetricsRendererWrapper {
331 fn drop(&mut self) {
332 if !std::thread::panicking() {
333 self.send_event(TuiRendererEvent::Close);
334 self.join_render_thread();
335 }
336 }
337}
338
339impl TuiMetricsRenderer {
340 fn update_metric(&mut self, split: TuiSplit, group: TuiGroup, state: MetricState) {
341 match state {
342 MetricState::Generic(entry) => {
343 let name = self
344 .metric_definitions
345 .get(&entry.metric_id)
346 .unwrap()
347 .name
348 .clone();
349 self.metrics_text.update(split, group, entry, name);
350 }
351 MetricState::Numeric(entry, value) => {
352 let name: Arc<String> = self
353 .metric_definitions
354 .get(&entry.metric_id)
355 .unwrap()
356 .name
357 .clone();
358 self.metrics_numeric
359 .push(TuiTag::new(split, group.clone()), name.clone(), value);
360 self.metrics_text.update(split, group, entry, name);
361 }
362 };
363 }
364
365 pub fn new(
366 interrupter: Interrupter,
367 checkpoint: Option<usize>,
368 kill_signal: Sender<()>,
369 ) -> Self {
370 let mut stdout = io::stdout();
371 execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
372 enable_raw_mode().unwrap();
373 let terminal = Terminal::new(CrosstermBackend::new(stdout)).unwrap();
374
375 let previous_panic_hook = Arc::new(take_hook());
378 set_hook(Box::new({
379 let previous_panic_hook = previous_panic_hook.clone();
380 move |panic_info| {
381 let _ = disable_raw_mode();
382 let _ = execute!(io::stdout(), DisableMouseCapture, LeaveAlternateScreen);
383
384 if USER_KILL_REQUESTED.load(Ordering::Relaxed) {
385 USER_KILL_NOTIFICATION.call_once(|| {
386 log::warn!("Training killed by user.");
387 eprintln!("\nTraining killed by user.");
388 });
389 return;
393 }
394
395 previous_panic_hook(panic_info);
396 }
397 }));
398
399 Self {
400 terminal,
401 last_update: Instant::now(),
402 progress: ProgressBarState::new(checkpoint),
403 metric_definitions: HashMap::default(),
404 metrics_numeric: NumericMetricsState::default(),
405 metrics_text: TextMetricsState::default(),
406 status: StatusState::default(),
407 interrupter,
408 popup: PopupState::Empty,
409 previous_panic_hook: Some(previous_panic_hook),
410 persistent: false,
411 manual_close: false,
412 close: false,
413 summary: None,
414 kill_signal,
415 }
416 }
417
418 fn handle_event(&mut self, event: TuiRendererEvent) {
419 match event {
420 TuiRendererEvent::MetricRegistration(definition) => {
421 if let MetricAttributes::Numeric(_) = &definition.attributes {
422 self.metrics_numeric.register(definition.name.clone());
425 }
426 self.metric_definitions
427 .insert(definition.metric_id.clone(), definition);
428 }
429 TuiRendererEvent::MetricsUpdate((split, group, state)) => {
430 self.update_metric(split, group, state);
431 }
432 TuiRendererEvent::StatusUpdateTrain((split, item)) => match split {
433 TuiSplit::Train => {
434 self.progress.update_train(&item);
435 self.metrics_numeric.update_progress_train(&item);
436 self.status.update_train(&item);
437 }
438 TuiSplit::Valid => {
439 self.progress.update_valid(&item);
440 self.metrics_numeric.update_progress_valid(&item);
441 self.status.update_valid(&item);
442 }
443 _ => (),
444 },
445 TuiRendererEvent::StatusUpdateTest(item) => {
446 self.progress.update_test(&item);
447 self.metrics_numeric.update_progress_test(&item);
448 self.status.update_test(&item);
449 }
450 TuiRendererEvent::ProcessEnd { summary, reset } => {
451 match (self.summary.take(), summary) {
452 (None, Some(summary)) => {
453 self.summary = Some(summary);
454 }
455 (Some(current), Some(other)) => self.summary = Some(current.merge(other)),
456 (_, _) => { }
457 }
458
459 if reset {
460 self.interrupter.reset();
461 }
462 }
463 TuiRendererEvent::CounterUpdate(event) => {
464 self.status.update_counter(event);
465 }
466 TuiRendererEvent::LabelUpdate(label) => self.status.update_label(label.as_deref()),
467 TuiRendererEvent::SplitEnd => {
468 self.status.reset_counters();
469 }
470 TuiRendererEvent::ManualClose => self.manual_close = true,
471 TuiRendererEvent::Persistent => self.persistent = true,
472 TuiRendererEvent::Close => self.close = true,
473 }
474 }
475
476 fn render(&mut self) -> Result<(), Box<dyn Error>> {
477 self.draw()?;
478 self.handle_user_input()?;
479
480 self.last_update = Instant::now();
481
482 Ok(())
483 }
484
485 fn draw(&mut self) -> Result<(), Box<dyn Error>> {
486 self.terminal.draw(|frame| {
487 let size = frame.area();
488
489 match self.popup.view() {
490 Some(view) => view.render(frame, size),
491 None => {
492 let view = MetricsView::new(
493 self.metrics_numeric.view(),
494 self.metrics_text.view(),
495 self.progress.view(),
496 ControlsView,
497 self.status.view(),
498 );
499
500 view.render(frame, size);
501 }
502 };
503 })?;
504
505 Ok(())
506 }
507
508 fn dispatch_user_event(&mut self, event: &Event) -> bool {
513 let mut redraw = self.popup.on_event(event);
514 if self.popup.is_empty() {
515 redraw |= self.metrics_numeric.on_event(event);
516 redraw |= match self.metrics_text.on_event(event) {
517 TextEventOutcome::Clicked(name) => {
518 self.metrics_numeric.select_by_name(&name);
519 true
520 }
521 TextEventOutcome::HoverChanged => true,
522 TextEventOutcome::Ignored => false,
523 };
524 }
525 redraw
526 }
527
528 fn handle_user_input(&mut self) -> Result<(), Box<dyn Error>> {
529 while event::poll(Duration::from_secs(0))? {
530 let event = event::read()?;
531 let _ = self.dispatch_user_event(&event);
532
533 if self.popup.is_empty()
534 && let Event::Key(key) = event
535 && let KeyCode::Char('q') = key.code
536 {
537 self.popup = PopupState::Full(
538 "Quit".to_string(),
539 vec![
540 Callback::new(
541 "Stop the training.",
542 "Stop the training immediately. This will break from the \
543 training loop, but any remaining code after the loop will be \
544 executed.",
545 's',
546 QuitPopupAccept(self.interrupter.clone()),
547 ),
548 Callback::new(
549 "Stop the training immediately.",
550 "Kill the program. This will abort the current training instantly. \
551 Any code following the training won't be executed.",
552 'k',
553 KillPopupAccept(self.kill_signal.clone()),
554 ),
555 Callback::new(
556 "Cancel",
557 "Cancel the action, continue the training.",
558 'c',
559 PopupCancel,
560 ),
561 ],
562 );
563 }
564 }
565
566 Ok(())
567 }
568
569 fn handle_post_training(&mut self) -> Result<(), Box<dyn Error>> {
570 self.popup = PopupState::Full(
571 "Training is done".to_string(),
572 vec![Callback::new(
573 "Training Done",
574 "Press 'x' to close this popup. Press 'q' to exit the application after the \
575 popup is closed.",
576 'x',
577 PopupCancel,
578 )],
579 );
580
581 self.draw().ok();
582
583 loop {
584 if let Ok(true) = event::poll(Duration::from_millis(MAX_REFRESH_RATE_MILLIS)) {
585 match event::read() {
586 Ok(event @ Event::Key(key)) => {
587 let redraw = self.dispatch_user_event(&event);
588 if self.popup.is_empty()
589 && let KeyCode::Char('q') = key.code
590 {
591 break;
592 }
593 if redraw {
594 self.draw().ok();
595 }
596 }
597
598 Ok(event @ Event::Mouse(_)) => {
599 if self.dispatch_user_event(&event) {
600 self.draw().ok();
601 }
602 }
603
604 Ok(Event::Resize(..)) => {
605 self.draw().ok();
606 }
607 Err(err) => {
608 eprintln!("Error reading event: {err}");
609 break;
610 }
611 _ => continue,
612 }
613 }
614 }
615 Ok(())
616 }
617
618 fn reset(&mut self) -> Result<(), Box<dyn Error>> {
620 if self.previous_panic_hook.is_some() {
622 if self.persistent
623 && let Err(err) = self.handle_post_training()
624 {
625 eprintln!("Error in post-training handling: {err}");
626 }
627
628 disable_raw_mode()?;
629 execute!(
630 self.terminal.backend_mut(),
631 DisableMouseCapture,
632 LeaveAlternateScreen
633 )?;
634 self.terminal.show_cursor()?;
635
636 let _ = take_hook();
638 if let Some(previous_panic_hook) =
639 Arc::into_inner(self.previous_panic_hook.take().unwrap())
640 {
641 set_hook(previous_panic_hook);
642 }
643 }
644 Ok(())
645 }
646}
647
648struct QuitPopupAccept(Interrupter);
649struct KillPopupAccept(Sender<()>);
650struct PopupCancel;
651
652impl CallbackFn for KillPopupAccept {
653 fn call(&self) -> bool {
654 USER_KILL_REQUESTED.store(true, Ordering::Relaxed);
655 self.0.send(()).unwrap();
656 panic!("Killing training from user input.");
657 }
658}
659
660impl CallbackFn for QuitPopupAccept {
661 fn call(&self) -> bool {
662 self.0.stop(Some("Stopping training from user input."));
663 true
664 }
665}
666
667impl CallbackFn for PopupCancel {
668 fn call(&self) -> bool {
669 true
670 }
671}
672
673impl Drop for TuiMetricsRenderer {
674 fn drop(&mut self) {
675 if !std::thread::panicking() {
678 self.reset().unwrap();
679
680 if let Some(summary) = &self.summary {
681 println!("{summary}");
682 log::info!("{summary}");
683 }
684 }
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 fn wrapper_with_dummy_thread() -> TuiMetricsRendererWrapper {
697 let (sender, _receiver) = mpsc::channel();
698 let (_kill_signal_sender, kill_signal_receiver) = mpsc::channel();
699 let handle_join = std::thread::spawn(|| {});
700
701 let init = Progress::new(0, 0, None);
702 TuiMetricsRendererWrapper {
703 sender,
704 interrupter: Interrupter::new(),
705 handle_join: Some(handle_join),
706 kill_signal: Arc::new(Mutex::new(kill_signal_receiver)),
707 current_split: TuiSplit::Train,
708 training_progress: ProgressSnapshot::new(init.clone(), init.clone()),
709 eval_progress: ProgressSnapshot::new(init.clone(), init),
710 }
711 }
712
713 #[test]
714 fn manual_close_twice_does_not_panic() {
715 let mut renderer = wrapper_with_dummy_thread();
716
717 renderer.manual_close();
719 renderer.manual_close();
722 }
723
724 #[test]
725 fn drop_after_manual_close_does_not_panic() {
726 let mut renderer = wrapper_with_dummy_thread();
727
728 renderer.manual_close();
729 drop(renderer);
730 }
731}