
Performance Profiling
- 17 installs
- 869 repo stars
- Updated June 8, 2026
- beita6969/scienceclaw
performance-profiling is a skill that analyzes computational simulation timing, scaling, and memory to identify bottlenecks and recommend optimizations.
About
This skill analyzes computational simulation performance to find bottlenecks and recommend optimizations. A developer uses it when simulations are slow or when investigating parallel efficiency and memory needs. It ships four scripts for timing analysis, scaling studies, memory profiling, and bottleneck detection, each emitting JSON, with interpretation tables for the results.
- Bundled scripts for timing, scaling, memory profiling, and bottleneck detection
- Threshold tables for phase dominance, parallel efficiency, and memory usage
- Uses only the Python standard library; runs on Linux, macOS, and Windows
Performance Profiling by the numbers
- 17 all-time installs (skills.sh)
- Ranked #407 of 597 Debugging skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
performance-profiling capabilities & compatibility
- Capabilities
- timing analysis · scaling analysis · memory profiling · bottleneck detection
- Use cases
- debugging
- Platforms
- Linux · macOS · Windows
- Pricing
- Free
What performance-profiling says it does
Identify computational bottlenecks, analyze scaling behavior, estimate memory requirements, and receive optimization recommendations for any computational simulation.
No external dependencies (uses Python standard library only)
Works on Linux, macOS, and Windows
npx skills add https://github.com/beita6969/scienceclaw --skill performance-profilingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 869 |
| Last updated | June 8, 2026 |
| Repository | beita6969/scienceclaw ↗ |
What it does
Profile simulation timing, scaling, and memory to identify bottlenecks and recommend optimizations.
Who is it for?
Developers diagnosing slow simulations and planning compute resource allocation
Skip if: Profiling web apps or general application code; it targets computational simulations
When should I use this skill?
Simulations are slow, or the user investigates parallel efficiency, memory needs, or bottlenecks
What you get
JSON reports of slow phases, scaling efficiency, memory estimates, and actionable optimization recommendations.
- timing analysis JSON
- scaling efficiency report
- memory profile
By the numbers
- 4 profiling scripts
- 3 threshold metric tables
Files
Performance Profiling
Goal
Provide tools to analyze simulation performance, identify bottlenecks, and recommend optimization strategies for computational materials science simulations.
Requirements
- Python 3.8+
- No external dependencies (uses Python standard library only)
- Works on Linux, macOS, and Windows
Inputs to Gather
Before running profiling scripts, collect from the user:
| Input | Description | Example |
|---|---|---|
| Simulation log | Log file with timing information | simulation.log |
| Scaling data | JSON with multi-run performance data | scaling_data.json |
| Simulation parameters | JSON with mesh, fields, solver config | params.json |
| Available memory | System memory in GB (optional) | 16.0 |
Decision Guidance
When to Use Each Script
Need to identify slow phases?
├── YES → Use timing_analyzer.py
│ └── Parse simulation logs for timing data
│
Need to understand parallel performance?
├── YES → Use scaling_analyzer.py
│ └── Analyze strong or weak scaling efficiency
│
Need to estimate memory requirements?
├── YES → Use memory_profiler.py
│ └── Estimate memory from problem parameters
│
Need optimization recommendations?
└── YES → Use bottleneck_detector.py
└── Combine analyses and get actionable adviceChoosing Analysis Thresholds
| Metric | Good | Acceptable | Poor |
|---|---|---|---|
| Phase dominance | <30% | 30-50% | >50% |
| Parallel efficiency | >0.80 | 0.70-0.80 | <0.70 |
| Memory usage | <60% | 60-80% | >80% |
Script Outputs (JSON Fields)
| Script | Key Outputs |
|---|---|
timing_analyzer.py | timing_data.phases, timing_data.slowest_phase, timing_data.total_time |
scaling_analyzer.py | scaling_analysis.results, scaling_analysis.efficiency_threshold_processors |
memory_profiler.py | memory_profile.total_memory_gb, memory_profile.per_process_gb, memory_profile.warnings |
bottleneck_detector.py | bottlenecks, recommendations |
Workflow
Complete Profiling Workflow
1. Analyze timing from simulation logs 2. Analyze scaling from multi-run data (if available) 3. Profile memory from simulation parameters 4. Detect bottlenecks and get recommendations 5. Implement optimizations based on recommendations 6. Re-profile to verify improvements
Quick Profiling (Timing Only)
1. Run timing analyzer on simulation log 2. Identify dominant phases (>50% of runtime) 3. Apply targeted optimizations to dominant phases
CLI Examples
Timing Analysis
# Basic timing analysis
python3 scripts/timing_analyzer.py \
--log simulation.log \
--json
# Custom timing pattern
python3 scripts/timing_analyzer.py \
--log simulation.log \
--pattern 'Step\s+(\w+)\s+took\s+([\d.]+)s' \
--jsonScaling Analysis
# Strong scaling (fixed problem size)
python3 scripts/scaling_analyzer.py \
--data scaling_data.json \
--type strong \
--json
# Weak scaling (constant work per processor)
python3 scripts/scaling_analyzer.py \
--data scaling_data.json \
--type weak \
--jsonMemory Profiling
# Estimate memory requirements
python3 scripts/memory_profiler.py \
--params simulation_params.json \
--available-gb 16.0 \
--jsonBottleneck Detection
# Detect bottlenecks from timing only
python3 scripts/bottleneck_detector.py \
--timing timing_results.json \
--json
# Comprehensive analysis with all inputs
python3 scripts/bottleneck_detector.py \
--timing timing_results.json \
--scaling scaling_results.json \
--memory memory_results.json \
--jsonConversational Workflow Example
User: My simulation is taking too long. Can you help me identify what's slow?
Agent workflow: 1. Ask for simulation log file 2. Run timing analyzer:
python3 scripts/timing_analyzer.py --log simulation.log --json3. Interpret results:
- If solver dominates (>50%): Recommend preconditioner tuning
- If assembly dominates: Recommend caching or vectorization
- If I/O dominates: Recommend reducing output frequency
4. If user has multi-run data, analyze scaling:
python3 scripts/scaling_analyzer.py --data scaling.json --type strong --json5. Generate comprehensive recommendations:
python3 scripts/bottleneck_detector.py --timing timing.json --scaling scaling.json --jsonInterpretation Guidance
Timing Analysis
| Scenario | Meaning | Action |
|---|---|---|
| Solver >70% | Solver-dominated | Tune preconditioner, check tolerance |
| Assembly >50% | Assembly-dominated | Cache matrices, vectorize, parallelize |
| I/O >30% | I/O-dominated | Reduce frequency, use parallel I/O |
| Balanced (<30% each) | Well-balanced | Look for algorithmic improvements |
Scaling Analysis
| Efficiency | Meaning | Action |
|---|---|---|
| >0.80 | Excellent scaling | Continue scaling up |
| 0.70-0.80 | Good scaling | Monitor at larger scales |
| 0.50-0.70 | Poor scaling | Investigate communication/load balance |
| <0.50 | Very poor scaling | Reduce processor count or redesign |
Memory Profile
| Usage | Meaning | Action |
|---|---|---|
| <60% available | Safe | No action needed |
| 60-80% available | Moderate | Monitor, consider optimization |
| >80% available | High | Reduce resolution or increase processors |
| >100% available | Exceeds capacity | Must reduce problem size |
Error Handling
| Error | Cause | Resolution |
|---|---|---|
Log file not found | Invalid path | Verify log file path |
No timing data found | Pattern mismatch | Provide custom pattern with --pattern |
At least 2 runs required | Insufficient data | Provide more scaling runs |
Missing required parameters | Incomplete params | Add mesh and fields to params file |
Optimization Strategies by Bottleneck Type
Solver Bottlenecks
- Use algebraic multigrid (AMG) preconditioner
- Tighten solver tolerance if over-solving
- Consider direct solver for small problems
- Profile matrix assembly vs solve time
Assembly Bottlenecks
- Cache element matrices if geometry is static
- Use vectorized assembly routines
- Consider matrix-free methods
- Parallelize assembly with coloring
I/O Bottlenecks
- Reduce output frequency
- Use parallel I/O (HDF5, MPI-IO)
- Write to fast scratch storage
- Compress output data
Scaling Bottlenecks
- Investigate communication overhead
- Check for load imbalance
- Reduce synchronization points
- Use asynchronous communication
- Consider hybrid MPI+OpenMP
Memory Bottlenecks
- Reduce mesh resolution
- Use iterative solver (lower memory than direct)
- Enable out-of-core computation
- Increase number of processors
- Use single precision where appropriate
Limitations
- Log parsing: Depends on pattern matching; may miss unusual formats
- Scaling analysis: Requires at least 2 runs for meaningful results
- Memory estimation: Approximate; actual usage may vary
- Recommendations: General guidance; may need domain-specific tuning
References
references/profiling_guide.md- Profiling concepts and interpretationreferences/optimization_strategies.md- Detailed optimization approaches
Version History
- v1.0.0 (2025-01-22): Initial release with 4 profiling scripts
Optimization Strategies
Introduction
This document provides detailed optimization strategies for common performance bottlenecks in computational materials science simulations. Strategies are organized by bottleneck type and include implementation guidance.
Solver Optimization
Strategy 1: Preconditioner Selection
When to use: Linear solver >50% of runtime, many iterations
Approach: Choose appropriate preconditioner for problem type
Preconditioner Options:
| Preconditioner | Best For | Pros | Cons |
|---|---|---|---|
| None | Well-conditioned problems | Zero setup cost | Many iterations |
| Jacobi | Diagonal-dominant matrices | Fast setup, parallel | Weak convergence |
| ILU(k) | General sparse matrices | Good convergence | Serial, expensive setup |
| AMG | Elliptic PDEs, diffusion | Excellent convergence | Complex setup |
| Multigrid | Structured grids | Optimal complexity | Problem-specific |
Implementation:
# Example: Switch from no preconditioner to AMG
solver.set_preconditioner('amg')
solver.set_amg_levels(3)
solver.set_amg_smoother('gauss-seidel')Expected Improvement: 2-10x reduction in solver time
Strategy 2: Solver Tolerance Tuning
When to use: Solver converges to very tight tolerance, over-solving
Approach: Relax tolerance to minimum needed for accuracy
Guidelines:
- Start with relative tolerance 1e-6
- If solution quality is acceptable, try 1e-5 or 1e-4
- Monitor residual vs solution error
- Tighten tolerance only if solution degrades
Implementation:
# Relax tolerance
solver.set_relative_tolerance(1e-5) # was 1e-8
solver.set_absolute_tolerance(1e-10) # was 1e-12Expected Improvement: 1.5-3x reduction in solver time
Strategy 3: Direct vs Iterative Solver
When to use: Small problems (<100k DOFs) or very ill-conditioned
Approach: Use direct solver for guaranteed convergence
Trade-offs:
- Direct: O(N^1.5) time, O(N^1.3) memory, guaranteed convergence
- Iterative: O(N) time per iteration, O(N) memory, may not converge
Decision Rule:
- N < 10k: Direct solver
- 10k < N < 100k: Try iterative first, fall back to direct
- N > 100k: Iterative solver (direct too expensive)
Implementation:
if num_dofs < 100000:
solver = DirectSolver('mumps')
else:
solver = IterativeSolver('gmres', preconditioner='amg')Expected Improvement: Varies (direct faster for small problems)
Assembly Optimization
Strategy 4: Matrix Caching
When to use: Geometry is static, matrix assembled repeatedly
Approach: Assemble matrix once, reuse for all time steps
Implementation:
# Assemble once
if not matrix_cached:
assemble_matrix(A)
matrix_cached = True
# Reuse in time loop
for t in time_steps:
# Only update RHS
assemble_rhs(b, t)
solve(A, x, b)Expected Improvement: 2-5x reduction in assembly time
Limitations: Only works for linear problems with static geometry
Strategy 5: Vectorized Assembly
When to use: Assembly is element-by-element, not vectorized
Approach: Batch element operations using NumPy/BLAS
Implementation:
# Before: Loop over elements
for elem in elements:
Ke = compute_element_matrix(elem)
assemble_into_global(A, Ke, elem.dofs)
# After: Vectorized
Ke_all = compute_all_element_matrices(elements) # Vectorized
assemble_batch(A, Ke_all, dof_map)Expected Improvement: 2-4x reduction in assembly time
Strategy 6: Parallel Assembly with Coloring
When to use: Assembly is serial, parallel simulation
Approach: Color elements to avoid race conditions, assemble in parallel
Implementation:
# Color elements (one-time cost)
colors = graph_coloring(element_connectivity)
# Parallel assembly
for color in colors:
# Elements in same color don't share DOFs
parallel_for elem in elements_with_color(color):
Ke = compute_element_matrix(elem)
assemble_into_global(A, Ke, elem.dofs)Expected Improvement: Near-linear speedup with processor count
I/O Optimization
Strategy 7: Reduce Output Frequency
When to use: I/O >20% of runtime, frequent output writes
Approach: Write output less frequently
Guidelines:
- Transient problems: Output every N time steps (N=10-100)
- Steady-state: Output only final solution
- Adaptive: Output when solution changes significantly
Implementation:
# Before: Output every step
for t in time_steps:
solve_step(t)
write_output(t)
# After: Output every 10 steps
for i, t in enumerate(time_steps):
solve_step(t)
if i % 10 == 0:
write_output(t)Expected Improvement: 5-10x reduction in I/O time
Strategy 8: Parallel I/O
When to use: Serial I/O in parallel simulation, large output files
Approach: Use parallel I/O library (HDF5, MPI-IO)
Implementation:
# Before: Serial I/O (rank 0 writes all data)
if rank == 0:
gather_data_from_all_ranks()
write_file(data)
# After: Parallel I/O (each rank writes its data)
h5file = h5py.File('output.h5', 'w', driver='mpio', comm=MPI.COMM_WORLD)
h5file.create_dataset('field', data=local_data)
h5file.close()Expected Improvement: Near-linear speedup with processor count
Strategy 9: Output Compression
When to use: Large output files, slow storage
Approach: Compress output data
Options:
- gzip: Good compression, moderate speed
- lz4: Fast compression, moderate ratio
- zstd: Balanced compression and speed
Implementation:
# HDF5 with compression
h5file.create_dataset('field', data=data, compression='gzip', compression_opts=4)Expected Improvement: 2-5x reduction in file size, 1.5-2x reduction in write time (if I/O bound)
Parallel Optimization
Strategy 10: Load Balancing
When to use: Poor scaling, uneven processor utilization
Approach: Redistribute work to balance load
Techniques:
- Static partitioning: Partition mesh evenly by element count
- Dynamic partitioning: Repartition based on measured work
- Work stealing: Idle processors steal work from busy ones
Implementation:
# Use graph partitioning library (METIS, ParMETIS)
partition = metis.partition_graph(mesh, nparts=num_procs, objtype='vol')
redistribute_mesh(mesh, partition)Expected Improvement: 1.5-3x improvement in scaling efficiency
Strategy 11: Asynchronous Communication
When to use: Communication overhead >20%, blocking communication
Approach: Overlap communication with computation
Implementation:
# Before: Blocking communication
send_data(neighbor)
receive_data(neighbor)
compute()
# After: Non-blocking communication
req_send = isend_data(neighbor)
req_recv = irecv_data(neighbor)
compute_interior() # Compute while communicating
wait(req_send, req_recv)
compute_boundary() # Compute boundary after communicationExpected Improvement: 1.5-2x reduction in communication overhead
Strategy 12: Hybrid MPI+OpenMP
When to use: Poor MPI scaling, many cores per node
Approach: Use MPI between nodes, OpenMP within nodes
Benefits:
- Reduced MPI communication (fewer processes)
- Better memory locality
- Reduced memory footprint
Implementation:
# Launch with fewer MPI ranks, more threads per rank
# mpirun -n 4 --bind-to socket --map-by socket:PE=8 ./simulation
# (4 MPI ranks, 8 OpenMP threads each = 32 cores)
# In code
#pragma omp parallel for
for (int i = 0; i < n; i++) {
compute_element(i);
}Expected Improvement: 1.5-2x improvement in scaling efficiency
Memory Optimization
Strategy 13: Reduce Mesh Resolution
When to use: Memory usage >80%, out-of-memory errors
Approach: Use coarser mesh
Guidelines:
- Reduce resolution by factor of 2 in each dimension
- Memory reduction: 8x (3D), 4x (2D)
- Verify solution accuracy is acceptable
Implementation:
# Before
mesh = create_mesh(nx=256, ny=256, nz=256) # 16M elements
# After
mesh = create_mesh(nx=128, ny=128, nz=128) # 2M elements (8x less memory)Expected Improvement: 2-8x reduction in memory usage
Strategy 14: Iterative vs Direct Solver
When to use: Memory-limited, using direct solver
Approach: Switch to iterative solver
Memory Comparison:
- Direct: O(N^1.3) memory (fill-in during factorization)
- Iterative: O(N) memory (matrix + workspace vectors)
Implementation:
# Before: Direct solver
solver = DirectSolver('mumps')
# After: Iterative solver
solver = IterativeSolver('gmres', preconditioner='ilu')Expected Improvement: 5-20x reduction in memory usage
Strategy 15: Single Precision
When to use: Memory-limited, accuracy requirements allow
Approach: Use single precision (float32) instead of double (float64)
Trade-offs:
- Memory: 2x reduction
- Accuracy: ~7 digits (single) vs ~15 digits (double)
- Speed: Often faster (better cache utilization, SIMD)
Implementation:
# Use float32 for field variables
field = np.zeros(n, dtype=np.float32) # was np.float64Expected Improvement: 2x reduction in memory usage, 1.2-1.5x speedup
Algorithm Optimization
Strategy 16: Adaptive Time Stepping
When to use: Fixed time step, solution varies slowly/rapidly
Approach: Adjust time step based on solution behavior
Benefits:
- Larger steps when solution is smooth (faster)
- Smaller steps when solution changes rapidly (accurate)
Implementation:
dt = dt_initial
for t in time_range:
solve_step(dt)
error = estimate_error()
if error < tol_low:
dt *= 1.5 # Increase step
elif error > tol_high:
dt *= 0.5 # Decrease step
redo_step()Expected Improvement: 2-10x reduction in number of time steps
Strategy 17: Matrix-Free Methods
When to use: Matrix assembly is expensive, matrix is not reused
Approach: Compute matrix-vector products on-the-fly
Benefits:
- No matrix storage (memory savings)
- No matrix assembly (time savings)
- Suitable for nonlinear problems
Implementation:
# Instead of assembling A, define matrix-vector product
def matvec(x):
y = np.zeros_like(x)
for elem in elements:
y_elem = compute_element_matvec(elem, x)
add_to_global(y, y_elem, elem.dofs)
return y
# Use with iterative solver
solver = IterativeSolver(matvec=matvec)Expected Improvement: 2-5x reduction in memory, 1.5-3x reduction in time
Profiling-Driven Optimization
General Workflow
1. Profile: Identify bottleneck (timing, scaling, memory) 2. Hypothesize: Determine likely cause 3. Optimize: Implement targeted optimization 4. Verify: Re-profile to measure improvement 5. Iterate: Repeat until performance goals met
Optimization Priority
1. High priority: Bottlenecks >50% of runtime or critical path 2. Medium priority: Bottlenecks 30-50% of runtime 3. Low priority: Bottlenecks <30% of runtime
Diminishing Returns
- First optimization: Often 2-5x improvement
- Second optimization: Often 1.5-2x improvement
- Third+ optimization: Often <1.5x improvement
Rule of thumb: Stop optimizing when improvement <20% or effort exceeds benefit
Case Studies
Case Study 1: Materials Simulation (Phase-Field Example)
Problem: Simulation taking 10 hours, solver dominates (80% of time)
Profiling:
- Linear solver: 8 hours (80%)
- Assembly: 1.5 hours (15%)
- Other: 0.5 hours (5%)
Optimizations: 1. Switch from no preconditioner to AMG: 8h → 2h (4x improvement) 2. Relax tolerance from 1e-8 to 1e-6: 2h → 1.5h (1.3x improvement) 3. Cache matrix (geometry is static): 1.5h assembly → 0.1h (15x improvement)
Result: 10 hours → 2.1 hours (4.8x overall improvement)
Case Study 2: Parallel Scaling
Problem: Poor scaling beyond 8 processors (efficiency 0.55 at 16 procs)
Profiling:
- 1 proc: 1000s
- 2 procs: 520s (efficiency 0.96)
- 4 procs: 270s (efficiency 0.93)
- 8 procs: 150s (efficiency 0.83)
- 16 procs: 90s (efficiency 0.69)
Optimizations: 1. Improve load balancing with METIS: efficiency 0.69 → 0.75 2. Use non-blocking communication: efficiency 0.75 → 0.82 3. Reduce synchronization points: efficiency 0.82 → 0.87
Result: Efficiency at 16 procs: 0.69 → 0.87 (26% improvement)
Case Study 3: Memory-Limited
Problem: Out-of-memory error with 256³ mesh (16M elements)
Profiling:
- Field memory: 2.0 GB
- Direct solver: 18.0 GB
- Total: 20.0 GB (exceeds 16 GB available)
Optimizations: 1. Switch to iterative solver: 18 GB → 2 GB (9x reduction) 2. Use single precision for fields: 2 GB → 1 GB (2x reduction)
Result: 20 GB → 3 GB (6.7x reduction, fits in memory)
References
- Saad, Y. (2003). "Iterative Methods for Sparse Linear Systems"
- Gropp, W., Lusk, E., & Skjellum, A. (1999). "Using MPI"
- Trottenberg, U., Oosterlee, C., & Schüller, A. (2001). "Multigrid"
- Karypis, G., & Kumar, V. (1998). "A Fast and High Quality Multilevel Scheme for Partitioning Irregular Graphs"
Performance Profiling Guide
Introduction
Performance profiling is the process of measuring and analyzing where computational resources (time, memory, communication) are spent during simulation execution. This guide explains key profiling concepts and how to interpret profiling results.
Profiling Concepts
Timing Analysis
Definition: Measuring how long different phases of a simulation take.
Key Metrics:
- Total time: Overall wall-clock time for the simulation
- Phase time: Time spent in each computational phase (assembly, solve, I/O, etc.)
- Percentage: Fraction of total time spent in each phase
- Count: Number of times each phase is executed
Interpretation:
- Phases consuming >50% of total time are dominant and should be optimized first
- Phases consuming 30-50% are significant and may benefit from optimization
- Phases consuming <30% are minor and typically not worth optimizing
Common Phases:
- Mesh generation: Creating the computational grid
- Assembly: Building matrices and vectors
- Linear solver: Solving Ax=b systems
- Nonlinear solver: Newton or fixed-point iterations
- Update fields: Computing derived quantities
- I/O: Writing output files
- Communication: MPI message passing (parallel simulations)
Scaling Analysis
Definition: Measuring how performance changes with problem size or processor count.
Strong Scaling
Definition: Fixed problem size, varying processor count.
Ideal behavior: Time decreases proportionally with processor count.
Metrics:
- Speedup: S(N) = T(1) / T(N) where T(N) is time on N processors
- Efficiency: E(N) = S(N) / N = T(1) / (N * T(N))
- Ideal efficiency: 1.0 (100%)
Interpretation:
- E > 0.80: Excellent scaling
- 0.70 < E < 0.80: Good scaling
- 0.50 < E < 0.70: Poor scaling (communication/load imbalance issues)
- E < 0.50: Very poor scaling (not worth using more processors)
Typical behavior: Efficiency decreases as processor count increases due to:
- Communication overhead
- Load imbalance
- Serial bottlenecks (Amdahl's law)
Weak Scaling
Definition: Constant work per processor, varying processor count.
Ideal behavior: Time remains constant as processors increase.
Metrics:
- Efficiency: E(N) = T(1) / T(N)
- Ideal efficiency: 1.0 (constant time)
Interpretation:
- E > 0.90: Excellent weak scaling
- 0.80 < E < 0.90: Good weak scaling
- E < 0.80: Poor weak scaling
Typical behavior: Time increases slightly due to:
- Communication overhead (grows with processor count)
- Synchronization costs
Memory Profiling
Definition: Estimating or measuring memory usage.
Key Components:
- Field memory: Storage for solution variables (concentration, temperature, etc.)
- Solver workspace: Temporary vectors for iterative solvers
- Matrix storage: Sparse matrix entries (if stored explicitly)
- Communication buffers: MPI send/receive buffers
Estimation Formula:
Total Memory = Field Memory + Solver Workspace + Matrix Storage
Field Memory = mesh_points × fields × components × bytes_per_value
Solver Workspace = mesh_points × workspace_multiplier × bytes_per_valueTypical Values:
- bytes_per_value: 8 (double precision), 4 (single precision)
- workspace_multiplier: 5-10 for iterative solvers, 0 for matrix-free
Interpretation:
- Memory usage < 60% available: Safe
- Memory usage 60-80% available: Moderate (monitor)
- Memory usage > 80% available: High (risk of swapping)
- Memory usage > 100% available: Exceeds capacity (will fail or swap)
Bottleneck Detection
Definition: Identifying the computational phase or resource that limits overall performance.
Types of Bottlenecks: 1. Timing bottlenecks: Phases consuming disproportionate time 2. Scaling bottlenecks: Poor parallel efficiency 3. Memory bottlenecks: Insufficient memory or excessive memory usage 4. I/O bottlenecks: Slow disk writes
Detection Criteria:
- Dominant phase: Any phase >50% of total time
- Poor scaling: Efficiency <0.70
- High memory: Usage >80% of available
Profiling Workflow
1. Baseline Profiling
Goal: Establish current performance characteristics.
Steps: 1. Run simulation with timing enabled 2. Extract timing data from logs 3. Identify dominant phases 4. Measure total runtime
Tools: timing_analyzer.py
2. Scaling Study
Goal: Understand parallel performance.
Steps: 1. Run simulation at multiple processor counts (strong scaling) or problem sizes (weak scaling) 2. Measure runtime for each configuration 3. Compute speedup and efficiency 4. Identify efficiency threshold
Tools: scaling_analyzer.py
Best Practices:
- Use at least 5 data points
- Double processor count between runs (1, 2, 4, 8, 16, ...)
- Keep problem size fixed (strong) or work per processor fixed (weak)
- Run multiple trials and average results
3. Memory Analysis
Goal: Ensure sufficient memory and identify memory-intensive components.
Steps: 1. Estimate memory from problem parameters 2. Compare to available system memory 3. Identify memory-intensive components 4. Plan resource allocation
Tools: memory_profiler.py
4. Bottleneck Identification
Goal: Pinpoint performance-limiting factors.
Steps: 1. Combine timing, scaling, and memory analyses 2. Identify bottlenecks using thresholds 3. Prioritize by severity (high, medium, low) 4. Generate optimization recommendations
Tools: bottleneck_detector.py
5. Optimization
Goal: Improve performance based on profiling insights.
Steps: 1. Implement recommended optimizations 2. Re-profile to measure improvement 3. Iterate until performance goals are met
See: optimization_strategies.md for detailed strategies
Common Profiling Patterns
Pattern 1: Solver-Dominated
Symptoms:
- Linear solver >70% of total time
- Assembly <20% of total time
Causes:
- Poor preconditioner
- Over-solving (tolerance too tight)
- Ill-conditioned matrix
Solutions:
- Use AMG or ILU preconditioner
- Relax solver tolerance
- Improve matrix conditioning (scaling, regularization)
Pattern 2: Assembly-Dominated
Symptoms:
- Assembly >50% of total time
- Solver <30% of total time
Causes:
- Inefficient assembly routines
- Repeated assembly of static matrices
- Lack of vectorization
Solutions:
- Cache element matrices
- Vectorize assembly loops
- Parallelize assembly with coloring
- Consider matrix-free methods
Pattern 3: I/O-Dominated
Symptoms:
- I/O >30% of total time
- Frequent output writes
Causes:
- Excessive output frequency
- Serial I/O in parallel simulations
- Slow storage
Solutions:
- Reduce output frequency
- Use parallel I/O (HDF5, MPI-IO)
- Write to fast scratch storage
- Compress output data
Pattern 4: Poor Scaling
Symptoms:
- Efficiency <0.70 at moderate processor counts
- Speedup plateaus
Causes:
- Communication overhead
- Load imbalance
- Serial bottlenecks
Solutions:
- Investigate communication patterns
- Improve load balancing
- Reduce synchronization points
- Use asynchronous communication
Profiling Best Practices
Do's
- ✅ Profile on representative problems (not toy examples)
- ✅ Run multiple trials and average results
- ✅ Profile at multiple scales (small, medium, large)
- ✅ Document profiling methodology and results
- ✅ Re-profile after optimizations to verify improvements
Don'ts
- ❌ Don't profile debug builds (use optimized builds)
- ❌ Don't profile on oversubscribed systems (more processes than cores)
- ❌ Don't optimize minor phases (<10% of runtime)
- ❌ Don't assume profiling results generalize to all problems
- ❌ Don't ignore statistical variation (run multiple trials)
Interpreting Profiling Results
Red Flags
- Any phase >70% of total time
- Efficiency <0.50 at low processor counts (<16)
- Memory usage >90% of available
- Speedup <2x when doubling processors
Green Flags
- No phase >40% of total time
- Efficiency >0.80 at moderate processor counts
- Memory usage <60% of available
- Speedup >1.8x when doubling processors
Advanced Topics
Amdahl's Law
Formula: S(N) = 1 / (f + (1-f)/N)
Where:
- S(N) = speedup on N processors
- f = fraction of serial code
Implication: Even small serial fractions limit scalability.
Example: If 5% of code is serial (f=0.05), maximum speedup is 20x regardless of processor count.
Gustafson's Law
Formula: S(N) = N - f(N-1)
Implication: Larger problems scale better (weak scaling perspective).
Communication Overhead
Sources:
- Latency: Time to initiate communication
- Bandwidth: Time to transfer data
- Synchronization: Waiting for all processes
Mitigation:
- Overlap communication with computation
- Use non-blocking MPI calls
- Reduce message frequency (batch communications)
- Increase message size (amortize latency)
Tools and Techniques
Built-in Profiling
- Use simulation's built-in timing (if available)
- Instrument code with timers
- Log timing information to files
External Profilers
- gprof: GNU profiler (function-level timing)
- Valgrind: Memory profiling and leak detection
- Intel VTune: Comprehensive performance analysis
- TAU: Parallel performance profiling
- Scalasca: Scalability analysis
Profiling Overhead
- Timing analysis: <1% overhead
- Memory profiling: 5-10% overhead
- Detailed profiling (VTune, TAU): 10-50% overhead
References
- Amdahl, G. M. (1967). "Validity of the single processor approach to achieving large scale computing capabilities"
- Gustafson, J. L. (1988). "Reevaluating Amdahl's law"
- Gropp, W., Lusk, E., & Skjellum, A. (1999). "Using MPI: Portable Parallel Programming with the Message-Passing Interface"
#!/usr/bin/env python3
"""
Bottleneck Detector - Identify performance bottlenecks and recommend optimizations.
"""
import argparse
import json
import sys
from typing import Dict, List, Optional
def load_analysis_results(timing_path: str, scaling_path: Optional[str] = None,
memory_path: Optional[str] = None) -> Dict:
"""
Load analysis results from JSON files.
Args:
timing_path: Path to timing analysis JSON
scaling_path: Path to scaling analysis JSON (optional)
memory_path: Path to memory profile JSON (optional)
Returns:
Combined analysis data
"""
results = {}
# Load timing data (required)
try:
with open(timing_path, 'r', encoding='utf-8') as f:
results['timing'] = json.load(f)
except FileNotFoundError:
raise FileNotFoundError(f"Timing analysis file not found: {timing_path}")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid timing JSON: {e}")
# Load scaling data (optional)
if scaling_path:
try:
with open(scaling_path, 'r', encoding='utf-8') as f:
results['scaling'] = json.load(f)
except FileNotFoundError:
print(f"Warning: Scaling file not found: {scaling_path}", file=sys.stderr)
except json.JSONDecodeError as e:
print(f"Warning: Invalid scaling JSON: {e}", file=sys.stderr)
# Load memory data (optional)
if memory_path:
try:
with open(memory_path, 'r', encoding='utf-8') as f:
results['memory'] = json.load(f)
except FileNotFoundError:
print(f"Warning: Memory file not found: {memory_path}", file=sys.stderr)
except json.JSONDecodeError as e:
print(f"Warning: Invalid memory JSON: {e}", file=sys.stderr)
return results
def detect_timing_bottlenecks(timing_data: Dict, threshold: float = 50.0) -> List[Dict]:
"""
Detect timing bottlenecks from timing analysis.
Args:
timing_data: Timing analysis results
threshold: Percentage threshold for bottleneck (default: 50%)
Returns:
List of bottleneck dictionaries
"""
bottlenecks = []
if 'timing_data' not in timing_data:
return bottlenecks
phases = timing_data['timing_data'].get('phases', [])
for phase in phases:
percentage = phase.get('percentage', 0)
if percentage > threshold:
severity = 'high' if percentage > 70 else 'medium'
bottlenecks.append({
'type': 'timing',
'phase': phase['name'],
'severity': severity,
'metric': 'percentage',
'value': percentage,
'threshold': threshold
})
return bottlenecks
def detect_scaling_bottlenecks(scaling_data: Dict, threshold: float = 0.70) -> List[Dict]:
"""
Detect scaling bottlenecks from scaling analysis.
Args:
scaling_data: Scaling analysis results
threshold: Efficiency threshold (default: 0.70)
Returns:
List of bottleneck dictionaries
"""
bottlenecks = []
if 'scaling_analysis' not in scaling_data:
return bottlenecks
analysis = scaling_data['scaling_analysis']
avg_efficiency = analysis.get('average_efficiency', 1.0)
if avg_efficiency < threshold:
bottlenecks.append({
'type': 'scaling',
'phase': 'parallel_efficiency',
'severity': 'high' if avg_efficiency < 0.5 else 'medium',
'metric': 'efficiency',
'value': avg_efficiency,
'threshold': threshold
})
return bottlenecks
def detect_memory_bottlenecks(memory_data: Dict, threshold: float = 0.80) -> List[Dict]:
"""
Detect memory bottlenecks from memory profile.
Args:
memory_data: Memory profile results
threshold: Memory usage threshold (default: 0.80 = 80%)
Returns:
List of bottleneck dictionaries
"""
bottlenecks = []
if 'memory_profile' not in memory_data:
return bottlenecks
profile = memory_data['memory_profile']
# Check if warnings exist (indicates high memory usage)
if profile.get('warnings'):
total_memory = profile.get('total_memory_gb', 0)
bottlenecks.append({
'type': 'memory',
'phase': 'memory_usage',
'severity': 'high',
'metric': 'total_memory_gb',
'value': total_memory,
'threshold': threshold
})
return bottlenecks
def generate_recommendations(bottlenecks: List[Dict], timing_data: Optional[Dict] = None) -> List[Dict]:
"""
Generate optimization recommendations based on bottlenecks.
Args:
bottlenecks: List of detected bottlenecks
timing_data: Optional timing data for context
Returns:
List of recommendation dictionaries
"""
recommendations = []
if not bottlenecks:
recommendations.append({
'priority': 'low',
'category': 'general',
'issue': 'No significant bottlenecks detected',
'strategies': ['Performance appears balanced', 'Consider profiling at larger scale']
})
return recommendations
# Process timing bottlenecks
for bottleneck in bottlenecks:
if bottleneck['type'] == 'timing':
phase = bottleneck['phase'].lower()
# Solver-related bottlenecks
if any(keyword in phase for keyword in ['solve', 'linear', 'iteration', 'cg', 'gmres']):
recommendations.append({
'priority': 'high',
'category': 'solver',
'issue': f"{bottleneck['phase']} dominates runtime ({bottleneck['value']:.1f}%)",
'strategies': [
'Use algebraic multigrid (AMG) preconditioner',
'Tighten solver tolerance if over-solving',
'Consider direct solver for small problems',
'Profile matrix assembly vs solve time'
]
})
# Assembly bottlenecks
elif any(keyword in phase for keyword in ['assembly', 'assemble', 'build']):
recommendations.append({
'priority': 'high',
'category': 'assembly',
'issue': f"{bottleneck['phase']} dominates runtime ({bottleneck['value']:.1f}%)",
'strategies': [
'Cache element matrices if geometry is static',
'Use vectorized assembly routines',
'Consider matrix-free methods',
'Parallelize assembly with coloring'
]
})
# I/O bottlenecks
elif any(keyword in phase for keyword in ['io', 'write', 'output', 'save']):
recommendations.append({
'priority': 'medium',
'category': 'io',
'issue': f"{bottleneck['phase']} dominates runtime ({bottleneck['value']:.1f}%)",
'strategies': [
'Reduce output frequency',
'Use parallel I/O (HDF5, MPI-IO)',
'Write to fast scratch storage',
'Compress output data'
]
})
# Generic timing bottleneck
else:
recommendations.append({
'priority': 'medium',
'category': 'general',
'issue': f"{bottleneck['phase']} dominates runtime ({bottleneck['value']:.1f}%)",
'strategies': [
'Profile this phase in detail',
'Look for algorithmic improvements',
'Consider parallelization opportunities'
]
})
# Process scaling bottlenecks
elif bottleneck['type'] == 'scaling':
recommendations.append({
'priority': 'high',
'category': 'parallel',
'issue': f"Poor parallel efficiency ({bottleneck['value']:.2f})",
'strategies': [
'Investigate communication overhead',
'Check for load imbalance',
'Reduce synchronization points',
'Use asynchronous communication',
'Consider hybrid MPI+OpenMP'
]
})
# Process memory bottlenecks
elif bottleneck['type'] == 'memory':
recommendations.append({
'priority': 'high',
'category': 'memory',
'issue': f"High memory usage ({bottleneck['value']:.2f} GB)",
'strategies': [
'Reduce mesh resolution',
'Use iterative solver (lower memory than direct)',
'Enable out-of-core computation',
'Increase number of processors',
'Use single precision where appropriate'
]
})
return recommendations
def main():
parser = argparse.ArgumentParser(
description='Identify performance bottlenecks and recommend optimizations'
)
parser.add_argument('--timing', required=True, help='Path to timing analysis JSON')
parser.add_argument('--scaling', help='Path to scaling analysis JSON (optional)')
parser.add_argument('--memory', help='Path to memory profile JSON (optional)')
parser.add_argument('--json', action='store_true', help='Output in JSON format')
args = parser.parse_args()
try:
# Load analysis results
results = load_analysis_results(args.timing, args.scaling, args.memory)
# Detect bottlenecks
bottlenecks = []
if 'timing' in results:
bottlenecks.extend(detect_timing_bottlenecks(results['timing']))
if 'scaling' in results:
bottlenecks.extend(detect_scaling_bottlenecks(results['scaling']))
if 'memory' in results:
bottlenecks.extend(detect_memory_bottlenecks(results['memory']))
# Generate recommendations
recommendations = generate_recommendations(bottlenecks, results.get('timing'))
# Format output
if args.json:
output = {
'inputs': {
'timing_file': args.timing,
'scaling_file': args.scaling,
'memory_file': args.memory
},
'results': {
'bottlenecks': bottlenecks,
'recommendations': recommendations
}
}
print(json.dumps(output, indent=2))
else:
print(f"Bottleneck Analysis")
print(f"=" * 60)
if bottlenecks:
print(f"\nDetected Bottlenecks:")
for bottleneck in bottlenecks:
print(f" [{bottleneck['severity'].upper()}] {bottleneck['phase']}: "
f"{bottleneck['metric']} = {bottleneck['value']:.2f}")
else:
print("\nNo significant bottlenecks detected")
print(f"\nRecommendations:")
for rec in recommendations:
print(f"\n [{rec['priority'].upper()}] {rec['category'].upper()}")
print(f" Issue: {rec['issue']}")
print(f" Strategies:")
for strategy in rec['strategies']:
print(f" - {strategy}")
except (FileNotFoundError, ValueError) as e:
if args.json:
print(json.dumps({'error': str(e)}))
else:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
if args.json:
print(json.dumps({'error': f'Unexpected error: {e}'}))
else:
print(f"Unexpected error: {e}", file=sys.stderr)
sys.exit(3)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Memory Profiler - Estimate memory requirements from simulation parameters.
"""
import argparse
import json
import sys
from typing import Dict, List, Optional
def load_parameters(path: str) -> Dict:
"""
Load simulation parameters from JSON file.
Args:
path: Path to JSON file with parameters
Returns:
Parameter dictionary
"""
try:
with open(path, 'r', encoding='utf-8') as f:
params = json.load(f)
# Validate required fields
missing = []
if 'mesh' not in params:
missing.append('mesh')
if 'fields' not in params:
missing.append('fields')
if missing:
raise ValueError(f"Missing required parameters: {', '.join(missing)}")
return params
except FileNotFoundError:
raise FileNotFoundError(f"Parameters file not found: {path}")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON format: {e}")
def estimate_field_memory(mesh: Dict, fields: Dict) -> float:
"""
Estimate memory for field variables.
Args:
mesh: Mesh parameters (nx, ny, nz)
fields: Field definitions
Returns:
Memory in GB
"""
# Calculate total mesh points
nx = mesh.get('nx', 1)
ny = mesh.get('ny', 1)
nz = mesh.get('nz', 1)
# Validate mesh dimensions
for name, val in [('nx', nx), ('ny', ny), ('nz', nz)]:
if not isinstance(val, int) or val <= 0:
raise ValueError(f"Mesh dimension '{name}' must be a positive integer, got {val}")
mesh_points = nx * ny * nz
# Calculate memory for all fields
total_bytes = 0
for field_name, field_spec in fields.items():
components = field_spec.get('components', 1)
bytes_per_value = field_spec.get('bytes_per_value', 8) # default: double precision
if not isinstance(components, int) or components <= 0:
raise ValueError(f"Field '{field_name}' components must be a positive integer, got {components}")
if not isinstance(bytes_per_value, (int, float)) or bytes_per_value <= 0:
raise ValueError(f"Field '{field_name}' bytes_per_value must be positive, got {bytes_per_value}")
total_bytes += mesh_points * components * bytes_per_value
# Convert to GB
return total_bytes / (1024 ** 3)
def estimate_solver_memory(mesh: Dict, solver: Dict) -> float:
"""
Estimate memory for solver workspace.
Args:
mesh: Mesh parameters
solver: Solver configuration
Returns:
Memory in GB
"""
# Calculate total mesh points
nx = mesh.get('nx', 1)
ny = mesh.get('ny', 1)
nz = mesh.get('nz', 1)
mesh_points = nx * ny * nz
# Get workspace multiplier based on solver type
solver_type = solver.get('type', 'iterative')
workspace_multiplier = solver.get('workspace_multiplier', 5)
# Estimate workspace (typically several vectors of size mesh_points)
bytes_per_value = 8 # double precision
workspace_bytes = mesh_points * workspace_multiplier * bytes_per_value
# Convert to GB
return workspace_bytes / (1024 ** 3)
def compute_total_memory(params: Dict, available_gb: Optional[float] = None) -> Dict:
"""
Compute total memory requirements.
Args:
params: Simulation parameters
available_gb: Available system memory (optional)
Returns:
Memory profile dictionary
"""
mesh = params['mesh']
fields = params['fields']
solver = params.get('solver', {'type': 'iterative', 'workspace_multiplier': 5})
processors = params.get('processors', 1)
if not isinstance(processors, int) or processors <= 0:
raise ValueError(f"processors must be a positive integer, got {processors}")
# Calculate mesh points
nx = mesh.get('nx', 1)
ny = mesh.get('ny', 1)
nz = mesh.get('nz', 1)
mesh_points = nx * ny * nz
# Estimate memory components
field_memory_gb = estimate_field_memory(mesh, fields)
solver_workspace_gb = estimate_solver_memory(mesh, solver)
total_memory_gb = field_memory_gb + solver_workspace_gb
per_process_gb = total_memory_gb / processors
# Generate warnings
warnings = []
if available_gb is not None:
if total_memory_gb > available_gb:
warnings.append(f"Total memory ({total_memory_gb:.2f} GB) exceeds available memory ({available_gb:.2f} GB)")
elif total_memory_gb > 0.8 * available_gb:
warnings.append(f"Memory usage ({total_memory_gb:.2f} GB) is high (>80% of available {available_gb:.2f} GB)")
return {
'mesh_points': mesh_points,
'field_memory_gb': field_memory_gb,
'solver_workspace_gb': solver_workspace_gb,
'total_memory_gb': total_memory_gb,
'per_process_gb': per_process_gb,
'warnings': warnings
}
def main():
parser = argparse.ArgumentParser(
description='Estimate memory requirements from simulation parameters'
)
parser.add_argument('--params', required=True, help='Path to JSON file with simulation parameters')
parser.add_argument('--available-gb', type=float, help='Available system memory in GB')
parser.add_argument('--json', action='store_true', help='Output in JSON format')
args = parser.parse_args()
try:
# Load parameters
params = load_parameters(args.params)
# Compute memory profile
profile = compute_total_memory(params, args.available_gb)
# Format output
if args.json:
output = {
'inputs': {
'params_file': args.params,
'available_gb': args.available_gb
},
'results': profile
}
print(json.dumps(output, indent=2))
else:
print(f"Memory Profile")
print(f"=" * 60)
print(f"Mesh points: {profile['mesh_points']:,}")
print(f"Field memory: {profile['field_memory_gb']:.3f} GB")
print(f"Solver workspace: {profile['solver_workspace_gb']:.3f} GB")
print(f"Total memory: {profile['total_memory_gb']:.3f} GB")
print(f"Per-process memory: {profile['per_process_gb']:.3f} GB")
if profile['warnings']:
print(f"\nWarnings:")
for warning in profile['warnings']:
print(f" - {warning}")
except (FileNotFoundError, ValueError) as e:
if args.json:
print(json.dumps({'error': str(e)}))
else:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
if args.json:
print(json.dumps({'error': f'Unexpected error: {e}'}))
else:
print(f"Unexpected error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Scaling Analyzer - Analyze strong and weak scaling from multi-run data.
"""
import argparse
import json
import sys
from typing import Dict, List, Optional
def load_scaling_data(path: str) -> List[Dict]:
"""
Load scaling data from JSON file.
Args:
path: Path to JSON file with scaling data
Returns:
List of run configurations
"""
try:
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
if 'runs' not in data:
raise ValueError("JSON must contain 'runs' array")
runs = data['runs']
if len(runs) < 2:
raise ValueError("At least 2 runs required for scaling analysis")
# Validate required fields
for i, run in enumerate(runs):
if 'processors' not in run:
raise ValueError(f"Run {i} missing 'processors' field")
if 'time' not in run:
raise ValueError(f"Run {i} missing 'time' field")
if run['time'] <= 0:
raise ValueError(f"Run {i} has invalid time: {run['time']}")
if run['processors'] <= 0:
raise ValueError(f"Run {i} has invalid processor count: {run['processors']}")
return runs
except FileNotFoundError:
raise FileNotFoundError(f"Scaling data file not found: {path}")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON format: {e}")
def compute_strong_scaling(runs: List[Dict]) -> Dict:
"""
Compute strong scaling efficiency (fixed problem size, varying processors).
Args:
runs: List of run configurations
Returns:
Dictionary with scaling analysis results
"""
# Sort by processor count
sorted_runs = sorted(runs, key=lambda x: x['processors'])
# Use smallest processor count as baseline
baseline = sorted_runs[0]
baseline_time = baseline['time']
baseline_procs = baseline['processors']
results = []
for run in sorted_runs:
procs = run['processors']
time = run['time']
# Speedup = T_baseline / T_N
speedup = baseline_time / time
# Efficiency = Speedup / (N / N_baseline) = T_baseline / (N * T_N / N_baseline)
efficiency = speedup / (procs / baseline_procs)
results.append({
'processors': procs,
'time': time,
'speedup': speedup,
'efficiency': efficiency
})
# Find efficiency threshold (first point where efficiency < 0.70)
threshold_procs = None
for result in results:
if result['efficiency'] < 0.70:
threshold_procs = result['processors']
break
# Calculate average efficiency
avg_efficiency = sum(r['efficiency'] for r in results) / len(results)
return {
'type': 'strong',
'baseline': {
'processors': baseline_procs,
'time': baseline_time
},
'results': results,
'efficiency_threshold_processors': threshold_procs,
'average_efficiency': avg_efficiency
}
def compute_weak_scaling(runs: List[Dict]) -> Dict:
"""
Compute weak scaling efficiency (constant work per processor, varying processors).
Args:
runs: List of run configurations
Returns:
Dictionary with scaling analysis results
"""
# Sort by processor count
sorted_runs = sorted(runs, key=lambda x: x['processors'])
# Use smallest processor count as baseline
baseline = sorted_runs[0]
baseline_time = baseline['time']
baseline_procs = baseline['processors']
results = []
for run in sorted_runs:
procs = run['processors']
time = run['time']
# For weak scaling, efficiency = T_baseline / T_N
efficiency = baseline_time / time
# Speedup is not meaningful for weak scaling (problem size changes)
speedup = efficiency * (procs / baseline_procs)
results.append({
'processors': procs,
'time': time,
'speedup': speedup,
'efficiency': efficiency
})
# Find efficiency threshold
threshold_procs = None
for result in results:
if result['efficiency'] < 0.70:
threshold_procs = result['processors']
break
# Calculate average efficiency
avg_efficiency = sum(r['efficiency'] for r in results) / len(results)
return {
'type': 'weak',
'baseline': {
'processors': baseline_procs,
'time': baseline_time
},
'results': results,
'efficiency_threshold_processors': threshold_procs,
'average_efficiency': avg_efficiency
}
def main():
parser = argparse.ArgumentParser(
description='Analyze strong and weak scaling from multi-run data'
)
parser.add_argument('--data', required=True, help='Path to JSON file with scaling data')
parser.add_argument('--type', required=True, choices=['strong', 'weak'],
help='Scaling type: strong or weak')
parser.add_argument('--json', action='store_true', help='Output in JSON format')
args = parser.parse_args()
try:
# Load scaling data
runs = load_scaling_data(args.data)
# Compute scaling analysis
if args.type == 'strong':
analysis = compute_strong_scaling(runs)
else:
analysis = compute_weak_scaling(runs)
# Format output
if args.json:
output = {
'inputs': {
'data_file': args.data,
'scaling_type': args.type
},
'results': analysis
}
print(json.dumps(output, indent=2))
else:
print(f"{args.type.capitalize()} Scaling Analysis")
print(f"=" * 60)
print(f"Baseline: {analysis['baseline']['processors']} processors, "
f"{analysis['baseline']['time']:.2f}s\n")
print(f"{'Procs':<8} {'Time (s)':<12} {'Speedup':<12} {'Efficiency':<12}")
print("-" * 60)
for result in analysis['results']:
print(f"{result['processors']:<8} {result['time']:<12.2f} "
f"{result['speedup']:<12.2f} {result['efficiency']:<12.3f}")
print(f"\nAverage efficiency: {analysis['average_efficiency']:.3f}")
if analysis['efficiency_threshold_processors']:
print(f"Efficiency drops below 0.70 at {analysis['efficiency_threshold_processors']} processors")
else:
print("Efficiency remains above 0.70 for all configurations")
except (FileNotFoundError, ValueError) as e:
if args.json:
print(json.dumps({'error': str(e)}))
else:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
if args.json:
print(json.dumps({'error': f'Unexpected error: {e}'}))
else:
print(f"Unexpected error: {e}", file=sys.stderr)
sys.exit(3)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Timing Analyzer - Extract and analyze timing information from simulation logs.
"""
import argparse
import json
import re
import sys
from typing import Dict, List, Optional, Tuple
def parse_timing_log(log_path: str, pattern: Optional[str] = None) -> List[Tuple[str, float]]:
"""
Parse simulation log file and extract timing entries.
Args:
log_path: Path to log file
pattern: Custom regex pattern (optional)
Returns:
List of (phase_name, time_seconds) tuples
"""
# Default patterns for common log formats
default_patterns = [
r'Phase:\s*([^,]+),\s*Time:\s*([\d.]+)s',
r'(\w+(?:\s+\w+)*)\s+took\s+([\d.]+)\s*s',
r'\[([^\]]+)\]\s*:\s*([\d.]+)\s*s',
r'Time\s+for\s+([^:]+):\s*([\d.]+)',
]
patterns_to_try = [pattern] if pattern else default_patterns
entries = []
malformed_count = 0
try:
with open(log_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
matched = False
for pat in patterns_to_try:
match = re.search(pat, line)
if match:
try:
phase = match.group(1).strip()
time_val = float(match.group(2))
if time_val >= 0:
entries.append((phase, time_val))
matched = True
break
except (ValueError, IndexError):
malformed_count += 1
if not matched and any(keyword in line.lower() for keyword in ['time', 'phase', 'took']):
malformed_count += 1
except FileNotFoundError:
raise FileNotFoundError(f"Log file not found: {log_path}")
except Exception as e:
raise ValueError(f"Error reading log file: {e}")
if malformed_count > 0:
print(f"Warning: Skipped {malformed_count} malformed timing entries", file=sys.stderr)
return entries
def aggregate_timings(entries: List[Tuple[str, float]]) -> Dict[str, Dict[str, float]]:
"""
Aggregate timing entries by phase.
Args:
entries: List of (phase_name, time_seconds) tuples
Returns:
Dictionary mapping phase names to aggregated statistics
"""
if not entries:
return {}
phase_times: Dict[str, List[float]] = {}
for phase, time_val in entries:
if phase not in phase_times:
phase_times[phase] = []
phase_times[phase].append(time_val)
total_time = sum(time_val for _, time_val in entries)
aggregated = {}
for phase, times in phase_times.items():
aggregated[phase] = {
'total_time': sum(times),
'count': len(times),
'mean_time': sum(times) / len(times),
'min_time': min(times),
'max_time': max(times),
'percentage': (sum(times) / total_time * 100) if total_time > 0 else 0.0
}
return aggregated
def identify_slowest_phases(aggregated: Dict[str, Dict[str, float]], top_n: int = 5) -> List[str]:
"""
Identify the slowest computational phases.
Args:
aggregated: Aggregated timing data
top_n: Number of slowest phases to return
Returns:
List of phase names sorted by total time (descending)
"""
if not aggregated:
return []
sorted_phases = sorted(aggregated.items(), key=lambda x: x[1]['total_time'], reverse=True)
return [phase for phase, _ in sorted_phases[:top_n]]
def main():
parser = argparse.ArgumentParser(
description='Extract and analyze timing information from simulation logs'
)
parser.add_argument('--log', required=True, help='Path to simulation log file')
parser.add_argument('--pattern', help='Custom regex pattern for timing entries')
parser.add_argument('--json', action='store_true', help='Output in JSON format')
args = parser.parse_args()
try:
# Parse log file
entries = parse_timing_log(args.log, args.pattern)
# Aggregate timings
aggregated = aggregate_timings(entries)
# Identify slowest phases
slowest = identify_slowest_phases(aggregated)
# Calculate total time
total_time = sum(data['total_time'] for data in aggregated.values())
# Format output
if args.json:
output = {
'inputs': {
'log_file': args.log,
'pattern': args.pattern or 'default'
},
'results': {
'phases': [
{
'name': phase,
**aggregated[phase]
}
for phase in aggregated
],
'total_time': total_time,
'slowest_phase': slowest[0] if slowest else None
}
}
print(json.dumps(output, indent=2))
else:
print(f"Timing Analysis Results")
print(f"=" * 60)
print(f"Total time: {total_time:.2f}s\n")
if aggregated:
print(f"{'Phase':<30} {'Total (s)':<12} {'Count':<8} {'Mean (s)':<12} {'%':<8}")
print("-" * 60)
for phase in slowest:
data = aggregated[phase]
print(f"{phase:<30} {data['total_time']:<12.2f} {data['count']:<8} "
f"{data['mean_time']:<12.2f} {data['percentage']:<8.1f}")
else:
print("No timing data found in log file")
except (FileNotFoundError, ValueError) as e:
if args.json:
print(json.dumps({'error': str(e)}))
else:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
if args.json:
print(json.dumps({'error': f'Unexpected error: {e}'}))
else:
print(f"Unexpected error: {e}", file=sys.stderr)
sys.exit(3)
if __name__ == '__main__':
main()
Related skills
FAQ
What does the skill consider a dominant phase?
A phase using more than 50% of runtime is flagged as poor/dominant per the threshold tables.
Does it need external dependencies?
No, it uses only the Python standard library and works on Linux, macOS, and Windows.