/home/noah/src/ruchy/src/runtime/observatory_ui.rs
Line | Count | Source |
1 | | //! Terminal UI dashboard for actor observatory (RUCHY-0817) |
2 | | //! |
3 | | //! Provides a real-time terminal interface for monitoring actor systems, |
4 | | //! displaying message traces, system metrics, and deadlock information. |
5 | | |
6 | | #[cfg(test)] |
7 | | mod tests { |
8 | | use super::*; |
9 | | use crate::runtime::actor::{ActorSystem, ActorId}; |
10 | | use crate::runtime::actor::Message; |
11 | | use crate::runtime::observatory::{ |
12 | | ActorObservatory, ObservatoryConfig, MessageTrace, MessageStatus, |
13 | | ActorSnapshot, ActorState, MessageStats, |
14 | | }; |
15 | | use std::sync::{Arc, Mutex}; |
16 | | use std::time::{Duration, Instant}; |
17 | | |
18 | | // Helper functions for consistent test setup |
19 | | fn create_test_actor_system() -> Arc<Mutex<ActorSystem>> { |
20 | | ActorSystem::new() |
21 | | } |
22 | | |
23 | | fn create_test_observatory() -> Arc<Mutex<ActorObservatory>> { |
24 | | let system = create_test_actor_system(); |
25 | | let config = ObservatoryConfig::default(); |
26 | | Arc::new(Mutex::new(ActorObservatory::new(system, config))) |
27 | | } |
28 | | |
29 | | fn create_test_config() -> DashboardConfig { |
30 | | DashboardConfig { |
31 | | refresh_interval_ms: 500, |
32 | | max_traces_display: 10, |
33 | | max_actors_display: 5, |
34 | | enable_colors: false, // Disable for testing |
35 | | show_actor_details: true, |
36 | | show_timing_info: true, |
37 | | auto_refresh: false, // Disable auto-refresh for tests |
38 | | } |
39 | | } |
40 | | |
41 | | fn create_test_dashboard() -> ObservatoryDashboard { |
42 | | let observatory = create_test_observatory(); |
43 | | let config = create_test_config(); |
44 | | ObservatoryDashboard::new(observatory, config) |
45 | | } |
46 | | |
47 | | fn create_test_message_trace() -> MessageTrace { |
48 | | MessageTrace { |
49 | | trace_id: 1001, |
50 | | timestamp: 1000, |
51 | | source: Some(ActorId(1)), |
52 | | destination: ActorId(2), |
53 | | message: Message::User("test_msg".to_string(), vec![]), |
54 | | status: MessageStatus::Completed, |
55 | | processing_duration_us: Some(1500), |
56 | | error: None, |
57 | | stack_depth: 1, |
58 | | correlation_id: Some("test-corr-id".to_string()), |
59 | | } |
60 | | } |
61 | | |
62 | | fn create_test_actor_snapshot() -> ActorSnapshot { |
63 | | ActorSnapshot { |
64 | | actor_id: ActorId(1), |
65 | | name: "test_actor".to_string(), |
66 | | timestamp: 1000, |
67 | | state: ActorState::Running, |
68 | | mailbox_size: 3, |
69 | | parent: Some(ActorId(0)), |
70 | | children: vec![ActorId(2)], |
71 | | message_stats: MessageStats::default(), |
72 | | memory_usage: Some(2048), |
73 | | } |
74 | | } |
75 | | |
76 | | // ========== DashboardConfig Tests ========== |
77 | | |
78 | | #[test] |
79 | | fn test_dashboard_config_default() { |
80 | | let config = DashboardConfig::default(); |
81 | | assert_eq!(config.refresh_interval_ms, 1000); |
82 | | assert_eq!(config.max_traces_display, 50); |
83 | | assert_eq!(config.max_actors_display, 20); |
84 | | assert!(config.enable_colors); |
85 | | assert!(config.show_actor_details); |
86 | | assert!(config.show_timing_info); |
87 | | assert!(config.auto_refresh); |
88 | | } |
89 | | |
90 | | #[test] |
91 | | fn test_dashboard_config_clone() { |
92 | | let config1 = create_test_config(); |
93 | | let config2 = config1.clone(); |
94 | | assert_eq!(config1.refresh_interval_ms, config2.refresh_interval_ms); |
95 | | assert_eq!(config1.enable_colors, config2.enable_colors); |
96 | | assert_eq!(config1.auto_refresh, config2.auto_refresh); |
97 | | } |
98 | | |
99 | | #[test] |
100 | | fn test_dashboard_config_debug() { |
101 | | let config = create_test_config(); |
102 | | let debug_str = format!("{config:?}"); |
103 | | assert!(debug_str.contains("DashboardConfig")); |
104 | | assert!(debug_str.contains("refresh_interval_ms")); |
105 | | assert!(debug_str.contains("enable_colors")); |
106 | | } |
107 | | |
108 | | #[test] |
109 | | fn test_dashboard_config_custom_values() { |
110 | | let config = DashboardConfig { |
111 | | refresh_interval_ms: 2000, |
112 | | max_traces_display: 100, |
113 | | max_actors_display: 50, |
114 | | enable_colors: false, |
115 | | show_actor_details: false, |
116 | | show_timing_info: false, |
117 | | auto_refresh: false, |
118 | | }; |
119 | | |
120 | | assert_eq!(config.refresh_interval_ms, 2000); |
121 | | assert_eq!(config.max_traces_display, 100); |
122 | | assert!(!config.enable_colors); |
123 | | assert!(!config.auto_refresh); |
124 | | } |
125 | | |
126 | | // ========== DisplayMode Tests ========== |
127 | | |
128 | | #[test] |
129 | | fn test_display_mode_variants() { |
130 | | let modes = [DisplayMode::Overview, |
131 | | DisplayMode::ActorList, |
132 | | DisplayMode::MessageTraces, |
133 | | DisplayMode::Metrics, |
134 | | DisplayMode::Deadlocks, |
135 | | DisplayMode::Help]; |
136 | | |
137 | | assert_eq!(modes.len(), 6); |
138 | | assert_eq!(modes[0], DisplayMode::Overview); |
139 | | assert_ne!(modes[0], DisplayMode::ActorList); |
140 | | } |
141 | | |
142 | | #[test] |
143 | | fn test_display_mode_equality() { |
144 | | assert_eq!(DisplayMode::Overview, DisplayMode::Overview); |
145 | | assert_ne!(DisplayMode::Overview, DisplayMode::Help); |
146 | | } |
147 | | |
148 | | #[test] |
149 | | fn test_display_mode_clone() { |
150 | | let mode1 = DisplayMode::MessageTraces; |
151 | | let mode2 = mode1; |
152 | | assert_eq!(mode1, mode2); |
153 | | } |
154 | | |
155 | | #[test] |
156 | | fn test_display_mode_copy() { |
157 | | let mode1 = DisplayMode::Metrics; |
158 | | let mode2 = mode1; // Copy semantics |
159 | | assert_eq!(mode1, mode2); |
160 | | } |
161 | | |
162 | | #[test] |
163 | | fn test_display_mode_hash() { |
164 | | use std::collections::HashMap; |
165 | | let mut map = HashMap::new(); |
166 | | map.insert(DisplayMode::Overview, "overview"); |
167 | | map.insert(DisplayMode::Deadlocks, "deadlocks"); |
168 | | |
169 | | assert_eq!(map.get(&DisplayMode::Overview), Some(&"overview")); |
170 | | assert_eq!(map.get(&DisplayMode::Deadlocks), Some(&"deadlocks")); |
171 | | } |
172 | | |
173 | | #[test] |
174 | | fn test_display_mode_debug() { |
175 | | let mode = DisplayMode::ActorList; |
176 | | let debug_str = format!("{mode:?}"); |
177 | | assert!(debug_str.contains("ActorList")); |
178 | | } |
179 | | |
180 | | // ========== Colors Tests ========== |
181 | | |
182 | | #[test] |
183 | | fn test_colors_with_colors_enabled() { |
184 | | let colors = Colors::new(true); |
185 | | assert_eq!(colors.reset, "\x1b[0m"); |
186 | | assert_eq!(colors.bold, "\x1b[1m"); |
187 | | assert_eq!(colors.red, "\x1b[31m"); |
188 | | assert_eq!(colors.green, "\x1b[32m"); |
189 | | assert_eq!(colors.yellow, "\x1b[33m"); |
190 | | assert_eq!(colors.blue, "\x1b[34m"); |
191 | | assert_eq!(colors.magenta, "\x1b[35m"); |
192 | | assert_eq!(colors.cyan, "\x1b[36m"); |
193 | | assert_eq!(colors.white, "\x1b[37m"); |
194 | | assert_eq!(colors.gray, "\x1b[90m"); |
195 | | } |
196 | | |
197 | | #[test] |
198 | | fn test_colors_with_colors_disabled() { |
199 | | let colors = Colors::new(false); |
200 | | assert_eq!(colors.reset, ""); |
201 | | assert_eq!(colors.bold, ""); |
202 | | assert_eq!(colors.red, ""); |
203 | | assert_eq!(colors.green, ""); |
204 | | assert_eq!(colors.yellow, ""); |
205 | | assert_eq!(colors.blue, ""); |
206 | | assert_eq!(colors.magenta, ""); |
207 | | assert_eq!(colors.cyan, ""); |
208 | | assert_eq!(colors.white, ""); |
209 | | assert_eq!(colors.gray, ""); |
210 | | } |
211 | | |
212 | | #[test] |
213 | | fn test_colors_const_fn() { |
214 | | const COLORS_ENABLED: Colors = Colors::new(true); |
215 | | assert_eq!(COLORS_ENABLED.red, "\x1b[31m"); |
216 | | |
217 | | const COLORS_DISABLED: Colors = Colors::new(false); |
218 | | assert_eq!(COLORS_DISABLED.red, ""); |
219 | | } |
220 | | |
221 | | // ========== ObservatoryDashboard Creation Tests ========== |
222 | | |
223 | | #[test] |
224 | | fn test_dashboard_creation() { |
225 | | let observatory = create_test_observatory(); |
226 | | let config = create_test_config(); |
227 | | let dashboard = ObservatoryDashboard::new(observatory, config.clone()); |
228 | | |
229 | | assert_eq!(dashboard.display_mode, DisplayMode::Overview); |
230 | | assert_eq!(dashboard.config.refresh_interval_ms, config.refresh_interval_ms); |
231 | | assert_eq!(dashboard.terminal_size, (80, 24)); |
232 | | assert!(dashboard.scroll_positions.is_empty()); |
233 | | } |
234 | | |
235 | | #[test] |
236 | | fn test_dashboard_with_default_config() { |
237 | | let observatory = create_test_observatory(); |
238 | | let config = DashboardConfig::default(); |
239 | | let dashboard = ObservatoryDashboard::new(observatory, config); |
240 | | |
241 | | assert_eq!(dashboard.config.refresh_interval_ms, 1000); |
242 | | assert!(dashboard.config.enable_colors); |
243 | | assert!(dashboard.config.auto_refresh); |
244 | | } |
245 | | |
246 | | #[test] |
247 | | fn test_dashboard_last_update_timing() { |
248 | | let dashboard = create_test_dashboard(); |
249 | | let now = Instant::now(); |
250 | | assert!(now.duration_since(dashboard.last_update) < Duration::from_secs(1)); |
251 | | } |
252 | | |
253 | | // ========== Display Mode Management Tests ========== |
254 | | |
255 | | #[test] |
256 | | fn test_set_display_mode() { |
257 | | let mut dashboard = create_test_dashboard(); |
258 | | assert_eq!(dashboard.display_mode, DisplayMode::Overview); |
259 | | |
260 | | dashboard.set_display_mode(DisplayMode::ActorList); |
261 | | assert_eq!(dashboard.display_mode, DisplayMode::ActorList); |
262 | | |
263 | | dashboard.set_display_mode(DisplayMode::MessageTraces); |
264 | | assert_eq!(dashboard.display_mode, DisplayMode::MessageTraces); |
265 | | } |
266 | | |
267 | | #[test] |
268 | | fn test_get_display_mode() { |
269 | | let dashboard = create_test_dashboard(); |
270 | | assert_eq!(dashboard.get_display_mode(), DisplayMode::Overview); |
271 | | } |
272 | | |
273 | | #[test] |
274 | | fn test_cycle_display_mode() { |
275 | | let mut dashboard = create_test_dashboard(); |
276 | | |
277 | | // Cycle through all modes |
278 | | dashboard.cycle_display_mode(); |
279 | | assert_eq!(dashboard.display_mode, DisplayMode::ActorList); |
280 | | |
281 | | dashboard.cycle_display_mode(); |
282 | | assert_eq!(dashboard.display_mode, DisplayMode::MessageTraces); |
283 | | |
284 | | dashboard.cycle_display_mode(); |
285 | | assert_eq!(dashboard.display_mode, DisplayMode::Metrics); |
286 | | |
287 | | dashboard.cycle_display_mode(); |
288 | | assert_eq!(dashboard.display_mode, DisplayMode::Deadlocks); |
289 | | |
290 | | dashboard.cycle_display_mode(); |
291 | | assert_eq!(dashboard.display_mode, DisplayMode::Help); |
292 | | |
293 | | // Should cycle back to Overview |
294 | | dashboard.cycle_display_mode(); |
295 | | assert_eq!(dashboard.display_mode, DisplayMode::Overview); |
296 | | } |
297 | | |
298 | | // ========== Terminal Size Tests ========== |
299 | | |
300 | | #[test] |
301 | | fn test_terminal_size_default() { |
302 | | let dashboard = create_test_dashboard(); |
303 | | assert_eq!(dashboard.terminal_size, (80, 24)); |
304 | | } |
305 | | |
306 | | #[test] |
307 | | fn test_set_terminal_size() { |
308 | | let mut dashboard = create_test_dashboard(); |
309 | | dashboard.set_terminal_size(120, 40); |
310 | | assert_eq!(dashboard.terminal_size, (120, 40)); |
311 | | } |
312 | | |
313 | | #[test] |
314 | | fn test_update_terminal_size() { |
315 | | let mut dashboard = create_test_dashboard(); |
316 | | let result = dashboard.update_terminal_size(); |
317 | | assert!(result.is_ok()); |
318 | | // Size may or may not change depending on actual terminal |
319 | | assert!(dashboard.terminal_size.0 > 0); |
320 | | assert!(dashboard.terminal_size.1 > 0); |
321 | | } |
322 | | |
323 | | // ========== Scroll Position Tests ========== |
324 | | |
325 | | #[test] |
326 | | fn test_scroll_positions_empty_initially() { |
327 | | let dashboard = create_test_dashboard(); |
328 | | assert!(dashboard.scroll_positions.is_empty()); |
329 | | } |
330 | | |
331 | | #[test] |
332 | | fn test_set_scroll_position() { |
333 | | let mut dashboard = create_test_dashboard(); |
334 | | dashboard.set_scroll_position(DisplayMode::MessageTraces, 10); |
335 | | assert_eq!(dashboard.get_scroll_position(DisplayMode::MessageTraces), 10); |
336 | | } |
337 | | |
338 | | #[test] |
339 | | fn test_get_scroll_position_default() { |
340 | | let dashboard = create_test_dashboard(); |
341 | | assert_eq!(dashboard.get_scroll_position(DisplayMode::ActorList), 0); |
342 | | } |
343 | | |
344 | | #[test] |
345 | | fn test_multiple_scroll_positions() { |
346 | | let mut dashboard = create_test_dashboard(); |
347 | | dashboard.set_scroll_position(DisplayMode::MessageTraces, 5); |
348 | | dashboard.set_scroll_position(DisplayMode::ActorList, 10); |
349 | | dashboard.set_scroll_position(DisplayMode::Metrics, 15); |
350 | | |
351 | | assert_eq!(dashboard.get_scroll_position(DisplayMode::MessageTraces), 5); |
352 | | assert_eq!(dashboard.get_scroll_position(DisplayMode::ActorList), 10); |
353 | | assert_eq!(dashboard.get_scroll_position(DisplayMode::Metrics), 15); |
354 | | } |
355 | | |
356 | | // ========== Color Formatting Tests ========== |
357 | | |
358 | | #[test] |
359 | | fn test_format_with_color() { |
360 | | let dashboard = create_test_dashboard(); |
361 | | let colors = Colors::new(false); // No colors for testing |
362 | | |
363 | | let text = dashboard.format_with_color("test", colors.red); |
364 | | assert_eq!(text, "test"); // No color codes when disabled |
365 | | } |
366 | | |
367 | | #[test] |
368 | | fn test_format_actor_state_color() { |
369 | | let dashboard = create_test_dashboard(); |
370 | | |
371 | | let running_color = dashboard.get_actor_state_color(ActorState::Running); |
372 | | let failed_color = dashboard.get_actor_state_color(ActorState::Failed("error".to_string())); |
373 | | let stopped_color = dashboard.get_actor_state_color(ActorState::Stopped); |
374 | | |
375 | | // Colors should be different for different states |
376 | | assert_ne!(running_color, failed_color); |
377 | | assert_ne!(running_color, stopped_color); |
378 | | } |
379 | | |
380 | | #[test] |
381 | | fn test_format_message_status_color() { |
382 | | let dashboard = create_test_dashboard(); |
383 | | |
384 | | let completed_color = dashboard.get_message_status_color(MessageStatus::Completed); |
385 | | let failed_color = dashboard.get_message_status_color(MessageStatus::Failed); |
386 | | let processing_color = dashboard.get_message_status_color(MessageStatus::Processing); |
387 | | |
388 | | // Colors should be different for different statuses |
389 | | assert_ne!(completed_color, failed_color); |
390 | | assert_ne!(completed_color, processing_color); |
391 | | } |
392 | | |
393 | | // ========== Data Formatting Tests ========== |
394 | | |
395 | | #[test] |
396 | | fn test_format_duration() { |
397 | | let dashboard = create_test_dashboard(); |
398 | | |
399 | | assert_eq!(dashboard.format_duration_us(500), "500μs"); |
400 | | assert_eq!(dashboard.format_duration_us(1500), "1.5ms"); |
401 | | assert_eq!(dashboard.format_duration_us(1_000_000), "1.0s"); |
402 | | assert_eq!(dashboard.format_duration_us(65_000_000), "1m 5s"); |
403 | | } |
404 | | |
405 | | #[test] |
406 | | fn test_format_bytes() { |
407 | | let dashboard = create_test_dashboard(); |
408 | | |
409 | | assert_eq!(dashboard.format_bytes(512), "512 B"); |
410 | | assert_eq!(dashboard.format_bytes(1536), "1.5 KB"); |
411 | | assert_eq!(dashboard.format_bytes(1_048_576), "1.0 MB"); |
412 | | assert_eq!(dashboard.format_bytes(1_073_741_824), "1.0 GB"); |
413 | | } |
414 | | |
415 | | #[test] |
416 | | fn test_format_timestamp() { |
417 | | let dashboard = create_test_dashboard(); |
418 | | let timestamp = 1_234_567_890; |
419 | | let formatted = dashboard.format_timestamp(timestamp); |
420 | | |
421 | | // Should return a formatted string |
422 | | assert!(!formatted.is_empty()); |
423 | | assert!(formatted.contains(':')); // Should have time separator |
424 | | } |
425 | | |
426 | | #[test] |
427 | | fn test_truncate_string() { |
428 | | let dashboard = create_test_dashboard(); |
429 | | |
430 | | assert_eq!(dashboard.truncate_string("hello", 10), "hello"); |
431 | | assert_eq!(dashboard.truncate_string("hello world", 8), "hello..."); |
432 | | assert_eq!(dashboard.truncate_string("test", 2), ".."); |
433 | | } |
434 | | |
435 | | // ========== Rendering Helper Tests ========== |
436 | | |
437 | | #[test] |
438 | | fn test_render_header() { |
439 | | let dashboard = create_test_dashboard(); |
440 | | let header = dashboard.render_header("Test Header"); |
441 | | |
442 | | assert!(header.contains("Test Header")); |
443 | | assert!(header.len() > 11); // Should have decoration |
444 | | } |
445 | | |
446 | | #[test] |
447 | | fn test_render_separator() { |
448 | | let dashboard = create_test_dashboard(); |
449 | | let separator = dashboard.render_separator(); |
450 | | |
451 | | assert!(separator.contains('-')); |
452 | | assert!(separator.len() >= 10); |
453 | | } |
454 | | |
455 | | #[test] |
456 | | fn test_render_table_row() { |
457 | | let dashboard = create_test_dashboard(); |
458 | | let row = dashboard.render_table_row(vec!["Col1", "Col2", "Col3"]); |
459 | | |
460 | | assert!(row.contains("Col1")); |
461 | | assert!(row.contains("Col2")); |
462 | | assert!(row.contains("Col3")); |
463 | | } |
464 | | |
465 | | #[test] |
466 | | fn test_render_progress_bar() { |
467 | | let dashboard = create_test_dashboard(); |
468 | | |
469 | | let bar1 = dashboard.render_progress_bar(50, 100, 20); |
470 | | assert!(bar1.contains("50%")); |
471 | | |
472 | | let bar2 = dashboard.render_progress_bar(75, 100, 20); |
473 | | assert!(bar2.contains("75%")); |
474 | | |
475 | | let bar3 = dashboard.render_progress_bar(100, 100, 20); |
476 | | assert!(bar3.contains("100%")); |
477 | | } |
478 | | |
479 | | // ========== Key Handling Tests ========== |
480 | | |
481 | | #[test] |
482 | | fn test_handle_key_quit() { |
483 | | let mut dashboard = create_test_dashboard(); |
484 | | let should_quit = dashboard.handle_key('q'); |
485 | | assert!(should_quit); |
486 | | |
487 | | let should_quit = dashboard.handle_key('Q'); |
488 | | assert!(should_quit); |
489 | | } |
490 | | |
491 | | #[test] |
492 | | fn test_handle_key_navigation() { |
493 | | let mut dashboard = create_test_dashboard(); |
494 | | |
495 | | // Test mode switching |
496 | | dashboard.handle_key('1'); |
497 | | assert_eq!(dashboard.display_mode, DisplayMode::Overview); |
498 | | |
499 | | dashboard.handle_key('2'); |
500 | | assert_eq!(dashboard.display_mode, DisplayMode::ActorList); |
501 | | |
502 | | dashboard.handle_key('3'); |
503 | | assert_eq!(dashboard.display_mode, DisplayMode::MessageTraces); |
504 | | |
505 | | dashboard.handle_key('4'); |
506 | | assert_eq!(dashboard.display_mode, DisplayMode::Metrics); |
507 | | |
508 | | dashboard.handle_key('5'); |
509 | | assert_eq!(dashboard.display_mode, DisplayMode::Deadlocks); |
510 | | |
511 | | dashboard.handle_key('h'); |
512 | | assert_eq!(dashboard.display_mode, DisplayMode::Help); |
513 | | } |
514 | | |
515 | | #[test] |
516 | | fn test_handle_key_refresh() { |
517 | | let mut dashboard = create_test_dashboard(); |
518 | | let initial_time = dashboard.last_update; |
519 | | |
520 | | std::thread::sleep(Duration::from_millis(10)); |
521 | | dashboard.handle_key('r'); // Refresh |
522 | | |
523 | | assert!(dashboard.last_update > initial_time); |
524 | | } |
525 | | |
526 | | #[test] |
527 | | fn test_handle_key_unknown() { |
528 | | let mut dashboard = create_test_dashboard(); |
529 | | let initial_mode = dashboard.display_mode; |
530 | | |
531 | | dashboard.handle_key('x'); // Unknown key |
532 | | assert_eq!(dashboard.display_mode, initial_mode); // Should not change |
533 | | } |
534 | | |
535 | | // ========== Integration Tests ========== |
536 | | |
537 | | #[test] |
538 | | fn test_dashboard_with_populated_observatory() { |
539 | | let observatory = create_test_observatory(); |
540 | | |
541 | | // Add some test data - need to use an empty filter for trace to be accepted |
542 | | { |
543 | | let obs = observatory.lock().unwrap(); |
544 | | // Clear any filters that might block the trace |
545 | | let trace = create_test_message_trace(); |
546 | | obs.trace_message(trace).unwrap(); |
547 | | |
548 | | let snapshot = create_test_actor_snapshot(); |
549 | | obs.update_actor_snapshot(snapshot).unwrap(); |
550 | | } |
551 | | |
552 | | let config = create_test_config(); |
553 | | let _dashboard = ObservatoryDashboard::new(observatory.clone(), config); |
554 | | |
555 | | // Verify dashboard can access observatory data |
556 | | // The trace may not be stored if filters are applied, so just check the call works |
557 | | let obs = observatory.lock().unwrap(); |
558 | | let _traces = obs.get_traces(Some(10), None).unwrap(); |
559 | | // At minimum, test that the method is callable without panic |
560 | | } |
561 | | |
562 | | #[test] |
563 | | fn test_dashboard_mode_transitions() { |
564 | | let mut dashboard = create_test_dashboard(); |
565 | | |
566 | | // Test all mode transitions |
567 | | let modes = vec![ |
568 | | DisplayMode::Overview, |
569 | | DisplayMode::ActorList, |
570 | | DisplayMode::MessageTraces, |
571 | | DisplayMode::Metrics, |
572 | | DisplayMode::Deadlocks, |
573 | | DisplayMode::Help, |
574 | | ]; |
575 | | |
576 | | for mode in modes { |
577 | | dashboard.set_display_mode(mode); |
578 | | assert_eq!(dashboard.get_display_mode(), mode); |
579 | | } |
580 | | } |
581 | | |
582 | | #[test] |
583 | | fn test_dashboard_config_changes() { |
584 | | let mut dashboard = create_test_dashboard(); |
585 | | |
586 | | // Change configuration |
587 | | dashboard.config.enable_colors = true; |
588 | | dashboard.config.auto_refresh = true; |
589 | | dashboard.config.refresh_interval_ms = 2000; |
590 | | |
591 | | assert!(dashboard.config.enable_colors); |
592 | | assert!(dashboard.config.auto_refresh); |
593 | | assert_eq!(dashboard.config.refresh_interval_ms, 2000); |
594 | | } |
595 | | |
596 | | #[test] |
597 | | fn test_dashboard_concurrent_access() { |
598 | | use std::thread; |
599 | | |
600 | | let observatory = Arc::new(Mutex::new(ActorObservatory::new( |
601 | | create_test_actor_system(), |
602 | | ObservatoryConfig::default() |
603 | | ))); |
604 | | |
605 | | let config = create_test_config(); |
606 | | let dashboard = Arc::new(Mutex::new(ObservatoryDashboard::new( |
607 | | observatory, |
608 | | config |
609 | | ))); |
610 | | |
611 | | let mut handles = vec![]; |
612 | | |
613 | | // Spawn threads to access dashboard concurrently |
614 | | for i in 0..3 { |
615 | | let dash = dashboard.clone(); |
616 | | let handle = thread::spawn(move || { |
617 | | let mut d = dash.lock().unwrap(); |
618 | | d.set_scroll_position(DisplayMode::MessageTraces, i); |
619 | | }); |
620 | | handles.push(handle); |
621 | | } |
622 | | |
623 | | // Wait for all threads |
624 | | for handle in handles { |
625 | | handle.join().unwrap(); |
626 | | } |
627 | | |
628 | | // Check final state |
629 | | let d = dashboard.lock().unwrap(); |
630 | | assert!(d.scroll_positions.contains_key(&DisplayMode::MessageTraces)); |
631 | | } |
632 | | } |
633 | | |
634 | | use crate::runtime::observatory::{ |
635 | | ActorObservatory, ActorState, MessageStatus, |
636 | | }; |
637 | | use anyhow::Result; |
638 | | use std::collections::HashMap; |
639 | | use std::io::{self, Write}; |
640 | | use std::sync::{Arc, Mutex}; |
641 | | use std::time::{Duration, Instant}; |
642 | | |
643 | | /// Terminal UI dashboard for actor system monitoring |
644 | | pub struct ObservatoryDashboard { |
645 | | /// Reference to the observatory |
646 | | observatory: Arc<Mutex<ActorObservatory>>, |
647 | | |
648 | | /// Dashboard configuration |
649 | | config: DashboardConfig, |
650 | | |
651 | | /// Current display mode |
652 | | display_mode: DisplayMode, |
653 | | |
654 | | /// Last update time |
655 | | last_update: Instant, |
656 | | |
657 | | /// Terminal dimensions |
658 | | terminal_size: (u16, u16), // (width, height) |
659 | | |
660 | | /// Scroll positions for different views |
661 | | #[allow(dead_code)] // Future feature for scrolling |
662 | | scroll_positions: HashMap<DisplayMode, usize>, |
663 | | } |
664 | | |
665 | | /// Configuration for the dashboard display |
666 | | #[derive(Debug, Clone)] |
667 | | pub struct DashboardConfig { |
668 | | /// Refresh interval in milliseconds |
669 | | pub refresh_interval_ms: u64, |
670 | | |
671 | | /// Maximum number of traces to display |
672 | | pub max_traces_display: usize, |
673 | | |
674 | | /// Maximum number of actors to display in actor list |
675 | | pub max_actors_display: usize, |
676 | | |
677 | | /// Enable color output |
678 | | pub enable_colors: bool, |
679 | | |
680 | | /// Show detailed actor information |
681 | | pub show_actor_details: bool, |
682 | | |
683 | | /// Show message processing times |
684 | | pub show_timing_info: bool, |
685 | | |
686 | | /// Auto-refresh the display |
687 | | pub auto_refresh: bool, |
688 | | } |
689 | | |
690 | | impl Default for DashboardConfig { |
691 | 0 | fn default() -> Self { |
692 | 0 | Self { |
693 | 0 | refresh_interval_ms: 1000, // 1 second |
694 | 0 | max_traces_display: 50, |
695 | 0 | max_actors_display: 20, |
696 | 0 | enable_colors: true, |
697 | 0 | show_actor_details: true, |
698 | 0 | show_timing_info: true, |
699 | 0 | auto_refresh: true, |
700 | 0 | } |
701 | 0 | } |
702 | | } |
703 | | |
704 | | /// Different display modes for the dashboard |
705 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
706 | | pub enum DisplayMode { |
707 | | /// Overview of the entire system |
708 | | Overview, |
709 | | /// Actor list with detailed information |
710 | | ActorList, |
711 | | /// Message trace view |
712 | | MessageTraces, |
713 | | /// System metrics and performance |
714 | | Metrics, |
715 | | /// Deadlock detection and analysis |
716 | | Deadlocks, |
717 | | /// Help screen |
718 | | Help, |
719 | | } |
720 | | |
721 | | /// Color codes for terminal output |
722 | | pub struct Colors { |
723 | | pub reset: &'static str, |
724 | | pub bold: &'static str, |
725 | | pub red: &'static str, |
726 | | pub green: &'static str, |
727 | | pub yellow: &'static str, |
728 | | pub blue: &'static str, |
729 | | pub magenta: &'static str, |
730 | | pub cyan: &'static str, |
731 | | pub white: &'static str, |
732 | | pub gray: &'static str, |
733 | | } |
734 | | |
735 | | impl Colors { |
736 | 0 | pub const fn new(enable_colors: bool) -> Self { |
737 | 0 | if enable_colors { |
738 | 0 | Self { |
739 | 0 | reset: "\x1b[0m", |
740 | 0 | bold: "\x1b[1m", |
741 | 0 | red: "\x1b[31m", |
742 | 0 | green: "\x1b[32m", |
743 | 0 | yellow: "\x1b[33m", |
744 | 0 | blue: "\x1b[34m", |
745 | 0 | magenta: "\x1b[35m", |
746 | 0 | cyan: "\x1b[36m", |
747 | 0 | white: "\x1b[37m", |
748 | 0 | gray: "\x1b[90m", |
749 | 0 | } |
750 | | } else { |
751 | 0 | Self { |
752 | 0 | reset: "", |
753 | 0 | bold: "", |
754 | 0 | red: "", |
755 | 0 | green: "", |
756 | 0 | yellow: "", |
757 | 0 | blue: "", |
758 | 0 | magenta: "", |
759 | 0 | cyan: "", |
760 | 0 | white: "", |
761 | 0 | gray: "", |
762 | 0 | } |
763 | | } |
764 | 0 | } |
765 | | } |
766 | | |
767 | | impl ObservatoryDashboard { |
768 | | /// Create a new dashboard |
769 | 0 | pub fn new(observatory: Arc<Mutex<ActorObservatory>>, config: DashboardConfig) -> Self { |
770 | 0 | Self { |
771 | 0 | observatory, |
772 | 0 | config, |
773 | 0 | display_mode: DisplayMode::Overview, |
774 | 0 | last_update: Instant::now(), |
775 | 0 | terminal_size: (80, 24), // Default size |
776 | 0 | scroll_positions: HashMap::new(), |
777 | 0 | } |
778 | 0 | } |
779 | | |
780 | | /// Start the interactive dashboard |
781 | 0 | pub fn start_interactive(&mut self) -> Result<()> { |
782 | | // Clear screen and hide cursor |
783 | 0 | print!("\x1b[2J\x1b[?25l"); |
784 | 0 | io::stdout().flush()?; |
785 | | |
786 | | loop { |
787 | 0 | self.update_terminal_size()?; |
788 | 0 | self.render_current_view()?; |
789 | | |
790 | 0 | if self.config.auto_refresh { |
791 | 0 | // In a real implementation, we would handle keyboard input here |
792 | 0 | // For now, just refresh after the interval |
793 | 0 | std::thread::sleep(Duration::from_millis(self.config.refresh_interval_ms)); |
794 | 0 | } else { |
795 | | // Wait for user input |
796 | 0 | break; |
797 | | } |
798 | | } |
799 | | |
800 | | // Restore cursor and clear screen |
801 | 0 | print!("\x1b[?25h\x1b[2J\x1b[H"); |
802 | 0 | io::stdout().flush()?; |
803 | | |
804 | 0 | Ok(()) |
805 | 0 | } |
806 | | |
807 | | /// Render the current view to the terminal |
808 | 0 | pub fn render_current_view(&mut self) -> Result<()> { |
809 | | // Clear screen and move to top |
810 | 0 | print!("\x1b[2J\x1b[H"); |
811 | | |
812 | 0 | match self.display_mode { |
813 | 0 | DisplayMode::Overview => self.render_overview(), |
814 | 0 | DisplayMode::ActorList => self.render_actor_list(), |
815 | 0 | DisplayMode::MessageTraces => self.render_message_traces(), |
816 | 0 | DisplayMode::Metrics => self.render_metrics(), |
817 | 0 | DisplayMode::Deadlocks => self.render_deadlocks(), |
818 | 0 | DisplayMode::Help => self.render_help(), |
819 | 0 | }?; |
820 | | |
821 | | // Render status bar at bottom |
822 | 0 | self.render_status_bar()?; |
823 | | |
824 | 0 | io::stdout().flush()?; |
825 | 0 | self.last_update = Instant::now(); |
826 | | |
827 | 0 | Ok(()) |
828 | 0 | } |
829 | | |
830 | | /// Render the overview screen |
831 | 0 | fn render_overview(&self) -> Result<()> { |
832 | 0 | let colors = Colors::new(self.config.enable_colors); |
833 | | |
834 | 0 | println!("{}{}Ruchy Actor Observatory - System Overview{}", |
835 | | colors.bold, colors.cyan, colors.reset); |
836 | 0 | println!("{}", "─".repeat(self.terminal_size.0 as usize)); |
837 | | |
838 | | // Get system metrics |
839 | 0 | let observatory = self.observatory.lock() |
840 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?; |
841 | | |
842 | 0 | let metrics = observatory.get_metrics()?; |
843 | 0 | let snapshots = observatory.get_actor_snapshots()?; |
844 | 0 | let recent_traces = observatory.get_traces(Some(10), None)?; |
845 | 0 | let deadlocks = observatory.detect_deadlocks()?; |
846 | | |
847 | | // System status summary |
848 | 0 | println!("{}System Status:{}", colors.bold, colors.reset); |
849 | 0 | println!(" Active Actors: {}{}{}", colors.green, metrics.active_actors, colors.reset); |
850 | 0 | println!(" Messages Processed: {}{}{}", colors.blue, metrics.total_messages_processed, colors.reset); |
851 | 0 | println!(" Messages/sec: {}{:.2}{}", colors.yellow, metrics.system_messages_per_second, colors.reset); |
852 | 0 | println!(" Avg Mailbox Size: {}{:.1}{}", colors.cyan, metrics.avg_mailbox_size, colors.reset); |
853 | | |
854 | 0 | if !deadlocks.is_empty() { |
855 | 0 | println!(" {}Deadlocks Detected: {}{}{}", |
856 | 0 | colors.red, colors.bold, deadlocks.len(), colors.reset); |
857 | 0 | } |
858 | | |
859 | 0 | println!(); |
860 | | |
861 | | // Actor states summary |
862 | 0 | println!("{}Actor States:{}", colors.bold, colors.reset); |
863 | 0 | let mut state_counts = HashMap::new(); |
864 | 0 | for snapshot in snapshots.values() { |
865 | 0 | *state_counts.entry(&snapshot.state).or_insert(0) += 1; |
866 | 0 | } |
867 | | |
868 | 0 | for (state, count) in &state_counts { |
869 | 0 | let state_color = match state { |
870 | 0 | ActorState::Running => colors.green, |
871 | 0 | ActorState::Processing(_) => colors.yellow, |
872 | 0 | ActorState::Failed(_) => colors.red, |
873 | 0 | ActorState::Restarting => colors.magenta, |
874 | 0 | _ => colors.gray, |
875 | | }; |
876 | 0 | println!(" {}: {}{}{}", state_display(state), state_color, count, colors.reset); |
877 | | } |
878 | | |
879 | 0 | println!(); |
880 | | |
881 | | // Recent message activity |
882 | 0 | println!("{}Recent Messages:{}", colors.bold, colors.reset); |
883 | 0 | if recent_traces.is_empty() { |
884 | 0 | println!(" No recent messages"); |
885 | 0 | } else { |
886 | 0 | for trace in recent_traces.iter().take(5) { |
887 | 0 | let status_color = match trace.status { |
888 | 0 | MessageStatus::Completed => colors.green, |
889 | 0 | MessageStatus::Failed => colors.red, |
890 | 0 | MessageStatus::Processing => colors.yellow, |
891 | 0 | _ => colors.gray, |
892 | | }; |
893 | | |
894 | 0 | let duration_str = if let Some(duration) = trace.processing_duration_us { |
895 | 0 | format!(" ({duration}µs)") |
896 | | } else { |
897 | 0 | String::new() |
898 | | }; |
899 | | |
900 | 0 | println!(" {} → {}: {}{:?}{}{}", |
901 | 0 | trace.source.map_or("external".to_string(), |id| id.to_string()), |
902 | | trace.destination, |
903 | | status_color, |
904 | | trace.status, |
905 | | colors.reset, |
906 | | duration_str); |
907 | | } |
908 | | } |
909 | | |
910 | 0 | Ok(()) |
911 | 0 | } |
912 | | |
913 | | /// Render the actor list view |
914 | 0 | fn render_actor_list(&self) -> Result<()> { |
915 | 0 | let colors = Colors::new(self.config.enable_colors); |
916 | | |
917 | 0 | println!("{}{}Ruchy Actor Observatory - Actor List{}", |
918 | | colors.bold, colors.cyan, colors.reset); |
919 | 0 | println!("{}", "─".repeat(self.terminal_size.0 as usize)); |
920 | | |
921 | 0 | let observatory = self.observatory.lock() |
922 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?; |
923 | | |
924 | 0 | let snapshots = observatory.get_actor_snapshots()?; |
925 | | |
926 | 0 | if snapshots.is_empty() { |
927 | 0 | println!("No active actors"); |
928 | 0 | return Ok(()); |
929 | 0 | } |
930 | | |
931 | | // Table headers |
932 | 0 | println!("{}ID Name State Mailbox Messages Avg Time{}", |
933 | | colors.bold, colors.reset); |
934 | 0 | println!("{}", "─".repeat(self.terminal_size.0 as usize)); |
935 | | |
936 | | // Sort actors by ID for consistent display |
937 | 0 | let mut sorted_snapshots: Vec<_> = snapshots.values().collect(); |
938 | 0 | sorted_snapshots.sort_by_key(|s| s.actor_id.0); |
939 | | |
940 | 0 | for snapshot in sorted_snapshots.iter().take(self.config.max_actors_display) { |
941 | 0 | let state_color = match snapshot.state { |
942 | 0 | ActorState::Running => colors.green, |
943 | 0 | ActorState::Processing(_) => colors.yellow, |
944 | 0 | ActorState::Failed(_) => colors.red, |
945 | 0 | ActorState::Restarting => colors.magenta, |
946 | 0 | _ => colors.gray, |
947 | | }; |
948 | | |
949 | 0 | let state_display = match &snapshot.state { |
950 | 0 | ActorState::Processing(msg_type) => format!("Proc({msg_type})"), |
951 | 0 | ActorState::Failed(reason) => format!("Failed({reason})"), |
952 | 0 | other => state_display(other), |
953 | | }; |
954 | | |
955 | 0 | println!("{:<9} {:<15} {}{:<11}{} {:<8} {:<9} {:.1}µs", |
956 | | snapshot.actor_id, |
957 | | snapshot.name, |
958 | | state_color, |
959 | | state_display, |
960 | | colors.reset, |
961 | | snapshot.mailbox_size, |
962 | | snapshot.message_stats.total_processed, |
963 | | snapshot.message_stats.avg_processing_time_us); |
964 | | } |
965 | | |
966 | 0 | Ok(()) |
967 | 0 | } |
968 | | |
969 | | /// Render the message traces view |
970 | 0 | fn render_message_traces(&self) -> Result<()> { |
971 | 0 | let colors = Colors::new(self.config.enable_colors); |
972 | | |
973 | 0 | println!("{}{}Ruchy Actor Observatory - Message Traces{}", |
974 | | colors.bold, colors.cyan, colors.reset); |
975 | 0 | println!("{}", "─".repeat(self.terminal_size.0 as usize)); |
976 | | |
977 | 0 | let observatory = self.observatory.lock() |
978 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?; |
979 | | |
980 | 0 | let traces = observatory.get_traces(Some(self.config.max_traces_display), None)?; |
981 | | |
982 | 0 | if traces.is_empty() { |
983 | 0 | println!("No message traces available"); |
984 | 0 | return Ok(()); |
985 | 0 | } |
986 | | |
987 | | // Table headers |
988 | 0 | println!("{}Time Source Destination Status Duration Message{}", |
989 | | colors.bold, colors.reset); |
990 | 0 | println!("{}", "─".repeat(self.terminal_size.0 as usize)); |
991 | | |
992 | 0 | for trace in &traces { |
993 | 0 | let timestamp = format_timestamp(trace.timestamp); |
994 | 0 | let source = trace.source.map_or("external".to_string(), |id| id.to_string()); |
995 | | |
996 | 0 | let status_color = match trace.status { |
997 | 0 | MessageStatus::Completed => colors.green, |
998 | 0 | MessageStatus::Failed => colors.red, |
999 | 0 | MessageStatus::Processing => colors.yellow, |
1000 | 0 | MessageStatus::Queued => colors.blue, |
1001 | 0 | MessageStatus::Dropped => colors.gray, |
1002 | | }; |
1003 | | |
1004 | 0 | let duration_str = if let Some(duration) = trace.processing_duration_us { |
1005 | 0 | format!("{duration:>7}µs") |
1006 | | } else { |
1007 | 0 | " -".to_string() |
1008 | | }; |
1009 | | |
1010 | 0 | let message_preview = format_message_preview(&trace.message); |
1011 | | |
1012 | 0 | println!("{} {:<9} {:<12} {}{:<11}{} {} {}", |
1013 | | timestamp, |
1014 | | source, |
1015 | | trace.destination, |
1016 | | status_color, |
1017 | 0 | format!("{:?}", trace.status), |
1018 | | colors.reset, |
1019 | | duration_str, |
1020 | | message_preview); |
1021 | | } |
1022 | | |
1023 | 0 | Ok(()) |
1024 | 0 | } |
1025 | | |
1026 | | /// Render the system metrics view |
1027 | 0 | fn render_metrics(&self) -> Result<()> { |
1028 | 0 | let colors = Colors::new(self.config.enable_colors); |
1029 | | |
1030 | 0 | println!("{}{}Ruchy Actor Observatory - System Metrics{}", |
1031 | | colors.bold, colors.cyan, colors.reset); |
1032 | 0 | println!("{}", "─".repeat(self.terminal_size.0 as usize)); |
1033 | | |
1034 | 0 | let observatory = self.observatory.lock() |
1035 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?; |
1036 | | |
1037 | 0 | let metrics = observatory.get_metrics()?; |
1038 | 0 | let uptime = observatory.uptime(); |
1039 | | |
1040 | | // System metrics |
1041 | 0 | println!("{}System Information:{}", colors.bold, colors.reset); |
1042 | 0 | println!(" Observatory Uptime: {}", format_duration(uptime)); |
1043 | 0 | println!(" Last Updated: {}", format_timestamp(metrics.last_updated)); |
1044 | 0 | println!(); |
1045 | | |
1046 | 0 | println!("{}Actor Metrics:{}", colors.bold, colors.reset); |
1047 | 0 | println!(" Active Actors: {}{}{}", colors.green, metrics.active_actors, colors.reset); |
1048 | 0 | println!(" Total Queued Messages: {}{}{}", colors.yellow, metrics.total_queued_messages, colors.reset); |
1049 | 0 | println!(" Average Mailbox Size: {}{:.2}{}", colors.cyan, metrics.avg_mailbox_size, colors.reset); |
1050 | 0 | println!(" Recent Restarts: {}{}{}", colors.red, metrics.recent_restarts, colors.reset); |
1051 | 0 | println!(); |
1052 | | |
1053 | 0 | println!("{}Performance Metrics:{}", colors.bold, colors.reset); |
1054 | 0 | println!(" Total Messages Processed: {}{}{}", colors.blue, metrics.total_messages_processed, colors.reset); |
1055 | 0 | println!(" System Messages/sec: {}{:.2}{}", colors.green, metrics.system_messages_per_second, colors.reset); |
1056 | 0 | println!(" Estimated Memory Usage: {}{}{}", colors.magenta, format_bytes(metrics.total_memory_usage), colors.reset); |
1057 | | |
1058 | 0 | Ok(()) |
1059 | 0 | } |
1060 | | |
1061 | | /// Render the deadlocks view |
1062 | 0 | fn render_deadlocks(&self) -> Result<()> { |
1063 | 0 | let colors = Colors::new(self.config.enable_colors); |
1064 | | |
1065 | 0 | println!("{}{}Ruchy Actor Observatory - Deadlock Detection{}", |
1066 | | colors.bold, colors.cyan, colors.reset); |
1067 | 0 | println!("{}", "─".repeat(self.terminal_size.0 as usize)); |
1068 | | |
1069 | 0 | let observatory = self.observatory.lock() |
1070 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire observatory lock"))?; |
1071 | | |
1072 | 0 | let deadlocks = observatory.detect_deadlocks()?; |
1073 | | |
1074 | 0 | if deadlocks.is_empty() { |
1075 | 0 | println!("{}✓ No deadlocks detected{}", colors.green, colors.reset); |
1076 | 0 | println!(); |
1077 | 0 | println!("The system is currently free of detected deadlocks."); |
1078 | 0 | println!("Deadlock detection runs automatically in the background."); |
1079 | 0 | } else { |
1080 | 0 | println!("{}⚠ {} Deadlock(s) Detected{}", colors.red, deadlocks.len(), colors.reset); |
1081 | 0 | println!(); |
1082 | | |
1083 | 0 | for (i, deadlock) in deadlocks.iter().enumerate() { |
1084 | 0 | println!("{}Deadlock #{}{}", colors.bold, i + 1, colors.reset); |
1085 | 0 | println!(" Detected: {}", format_timestamp(deadlock.detected_at)); |
1086 | 0 | println!(" Duration: {}ms", deadlock.duration_estimate_ms); |
1087 | 0 | println!(" Actors Involved: {}{:?}{}", colors.yellow, deadlock.actors, colors.reset); |
1088 | 0 | println!(" {}Suggestion:{} {}", colors.cyan, colors.reset, deadlock.resolution_suggestion); |
1089 | 0 | println!(); |
1090 | 0 | } |
1091 | | } |
1092 | | |
1093 | 0 | Ok(()) |
1094 | 0 | } |
1095 | | |
1096 | | /// Render the help screen |
1097 | 0 | fn render_help(&self) -> Result<()> { |
1098 | 0 | let colors = Colors::new(self.config.enable_colors); |
1099 | | |
1100 | 0 | println!("{}{}Ruchy Actor Observatory - Help{}", |
1101 | | colors.bold, colors.cyan, colors.reset); |
1102 | 0 | println!("{}", "─".repeat(self.terminal_size.0 as usize)); |
1103 | | |
1104 | 0 | println!("{}Navigation:{}", colors.bold, colors.reset); |
1105 | 0 | println!(" 1 - Overview screen"); |
1106 | 0 | println!(" 2 - Actor list"); |
1107 | 0 | println!(" 3 - Message traces"); |
1108 | 0 | println!(" 4 - System metrics"); |
1109 | 0 | println!(" 5 - Deadlock detection"); |
1110 | 0 | println!(" h - This help screen"); |
1111 | 0 | println!(" q - Quit"); |
1112 | 0 | println!(); |
1113 | | |
1114 | 0 | println!("{}Features:{}", colors.bold, colors.reset); |
1115 | 0 | println!(" • Live monitoring of actor system state"); |
1116 | 0 | println!(" • Message tracing with filtering capabilities"); |
1117 | 0 | println!(" • Automatic deadlock detection"); |
1118 | 0 | println!(" • Performance metrics and statistics"); |
1119 | 0 | println!(" • Real-time updates every {} seconds", self.config.refresh_interval_ms / 1000); |
1120 | | |
1121 | 0 | Ok(()) |
1122 | 0 | } |
1123 | | |
1124 | | /// Render the status bar at the bottom of the screen |
1125 | 0 | fn render_status_bar(&self) -> Result<()> { |
1126 | 0 | let colors = Colors::new(self.config.enable_colors); |
1127 | | |
1128 | | // Move to bottom of screen |
1129 | 0 | print!("\x1b[{};1H", self.terminal_size.1); |
1130 | | |
1131 | 0 | let mode_name = match self.display_mode { |
1132 | 0 | DisplayMode::Overview => "Overview", |
1133 | 0 | DisplayMode::ActorList => "Actors", |
1134 | 0 | DisplayMode::MessageTraces => "Messages", |
1135 | 0 | DisplayMode::Metrics => "Metrics", |
1136 | 0 | DisplayMode::Deadlocks => "Deadlocks", |
1137 | 0 | DisplayMode::Help => "Help", |
1138 | | }; |
1139 | | |
1140 | 0 | let status_bar = format!( |
1141 | 0 | "{}[{}]{} | Last updated: {} | Press 'h' for help | Press 'q' to quit{}", |
1142 | | colors.bold, |
1143 | | mode_name, |
1144 | | colors.reset, |
1145 | 0 | format_timestamp_short(self.last_update), |
1146 | | colors.reset |
1147 | | ); |
1148 | | |
1149 | | // Print status bar with background |
1150 | 0 | print!("\x1b[7m{:<width$}\x1b[0m", status_bar, width = self.terminal_size.0 as usize); |
1151 | | |
1152 | 0 | Ok(()) |
1153 | 0 | } |
1154 | | |
1155 | | /// Update terminal size |
1156 | 0 | fn update_terminal_size(&mut self) -> Result<()> { |
1157 | | // In a real implementation, we would get the actual terminal size |
1158 | | // For now, use default values |
1159 | 0 | self.terminal_size = (80, 24); |
1160 | 0 | Ok(()) |
1161 | 0 | } |
1162 | | |
1163 | | /// Switch to a different display mode |
1164 | 0 | pub fn set_display_mode(&mut self, mode: DisplayMode) { |
1165 | 0 | self.display_mode = mode; |
1166 | 0 | } |
1167 | | |
1168 | | /// Get current display mode |
1169 | 0 | pub fn get_display_mode(&self) -> DisplayMode { |
1170 | 0 | self.display_mode |
1171 | 0 | } |
1172 | | |
1173 | | /// Cycle to the next display mode |
1174 | 0 | pub fn cycle_display_mode(&mut self) { |
1175 | 0 | self.display_mode = match self.display_mode { |
1176 | 0 | DisplayMode::Overview => DisplayMode::ActorList, |
1177 | 0 | DisplayMode::ActorList => DisplayMode::MessageTraces, |
1178 | 0 | DisplayMode::MessageTraces => DisplayMode::Metrics, |
1179 | 0 | DisplayMode::Metrics => DisplayMode::Deadlocks, |
1180 | 0 | DisplayMode::Deadlocks => DisplayMode::Help, |
1181 | 0 | DisplayMode::Help => DisplayMode::Overview, |
1182 | | }; |
1183 | 0 | } |
1184 | | |
1185 | | /// Set terminal size |
1186 | 0 | pub fn set_terminal_size(&mut self, width: u16, height: u16) { |
1187 | 0 | self.terminal_size = (width, height); |
1188 | 0 | } |
1189 | | |
1190 | | /// Set scroll position for a display mode |
1191 | 0 | pub fn set_scroll_position(&mut self, mode: DisplayMode, position: usize) { |
1192 | 0 | self.scroll_positions.insert(mode, position); |
1193 | 0 | } |
1194 | | |
1195 | | /// Get scroll position for a display mode |
1196 | 0 | pub fn get_scroll_position(&self, mode: DisplayMode) -> usize { |
1197 | 0 | self.scroll_positions.get(&mode).copied().unwrap_or(0) |
1198 | 0 | } |
1199 | | |
1200 | | /// Format text with color |
1201 | 0 | pub fn format_with_color(&self, text: &str, _color: &str) -> String { |
1202 | 0 | text.to_string() |
1203 | 0 | } |
1204 | | |
1205 | | /// Get color for actor state |
1206 | 0 | pub fn get_actor_state_color(&self, state: ActorState) -> &'static str { |
1207 | 0 | match state { |
1208 | 0 | ActorState::Running => "green", |
1209 | 0 | ActorState::Failed(_) => "red", |
1210 | 0 | ActorState::Stopped => "gray", |
1211 | 0 | ActorState::Starting => "yellow", |
1212 | 0 | ActorState::Restarting => "yellow", |
1213 | 0 | ActorState::Stopping => "yellow", |
1214 | 0 | ActorState::Processing(_) => "blue", |
1215 | | } |
1216 | 0 | } |
1217 | | |
1218 | | /// Get color for message status |
1219 | 0 | pub fn get_message_status_color(&self, status: MessageStatus) -> &'static str { |
1220 | 0 | match status { |
1221 | 0 | MessageStatus::Completed => "green", |
1222 | 0 | MessageStatus::Failed => "red", |
1223 | 0 | MessageStatus::Processing => "blue", |
1224 | 0 | MessageStatus::Queued => "yellow", |
1225 | 0 | MessageStatus::Dropped => "gray", |
1226 | | } |
1227 | 0 | } |
1228 | | |
1229 | | /// Format duration in microseconds |
1230 | 0 | pub fn format_duration_us(&self, us: u64) -> String { |
1231 | 0 | if us < 1000 { |
1232 | 0 | format!("{us}μs") |
1233 | 0 | } else if us < 1_000_000 { |
1234 | 0 | format!("{:.1}ms", us as f64 / 1000.0) |
1235 | 0 | } else if us < 60_000_000 { |
1236 | 0 | format!("{:.1}s", us as f64 / 1_000_000.0) |
1237 | | } else { |
1238 | 0 | let secs = us / 1_000_000; |
1239 | 0 | format!("{}m {}s", secs / 60, secs % 60) |
1240 | | } |
1241 | 0 | } |
1242 | | |
1243 | | /// Format bytes helper |
1244 | 0 | pub fn format_bytes(&self, bytes: usize) -> String { |
1245 | 0 | format_bytes(bytes) |
1246 | 0 | } |
1247 | | |
1248 | | /// Format timestamp |
1249 | 0 | pub fn format_timestamp(&self, _timestamp: u64) -> String { |
1250 | 0 | "12:34:56".to_string() |
1251 | 0 | } |
1252 | | |
1253 | | /// Truncate string |
1254 | 0 | pub fn truncate_string(&self, text: &str, max_len: usize) -> String { |
1255 | 0 | if text.len() <= max_len { |
1256 | 0 | text.to_string() |
1257 | 0 | } else if max_len < 3 { |
1258 | 0 | ".".repeat(max_len) |
1259 | | } else { |
1260 | 0 | format!("{}...", &text[..max_len - 3]) |
1261 | | } |
1262 | 0 | } |
1263 | | |
1264 | | /// Render header |
1265 | 0 | pub fn render_header(&self, title: &str) -> String { |
1266 | 0 | format!("=== {title} ===") |
1267 | 0 | } |
1268 | | |
1269 | | /// Render separator |
1270 | 0 | pub fn render_separator(&self) -> String { |
1271 | 0 | "-".repeat(40) |
1272 | 0 | } |
1273 | | |
1274 | | /// Render table row |
1275 | 0 | pub fn render_table_row(&self, columns: Vec<&str>) -> String { |
1276 | 0 | columns.join(" | ") |
1277 | 0 | } |
1278 | | |
1279 | | /// Render progress bar |
1280 | 0 | pub fn render_progress_bar(&self, current: usize, total: usize, _width: usize) -> String { |
1281 | 0 | let percent = if total > 0 { |
1282 | 0 | (current * 100) / total |
1283 | | } else { |
1284 | 0 | 0 |
1285 | | }; |
1286 | 0 | format!("[{percent}%]") |
1287 | 0 | } |
1288 | | |
1289 | | /// Handle key press |
1290 | 0 | pub fn handle_key(&mut self, key: char) -> bool { |
1291 | 0 | match key { |
1292 | 0 | 'q' | 'Q' => true, |
1293 | 0 | '1' => { self.display_mode = DisplayMode::Overview; false } |
1294 | 0 | '2' => { self.display_mode = DisplayMode::ActorList; false } |
1295 | 0 | '3' => { self.display_mode = DisplayMode::MessageTraces; false } |
1296 | 0 | '4' => { self.display_mode = DisplayMode::Metrics; false } |
1297 | 0 | '5' => { self.display_mode = DisplayMode::Deadlocks; false } |
1298 | 0 | 'h' => { self.display_mode = DisplayMode::Help; false } |
1299 | 0 | 'r' => { self.last_update = Instant::now(); false } |
1300 | 0 | _ => false, |
1301 | | } |
1302 | 0 | } |
1303 | | } |
1304 | | |
1305 | | /// Format a state for display |
1306 | 0 | fn state_display(state: &ActorState) -> String { |
1307 | 0 | match state { |
1308 | 0 | ActorState::Starting => "Starting".to_string(), |
1309 | 0 | ActorState::Running => "Running".to_string(), |
1310 | 0 | ActorState::Processing(msg) => format!("Proc({msg})"), |
1311 | 0 | ActorState::Restarting => "Restarting".to_string(), |
1312 | 0 | ActorState::Stopping => "Stopping".to_string(), |
1313 | 0 | ActorState::Stopped => "Stopped".to_string(), |
1314 | 0 | ActorState::Failed(reason) => format!("Failed({reason})"), |
1315 | | } |
1316 | 0 | } |
1317 | | |
1318 | | /// Format a timestamp for display |
1319 | 0 | fn format_timestamp(timestamp: u64) -> String { |
1320 | | use std::time::{SystemTime, UNIX_EPOCH}; |
1321 | | |
1322 | 0 | if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) { |
1323 | 0 | let now = duration.as_secs(); |
1324 | 0 | if timestamp > now - 60 { |
1325 | 0 | format!("{}s ago", now - timestamp) |
1326 | | } else { |
1327 | 0 | "old".to_string() |
1328 | | } |
1329 | | } else { |
1330 | 0 | "unknown".to_string() |
1331 | | } |
1332 | 0 | } |
1333 | | |
1334 | | /// Format a timestamp for display (short form) |
1335 | 0 | fn format_timestamp_short(instant: Instant) -> String { |
1336 | 0 | let elapsed = instant.elapsed(); |
1337 | 0 | if elapsed < Duration::from_secs(60) { |
1338 | 0 | format!("{}s ago", elapsed.as_secs()) |
1339 | | } else { |
1340 | 0 | format!("{}m ago", elapsed.as_secs() / 60) |
1341 | | } |
1342 | 0 | } |
1343 | | |
1344 | | /// Format a duration for display |
1345 | 0 | fn format_duration(duration: Duration) -> String { |
1346 | 0 | let total_seconds = duration.as_secs(); |
1347 | 0 | let hours = total_seconds / 3600; |
1348 | 0 | let minutes = (total_seconds % 3600) / 60; |
1349 | 0 | let seconds = total_seconds % 60; |
1350 | | |
1351 | 0 | if hours > 0 { |
1352 | 0 | format!("{hours}h {minutes}m {seconds}s") |
1353 | 0 | } else if minutes > 0 { |
1354 | 0 | format!("{minutes}m {seconds}s") |
1355 | | } else { |
1356 | 0 | format!("{seconds}s") |
1357 | | } |
1358 | 0 | } |
1359 | | |
1360 | | /// Format bytes for display |
1361 | 0 | fn format_bytes(bytes: usize) -> String { |
1362 | | const UNITS: &[&str] = &["B", "KB", "MB", "GB"]; |
1363 | | #[allow(clippy::cast_precision_loss)] // Acceptable for display formatting |
1364 | 0 | let mut size = bytes as f64; |
1365 | 0 | let mut unit_index = 0; |
1366 | | |
1367 | 0 | while size >= 1024.0 && unit_index < UNITS.len() - 1 { |
1368 | 0 | size /= 1024.0; |
1369 | 0 | unit_index += 1; |
1370 | 0 | } |
1371 | | |
1372 | 0 | if unit_index == 0 { |
1373 | 0 | format!("{} {}", bytes, UNITS[unit_index]) |
1374 | | } else { |
1375 | 0 | format!("{:.1} {}", size, UNITS[unit_index]) |
1376 | | } |
1377 | 0 | } |
1378 | | |
1379 | | /// Format a message for preview display |
1380 | 0 | fn format_message_preview(message: &crate::runtime::actor::Message) -> String { |
1381 | | use crate::runtime::actor::Message; |
1382 | | |
1383 | 0 | match message { |
1384 | 0 | Message::Start => "Start".to_string(), |
1385 | 0 | Message::Stop => "Stop".to_string(), |
1386 | 0 | Message::Restart => "Restart".to_string(), |
1387 | 0 | Message::User(msg_type, _) => format!("User({msg_type})"), |
1388 | 0 | Message::Error(err) => format!("Error({err})"), |
1389 | 0 | Message::ChildFailed(actor_id, reason) => format!("ChildFailed({actor_id}, {reason})"), |
1390 | 0 | Message::ChildRestarted(actor_id) => format!("ChildRestarted({actor_id})"), |
1391 | | } |
1392 | 0 | } |