Add build trace visualization.

This commit is contained in:
John Shumway
2026-01-05 12:06:13 -05:00
parent 814b609476
commit d4321120f6
7 changed files with 2809 additions and 1243 deletions

View File

@@ -0,0 +1,960 @@
# Chrome Trace Export for Cross-Validation
**Status**: Design Document
**Author**: Build Analysis Team
**Date**: January 2026
**Version**: 1.0
## Executive Summary
This document proposes adding Chrome Trace Event Format export capabilities to the `analyze_build` library to enable cross-validation with the existing `ninja_json_converter.py` tool. The two tools serve complementary purposes and this enhancement will allow verification of data consistency between them.
## Background
### Current State: Two Complementary Tools
The project currently has two distinct build analysis tools:
#### 1. `ninja_json_converter.py` - Build System Monitoring
- **Purpose**: Monitor build-level parallelism and efficiency
- **Primary Users**: Build engineers, CI/CD optimization teams
- **Key Metrics**: Worker utilization, critical path, slow compilation units
- **Output Format**: Chrome Trace Event Format (JSON)
- **Granularity**: File-level (compilation units)
- **Visualization**: Perfetto / Chrome Tracing UI
- **Use Cases**:
- Is our build sharding efficient?
- Which files are compilation bottlenecks?
- How well are we utilizing available CPU cores?
- What's the critical path in our build?
#### 2. `analyze_build` Library - Compiler Performance Analysis
- **Purpose**: Deep analysis of C++ template metaprogramming costs
- **Primary Users**: C++ developers, library maintainers, performance engineers
- **Key Metrics**: Template instantiation times, template relationships, compiler event breakdown
- **Output Format**: Pandas DataFrames for statistical analysis
- **Granularity**: Template-level and compiler event-level (within compilation)
- **Visualization**: Jupyter notebooks with statistical analysis
- **Use Cases**:
- Which templates are most expensive to instantiate?
- What are the template dependency relationships?
- How can we optimize our metaprogramming patterns?
- How can we measure improved build times with better metaprogramming?
- What percentage of build time is template instantiation?
### The Problem: Need for Cross-Validation
Currently, these tools operate independently with no mechanism to verify consistency. This creates several challenges:
1. **Data Accuracy**: No way to verify both tools are parsing the same underlying data correctly
2. **Discrepancy Detection**: When numbers differ, unclear which tool is correct
3. **Cross-Referencing**: Difficult to correlate findings (e.g., "slow file in ninja" vs "high template time in analyzer")
4. **Debugging**: Hard to diagnose when tools report different build times
5. **Trust**: Users may question which tool's numbers to believe
## Goals and Non-Goals
### Primary Goals
1. **Enable Cross-Validation**: Export analyze_build data to Chrome Trace format for comparison with ninja_json_converter
2. **Verify Consistency**: Provide utilities to compare outputs and identify discrepancies
3. **Sanity Checking**: Quick visual verification in Perfetto that data looks correct
4. **Cross-Reference Findings**: Correlate slow files with expensive templates
### Secondary Goals
1. **Template Event Visualization**: Optionally export template instantiation events as additional trace layer
2. **Debugging Support**: Help diagnose when tools report different results
3. **Documentation**: Clear workflow for validation process
### Explicit Non-Goals
1. **Not Replacing ninja_json_converter**: The tools serve different purposes and both should continue to exist
2. **Not Full-Featured Visualization**: analyze_build focuses on statistical analysis, not interactive timelines
3. **Not Advanced Timeline Features**: Keep it simple - just export for validation
4. **Not Multi-Build Comparison**: ninja_json_converter already handles this well
## Technical Design
### Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ analyze_build Library │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ NinjaParser │─────▶│ builds_df │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ┌──────────────┐ ┌──────▼───────┐ │
│ │ TraceParser │─────▶│ events_df │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────────┐ │
│ │ ChromeTraceExporter│ │
│ └──────┬───────────┘ │
│ │ │
│ ┌──────▼───────────┐ │
│ │ trace_events │ │
│ │ (Chrome Format) │ │
│ └──────┬───────────┘ │
│ │ │
└───────────────────────────────┼──────────────────────────────┘
┌───────────▼────────────┐
│ Validation Utilities │
└───────────┬────────────┘
┌───────────▼────────────┐
│ ninja_json_converter │
│ output │
└────────────────────────┘
```
### New Module: `trace_analysis/chrome_trace.py`
```python
"""
Chrome Trace Event Format export for cross-validation.
Exports trace analysis data to Chrome Trace Event Format compatible
with ninja_json_converter.py output for validation purposes.
"""
from typing import Dict, List, Optional, Any
import pandas as pd
class ChromeTraceExporter:
"""Export trace analysis data to Chrome Trace Event Format."""
@staticmethod
def export_ninja_timeline(
builds_df: pd.DataFrame,
process_id: int = 1,
include_metadata: bool = True
) -> Dict[str, Any]:
"""
Export ninja build timeline to Chrome Trace format.
Creates trace events compatible with ninja_json_converter.py output
for cross-validation purposes.
Args:
builds_df: DataFrame with columns: target, start_ms, end_ms,
duration_ms, worker_id, (optional) category
process_id: Process ID for trace events (default: 1)
include_metadata: Include trace metadata (default: True)
Returns:
Dictionary in Chrome Trace Event Format:
{
'traceEvents': [...],
'displayTimeUnit': 'ms',
'otherData': {...}
}
Example:
>>> trace_data = ChromeTraceExporter.export_ninja_timeline(builds_df)
>>> with open('trace.json', 'w') as f:
... json.dump(trace_data, f)
"""
@staticmethod
def export_template_events(
instantiations_df: pd.DataFrame,
templates_df: pd.DataFrame,
builds_df: pd.DataFrame,
process_id: int = 1,
granularity_us: int = 50000
) -> Dict[str, Any]:
"""
Export template instantiation events as Chrome Trace layer.
Creates template-level trace events that can be overlaid on the
ninja build timeline for detailed compiler analysis.
Args:
instantiations_df: Template instantiation events
templates_df: Template definitions
builds_df: Ninja builds (for timing alignment)
process_id: Process ID for trace events
granularity_us: Minimum duration threshold in microseconds
Returns:
Chrome Trace Event Format dictionary with template events
Note:
Template events are aligned with ninja build timing and
filtered by granularity threshold to reduce trace size.
"""
@staticmethod
def merge_traces(
ninja_trace: Dict[str, Any],
template_trace: Dict[str, Any]
) -> Dict[str, Any]:
"""
Merge ninja and template traces into single trace file.
Combines build-level and template-level events for unified
visualization in Perfetto.
Args:
ninja_trace: Ninja build timeline trace
template_trace: Template instantiation trace
Returns:
Merged trace with both event types
"""
```
### New Module: `trace_analysis/validation.py`
```python
"""
Validation utilities for cross-checking trace analysis tools.
Compares outputs from analyze_build and ninja_json_converter to
verify data consistency and identify discrepancies.
"""
from typing import Dict, List, Any, Optional
import pandas as pd
class TraceValidator:
"""Validate consistency between trace analysis tools."""
@staticmethod
def compare_traces(
analyzer_trace: Dict[str, Any],
ninja_converter_trace: Dict[str, Any],
tolerance_ms: float = 1.0
) -> Dict[str, Any]:
"""
Compare Chrome Trace outputs from both tools.
Validates that analyze_build and ninja_json_converter produce
consistent results from the same underlying data.
Args:
analyzer_trace: Trace from ChromeTraceExporter
ninja_converter_trace: Trace from ninja_json_converter.py
tolerance_ms: Acceptable time difference in milliseconds
Returns:
Validation report:
{
'total_time_match': bool,
'total_time_diff_ms': float,
'event_count_match': bool,
'event_count_diff': int,
'file_discrepancies': [
{
'file': str,
'analyzer_ms': float,
'ninja_ms': float,
'diff_ms': float,
'diff_pct': float
}
],
'summary': str
}
"""
@staticmethod
def validate_ninja_log_parsing(
builds_df: pd.DataFrame,
ninja_log_path: str
) -> Dict[str, Any]:
"""
Validate that NinjaLogParser correctly parsed .ninja_log.
Cross-checks parsed DataFrame against raw .ninja_log file
to ensure no data loss or corruption.
Args:
builds_df: Parsed builds DataFrame
ninja_log_path: Path to original .ninja_log file
Returns:
Validation report with any parsing issues
"""
@staticmethod
def generate_validation_report(
validation_results: Dict[str, Any],
output_path: Optional[str] = None
) -> str:
"""
Generate human-readable validation report.
Creates formatted report of validation results for review.
Args:
validation_results: Results from compare_traces()
output_path: Optional path to save report
Returns:
Formatted report string
"""
```
### Data Flow
```
1. Parse .ninja_log
└─> NinjaLogParser.parse() -> builds_df
2. Export to Chrome Trace
└─> ChromeTraceExporter.export_ninja_timeline(builds_df) -> trace_data
3. Save trace file
└─> json.dump(trace_data, 'analyzer_trace.json')
4. Generate ninja_json_converter trace (separately)
└─> python ninja_json_converter.py .ninja_log -o ninja_trace.json
5. Validate consistency
└─> TraceValidator.compare_traces(analyzer_trace, ninja_trace) -> report
6. Review discrepancies
└─> TraceValidator.generate_validation_report(report)
```
## Chrome Trace Event Format Specification
### Event Structure
Each trace event follows the Chrome Trace Event Format:
```json
{
"name": "target_name.o",
"cat": "compile",
"ph": "X",
"ts": 1234567890,
"dur": 5000000,
"pid": 1,
"tid": 3,
"args": {
"output": "target_name.o",
"duration_ms": 5000,
"cmd_hash": "abc123"
}
}
```
**Field Descriptions:**
- `name`: Target name (file being built)
- `cat`: Category (compile, link_shared, link_executable, archive, test, other)
- `ph`: Phase ("X" for complete events)
- `ts`: Timestamp in microseconds
- `dur`: Duration in microseconds
- `pid`: Process ID (1 for ninja builds)
- `tid`: Thread ID (worker ID)
- `args`: Additional metadata
### Trace File Structure
```json
{
"traceEvents": [
{ /* event 1 */ },
{ /* event 2 */ },
...
],
"displayTimeUnit": "ms",
"otherData": {
"version": "1.0",
"generator": "trace_analysis",
"source": "analyze_build"
}
}
```
### Compatibility with ninja_json_converter
The export format must be **byte-for-byte compatible** with ninja_json_converter output for the same input data, with these exceptions:
**Acceptable Differences:**
- `otherData.generator`: Different tool name
- Event ordering: May differ if timestamps are identical
- Floating point precision: ±0.001ms acceptable
**Must Match Exactly:**
- Total build time
- Per-file durations (within tolerance)
- Worker assignments
- Event counts
- Category assignments
## Validation Strategy
### Validation Checks
1. **Total Build Time**
- Sum of all event durations
- Should match within ±1ms (rounding tolerance)
2. **Event Count**
- Number of trace events
- Should match exactly
3. **Per-File Duration**
- Duration for each compilation unit
- Should match within ±1ms per file
4. **Worker Assignment**
- Thread ID (worker) for each event
- Should match exactly (deterministic algorithm)
5. **Category Assignment**
- Event category based on file extension
- Should match exactly
### Expected Discrepancies
Some differences are expected and acceptable:
1. **Timestamp Precision**: Microsecond rounding differences
2. **Event Ordering**: When timestamps are identical
3. **Metadata Fields**: Different tool names, versions
4. **Floating Point**: Minor precision differences (< 0.001ms)
### Validation Workflow
```python
# 1. Generate trace from analyze_build
from trace_analysis import NinjaLogParser, ChromeTraceExporter
import json
builds = NinjaLogParser.parse(Path('.ninja_log'))
builds_df = NinjaLogParser.to_dataframe(builds)
analyzer_trace = ChromeTraceExporter.export_ninja_timeline(builds_df)
with open('analyzer_trace.json', 'w') as f:
json.dump(analyzer_trace, f)
# 2. Generate trace from ninja_json_converter (shell)
# $ python script/ninja_json_converter.py .ninja_log -o ninja_trace.json
# 3. Load both traces
with open('ninja_trace.json') as f:
ninja_trace = json.load(f)
# 4. Validate
from trace_analysis import TraceValidator
report = TraceValidator.compare_traces(analyzer_trace, ninja_trace)
# 5. Review results
print(TraceValidator.generate_validation_report(report))
```
### Validation Report Format
```
=== Trace Validation Report ===
Overall Status: PASS / FAIL
Build Statistics:
Total Events: 1,234 (analyzer) vs 1,234 (ninja) ✓
Total Time: 123.456s (analyzer) vs 123.457s (ninja) ✓ (diff: 0.001s)
Worker Assignment:
Match Rate: 100% (1,234/1,234 events) ✓
Per-File Duration:
Files Checked: 1,234
Exact Matches: 1,230 (99.7%)
Within Tolerance: 4 (0.3%)
Outside Tolerance: 0 (0.0%) ✓
Discrepancies:
file1.o: 1234ms (analyzer) vs 1235ms (ninja) - diff: 1ms (0.08%)
file2.o: 5678ms (analyzer) vs 5677ms (ninja) - diff: 1ms (0.02%)
Conclusion: Tools are consistent within acceptable tolerance.
```
## Implementation Plan
### Phase 1: Basic Export (Week 1)
**Deliverables:**
- `trace_analysis/chrome_trace.py` with `export_ninja_timeline()`
- Unit tests for Chrome Trace format
- Integration test comparing with ninja_json_converter
**Tasks:**
- [ ] Implement ChromeTraceExporter class
- [ ] Add event categorization logic
- [ ] Write unit tests for event generation
- [ ] Test with sample .ninja_log files
- [ ] Verify format matches ninja_json_converter exactly
**Success Criteria:**
- Exports valid Chrome Trace JSON
- Loads correctly in Perfetto
- Matches ninja_json_converter output for same input
### Phase 2: Validation Utilities (Week 1-2)
**Deliverables:**
- `trace_analysis/validation.py` with comparison utilities
- Validation report generator
- Documentation of validation workflow
**Tasks:**
- [ ] Implement TraceValidator class
- [ ] Add comparison algorithms
- [ ] Create validation report formatter
- [ ] Write tests for validation logic
- [ ] Document expected discrepancies
**Success Criteria:**
- Accurately identifies discrepancies
- Generates clear validation reports
- Handles edge cases gracefully
### Phase 3: Template Event Export (Week 2)
**Deliverables:**
- Template event export in `chrome_trace.py`
- Merged trace generation
- Examples in notebook
**Tasks:**
- [ ] Implement `export_template_events()`
- [ ] Add timing alignment logic
- [ ] Implement granularity filtering
- [ ] Add merge functionality
- [ ] Test with real -ftime-trace data
**Success Criteria:**
- Template events align with ninja timeline
- Granularity filtering works correctly
- Merged traces load in Perfetto
### Phase 4: Documentation & Examples (Week 2-3)
**Deliverables:**
- Updated README with validation workflow
- Notebook section demonstrating export
- API documentation
- Validation guide
**Tasks:**
- [ ] Add notebook section for Chrome Trace export
- [ ] Document validation workflow
- [ ] Create troubleshooting guide
- [ ] Add API documentation
- [ ] Write migration guide for ninja_json_converter users
**Success Criteria:**
- Clear documentation of validation process
- Working examples in notebook
- Users can successfully validate traces
## Testing Strategy
### Unit Tests
```python
# test_chrome_trace.py
def test_export_ninja_timeline_format():
"""Verify Chrome Trace format is valid."""
def test_export_ninja_timeline_compatibility():
"""Verify compatibility with ninja_json_converter."""
def test_event_categorization():
"""Verify file extension -> category mapping."""
def test_worker_assignment():
"""Verify worker IDs match ninja_json_converter."""
```
### Integration Tests
```python
# test_validation.py
def test_compare_identical_traces():
"""Validation passes for identical traces."""
def test_detect_discrepancies():
"""Validation detects timing differences."""
def test_tolerance_handling():
"""Small differences within tolerance pass."""
```
### Validation Tests
```python
# test_cross_validation.py
def test_real_ninja_log():
"""Compare with actual ninja_json_converter output."""
def test_large_build():
"""Handle large builds (1000+ files)."""
def test_incremental_build():
"""Handle incremental build scenarios."""
```
## Usage Examples
### Basic Export
```python
from pathlib import Path
from trace_analysis import NinjaLogParser, ChromeTraceExporter
import json
# Parse ninja log
builds = NinjaLogParser.parse(Path('build/.ninja_log'))
builds_df = NinjaLogParser.to_dataframe(builds)
# Export to Chrome Trace
trace_data = ChromeTraceExporter.export_ninja_timeline(builds_df)
# Save for Perfetto
with open('build_trace.json', 'w') as f:
json.dump(trace_data, f)
print("Open build_trace.json in chrome://tracing or https://ui.perfetto.dev")
```
### Cross-Validation
```python
from trace_analysis import ChromeTraceExporter, TraceValidator
import json
import subprocess
# Generate trace from analyze_build
analyzer_trace = ChromeTraceExporter.export_ninja_timeline(builds_df)
# Generate trace from ninja_json_converter
subprocess.run([
'python', 'script/ninja_json_converter.py',
'build/.ninja_log',
'-o', 'ninja_trace.json'
])
# Load ninja_json_converter output
with open('ninja_trace.json') as f:
ninja_trace = json.load(f)
# Validate
report = TraceValidator.compare_traces(analyzer_trace, ninja_trace)
# Print report
print(TraceValidator.generate_validation_report(report))
# Check if validation passed
if report['total_time_match'] and report['event_count_match']:
print("✓ Validation PASSED - Tools are consistent")
else:
print("✗ Validation FAILED - Discrepancies found")
for disc in report['file_discrepancies']:
print(f" {disc['file']}: {disc['diff_ms']}ms difference")
```
### Template Event Export
```python
from trace_analysis import (
TraceParser, TraceTransformer,
ChromeTraceExporter, find_trace_files
)
# Parse -ftime-trace files
trace_files = find_trace_files(Path('build'))
all_events = []
all_instantiations = []
for trace_file in trace_files:
events = TraceParser.parse(trace_file)
schema = TraceTransformer.to_enhanced_schema(events, file_id=0)
all_instantiations.append(schema['instantiations'])
instantiations_df = pd.concat(all_instantiations, ignore_index=True)
# Export template events
template_trace = ChromeTraceExporter.export_template_events(
instantiations_df,
templates_df,
builds_df,
granularity_us=50000 # Only events > 50ms
)
# Merge with ninja timeline
merged_trace = ChromeTraceExporter.merge_traces(
ninja_trace,
template_trace
)
# Save merged trace
with open('merged_trace.json', 'w') as f:
json.dump(merged_trace, f)
```
### Notebook Integration
```python
# In comprehensive_example.ipynb
## Chrome Trace Export for Validation
# Export ninja timeline
from trace_analysis import ChromeTraceExporter
import json
trace_data = ChromeTraceExporter.export_ninja_timeline(builds_df)
# Save trace
with open('../data/analyzer_trace.json', 'w') as f:
json.dump(trace_data, f, indent=2)
print(f"Exported {len(trace_data['traceEvents'])} events")
print(f"Total build time: {sum(e['dur'] for e in trace_data['traceEvents']) / 1e6:.2f}s")
# Validate against ninja_json_converter
# (Assuming ninja_trace.json was generated separately)
with open('../data/ninja_trace.json') as f:
ninja_trace = json.load(f)
from trace_analysis import TraceValidator
report = TraceValidator.compare_traces(trace_data, ninja_trace)
print(TraceValidator.generate_validation_report(report))
```
## Open Questions
### Critical Questions
1. **Data Consistency**
- Q: Do you currently see discrepancies between the tools?
- Q: What tolerance is acceptable? (±1ms suggested)
- Q: Are there known sources of differences?
2. **Validation Workflow**
- Q: How often do you need to cross-validate?
- Q: Should this be automated in CI?
- Q: What triggers a validation run?
3. **Template Event Export**
- Q: Should template events be in same file as ninja events?
- Q: Or separate files for different analysis?
- Q: Priority: High, Medium, or Low?
### Technical Questions
4. **Output Format**
- Q: Must we match ninja_json_converter format exactly?
- Q: Or can we use enhanced format with metadata?
- Q: Is backward compatibility required?
5. **Performance**
- Q: What's the largest build to support?
- Q: Number of targets? (hundreds, thousands, tens of thousands?)
- Q: Should we implement sampling for huge builds?
## Success Metrics
### Functional Metrics
- ✅ Exports valid Chrome Trace JSON
- ✅ Loads correctly in Perfetto
- ✅ Matches ninja_json_converter output (within tolerance)
- ✅ Validation detects discrepancies accurately
- ✅ Clear validation reports
### Quality Metrics
- ✅ 100% unit test coverage for new modules
- ✅ Integration tests with real data pass
- ✅ Documentation complete and clear
- ✅ Examples work in notebook
### Performance Metrics
- ✅ Export completes in < 1s for 1000 files
- ✅ Validation completes in < 5s for 1000 files
- ✅ Memory usage < 100MB for typical builds
## Future Enhancements
### Potential Phase 2 Features
1. **Automated Validation in CI**
- Run validation on every build
- Fail CI if discrepancies exceed threshold
- Track validation metrics over time
2. **Differential Analysis**
- Compare traces from different builds
- Identify performance regressions
- Track optimization progress
3. **Enhanced Visualization**
- Plotly timeline charts in notebooks
- Interactive exploration of discrepancies
- Side-by-side comparison views
4. **Template Optimization Recommendations**
- Correlate slow files with expensive templates
- Suggest optimization targets
- Estimate potential improvements
## References
- [Chrome Trace Event Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview)
- [Perfetto UI](https://ui.perfetto.dev)
- [Clang -ftime-trace Documentation](https://releases.llvm.org/11.0.0/tools/clang/docs/ClangCommandLineReference.html#cmdoption-clang-ftime-trace)
- [Ninja Build System](https://ninja-build.org/)
## Appendix A: Chrome Trace Event Format Details
### Complete Event Structure
```json
{
"name": "event_name",
"cat": "category",
"ph": "X",
"ts": 1234567890,
"dur": 5000000,
"pid": 1,
"tid": 3,
"args": {
"custom_field": "value"
}
}
```
### Phase Types
- `X`: Complete event (has duration)
- `B`: Begin event
- `E`: End event
- `i`: Instant event
- `M`: Metadata event
For build traces, we use `X` (complete events) exclusively.
### Category Conventions
Standard categories for build events:
- `compile`: Compilation of source files (.o, .obj)
- `link_shared`: Shared library linking (.so, .dll, .dylib)
- `link_executable`: Executable linking (.exe, .out)
- `archive`: Static library creation (.a, .lib)
- `test`: Test execution
- `other`: Other build steps
## Appendix B: Validation Algorithm
### Comparison Algorithm
```python
def compare_events(event1, event2, tolerance_ms=1.0):
"""Compare two trace events for equivalence."""
# Must match exactly
if event1['name'] != event2['name']:
return False, "Name mismatch"
if event1['tid'] != event2['tid']:
return False, "Worker ID mismatch"
if event1['cat'] != event2['cat']:
return False, "Category mismatch"
# Must match within tolerance
dur1_ms = event1['dur'] / 1000
dur2_ms = event2['dur'] / 1000
diff_ms = abs(dur1_ms - dur2_ms)
if diff_ms > tolerance_ms:
return False, f"Duration mismatch: {diff_ms}ms"
return True, "Match"
```
### Discrepancy Categorization
**Critical**: Must be fixed
- Total time difference > 1%
- Event count mismatch
- Worker assignment errors
**Warning**: Should investigate
- Per-file duration > 1ms difference
- Category mismatches
- Timestamp ordering issues
**Info**: Acceptable
- Floating point precision differences
- Metadata differences
- Event ordering when timestamps identical
## Appendix C: Migration Guide
### For ninja_json_converter Users
If you currently use `ninja_json_converter.py`, you can continue to do so. The new Chrome Trace export in `analyze_build` is complementary, not a replacement.
**When to use ninja_json_converter:**
- Quick build timeline visualization
- Build system optimization
- CI/CD monitoring
- Multi-build comparison
**When to use analyze_build Chrome Trace export:**
- Cross-validation with template analysis
- Verifying data consistency
- Debugging discrepancies
- Correlating build and template metrics
**Using both together:**
```bash
# Generate trace from ninja_json_converter
python script/ninja_json_converter.py build/.ninja_log -o ninja_trace.json
# Generate trace from analyze_build
python -c "
from pathlib import Path
from trace_analysis import NinjaLogParser, ChromeTraceExporter
import json
builds = NinjaLogParser.parse(Path('build/.ninja_log'))
builds_df = NinjaLogParser.to_dataframe(builds)
trace = ChromeTraceExporter.export_ninja_timeline(builds_df)
with open('analyzer_trace.json', 'w') as f:
json.dump(trace, f)
"
# Compare
python -c "
from trace_analysis import TraceValidator
import json
with open('ninja_trace.json') as f:
ninja = json.load(f)
with open('analyzer_trace.json') as f:
analyzer = json.load(f)
report = TraceValidator.compare_traces(analyzer, ninja)
print(TraceValidator.generate_validation_report(report))
"

