async_inspect/reporter/
mod.rs

1//! Reporting and output formatting
2//!
3//! This module provides utilities for displaying inspection results.
4
5use crate::inspector::{Inspector, InspectorStats};
6use crate::task::{TaskInfo, TaskState};
7use crate::timeline::Event;
8use std::fmt::Write as FmtWrite;
9
10pub mod html;
11
12/// Reporter for inspection results
13pub struct Reporter {
14    inspector: Inspector,
15}
16
17impl Reporter {
18    /// Create a new reporter
19    #[must_use]
20    pub fn new(inspector: Inspector) -> Self {
21        Self { inspector }
22    }
23
24    /// Create a reporter using the global inspector
25    #[must_use]
26    pub fn global() -> Self {
27        Self::new(Inspector::global().clone())
28    }
29
30    /// Print a summary of all tasks
31    pub fn print_summary(&self) {
32        let stats = self.inspector.stats();
33        let tasks = self.inspector.get_all_tasks();
34
35        println!("┌─────────────────────────────────────────────────────────────┐");
36        println!("│ async-inspect - Task Summary                                │");
37        println!("├─────────────────────────────────────────────────────────────┤");
38        println!("│                                                             │");
39
40        self.print_stats(&stats);
41
42        println!("│                                                             │");
43        println!("├─────────────────────────────────────────────────────────────┤");
44        println!("│ Tasks                                                       │");
45        println!("├─────────────────────────────────────────────────────────────┤");
46
47        if tasks.is_empty() {
48            println!("│ No tasks tracked                                            │");
49        } else {
50            for task in &tasks {
51                self.print_task_line(task);
52            }
53        }
54
55        println!("└─────────────────────────────────────────────────────────────┘");
56    }
57
58    /// Print statistics
59    fn print_stats(&self, stats: &InspectorStats) {
60        println!(
61            "│ Total Tasks:     {:>3}                                      │",
62            stats.total_tasks
63        );
64        println!(
65            "│ Active:          {:>3} (Running: {}, Blocked: {})           │",
66            stats.running_tasks + stats.blocked_tasks,
67            stats.running_tasks,
68            stats.blocked_tasks
69        );
70        println!(
71            "│ Completed:       {:>3}                                      │",
72            stats.completed_tasks
73        );
74        println!(
75            "│ Failed:          {:>3}                                      │",
76            stats.failed_tasks
77        );
78        println!(
79            "│ Total Events:    {:>3}                                      │",
80            stats.total_events
81        );
82        println!(
83            "│ Duration:        {:.2}s                                   │",
84            stats.timeline_duration.as_secs_f64()
85        );
86    }
87
88    /// Print a single task line
89    fn print_task_line(&self, task: &TaskInfo) {
90        let state_icon = match task.state {
91            TaskState::Pending => "[PEND]",
92            TaskState::Running => "[RUN] ",
93            TaskState::Blocked { .. } => "[WAIT]",
94            TaskState::Completed => "[OK]  ",
95            TaskState::Failed => "[FAIL]",
96        };
97
98        let status = format!("{} {} {}", task.id, state_icon, task.name);
99        println!("│ {status:<59} │");
100
101        // Show additional info for blocked tasks
102        if let TaskState::Blocked { ref await_point } = task.state {
103            let detail = format!(
104                "    └─> Waiting: {} ({:.2}s)",
105                await_point,
106                task.time_since_update().as_secs_f64()
107            );
108            println!("│ {detail:<59} │");
109        }
110    }
111
112    /// Print detailed information about a specific task
113    pub fn print_task_details(&self, task_id: crate::task::TaskId) {
114        let Some(task) = self.inspector.get_task(task_id) else {
115            println!("Task {task_id} not found");
116            return;
117        };
118
119        println!("┌─────────────────────────────────────────────────────────────┐");
120        println!(
121            "│ Task Details: {}                                           │",
122            task.id
123        );
124        println!("├─────────────────────────────────────────────────────────────┤");
125        println!("│                                                             │");
126        println!("│ Name:            {:<44}│", task.name);
127        println!("│ State:           {:<44}│", task.state.to_string());
128        println!(
129            "│ Age:             {:.2}s{:<38}│",
130            task.age().as_secs_f64(),
131            ""
132        );
133        println!("│ Poll Count:      {:<44}│", task.poll_count);
134        println!(
135            "│ Total Runtime:   {:.2}s{:<38}│",
136            task.total_run_time.as_secs_f64(),
137            ""
138        );
139
140        if let Some(parent) = task.parent {
141            println!("│ Parent:          {:<44}│", parent.to_string());
142        }
143
144        if let Some(location) = &task.location {
145            println!("│ Location:        {location:<44}│");
146        }
147
148        println!("│                                                             │");
149        println!("├─────────────────────────────────────────────────────────────┤");
150        println!("│ Events                                                      │");
151        println!("├─────────────────────────────────────────────────────────────┤");
152
153        let events = self.inspector.get_task_events(task_id);
154        if events.is_empty() {
155            println!("│ No events recorded                                          │");
156        } else {
157            for event in events.iter().take(20) {
158                let event_str = format!("{}", event.kind);
159                println!("│ {event_str:<59} │");
160            }
161
162            if events.len() > 20 {
163                println!(
164                    "│ ... and {} more events                                    │",
165                    events.len() - 20
166                );
167            }
168        }
169
170        println!("└─────────────────────────────────────────────────────────────┘");
171    }
172
173    /// Print timeline of all events
174    pub fn print_timeline(&self) {
175        let events = self.inspector.get_events();
176
177        println!("┌─────────────────────────────────────────────────────────────┐");
178        println!("│ async-inspect - Timeline                                    │");
179        println!("├─────────────────────────────────────────────────────────────┤");
180
181        if events.is_empty() {
182            println!("│ No events recorded                                          │");
183        } else {
184            for event in events.iter().take(50) {
185                self.print_event_line(event);
186            }
187
188            if events.len() > 50 {
189                println!("│                                                             │");
190                println!(
191                    "│ ... and {} more events                                    │",
192                    events.len() - 50
193                );
194            }
195        }
196
197        println!("└─────────────────────────────────────────────────────────────┘");
198    }
199
200    /// Print a single event line
201    fn print_event_line(&self, event: &Event) {
202        let time_str = format!("[{:.3}s]", event.age().as_secs_f64());
203        let event_str = format!("{} {}: {}", time_str, event.task_id, event.kind);
204
205        // Truncate if too long
206        let truncated = if event_str.len() > 59 {
207            format!("{}...", &event_str[..56])
208        } else {
209            event_str
210        };
211
212        println!("│ {truncated:<59} │");
213    }
214
215    /// Generate a text report
216    #[must_use]
217    pub fn generate_report(&self) -> String {
218        let mut report = String::new();
219        let stats = self.inspector.stats();
220        let tasks = self.inspector.get_all_tasks();
221
222        writeln!(report, "[async-inspect] Report").unwrap();
223        writeln!(report, "=======================").unwrap();
224        writeln!(report).unwrap();
225        writeln!(report, "Statistics:").unwrap();
226        writeln!(report, "  Total Tasks:     {}", stats.total_tasks).unwrap();
227        writeln!(report, "  Pending:         {}", stats.pending_tasks).unwrap();
228        writeln!(report, "  Running:         {}", stats.running_tasks).unwrap();
229        writeln!(report, "  Blocked:         {}", stats.blocked_tasks).unwrap();
230        writeln!(report, "  Completed:       {}", stats.completed_tasks).unwrap();
231        writeln!(report, "  Failed:          {}", stats.failed_tasks).unwrap();
232        writeln!(report, "  Total Events:    {}", stats.total_events).unwrap();
233        writeln!(
234            report,
235            "  Duration:        {:.2}s",
236            stats.timeline_duration.as_secs_f64()
237        )
238        .unwrap();
239        writeln!(report).unwrap();
240
241        writeln!(report, "Tasks:").unwrap();
242        for task in &tasks {
243            writeln!(report, "  {task}").unwrap();
244        }
245
246        report
247    }
248
249    /// Print a compact one-line summary
250    pub fn print_compact_summary(&self) {
251        let stats = self.inspector.stats();
252        println!(
253            "async-inspect: {} tasks ({} active, {} completed, {} failed) | {} events | {:.2}s",
254            stats.total_tasks,
255            stats.running_tasks + stats.blocked_tasks,
256            stats.completed_tasks,
257            stats.failed_tasks,
258            stats.total_events,
259            stats.timeline_duration.as_secs_f64()
260        );
261    }
262
263    /// Print a Gantt-style concurrency timeline
264    pub fn print_gantt_timeline(&self) {
265        let tasks = self.inspector.get_all_tasks();
266
267        if tasks.is_empty() {
268            println!("No tasks to display");
269            return;
270        }
271
272        // Calculate time bounds
273        let start_time = tasks
274            .iter()
275            .map(|t| t.created_at)
276            .min()
277            .expect("At least one task");
278
279        let end_time = tasks
280            .iter()
281            .map(|t| t.created_at + t.age())
282            .max()
283            .expect("At least one task");
284
285        let total_duration = end_time.duration_since(start_time);
286
287        // Timeline configuration
288        const TIMELINE_WIDTH: usize = 50;
289
290        println!("┌────────────────────────────────────────────────────────────────────────────┐");
291        println!("│ Concurrency Timeline (Gantt View)                                         │");
292        println!("├────────────────────────────────────────────────────────────────────────────┤");
293        println!("│                                                                            │");
294
295        // Print time scale
296        let time_markers = self.generate_time_markers(total_duration, TIMELINE_WIDTH);
297        println!("│ Time:  {time_markers}│");
298        println!("│        {}│", self.generate_timeline_ruler(TIMELINE_WIDTH));
299        println!("│                                                                            │");
300
301        // Print each task as a timeline bar
302        for task in &tasks {
303            let task_line =
304                self.generate_task_timeline(task, start_time, total_duration, TIMELINE_WIDTH);
305            println!("│ {task_line}│");
306        }
307
308        println!("│                                                                            │");
309        println!("│ Legend: █ Running  ░ Blocked  ─ Waiting  ✓ Completed  ✗ Failed           │");
310        println!("└────────────────────────────────────────────────────────────────────────────┘");
311    }
312
313    /// Generate time markers for the timeline
314    fn generate_time_markers(&self, total_duration: std::time::Duration, width: usize) -> String {
315        let mut markers = String::new();
316        let millis = total_duration.as_millis();
317
318        // Show markers at 0%, 25%, 50%, 75%, 100%
319        let positions = [0, width / 4, width / 2, 3 * width / 4, width];
320        let mut last_end = 0;
321
322        for &pos in &positions {
323            let time_ms = (millis as f64 * pos as f64 / width as f64) as u128;
324            let marker = format!("{time_ms}ms");
325
326            // Add spacing
327            if pos > last_end {
328                let spaces = pos.saturating_sub(last_end);
329                markers.push_str(&" ".repeat(spaces));
330            }
331
332            markers.push_str(&marker);
333            last_end = pos + marker.len();
334        }
335
336        // Pad to width
337        if markers.len() < width {
338            markers.push_str(&" ".repeat(width - markers.len()));
339        }
340
341        markers
342    }
343
344    /// Generate timeline ruler
345    fn generate_timeline_ruler(&self, width: usize) -> String {
346        let mut ruler = String::new();
347        for i in 0..width {
348            if i % 10 == 0 {
349                ruler.push('|');
350            } else if i % 5 == 0 {
351                ruler.push('·');
352            } else {
353                ruler.push('─');
354            }
355        }
356        ruler
357    }
358
359    /// Generate a timeline bar for a single task
360    fn generate_task_timeline(
361        &self,
362        task: &TaskInfo,
363        start_time: std::time::Instant,
364        total_duration: std::time::Duration,
365        width: usize,
366    ) -> String {
367        let mut line = String::new();
368
369        // Task name (first 12 chars)
370        let name = if task.name.len() > 12 {
371            format!("{:.9}...", task.name)
372        } else {
373            format!("{:<12}", task.name)
374        };
375        line.push_str(&name);
376        line.push_str(": ");
377
378        // Calculate task position and length
379        let task_start = task.created_at.duration_since(start_time);
380        let task_duration = task.age();
381
382        let start_pos = ((task_start.as_millis() as f64 / total_duration.as_millis() as f64)
383            * width as f64) as usize;
384        let task_len = ((task_duration.as_millis() as f64 / total_duration.as_millis() as f64)
385            * width as f64)
386            .max(1.0) as usize;
387
388        // Build the timeline bar
389        for i in 0..width {
390            if i < start_pos {
391                line.push(' ');
392            } else if i < start_pos + task_len {
393                // Determine character based on task state
394                let ch = match task.state {
395                    TaskState::Running => '█',
396                    TaskState::Blocked { .. } => '░',
397                    TaskState::Completed => '█',
398                    TaskState::Failed => '▓',
399                    TaskState::Pending => '─',
400                };
401                line.push(ch);
402            } else {
403                line.push(' ');
404            }
405        }
406
407        // Add state indicator
408        let indicator = match task.state {
409            TaskState::Completed => " ✓",
410            TaskState::Failed => " ✗",
411            TaskState::Running => " →",
412            TaskState::Blocked { .. } => " ⏸",
413            TaskState::Pending => " ○",
414        };
415        line.push_str(indicator);
416
417        // Pad to consistent width
418        while line.len() < 74 {
419            line.push(' ');
420        }
421
422        line
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_reporter_creation() {
432        let inspector = Inspector::new();
433        let reporter = Reporter::new(inspector);
434        // Just verify it doesn't panic
435        reporter.print_compact_summary();
436    }
437
438    #[test]
439    fn test_generate_report() {
440        let inspector = Inspector::new();
441        inspector.register_task("test".to_string());
442
443        let reporter = Reporter::new(inspector);
444        let report = reporter.generate_report();
445
446        assert!(report.contains("[async-inspect] Report"));
447        assert!(report.contains("Total Tasks:     1"));
448    }
449}