Skip to content

Commit 7e091c0

Browse files
tenpercentclaude
andcommitted
Extract Python script and make PEP 723 compliant
- Extract analysis script from bash heredoc into standalone Python file - Add PEP 723 inline script metadata for dependency management - Make script compatible with pipx and uv for automatic dependency installation - Improve code organization with proper functions and docstrings - Update documentation with PEP 723 usage examples Changes: - New file: analyze_build_trace.py (PEP 723 compliant) - Modified: ck-build-analysis (now uses external Python script) - Modified: ck-build-analysis.md (added implementation details section) Benefits: - Script can be run standalone with pipx/uv - Better code organization and maintainability - Clear dependency declaration - Easier to test and develop independently Example standalone usage: pipx run .claude/skills/analyze_build_trace.py trace.json report.md target 100 22 templates/ Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fc53e81 commit 7e091c0

3 files changed

Lines changed: 278 additions & 148 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
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

Comments
 (0)