View File

@@ -0,0 +1,231 @@
# Perfetto Visualization Guide
This guide shows how to visualize ninja build timelines in Perfetto UI using the `trace_analysis` library.
## Quick Start
### Command Line Usage
```bash
# Run the example script
python examples/perfetto_visualization_example.py path/to/.ninja_log
# This will:
# 1. Parse the ninja log
# 2. Assign workers for parallelism visualization
# 3. Export to Chrome Trace format
# 4. Save to build_trace.json
```
### Jupyter Notebook Usage
```python
from pathlib import Path
from trace_analysis import NinjaLogParser, ChromeTraceExporter
from trace_analysis.perfetto_display import display_perfetto, print_trace_summary
# Parse ninja log
builds = NinjaLogParser.parse(Path('build/.ninja_log'))
builds_df = NinjaLogParser.to_dataframe(builds)
builds_df = NinjaLogParser.assign_workers(builds_df)
# Export to Chrome Trace format
trace_data = ChromeTraceExporter.export_ninja_timeline(builds_df)
# Print summary
print_trace_summary(trace_data)
# Display in Perfetto UI (embedded in notebook)
display_perfetto(trace_data)
# Or save to file for large traces
from trace_analysis.perfetto_display import save_and_link
save_and_link(trace_data, '../data/build_trace.json')
```
## What You Get
The Chrome Trace export provides:
- **Build Timeline**: Visual representation of when each target was built
- **Parallelism Analysis**: See how many workers were active at any time
- **Category Breakdown**: Targets categorized by type (compile, link, archive, etc.)
- **Duration Analysis**: Identify slow compilation units
- **Critical Path**: Understand build dependencies and bottlenecks
## Viewing in Perfetto UI
### Option 1: Embedded in Jupyter (Small Traces)
For traces < 10MB, use `display_perfetto()` to embed directly in the notebook:
```python
display_perfetto(trace_data, height=600)
```
### Option 2: Manual Upload (Large Traces)
For larger traces, save to file and upload manually:
```python
ChromeTraceExporter.export_to_file(trace_data, 'build_trace.json')
```
Then:
1. Go to https://ui.perfetto.dev
2. Click "Open trace file"
3. Select your `build_trace.json`
Or drag and drop the file directly into Perfetto UI.
## DataFrame Schema
The `builds_df` DataFrame has the following columns:
| Column | Type | Description |
|--------|------|-------------|
| `target` | str | Build target name (e.g., "obj/foo.o") |
| `start_ms` | int64 | Start time in milliseconds since epoch |
| `end_ms` | int64 | End time in milliseconds since epoch |
| `duration_ms` | int32 | Build duration in milliseconds |
| `cmd_hash` | str | Command hash from ninja |
| `worker_id` | int16 | Assigned worker ID (0-based) |
### Adding Category Column
The Chrome Trace exporter automatically categorizes targets based on file extension:
- `.o`, `.obj``compile`
- `.a`, `.lib``archive`
- `.so`, `.dll`, `.dylib``link_shared`
- `.exe`, `.out``link_executable`
- Contains "test" → `test`
- Everything else → `other`
## Chrome Trace Event Format
Each build target is exported as a Chrome Trace event:
```json
{
"name": "obj/foo.o",
"cat": "compile",
"ph": "X",
"ts": 1234567890000,
"dur": 5000000,
"pid": 1,
"tid": 3,
"args": {
"output": "obj/foo.o",
"duration_ms": 5000,
"cmd_hash": "abc123"
}
}
```
## Comparison with ninja_json_converter.py
The `trace_analysis` library provides similar functionality to `ninja_json_converter.py` but with additional features:
### Similarities
- Both parse `.ninja_log` files
- Both export to Chrome Trace Event Format
- Both can be viewed in Perfetto UI
### Differences
| Feature | ninja_json_converter.py | trace_analysis |
|---------|------------------------|----------------|
| **Primary Use** | Quick build visualization | Integrated analysis workflow |
| **Output** | Chrome Trace JSON only | DataFrames + Chrome Trace |
| **Analysis** | External (Perfetto UI) | In-notebook with pandas |
| **Template Data** | No | Yes (with -ftime-trace) |
| **Worker Assignment** | Built-in algorithm | Same algorithm, exposed as DataFrame |
| **Customization** | Command-line flags | Programmatic API |
### When to Use Each
**Use `ninja_json_converter.py` when:**
- You just want a quick visualization
- You're working from the command line
- You don't need further analysis
**Use `trace_analysis` when:**
- You want to analyze build data with pandas
- You're working in Jupyter notebooks
- You want to correlate build times with template analysis
- You need programmatic access to build data
## Examples
### Example 1: Find Slowest Builds
```python
# Get top 10 slowest builds
slowest = builds_df.nlargest(10, 'duration_ms')
print(slowest[['target', 'duration_ms', 'worker_id']])
```
### Example 2: Analyze Worker Utilization
```python
worker_stats = NinjaLogParser.compute_worker_stats(builds_df)
print(worker_stats)
```
### Example 3: Category Breakdown
```python
from trace_analysis.perfetto_display import get_trace_summary
summary = get_trace_summary(trace_data)
print(f"Total events: {summary['event_count']}")
print(f"Total duration: {summary['total_duration_s']:.2f}s")
print(f"Workers: {summary['worker_count']}")
print("\nBy category:")
for cat, count in summary['categories'].items():
print(f" {cat}: {count} events")
```
### Example 4: Export with Custom Process ID
```python
# Useful when combining multiple build logs
trace_data = ChromeTraceExporter.export_ninja_timeline(
builds_df,
process_id=2, # Use different PID for each log
include_metadata=True
)
```
## Troubleshooting
### Issue: Trace file too large for embedded display
**Solution**: Use `save_and_link()` instead of `display_perfetto()`:
```python
save_and_link(trace_data, 'build_trace.json')
```
### Issue: Worker IDs all show as -1
**Solution**: Make sure to call `assign_workers()`:
```python
builds_df = NinjaLogParser.assign_workers(builds_df)
```
### Issue: Import error for perfetto_display
**Solution**: The perfetto display functions are in a separate module:
```python
from trace_analysis.perfetto_display import display_perfetto
```
## See Also
- [CHROME_TRACE_EXPORT.md](CHROME_TRACE_EXPORT.md) - Full design document
- [comprehensive_example.ipynb](../notebooks/comprehensive_example.ipynb) - Complete analysis workflow
- [ninja_json_converter.py](../../ninja_json_converter.py) - Command-line alternative

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""
Example: Visualizing Build Timeline in Perfetto UI
This example demonstrates how to:
1. Parse a ninja .ninja_log file
2. Export to Chrome Trace format
3. Display in Perfetto UI (for Jupyter notebooks)
4. Save to file for manual upload
Usage:
python perfetto_visualization_example.py path/to/.ninja_log
"""
import sys
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from trace_analysis import NinjaLogParser, ChromeTraceExporter
from trace_analysis.perfetto_display import (
print_trace_summary,
)
def main():
"""Main example function."""
if len(sys.argv) < 2:
print("Usage: python perfetto_visualization_example.py path/to/.ninja_log")
sys.exit(1)
ninja_log_path = Path(sys.argv[1])
if not ninja_log_path.exists():
print(f"Error: {ninja_log_path} not found")
sys.exit(1)
print(f"Parsing {ninja_log_path}...")
# Step 1: Parse ninja log
builds = NinjaLogParser.parse(ninja_log_path)
builds_df = NinjaLogParser.to_dataframe(builds)
print(f"Found {len(builds_df):,} build targets")
# Step 2: Assign workers (for parallelism visualization)
builds_df = NinjaLogParser.assign_workers(builds_df)
print(f"Assigned {builds_df['worker_id'].max() + 1} workers")
# Step 3: Export to Chrome Trace format
trace_data = ChromeTraceExporter.export_ninja_timeline(builds_df)
print(f"\nGenerated {len(trace_data['traceEvents']):,} trace events")
# Step 4: Print summary
print_trace_summary(trace_data)
# Step 5: Save to file
output_path = ninja_log_path.parent / "build_trace.json"
ChromeTraceExporter.export_to_file(trace_data, str(output_path))
print(f"\n✓ Trace saved to: {output_path}")
print("\nTo view in Perfetto UI:")
print(" 1. Go to https://ui.perfetto.dev")
print(" 2. Click 'Open trace file'")
print(f" 3. Select: {output_path}")
print("\nOr drag and drop the file directly into Perfetto UI")
# For Jupyter notebook usage, you would use:
# display_perfetto(trace_data)
# or for large traces:
# save_and_link(trace_data, str(output_path))
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,7 @@ from .parser import TraceParser
from .transformer import TraceTransformer
from .template_parser import TemplateParser
from .ninja_parser import NinjaLogParser
from .chrome_trace import ChromeTraceExporter
from .utils import find_trace_files
__all__ = [
@@ -87,6 +88,8 @@ __all__ = [
"NinjaLogParser",
"NinjaBuild",
"CompilationTimeline",
# Chrome Trace export
"ChromeTraceExporter",
# Metadata and statistics
"FileMetadata",
"BuildStatistics",

View File

@@ -0,0 +1,133 @@
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""
Chrome Trace Event Format export for Perfetto visualization.
Exports ninja build timeline data to Chrome Trace Event Format
for visualization in Perfetto UI within Jupyter notebooks.
"""
from pathlib import Path
from typing import Dict, Any
import pandas as pd
class ChromeTraceExporter:
"""Export trace analysis data to Chrome Trace Event Format."""
@staticmethod
def categorize_target(target: str) -> str:
"""
Categorize a build target based on file extension.
Args:
target: Build target name (e.g., "obj/foo.o")
Returns:
Category string for Chrome Trace format
"""
ext = Path(target).suffix.lower()
if ext in [".o", ".obj"]:
return "compile"
elif ext in [".a", ".lib"]:
return "archive"
elif ext in [".so", ".dll", ".dylib"]:
return "link_shared"
elif ext in [".exe", ".out"]:
return "link_executable"
elif "test" in target.lower():
return "test"
else:
return "other"
@staticmethod
def export_ninja_timeline(
builds_df: pd.DataFrame, process_id: int = 1, include_metadata: bool = True
) -> Dict[str, Any]:
"""
Export ninja build timeline to Chrome Trace format.
Creates trace events compatible with Perfetto UI for visualization
in Jupyter notebooks or chrome://tracing.
Args:
builds_df: DataFrame with columns: target, start_ms, end_ms,
duration_ms, worker_id, cmd_hash
process_id: Process ID for trace events (default: 1)
include_metadata: Include trace metadata (default: True)
Returns:
Dictionary in Chrome Trace Event Format:
{
'traceEvents': [...],
'displayTimeUnit': 'ms',
'otherData': {...}
}
Example:
>>> from trace_analysis import NinjaLogParser, ChromeTraceExporter
>>> builds = NinjaLogParser.parse(Path('build/.ninja_log'))
>>> builds_df = NinjaLogParser.to_dataframe(builds)
>>> builds_df = NinjaLogParser.assign_workers(builds_df)
>>> trace_data = ChromeTraceExporter.export_ninja_timeline(builds_df)
>>> # Display in notebook or save to file
"""
if len(builds_df) == 0:
return {
"traceEvents": [],
"displayTimeUnit": "ms",
"otherData": {"version": "1.0", "generator": "analyze_build"},
}
events = []
for _, row in builds_df.iterrows():
# Categorize based on file extension
category = ChromeTraceExporter.categorize_target(row["target"])
# Create Chrome Trace event
event = {
"name": row["target"],
"cat": category,
"ph": "X", # Complete event (has duration)
"ts": int(row["start_ms"] * 1000), # Convert to microseconds
"dur": int(row["duration_ms"] * 1000), # Convert to microseconds
"pid": process_id,
"tid": int(row["worker_id"]),
"args": {
"output": row["target"],
"duration_ms": int(row["duration_ms"]),
"cmd_hash": row["cmd_hash"],
},
}
events.append(event)
if include_metadata:
return {
"traceEvents": events,
"displayTimeUnit": "ms",
"otherData": {"version": "1.0", "generator": "analyze_build"},
}
else:
# Simple format (just events array)
return {"traceEvents": events}
@staticmethod
def export_to_file(trace_data: Dict[str, Any], output_path: str) -> None:
"""
Export trace data to a JSON file.
Args:
trace_data: Chrome Trace format dictionary
output_path: Path to output file
Example:
>>> ChromeTraceExporter.export_to_file(trace_data, 'trace.json')
"""
import json
with open(output_path, "w") as f:
json.dump(trace_data, f, indent=2)

