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>
This commit is contained in:
Max Podkorytov
2026-01-13 22:50:17 -06:00
parent fc53e81355
commit 7e091c06c5
3 changed files with 278 additions and 148 deletions

View File

@@ -0,0 +1,234 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.8"
# dependencies = [
# "jinja2>=3.0.0",
# ]
# ///
"""
Build Time Analysis Tool for Composable Kernel
Analyzes Clang -ftime-trace output to identify template instantiation
bottlenecks and generate comprehensive build time reports.
"""
import json
import re
import sys
from collections import defaultdict
from datetime import datetime
from pathlib import Path
try:
from jinja2 import Environment, FileSystemLoader
except ImportError:
print("Error: jinja2 is required but not installed.", file=sys.stderr)
print("Install with: apt-get install python3-jinja2", file=sys.stderr)
print("Or with pip: pip install jinja2", file=sys.stderr)
sys.exit(1)
def parse_arguments():
"""Parse command-line arguments."""
if len(sys.argv) < 7:
print("Usage: analyze_build_trace.py <trace_file> <output_file> <target> <granularity> <build_time> <template_dir>")
sys.exit(1)
return {
'trace_file': sys.argv[1],
'output_file': sys.argv[2],
'target': sys.argv[3],
'granularity': sys.argv[4],
'build_time': sys.argv[5],
'template_dir': sys.argv[6],
}
def load_trace_data(trace_file):
"""Load and parse the trace JSON file."""
print(f'Loading trace file: {trace_file}')
with open(trace_file, 'r') as f:
return json.load(f)
def process_events(data):
"""Process trace events and extract template instantiation statistics."""
print('Processing events...')
template_stats = defaultdict(lambda: {'count': 0, 'total_dur': 0.0})
phase_stats = defaultdict(float)
top_individual = []
for event in data.get('traceEvents', []):
name = event.get('name', '')
dur = event.get('dur', 0) / 1000.0 # Convert to milliseconds
if name and dur > 0:
phase_stats[name] += dur
if name in ['InstantiateFunction', 'InstantiateClass']:
detail = event.get('args', {}).get('detail', '')
top_individual.append({
'detail': detail,
'dur': dur,
'type': name
})
# Extract template name (everything before '<' or '(')
match = re.match(r'^([^<(]+)', detail)
if match:
template_name = match.group(1).strip()
# Normalize template names
template_name = re.sub(r'^ck::', '', template_name)
template_name = re.sub(r'^std::', 'std::', template_name)
template_stats[template_name]['count'] += 1
template_stats[template_name]['total_dur'] += dur
return template_stats, phase_stats, top_individual
def prepare_template_data(template_stats, phase_stats, top_individual):
"""Prepare and calculate derived statistics for template rendering."""
print('Sorting data...')
# Sort data
sorted_phases = sorted(phase_stats.items(), key=lambda x: x[1], reverse=True)
top_individual.sort(key=lambda x: x['dur'], reverse=True)
# Calculate totals
total_template_time = sum(s['total_dur'] for s in template_stats.values())
total_trace_time = sum(phase_stats.values())
total_inst = sum(s['count'] for s in template_stats.values())
# Prepare templates by time with calculated fields
templates_by_time = []
for name, stats in sorted(template_stats.items(), key=lambda x: x[1]['total_dur'], reverse=True):
templates_by_time.append((name, {
'count': stats['count'],
'total_dur': stats['total_dur'],
'avg': stats['total_dur'] / stats['count'] if stats['count'] > 0 else 0,
'pct': 100 * stats['total_dur'] / total_template_time if total_template_time > 0 else 0
}))
# Prepare templates by count
templates_by_count = []
for name, stats in sorted(template_stats.items(), key=lambda x: x[1]['count'], reverse=True):
templates_by_count.append((name, {
'count': stats['count'],
'total_dur': stats['total_dur'],
'avg': stats['total_dur'] / stats['count'] if stats['count'] > 0 else 0
}))
# Add friendly type names to individual instantiations
for inst in top_individual:
inst['inst_type'] = 'Func' if inst['type'] == 'InstantiateFunction' else 'Class'
# Calculate additional metrics
median_count = 0
if len(template_stats) > 0:
median_count = sorted([s["count"] for s in template_stats.values()])[len(template_stats) // 2]
top10_pct = 0
if len(templates_by_time) >= 10:
top10_pct = 100 * sum(s[1]["total_dur"] for s in templates_by_time[:10]) / total_template_time
return {
'sorted_phases': sorted_phases,
'top_individual': top_individual,
'templates_by_time': templates_by_time,
'templates_by_count': templates_by_count,
'total_template_time': total_template_time,
'total_trace_time': total_trace_time,
'total_inst': total_inst,
'median_count': median_count,
'top10_pct': top10_pct,
'unique_families': len(template_stats),
}
def setup_jinja_environment(template_dir):
"""Set up Jinja2 environment with custom filters."""
env = Environment(loader=FileSystemLoader(template_dir))
def format_number(value):
"""Format number with thousand separators."""
return f'{value:,}'
def truncate(value, length):
"""Truncate string to length with ellipsis."""
if len(value) > length:
return value[:length - 3] + '...'
return value
def pad(value, length):
"""Pad string to specified length."""
return f'{value:<{length}}'
env.filters['format_number'] = format_number
env.filters['truncate'] = truncate
env.filters['pad'] = pad
return env
def generate_report(env, data, args, total_events):
"""Generate the final report using Jinja2 template."""
print('Rendering report with Jinja2...')
template = env.get_template('build_analysis_report.md.jinja')
report_content = template.render(
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
target=args['target'],
granularity=args['granularity'],
build_time=args['build_time'],
trace_time_sec=f'{data["total_trace_time"] / 1000:.1f}',
template_time_sec=f'{data["total_template_time"] / 1000:.1f}',
template_pct=f'{100 * data["total_template_time"] / data["total_trace_time"]:.1f}',
total_events=total_events,
total_instantiations=data['total_inst'],
unique_families=data['unique_families'],
total_trace_time=data['total_trace_time'],
total_template_time=data['total_template_time'],
phases=data['sorted_phases'],
top_individual=data['top_individual'],
templates_by_time=data['templates_by_time'],
templates_by_count=data['templates_by_count'],
median_count=data['median_count'],
top10_pct=f'{data["top10_pct"]:.1f}'
)
return report_content
def main():
"""Main entry point for the analysis tool."""
args = parse_arguments()
# Load trace data
trace_data = load_trace_data(args['trace_file'])
total_events = len(trace_data.get('traceEvents', []))
# Process events
template_stats, phase_stats, top_individual = process_events(trace_data)
# Prepare template data
data = prepare_template_data(template_stats, phase_stats, top_individual)
# Setup Jinja2 environment
env = setup_jinja_environment(args['template_dir'])
# Generate report
report_content = generate_report(env, data, args, total_events)
# Write output
with open(args['output_file'], 'w') as f:
f.write(report_content)
print(f'Report generated: {args["output_file"]}')
print(f'Report size: {len(report_content)} bytes')
if __name__ == '__main__':
main()

View File

@@ -171,158 +171,16 @@ fi
TRACE_SIZE=$(docker exec "${CONTAINER_NAME}" bash -c "ls -lh ${TRACE_FILE} | awk '{print \$5}'")
echo "Found trace file: ${TRACE_FILE} (${TRACE_SIZE})"
# Generate analysis script
# Generate analysis report
echo ""
echo "Generating analysis report..."
ANALYSIS_SCRIPT="/tmp/analyze_${TARGET}_$$.py"
cat > "${ANALYSIS_SCRIPT}" << 'PYSCRIPT'
#!/usr/bin/env python3
import json
import re
import sys
from collections import defaultdict
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
if len(sys.argv) < 4:
print("Usage: analyze.py <trace_file> <output_file> <target> <granularity> <build_time> <template_dir>")
sys.exit(1)
trace_file = sys.argv[1]
output_file = sys.argv[2]
target = sys.argv[3]
granularity = sys.argv[4]
build_time = sys.argv[5]
template_dir = sys.argv[6]
print(f'Loading trace file: {trace_file}')
with open(trace_file, 'r') as f:
data = json.load(f)
print('Processing events...')
template_stats = defaultdict(lambda: {'count': 0, 'total_dur': 0.0})
phase_stats = defaultdict(float)
top_individual = []
for event in data.get('traceEvents', []):
name = event.get('name', '')
dur = event.get('dur', 0) / 1000.0
if name and dur > 0:
phase_stats[name] += dur
if name in ['InstantiateFunction', 'InstantiateClass']:
detail = event.get('args', {}).get('detail', '')
top_individual.append({'detail': detail, 'dur': dur, 'type': name})
match = re.match(r'^([^<(]+)', detail)
if match:
template_name = match.group(1).strip()
template_name = re.sub(r'^ck::', '', template_name)
template_name = re.sub(r'^std::', 'std::', template_name)
template_stats[template_name]['count'] += 1
template_stats[template_name]['total_dur'] += dur
print('Sorting data...')
sorted_phases = sorted(phase_stats.items(), key=lambda x: x[1], reverse=True)
top_individual.sort(key=lambda x: x['dur'], reverse=True)
total_template_time = sum(s['total_dur'] for s in template_stats.values())
total_trace_time = sum(phase_stats.values())
total_events = len(data.get('traceEvents', []))
total_inst = sum(s['count'] for s in template_stats.values())
# Prepare templates by time with calculated fields
templates_by_time = []
for name, stats in sorted(template_stats.items(), key=lambda x: x[1]['total_dur'], reverse=True):
templates_by_time.append((name, {
'count': stats['count'],
'total_dur': stats['total_dur'],
'avg': stats['total_dur'] / stats['count'] if stats['count'] > 0 else 0,
'pct': 100 * stats['total_dur'] / total_template_time if total_template_time > 0 else 0
}))
# Prepare templates by count
templates_by_count = []
for name, stats in sorted(template_stats.items(), key=lambda x: x[1]['count'], reverse=True):
templates_by_count.append((name, {
'count': stats['count'],
'total_dur': stats['total_dur'],
'avg': stats['total_dur'] / stats['count'] if stats['count'] > 0 else 0
}))
# Prepare top individual instantiations with friendly type names
for inst in top_individual:
inst['inst_type'] = 'Func' if inst['type'] == 'InstantiateFunction' else 'Class'
# Calculate additional metrics
median_count = 0
if len(template_stats) > 0:
median_count = sorted([s["count"] for s in template_stats.values()])[len(template_stats)//2]
top10_pct = 0
if len(templates_by_time) >= 10:
top10_pct = 100 * sum(s[1]["total_dur"] for s in templates_by_time[:10]) / total_template_time
print('Rendering report with Jinja2...')
# Set up Jinja2 environment with custom filters
env = Environment(loader=FileSystemLoader(template_dir))
def format_number(value):
"""Format number with thousand separators"""
return f'{value:,}'
def truncate(value, length):
"""Truncate string to length with ellipsis"""
if len(value) > length:
return value[:length-3] + '...'
return value
def pad(value, length):
"""Pad string to specified length"""
return f'{value:<{length}}'
env.filters['format_number'] = format_number
env.filters['truncate'] = truncate
env.filters['pad'] = pad
# Load and render template
template = env.get_template('build_analysis_report.md.jinja')
report_content = template.render(
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
target=target,
granularity=granularity,
build_time=build_time,
trace_time_sec=f'{total_trace_time/1000:.1f}',
template_time_sec=f'{total_template_time/1000:.1f}',
template_pct=f'{100*total_template_time/total_trace_time:.1f}',
total_events=total_events,
total_instantiations=total_inst,
unique_families=len(template_stats),
total_trace_time=total_trace_time,
total_template_time=total_template_time,
phases=sorted_phases,
top_individual=top_individual,
templates_by_time=templates_by_time,
templates_by_count=templates_by_count,
median_count=median_count,
top10_pct=f'{top10_pct:.1f}'
)
with open(output_file, 'w') as f:
f.write(report_content)
print(f'Report generated: {output_file}')
print(f'Report size: {len(report_content)} bytes')
PYSCRIPT
# Copy analysis script and templates to container
docker cp "${ANALYSIS_SCRIPT}" "${CONTAINER_NAME}:/tmp/analyze.py"
docker cp "${SCRIPT_DIR}/analyze_build_trace.py" "${CONTAINER_NAME}:/tmp/analyze_build_trace.py"
docker cp "${SCRIPT_DIR}/templates" "${CONTAINER_NAME}:/tmp/ck_build_analysis_templates"
docker exec "${CONTAINER_NAME}" python3 /tmp/analyze.py \
# Run analysis
docker exec "${CONTAINER_NAME}" python3 /tmp/analyze_build_trace.py \
"${TRACE_FILE}" \
"/workspace/${OUTPUT_FILE}" \
"${TARGET}" \
@@ -334,8 +192,7 @@ docker exec "${CONTAINER_NAME}" python3 /tmp/analyze.py \
docker cp "${CONTAINER_NAME}:/workspace/${OUTPUT_FILE}" "${PROJECT_ROOT}/${OUTPUT_FILE}"
# Cleanup
rm -f "${ANALYSIS_SCRIPT}"
docker exec "${CONTAINER_NAME}" rm -f /tmp/analyze.py
docker exec "${CONTAINER_NAME}" rm -f /tmp/analyze_build_trace.py
docker exec "${CONTAINER_NAME}" rm -rf /tmp/ck_build_analysis_templates
echo ""

View File

@@ -110,3 +110,42 @@ The report includes:
- Default 500µs captures major bottlenecks (filters out 86% of instantiations)
- 1µs granularity reveals all 36,000+ instantiations but takes longer to analyze
- 100µs is a good middle ground for most use cases
## Implementation Details
### PEP 723 Compliance
The analysis script (`analyze_build_trace.py`) is PEP 723 compliant with inline dependency metadata:
```python
# /// script
# requires-python = ">=3.8"
# dependencies = [
# "jinja2>=3.0.0",
# ]
# ///
```
This allows tools like `pipx run` or `uv run` to automatically manage dependencies:
```bash
# Run standalone with pipx (auto-installs dependencies)
pipx run .claude/skills/analyze_build_trace.py trace.json report.md target 100 22 templates/
# Or with uv
uv run .claude/skills/analyze_build_trace.py trace.json report.md target 100 22 templates/
```
### Components
- **ck-build-analysis** - Main bash script that orchestrates Docker, CMake, and analysis
- **analyze_build_trace.py** - PEP 723 compliant Python script for trace analysis
- **templates/build_analysis_report.md.jinja** - Jinja2 template for report generation
### Requirements
In Docker container:
- `python3-jinja2` (installed via `apt-get install python3-jinja2`)
For standalone use:
- Python 3.8+ with `jinja2>=3.0.0` (auto-managed if using `pipx` or `uv`)