|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# /// script |
| 3 | +# requires-python = ">=3.8" |
| 4 | +# dependencies = [ |
| 5 | +# "jinja2>=3.0.0", |
| 6 | +# ] |
| 7 | +# /// |
| 8 | +""" |
| 9 | +Build Time Analysis Tool for Composable Kernel |
| 10 | +
|
| 11 | +Analyzes Clang -ftime-trace output to identify template instantiation |
| 12 | +bottlenecks and generate comprehensive build time reports. |
| 13 | +""" |
| 14 | + |
| 15 | +import json |
| 16 | +import re |
| 17 | +import sys |
| 18 | +from collections import defaultdict |
| 19 | +from datetime import datetime |
| 20 | +from pathlib import Path |
| 21 | + |
| 22 | +try: |
| 23 | + from jinja2 import Environment, FileSystemLoader |
| 24 | +except ImportError: |
| 25 | + print("Error: jinja2 is required but not installed.", file=sys.stderr) |
| 26 | + print("Install with: apt-get install python3-jinja2", file=sys.stderr) |
| 27 | + print("Or with pip: pip install jinja2", file=sys.stderr) |
| 28 | + sys.exit(1) |
| 29 | + |
| 30 | + |
| 31 | +def parse_arguments(): |
| 32 | + """Parse command-line arguments.""" |
| 33 | + if len(sys.argv) < 7: |
| 34 | + print("Usage: analyze_build_trace.py <trace_file> <output_file> <target> <granularity> <build_time> <template_dir>") |
| 35 | + sys.exit(1) |
| 36 | + |
| 37 | + return { |
| 38 | + 'trace_file': sys.argv[1], |
| 39 | + 'output_file': sys.argv[2], |
| 40 | + 'target': sys.argv[3], |
| 41 | + 'granularity': sys.argv[4], |
| 42 | + 'build_time': sys.argv[5], |
| 43 | + 'template_dir': sys.argv[6], |
| 44 | + } |
| 45 | + |
| 46 | + |
| 47 | +def load_trace_data(trace_file): |
| 48 | + """Load and parse the trace JSON file.""" |
| 49 | + print(f'Loading trace file: {trace_file}') |
| 50 | + with open(trace_file, 'r') as f: |
| 51 | + return json.load(f) |
| 52 | + |
| 53 | + |
| 54 | +def process_events(data): |
| 55 | + """Process trace events and extract template instantiation statistics.""" |
| 56 | + print('Processing events...') |
| 57 | + |
| 58 | + template_stats = defaultdict(lambda: {'count': 0, 'total_dur': 0.0}) |
| 59 | + phase_stats = defaultdict(float) |
| 60 | + top_individual = [] |
| 61 | + |
| 62 | + for event in data.get('traceEvents', []): |
| 63 | + name = event.get('name', '') |
| 64 | + dur = event.get('dur', 0) / 1000.0 # Convert to milliseconds |
| 65 | + |
| 66 | + if name and dur > 0: |
| 67 | + phase_stats[name] += dur |
| 68 | + |
| 69 | + if name in ['InstantiateFunction', 'InstantiateClass']: |
| 70 | + detail = event.get('args', {}).get('detail', '') |
| 71 | + top_individual.append({ |
| 72 | + 'detail': detail, |
| 73 | + 'dur': dur, |
| 74 | + 'type': name |
| 75 | + }) |
| 76 | + |
| 77 | + # Extract template name (everything before '<' or '(') |
| 78 | + match = re.match(r'^([^<(]+)', detail) |
| 79 | + if match: |
| 80 | + template_name = match.group(1).strip() |
| 81 | + # Normalize template names |
| 82 | + template_name = re.sub(r'^ck::', '', template_name) |
| 83 | + template_name = re.sub(r'^std::', 'std::', template_name) |
| 84 | + |
| 85 | + template_stats[template_name]['count'] += 1 |
| 86 | + template_stats[template_name]['total_dur'] += dur |
| 87 | + |
| 88 | + return template_stats, phase_stats, top_individual |
| 89 | + |
| 90 | + |
| 91 | +def prepare_template_data(template_stats, phase_stats, top_individual): |
| 92 | + """Prepare and calculate derived statistics for template rendering.""" |
| 93 | + print('Sorting data...') |
| 94 | + |
| 95 | + # Sort data |
| 96 | + sorted_phases = sorted(phase_stats.items(), key=lambda x: x[1], reverse=True) |
| 97 | + top_individual.sort(key=lambda x: x['dur'], reverse=True) |
| 98 | + |
| 99 | + # Calculate totals |
| 100 | + total_template_time = sum(s['total_dur'] for s in template_stats.values()) |
| 101 | + total_trace_time = sum(phase_stats.values()) |
| 102 | + total_inst = sum(s['count'] for s in template_stats.values()) |
| 103 | + |
| 104 | + # Prepare templates by time with calculated fields |
| 105 | + templates_by_time = [] |
| 106 | + for name, stats in sorted(template_stats.items(), key=lambda x: x[1]['total_dur'], reverse=True): |
| 107 | + templates_by_time.append((name, { |
| 108 | + 'count': stats['count'], |
| 109 | + 'total_dur': stats['total_dur'], |
| 110 | + 'avg': stats['total_dur'] / stats['count'] if stats['count'] > 0 else 0, |
| 111 | + 'pct': 100 * stats['total_dur'] / total_template_time if total_template_time > 0 else 0 |
| 112 | + })) |
| 113 | + |
| 114 | + # Prepare templates by count |
| 115 | + templates_by_count = [] |
| 116 | + for name, stats in sorted(template_stats.items(), key=lambda x: x[1]['count'], reverse=True): |
| 117 | + templates_by_count.append((name, { |
| 118 | + 'count': stats['count'], |
| 119 | + 'total_dur': stats['total_dur'], |
| 120 | + 'avg': stats['total_dur'] / stats['count'] if stats['count'] > 0 else 0 |
| 121 | + })) |
| 122 | + |
| 123 | + # Add friendly type names to individual instantiations |
| 124 | + for inst in top_individual: |
| 125 | + inst['inst_type'] = 'Func' if inst['type'] == 'InstantiateFunction' else 'Class' |
| 126 | + |
| 127 | + # Calculate additional metrics |
| 128 | + median_count = 0 |
| 129 | + if len(template_stats) > 0: |
| 130 | + median_count = sorted([s["count"] for s in template_stats.values()])[len(template_stats) // 2] |
| 131 | + |
| 132 | + top10_pct = 0 |
| 133 | + if len(templates_by_time) >= 10: |
| 134 | + top10_pct = 100 * sum(s[1]["total_dur"] for s in templates_by_time[:10]) / total_template_time |
| 135 | + |
| 136 | + return { |
| 137 | + 'sorted_phases': sorted_phases, |
| 138 | + 'top_individual': top_individual, |
| 139 | + 'templates_by_time': templates_by_time, |
| 140 | + 'templates_by_count': templates_by_count, |
| 141 | + 'total_template_time': total_template_time, |
| 142 | + 'total_trace_time': total_trace_time, |
| 143 | + 'total_inst': total_inst, |
| 144 | + 'median_count': median_count, |
| 145 | + 'top10_pct': top10_pct, |
| 146 | + 'unique_families': len(template_stats), |
| 147 | + } |
| 148 | + |
| 149 | + |
| 150 | +def setup_jinja_environment(template_dir): |
| 151 | + """Set up Jinja2 environment with custom filters.""" |
| 152 | + env = Environment(loader=FileSystemLoader(template_dir)) |
| 153 | + |
| 154 | + def format_number(value): |
| 155 | + """Format number with thousand separators.""" |
| 156 | + return f'{value:,}' |
| 157 | + |
| 158 | + def truncate(value, length): |
| 159 | + """Truncate string to length with ellipsis.""" |
| 160 | + if len(value) > length: |
| 161 | + return value[:length - 3] + '...' |
| 162 | + return value |
| 163 | + |
| 164 | + def pad(value, length): |
| 165 | + """Pad string to specified length.""" |
| 166 | + return f'{value:<{length}}' |
| 167 | + |
| 168 | + env.filters['format_number'] = format_number |
| 169 | + env.filters['truncate'] = truncate |
| 170 | + env.filters['pad'] = pad |
| 171 | + |
| 172 | + return env |
| 173 | + |
| 174 | + |
| 175 | +def generate_report(env, data, args, total_events): |
| 176 | + """Generate the final report using Jinja2 template.""" |
| 177 | + print('Rendering report with Jinja2...') |
| 178 | + |
| 179 | + template = env.get_template('build_analysis_report.md.jinja') |
| 180 | + |
| 181 | + report_content = template.render( |
| 182 | + timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), |
| 183 | + target=args['target'], |
| 184 | + granularity=args['granularity'], |
| 185 | + build_time=args['build_time'], |
| 186 | + trace_time_sec=f'{data["total_trace_time"] / 1000:.1f}', |
| 187 | + template_time_sec=f'{data["total_template_time"] / 1000:.1f}', |
| 188 | + template_pct=f'{100 * data["total_template_time"] / data["total_trace_time"]:.1f}', |
| 189 | + total_events=total_events, |
| 190 | + total_instantiations=data['total_inst'], |
| 191 | + unique_families=data['unique_families'], |
| 192 | + total_trace_time=data['total_trace_time'], |
| 193 | + total_template_time=data['total_template_time'], |
| 194 | + phases=data['sorted_phases'], |
| 195 | + top_individual=data['top_individual'], |
| 196 | + templates_by_time=data['templates_by_time'], |
| 197 | + templates_by_count=data['templates_by_count'], |
| 198 | + median_count=data['median_count'], |
| 199 | + top10_pct=f'{data["top10_pct"]:.1f}' |
| 200 | + ) |
| 201 | + |
| 202 | + return report_content |
| 203 | + |
| 204 | + |
| 205 | +def main(): |
| 206 | + """Main entry point for the analysis tool.""" |
| 207 | + args = parse_arguments() |
| 208 | + |
| 209 | + # Load trace data |
| 210 | + trace_data = load_trace_data(args['trace_file']) |
| 211 | + total_events = len(trace_data.get('traceEvents', [])) |
| 212 | + |
| 213 | + # Process events |
| 214 | + template_stats, phase_stats, top_individual = process_events(trace_data) |
| 215 | + |
| 216 | + # Prepare template data |
| 217 | + data = prepare_template_data(template_stats, phase_stats, top_individual) |
| 218 | + |
| 219 | + # Setup Jinja2 environment |
| 220 | + env = setup_jinja_environment(args['template_dir']) |
| 221 | + |
| 222 | + # Generate report |
| 223 | + report_content = generate_report(env, data, args, total_events) |
| 224 | + |
| 225 | + # Write output |
| 226 | + with open(args['output_file'], 'w') as f: |
| 227 | + f.write(report_content) |
| 228 | + |
| 229 | + print(f'Report generated: {args["output_file"]}') |
| 230 | + print(f'Report size: {len(report_content)} bytes') |
| 231 | + |
| 232 | + |
| 233 | +if __name__ == '__main__': |
| 234 | + main() |
0 commit comments