scirs2_core/memory/metrics/
reporter.rs

1//! Memory metrics reporting and visualization
2//!
3//! This module provides formatters and visualizers for memory metrics.
4
5#[cfg(feature = "memory_metrics")]
6use serde_json::{json, Value as JsonValue};
7use std::time::Duration;
8
9use crate::memory::metrics::collector::MemoryReport;
10
11/// Format bytes in human-readable format
12#[allow(dead_code)]
13pub fn format_bytes(bytes: usize) -> String {
14    const KB: usize = 1024;
15    const MB: usize = KB * 1024;
16    const GB: usize = MB * 1024;
17
18    if bytes >= GB {
19        format!("{:.2} GB", bytes as f64 / GB as f64)
20    } else if bytes >= MB {
21        format!("{:.2} MB", bytes as f64 / MB as f64)
22    } else if bytes >= KB {
23        format!("{:.2} KB", bytes as f64 / KB as f64)
24    } else {
25        format!("{bytes} bytes")
26    }
27}
28
29/// Format duration in human-readable format
30#[allow(dead_code)]
31pub fn format_duration(duration: Duration) -> String {
32    let total_secs = duration.as_secs();
33
34    if total_secs < 60 {
35        return format!("{total_secs}s");
36    }
37
38    let mins = total_secs / 60;
39    let secs = total_secs % 60;
40
41    if mins < 60 {
42        return format!("{mins}m {secs}s");
43    }
44
45    let hours = mins / 60;
46    let mins = mins % 60;
47
48    format!("{hours}h {mins}m {secs}s")
49}
50
51impl MemoryReport {
52    /// Format the report as a string
53    pub fn format(&self) -> String {
54        let mut output = String::new();
55
56        output.push_str(&format!(
57            "Memory Report (duration: {})\n",
58            format_duration(Duration::from_secs(1))
59        ));
60        output.push_str(&format!(
61            "Total Current Usage: {}\n",
62            format_bytes(self.total_current_usage)
63        ));
64        output.push_str(&format!(
65            "Total Peak Usage: {}\n",
66            format_bytes(self.total_peak_usage)
67        ));
68        output.push_str(&format!(
69            "Total Allocations: {}\n",
70            self.total_allocation_count
71        ));
72        output.push_str(&format!(
73            "Total Allocated Bytes: {}\n",
74            format_bytes(self.total_allocated_bytes)
75        ));
76
77        output.push_str("\nComponent Statistics:\n");
78
79        // Sort components by peak usage (descending)
80        let mut components: Vec<_> = self.component_stats.iter().collect();
81        components.sort_by(|a, b| b.1.peak_usage.cmp(&a.1.peak_usage));
82
83        for (component, stats) in components {
84            output.push_str(&format!("\n  {component}\n"));
85            output.push_str(&format!(
86                "    Current Usage: {}\n",
87                format_bytes(stats.current_usage)
88            ));
89            output.push_str(&format!(
90                "    Peak Usage: {}\n",
91                format_bytes(stats.peak_usage)
92            ));
93            output.push_str(&format!(
94                "    Allocation Count: {}\n",
95                stats.allocation_count
96            ));
97            output.push_str(&format!(
98                "    Total Allocated: {}\n",
99                format_bytes(stats.total_allocated)
100            ));
101            output.push_str(&format!(
102                "    Avg Allocation Size: {}\n",
103                format_bytes(stats.avg_allocation_size as usize)
104            ));
105
106            // Calculate reuse ratio (if peak is non-zero)
107            if stats.peak_usage > 0 {
108                let reuse_ratio = stats.total_allocated as f64 / stats.peak_usage as f64;
109                output.push_str(&format!("    Memory Reuse Ratio: {reuse_ratio:.2}\n"));
110            }
111
112            // Calculate allocation frequency
113            if self.duration.as_secs_f64() > 0.0 {
114                let alloc_per_sec = stats.allocation_count as f64 / self.duration.as_secs_f64();
115                output.push_str(&format!("    Allocations/sec: {alloc_per_sec:.2}\n"));
116            }
117        }
118
119        output
120    }
121
122    /// Export the report as JSON
123    #[cfg(feature = "memory_metrics")]
124    pub fn to_json(&self) -> JsonValue {
125        let mut component_stats = serde_json::Map::new();
126
127        for (component, stats) in &self.component_stats {
128            let reuse_ratio = if stats.peak_usage > 0 {
129                stats.total_allocated as f64 / stats.peak_usage as f64
130            } else {
131                0.0
132            };
133
134            let alloc_per_sec = if self.duration.as_secs_f64() > 0.0 {
135                stats.allocation_count as f64 / self.duration.as_secs_f64()
136            } else {
137                0.0
138            };
139
140            let stats_obj = json!({
141                "current_usage": stats.current_usage,
142                "current_usage_formatted": format_bytes(stats.current_usage),
143                "peak_usage": stats.peak_usage,
144                "peak_usage_formatted": format_bytes(stats.peak_usage),
145                "allocation_count": stats.allocation_count,
146                "total_allocated": stats.total_allocated,
147                "total_allocated_formatted": format_bytes(stats.total_allocated),
148                "avg_allocation_size": stats.avg_allocation_size,
149                "avg_allocation_size_formatted": format_bytes(stats.avg_allocation_size as usize),
150                "reuse_ratio": reuse_ratio,
151                "alloc_per_sec": alloc_per_sec,
152            });
153
154            component_stats.insert(component.clone(), stats_obj);
155        }
156
157        json!({
158            "total_current_usage": self.total_current_usage,
159            "total_current_usage_formatted": format_bytes(self.total_current_usage),
160            "total_peak_usage": self.total_peak_usage,
161            "total_peak_usage_formatted": format_bytes(self.total_peak_usage),
162            "total_allocation_count": self.total_allocation_count,
163            "total_allocated_bytes": self.total_allocated_bytes,
164            "total_allocated_bytes_formatted": format_bytes(self.total_allocated_bytes),
165            "duration_seconds": self.duration.as_secs_f64(),
166            "duration_formatted": format_duration(self.duration),
167            "components": component_stats,
168        })
169    }
170
171    /// Export the report as JSON - stub when memory_metrics is disabled
172    #[cfg(not(feature = "memory_metrics"))]
173    pub fn to_json_2(&self) -> String {
174        "{}".to_string()
175    }
176
177    /// Generate a visual chart (ASCII or SVG)
178    #[cfg(feature = "memory_visualization")]
179    pub fn generate_chart(&self, format: ChartFormat) -> String {
180        match format {
181            ChartFormat::Ascii => self.generate_ascii_chart(),
182            ChartFormat::Svg => self.generate_svg_chart(),
183        }
184    }
185
186    /// Generate an ASCII chart of memory usage by component
187    #[cfg(feature = "memory_visualization")]
188    fn generate_ascii_chart(&self) -> String {
189        let mut output = String::new();
190
191        output.push_str("Memory Usage by Component (ASCII Chart)\n\n");
192
193        // Sort components by peak usage (descending)
194        let mut components: Vec<_> = self.component_stats.iter().collect();
195        components.sort_by(|a, b| b.1.peak_usage.cmp(&a.1.peak_usage));
196
197        // Find maximum usage for scaling
198        let max_usage = components
199            .first()
200            .map(|(_, stats)| stats.peak_usage)
201            .unwrap_or(0);
202
203        if max_usage == 0 {
204            return "No memory usage data available.".to_string();
205        }
206
207        // Chart width in characters
208        const CHART_WIDTH: usize = 50;
209
210        for (component, stats) in components {
211            let peak_width =
212                (stats.peak_usage as f64 / max_usage as f64 * CHART_WIDTH as f64) as usize;
213            let current_width =
214                (stats.current_usage as f64 / max_usage as f64 * CHART_WIDTH as f64) as usize;
215
216            let peak_bar = "#".repeat(peak_width);
217            let current_bar = if current_width > 0 {
218                "|".repeat(current_width)
219            } else {
220                String::new()
221            };
222
223            output.push_str(&format!(
224                "{:<20} [{:<50}] {}\n",
225                component,
226                format!("{current_bar}{peak_bar}"),
227                format_bytes(stats.peak_usage)
228            ));
229        }
230
231        output.push_str("\nLegend: | = Current Usage, # = Peak Usage\n");
232
233        output
234    }
235
236    /// Generate an SVG chart
237    #[cfg(feature = "memory_visualization")]
238    fn generate_svg_chart(&self) -> String {
239        // In a real implementation, this would generate an SVG chart
240        // For now, just return a placeholder
241        "SVG chart generation not implemented".to_string()
242    }
243}
244
245/// Chart format for visualization
246#[cfg(feature = "memory_visualization")]
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum ChartFormat {
249    /// ASCII chart
250    Ascii,
251    /// SVG chart
252    Svg,
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::memory::metrics::collector::ComponentMemoryStats;
259    use std::collections::HashMap;
260    use std::time::Duration;
261
262    #[test]
263    fn test_format_bytes() {
264        assert_eq!(format_bytes(512), "512 bytes");
265        assert_eq!(format_bytes(1024), "1.00 KB");
266        assert_eq!(format_bytes(1536), "1.50 KB");
267        assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
268        assert_eq!(format_bytes(1536 * 1024), "1.50 MB");
269        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
270    }
271
272    #[test]
273    fn test_format_duration() {
274        assert_eq!(format_duration(Duration::from_secs(30)), "30s");
275        assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
276        assert_eq!(format_duration(Duration::from_secs(3600 + 90)), "1h 1m 30s");
277    }
278
279    #[test]
280    fn test_memory_report_format() {
281        let mut component_stats = HashMap::new();
282
283        component_stats.insert(
284            "Component1".to_string(),
285            ComponentMemoryStats {
286                current_usage: 1024,
287                peak_usage: 2048,
288                allocation_count: 10,
289                total_allocated: 4096,
290                avg_allocation_size: 409.6,
291            },
292        );
293
294        component_stats.insert(
295            "Component2".to_string(),
296            ComponentMemoryStats {
297                current_usage: 512,
298                peak_usage: 1024,
299                allocation_count: 5,
300                total_allocated: 2048,
301                avg_allocation_size: 409.6,
302            },
303        );
304
305        let report = MemoryReport {
306            total_current_usage: 1536,
307            total_peak_usage: 3072,
308            total_allocation_count: 15,
309            total_allocated_bytes: 6144,
310            component_stats,
311            duration: Duration::from_secs(60),
312        };
313
314        let formatted = report.format();
315
316        // Basic checks on the formatted output
317        assert!(formatted.contains("Total Current Usage: 1.50 KB"));
318        assert!(formatted.contains("Total Peak Usage: 3.00 KB"));
319        assert!(formatted.contains("Total Allocations: 15"));
320        assert!(formatted.contains("Component1"));
321        assert!(formatted.contains("Component2"));
322    }
323
324    #[test]
325    fn test_memory_report_to_json() {
326        let mut component_stats = HashMap::new();
327
328        component_stats.insert(
329            "Component1".to_string(),
330            ComponentMemoryStats {
331                current_usage: 1024,
332                peak_usage: 2048,
333                allocation_count: 10,
334                total_allocated: 4096,
335                avg_allocation_size: 409.6,
336            },
337        );
338
339        let report = MemoryReport {
340            total_current_usage: 1024,
341            total_peak_usage: 2048,
342            total_allocation_count: 10,
343            total_allocated_bytes: 4096,
344            component_stats,
345            duration: Duration::from_secs(30),
346        };
347
348        #[cfg(feature = "memory_metrics")]
349        {
350            let json = report.to_json();
351            assert_eq!(json["total_current_usage"], 1024);
352            assert_eq!(json["total_peak_usage"], 2048);
353            assert_eq!(json["total_allocation_count"], 10);
354            assert_eq!(json["components"]["Component1"]["current_usage"], 1024);
355        }
356
357        #[cfg(not(feature = "memory_metrics"))]
358        {
359            // Just to avoid the unused variable warning
360            let _ = report;
361        }
362    }
363}