View File

@@ -0,0 +1,190 @@
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""
Perfetto UI display utilities for Jupyter notebooks.
Provides functions to display Chrome Trace data in Perfetto UI
directly within Jupyter notebooks.
"""
import json
import base64
from typing import Dict, Any, Optional
def display_perfetto(trace_data: Dict[str, Any], height: int = 600):
"""
Display Perfetto UI in Jupyter notebook with embedded trace data.
Args:
trace_data: Chrome Trace Event Format dictionary
height: Height of the IFrame in pixels (default: 600)
Returns:
IPython IFrame object for display in notebook
Example:
>>> from trace_analysis import NinjaLogParser, ChromeTraceExporter
>>> from trace_analysis.perfetto_display import display_perfetto
>>> builds_df = NinjaLogParser.to_dataframe(builds)
>>> trace_data = ChromeTraceExporter.export_ninja_timeline(builds_df)
>>> display_perfetto(trace_data)
Note:
This function requires IPython to be installed (available in Jupyter).
The trace data is base64-encoded and embedded in the Perfetto UI URL.
For very large traces (>10MB), consider using save_and_link() instead.
"""
try:
from IPython.display import IFrame
except ImportError:
raise ImportError(
"IPython is required for display_perfetto(). "
"Install it with: pip install ipython"
)
# Convert trace to JSON string
trace_json = json.dumps(trace_data)
# Base64 encode for URL
trace_b64 = base64.b64encode(trace_json.encode()).decode()
# Perfetto UI URL with embedded trace
perfetto_url = f"https://ui.perfetto.dev/#!/?s={trace_b64}"
# Display in IFrame
return IFrame(perfetto_url, width="100%", height=height)
def save_and_link(
trace_data: Dict[str, Any], output_path: str, link_text: Optional[str] = None
):
"""
Save trace to file and display a link to open in Perfetto UI.
This is useful for large traces that are too big to embed in a URL.
Args:
trace_data: Chrome Trace Event Format dictionary
output_path: Path to save the trace file
link_text: Custom link text (default: "Open trace in Perfetto UI")
Returns:
IPython HTML object with download link and instructions
Example:
>>> save_and_link(trace_data, '../data/build_trace.json')
Note:
The user will need to manually upload the saved file to
https://ui.perfetto.dev
"""
try:
from IPython.display import HTML
except ImportError:
raise ImportError(
"IPython is required for save_and_link(). "
"Install it with: pip install ipython"
)
# Save trace to file
with open(output_path, "w") as f:
json.dump(trace_data, f, indent=2)
if link_text is None:
link_text = "Open trace in Perfetto UI"
# Create HTML with instructions
html = f"""
<div style="padding: 10px; border: 1px solid #ddd; border-radius: 5px; background-color: #f9f9f9;">
<h4>Trace saved to: <code>{output_path}</code></h4>
<p>To view in Perfetto UI:</p>
<ol>
<li>Go to <a href="https://ui.perfetto.dev" target="_blank">{link_text}</a></li>
<li>Click "Open trace file" and select: <code>{output_path}</code></li>
</ol>
<p><em>Or drag and drop the file directly into the Perfetto UI.</em></p>
</div>
"""
return HTML(html)
def get_trace_summary(trace_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Get summary statistics from trace data.
Args:
trace_data: Chrome Trace Event Format dictionary
Returns:
Dictionary with summary statistics
Example:
>>> summary = get_trace_summary(trace_data)
>>> print(f"Total events: {summary['event_count']}")
>>> print(f"Total time: {summary['total_duration_s']:.2f}s")
"""
events = trace_data.get("traceEvents", [])
if not events:
return {
"event_count": 0,
"total_duration_s": 0.0,
"categories": {},
"worker_count": 0,
}
# Count events by category
categories = {}
total_duration_us = 0
worker_ids = set()
for event in events:
cat = event.get("cat", "unknown")
categories[cat] = categories.get(cat, 0) + 1
dur = event.get("dur", 0)
total_duration_us += dur
tid = event.get("tid")
if tid is not None:
worker_ids.add(tid)
return {
"event_count": len(events),
"total_duration_s": total_duration_us / 1e6,
"categories": categories,
"worker_count": len(worker_ids),
}
def print_trace_summary(trace_data: Dict[str, Any]) -> None:
"""
Print a formatted summary of trace data.
Args:
trace_data: Chrome Trace Event Format dictionary
Example:
>>> print_trace_summary(trace_data)
=== Trace Summary ===
Total events: 1,234
Total duration: 123.45s
Workers: 8
...
"""
summary = get_trace_summary(trace_data)
print("=== Trace Summary ===")
print(f"Total events: {summary['event_count']:,}")
print(f"Total duration: {summary['total_duration_s']:.2f}s")
print(f"Workers: {summary['worker_count']}")
if summary["categories"]:
print("\nEvents by category:")
for cat, count in sorted(
summary["categories"].items(), key=lambda x: x[1], reverse=True
):
print(f" {cat:15} {count:6,} events")