Skip to main content

trueno/brick/profiler/
reporting.rs

1//! Reporting and serialization methods for BrickProfiler.
2//!
3//! Extracted from mod.rs to keep file sizes manageable.
4//! Contains: summary(), to_json(), write_json(), print_category_stats(),
5//! tile_summary(), tile_stats_to_json().
6
7use super::BrickProfiler;
8use crate::brick::exec_graph::{BrickCategory, BrickId};
9
10impl BrickProfiler {
11    /// Print category breakdown to console.
12    pub fn print_category_stats(&self) {
13        let cats = self.category_stats();
14        let total = self.total_ns;
15
16        println!("╭─────────────────────────────────────────────────────────╮");
17        println!("│            Category Breakdown (PAR-200)                 │");
18        println!("├─────────────────────────────────────────────────────────┤");
19        for (i, cat_stats) in cats.iter().enumerate() {
20            let cat = BrickCategory::ALL[i];
21            if cat_stats.count > 0 {
22                println!(
23                    "│ {:12} {:8.2}µs avg {:6.1}% [{:5} samples]        │",
24                    cat.name(),
25                    cat_stats.avg_us(),
26                    cat_stats.percentage(total),
27                    cat_stats.count
28                );
29            }
30        }
31        println!("╰─────────────────────────────────────────────────────────╯");
32    }
33
34    /// Generate a summary report.
35    #[must_use]
36    pub fn summary(&self) -> String {
37        let mut report = String::new();
38        report.push_str("=== Brick Profiler Summary (PAR-200) ===\n");
39        report.push_str(&format!(
40            "Total: {} tokens, {:.2}µs, {:.1} tok/s\n",
41            self.total_tokens,
42            self.total_ns as f64 / 1000.0,
43            self.total_throughput()
44        ));
45        report.push_str("\nPer-Brick Breakdown:\n");
46
47        // Collect all stats (known + dynamic)
48        let mut all_stats: Vec<(&str, &crate::brick::exec_graph::BrickStats)> = Vec::new();
49
50        // Add known bricks with non-zero counts
51        for (i, stats) in self.brick_stats.iter().enumerate() {
52            if stats.count > 0 {
53                let brick_id = BrickId::ALL[i];
54                all_stats.push((brick_id.name(), stats));
55            }
56        }
57
58        // Add dynamic bricks
59        for (name, stats) in &self.dynamic_stats {
60            all_stats.push((name.as_str(), stats));
61        }
62
63        // Sort by total time descending
64        all_stats.sort_by(|a, b| b.1.total_ns.cmp(&a.1.total_ns));
65
66        for (name, stats) in &all_stats {
67            let pct = if self.total_ns > 0 {
68                100.0 * stats.total_ns as f64 / self.total_ns as f64
69            } else {
70                0.0
71            };
72            report.push_str(&format!(
73                "  {:20} {:8.2}µs avg ({:5.1}%) [{} samples]\n",
74                name,
75                stats.avg_us(),
76                pct,
77                stats.count
78            ));
79        }
80
81        // Add category breakdown
82        report.push_str("\nCategory Breakdown:\n");
83        let cats = self.category_stats();
84        for (i, cat_stats) in cats.iter().enumerate() {
85            if cat_stats.count > 0 {
86                let cat = BrickCategory::ALL[i];
87                report.push_str(&format!(
88                    "  {:12} {:8.2}µs avg ({:5.1}%)\n",
89                    cat.name(),
90                    cat_stats.avg_us(),
91                    cat_stats.percentage(self.total_ns)
92                ));
93            }
94        }
95
96        report
97    }
98
99    /// Export profiling data as JSON for pmat metrics integration.
100    ///
101    /// Format compatible with `.pmat-metrics/trends/` structure:
102    /// ```json
103    /// {
104    ///   "total_tokens": 1000,
105    ///   "total_ns": 5000000,
106    ///   "total_throughput": 200000.0,
107    ///   "bricks": [
108    ///     {
109    ///       "name": "RmsNorm",
110    ///       "count": 10,
111    ///       "total_ns": 1000000,
112    ///       "avg_us": 100.0,
113    ///       "min_us": 90.0,
114    ///       "max_us": 120.0,
115    ///       "throughput": 10000.0,
116    ///       "pct": 20.0
117    ///     }
118    ///   ]
119    /// }
120    /// ```
121    #[must_use]
122    pub fn to_json(&self) -> String {
123        let mut bricks = Vec::new();
124
125        // Collect all stats (known + dynamic)
126        let mut all_stats: Vec<(&str, &crate::brick::exec_graph::BrickStats)> = Vec::new();
127
128        // Add known bricks with non-zero counts
129        for (i, stats) in self.brick_stats.iter().enumerate() {
130            if stats.count > 0 {
131                let brick_id = BrickId::ALL[i];
132                all_stats.push((brick_id.name(), stats));
133            }
134        }
135
136        // Add dynamic bricks
137        for (name, stats) in &self.dynamic_stats {
138            all_stats.push((name.as_str(), stats));
139        }
140
141        // Sort by total time descending
142        all_stats.sort_by(|a, b| b.1.total_ns.cmp(&a.1.total_ns));
143
144        for (name, stats) in all_stats {
145            let pct = if self.total_ns > 0 {
146                100.0 * stats.total_ns as f64 / self.total_ns as f64
147            } else {
148                0.0
149            };
150            // PMAT-451: Include compression_ratio, throughput_gbps, and bottleneck
151            let compression = stats.compression_ratio();
152            let throughput_gbps = stats.throughput_gbps();
153            let bottleneck = stats.get_bottleneck();
154            bricks.push(format!(
155                r#"{{"name":"{}","count":{},"total_ns":{},"avg_us":{:.2},"min_us":{:.2},"max_us":{:.2},"throughput":{:.1},"pct":{:.1},"total_bytes":{},"compression_ratio":{:.2},"throughput_gbps":{:.2},"bottleneck":"{}"}}"#,
156                name,
157                stats.count,
158                stats.total_ns,
159                stats.avg_us(),
160                stats.min_us(),
161                stats.max_us(),
162                stats.throughput(),
163                pct,
164                stats.total_bytes,
165                compression,
166                throughput_gbps,
167                bottleneck
168            ));
169        }
170
171        format!(
172            r#"{{"total_tokens":{},"total_ns":{},"total_throughput":{:.1},"bricks":[{}]}}"#,
173            self.total_tokens,
174            self.total_ns,
175            self.total_throughput(),
176            bricks.join(",")
177        )
178    }
179
180    /// Write profiling data to a JSON file for pmat tracking.
181    ///
182    /// # Errors
183    /// Returns error if file cannot be written.
184    pub fn write_json(&self, path: &std::path::Path) -> std::io::Result<()> {
185        std::fs::write(path, self.to_json())
186    }
187
188    /// Generate tile profiling summary report.
189    ///
190    /// # Example Output
191    /// ```text
192    /// === Tile Profiling Summary (TILING-SPEC-001) ===
193    /// Level       Samples   Avg µs    GFLOP/s   AI      Elements
194    /// Macro           128    1234.5     12.34  0.50    1048576
195    /// Midi           2048      78.2     45.67  2.00      65536
196    /// Micro         32768       4.9     89.12  4.00       4096
197    /// ```
198    #[must_use]
199    pub fn tile_summary(&self) -> String {
200        let mut report = String::new();
201        report.push_str("=== Tile Profiling Summary (TILING-SPEC-001) ===\n");
202        report.push_str("Level       Samples   Avg µs    GFLOP/s   AI      Elements\n");
203
204        for stats in &self.tile_stats {
205            if stats.count > 0 {
206                report.push_str(&format!(
207                    "{:8}  {:9}  {:8.1}  {:8.2}  {:4.2}  {:10}\n",
208                    stats.level.name(),
209                    stats.count,
210                    stats.avg_us(),
211                    stats.gflops(),
212                    stats.arithmetic_intensity(),
213                    stats.total_elements / stats.count.max(1)
214                ));
215            }
216        }
217
218        report
219    }
220
221    /// Export tile statistics as JSON.
222    ///
223    /// Compatible with pmat metrics integration.
224    #[must_use]
225    pub fn tile_stats_to_json(&self) -> String {
226        let tiles: Vec<String> = self
227            .tile_stats
228            .iter()
229            .filter(|s| s.count > 0)
230            .map(|s| {
231                format!(
232                    r#"{{"level":"{}","count":{},"total_ns":{},"avg_us":{:.2},"min_us":{:.2},"max_us":{:.2},"gflops":{:.2},"arithmetic_intensity":{:.2},"total_elements":{},"total_flops":{}}}"#,
233                    s.level.name(),
234                    s.count,
235                    s.total_ns,
236                    s.avg_us(),
237                    s.min_ns as f64 / 1000.0,
238                    s.max_ns as f64 / 1000.0,
239                    s.gflops(),
240                    s.arithmetic_intensity(),
241                    s.total_elements,
242                    s.total_flops
243                )
244            })
245            .collect();
246
247        format!(
248            r#"{{"tile_profiling_enabled":{},"tiles":[{}]}}"#,
249            self.tile_profiling_enabled,
250            tiles.join(",")
251        )
252    }
253}