executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
build-performance-optimizationProfiles, analyzes, and optimizes slow builds through bottleneck identification, incremental compilation strategies, intelligent caching, and parallelization techniques for C++, Java, Python, TypeScript, and Go projects.
Independent GitHub snapshot · Automated classification · Original documentation governs
name: build-performance-optimization description: Profiles, analyzes, and optimizes slow builds through bottleneck identification, incremental compilation strategies, intelligent caching, and parallelization techniques for C++, Java, Python, TypeScript, and Go projects. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding role: implementation scope: implementation output-format: code triggers: build performance, compilation time, build profiling, incremental build, parallel build, bottleneck analysis, build cache, build speed related-skills: makefile-best-practices,cicd-build-orchestration archetypes: tactical, diagnostic anti_triggers: vague build questions, general build overview, build system migration response_profile: verbosity: high directive_strength: high abstraction_level: operational
When a build takes 20 minutes and should take 2, the problem isn't how you're running it—it's what you're building and how. This skill teaches you to measure precisely, identify bottlenecks ruthlessly, and optimize systematically. We focus on profiling tools, dependency analysis, caching strategies, and parallelization that actually work.
--verbose --profile flags or custom timing instrumentation.-j$(nproc) (Make), --parallel (Gradle), -j (Ninja), compiler flags for parallel codegen.-fuse-ld=mold or lld, reduce debug symbol bloat with split-dwarf, parallelize linking with -Wl,--threads.Use this skill when:
Avoid this skill for:
makefile-best-practices instead)cicd-build-orchestration instead)Before touching any optimization, you must have numbers. Run a timing harness that captures:
-j4 vs -j1)Checkpoint: You have a spreadsheet or JSON file with at least 3 runs of each metric. Standard deviation is <10%.
Use your build system's native profiling:
time make -j$(nproc) + make --debug=b for detailed tracingninja -d stats to dump build profilegradle build --profile generates HTML reporttsc --diagnostics shows compilation time per filego build -x shows all commands; time each with custom wrapperbazel build --profile=/tmp/profile.json then analyze with bazel analyze-profileParse the output to identify which phase is slowest:
Checkpoint: You have a ranked list of top 5 slowest compilation units or link phases, with times.
For the top bottleneck, drill deeper:
If compilation is slow:
clang++ -ftime-trace (C++), javac -verbose (Java), tsc --extendedDiagnostics (TypeScript)-O0 instead of -O2)If linking is slow:
nm --size-sort on object files to find the largest ones (often opportunities for lazy linking)--whole-archive or equivalent that's forcing all symbols to be linkedld.lld --time-trace-file=/tmp/lld.json (if using lld)If incremental builds are slow (proportional to clean builds):
make -n -j1 and count headers included per translation unitgcc -MM or clang++ -M to dump dependencies; look for headers included by everythingCheckpoint: You have identified the specific, measurable bottleneck (e.g., "linking takes 45 seconds", "header template.hpp is included 342 times").
Based on the bottleneck type:
Compilation Bottleneck:
-j$(nproc) or build --jobs=autoLinking Bottleneck:
fuse-ld=mold (GNU gold alternative, 10-20x faster), lld (LLVM), or zld (Mach-O)-Wl,--threads (GNU gold), already parallel in lld/mold-gsplit-dwarf separates debug info from binaries (Clang/GCC)-flto=thin reduces linker overheadIncremental Build Bottleneck:
Parallelization Not Working:
ninja -d graph or bazel query 'deps(...)'-j8 on 4-core, observe wall timeCheckpoint: You've applied 1-2 targeted optimizations and re-measured. Verify improvement is >15% (noise floor). If <15%, revert and try next bottleneck.
Once optimizations are in place, run a formal benchmark:
sha256sum or cmp object files before/after.Checkpoint: Benchmark results are reproducible, improvement is documented (e.g., "15% faster on incremental builds"), and no artifacts changed.
Create a build-optimization checklist for your team:
ninja -d stats, etc.)Automate checks in CI: run a build-time-benchmark step that fails if incremental builds exceed a threshold (e.g., >60 seconds).
This script measures clean build, incremental build, and parallelization gains. Provides a baseline for all optimizations.
#!/usr/bin/env python3
"""
Build profiler: measures clean build time, incremental build time, and parallelization factor.
Outputs JSON for comparison across builds.
"""
import json
import subprocess
import sys
import time
from pathlib import Path
from hashlib import sha256
def run_command(cmd, cwd=None):
"""Run command, return (exit_code, stdout, stderr, elapsed_seconds)."""
start = time.time()
result = subprocess.run(
cmd,
cwd=cwd,
shell=isinstance(cmd, str),
capture_output=True,
text=True
)
elapsed = time.time() - start
return result.returncode, result.stdout, result.stderr, elapsed
def clean_build(build_dir, build_cmd, runs=3):
"""Run clean build N times, return mean time in seconds."""
times = []
for i in range(runs):
# Remove build artifacts
subprocess.run(f"rm -rf {build_dir}", shell=True, check=True)
# Run clean build
_, _, _, elapsed = run_command(build_cmd)
times.append(elapsed)
print(f" Clean build {i+1}: {elapsed:.2f}s")
return {
"mean": sum(times) / len(times),
"min": min(times),
"max": max(times),
"runs": times,
}
def incremental_build(build_dir, build_cmd, source_file, runs=3):
"""
Run a clean build, then modify a source file and rebuild N times.
Measures incremental rebuild time.
"""
# Ensure clean state
subprocess.run(f"rm -rf {build_dir}", shell=True, check=True)
_, _, _, _ = run_command(build_cmd)
# Get a source file to touch
if not Path(source_file).exists():
print(f"Warning: {source_file} not found, skipping incremental build test")
return None
times = []
original_mtime = Path(source_file).stat().st_mtime
for i in range(runs):
# Touch source file to trigger rebuild
Path(source_file).touch()
time.sleep(0.1) # Ensure new mtime
_, _, _, elapsed = run_command(build_cmd)
times.append(elapsed)
print(f" Incremental build {i+1}: {elapsed:.2f}s")
# Restore original mtime
Path(source_file).utime((original_mtime, original_mtime))
return {
"mean": sum(times) / len(times),
"min": min(times),
"max": max(times),
"runs": times,
}
def parallelization_factor(build_cmd, num_jobs_list=[1, 2, 4, 8, 16]):
"""
Measure build time with different -j values.
Returns dict of job_count -> elapsed_seconds.
"""
results = {}
for jobs in num_jobs_list:
cmd = build_cmd.replace("$(nproc)", str(jobs)).replace("-j", f"-j{jobs}")
_, _, _, elapsed = run_command(cmd)
results[jobs] = elapsed
print(f" -j{jobs}: {elapsed:.2f}s")
# Calculate speedup relative to -j1
if 1 in results:
baseline = results[1]
speedup = {jobs: baseline / elapsed for jobs, elapsed in results.items()}
results["speedup_vs_j1"] = speedup
return results
def main():
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <build_dir> <build_command> [source_file]")
print(f"Example: {sys.argv[0]} build 'ninja -C build -j$(nproc)' src/main.cpp")
sys.exit(1)
build_dir = sys.argv[1]
build_cmd = sys.argv[2]
source_file = sys.argv[3] if len(sys.argv) > 3 else None
results = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"build_command": build_cmd,
"build_dir": build_dir,
}
print("\n=== Clean Build Profile ===")
results["clean_build"] = clean_build(build_dir, build_cmd, runs=3)
print(f"Mean: {results['clean_build']['mean']:.2f}s")
if source_file:
print("\n=== Incremental Build Profile ===")
results["incremental_build"] = incremental_build(
build_dir, build_cmd, source_file, runs=3
)
print(f"Mean: {results['incremental_build']['mean']:.2f}s")
ratio = results["incremental_build"]["mean"] / results["clean_build"]["mean"]
print(f"Incremental/Clean ratio: {ratio:.2%}")
print("\n=== Parallelization Factor ===")
results["parallelization"] = parallelization_factor(build_cmd)
print("\n=== Results (JSON) ===")
print(json.dumps(results, indent=2))
# Save to file for comparison
output_file = Path("build_profile.json")
output_file.write_text(json.dumps(results, indent=2))
print(f"\nResults saved to {output_file}")
if __name__ == "__main__":
main()
Usage:
./build_profiler.py build "ninja -C build -j$(nproc)" src/main.cpp
Output: JSON with clean build time, incremental build time, parallelization speedup curves, and statistical measures.
# BAD: Serializes linking, no cache, forces full relink on any change
CC = g++
CFLAGS = -std=c++17 -O0 # -O0 disables optimizations by accident!
LDFLAGS = -Wl,--whole-archive # Forces all symbols linked
SRCS = $(wildcard src/*.cpp)
OBJS = $(SRCS:.cpp=.o)
app: $(OBJS)
$(CC) $(LDFLAGS) -o app $(OBJS) -lm # Serial link, no -j
%.o: %.cpp
$(CC) $(CFLAGS) -c lt; -o $@
clean:
rm -f $(OBJS) app
Problems:
-O0 means slow code generation.cpp forces relink of entire appMeasured impact: Clean build 45 seconds, incremental 38 seconds (should be <2 seconds for one file change).
# GOOD: Parallel build, fast linker, ccache integration, incremental checks
CC = ccache g++ # Cache compiled objects
CXX_FLAGS = -std=c++17 -O2 -fuse-ld=mold # mold is 10x faster than GNU ld
LDFLAGS = -Wl,--threads -flto=thin # Parallel link, thin LTO for speed
SRCS = $(wildcard src/*.cpp)
OBJS = $(SRCS:.cpp=.o)
DEPS = $(OBJS:.o=.d)
-include $(DEPS) # Include generated dependencies
app: $(OBJS)
@echo "Linking $@..."
$(CC) $(LDFLAGS) -o app $(OBJS) -lm
%.o: %.cpp
@echo "Building lt;..."
$(CC) $(CXX_FLAGS) -MMD -MP -c lt; -o $@
clean:
rm -f $(OBJS) $(DEPS) app
# Validate incremental rebuild time
benchmark:
@echo "Clean build:"
@time make clean && time make -j$(nproc)
@echo "\nIncremental (touch one file):"
@touch src/main.cpp && time make -j$(nproc)
Improvements:
-Wl,--threads-MMD -MP) makes incremental builds accurateMeasured impact: Clean build 8 seconds (5.6x faster), incremental 0.3 seconds (127x faster).
For C++ projects where headers are recompiled excessively, analyze and fix dependency chains.
#!/bin/bash
# analyze_header_deps.sh - Find which headers are slowing incremental builds
TARGET_SOURCE="${1:-src/main.cpp}"
BUILD_DIR="${2:-.}"
if [ ! -f "$TARGET_SOURCE" ]; then
echo "Usage: $0 <source_file> [build_dir]"
exit 1
fi
echo "=== Header Dependency Analysis for $TARGET_SOURCE ==="
echo ""
# Step 1: Dump all included headers using preprocessor
echo "Step 1: Extracting included headers..."
gcc -E -H "$TARGET_SOURCE" 2>&1 | grep "^ " | sed 's/^ //' > /tmp/headers.txt
HEADER_COUNT=$(wc -l < /tmp/headers.txt)
echo "Total headers included: $HEADER_COUNT"
# Step 2: Find headers included in multiple compilation units
echo ""
echo "Step 2: Headers included in every compilation unit (likely culprits)..."
for src in src/*.cpp; do
gcc -E -H "$src" 2>&1 | grep "^ " | sed 's/^ //'
done | sort | uniq -c | sort -rn | head -20 > /tmp/shared_headers.txt
echo "Top 20 most-included headers:"
cat /tmp/shared_headers.txt | awk '{print $2, "(" $1 " files)"}'
# Step 3: Analyze size and compile time of problematic headers
echo ""
echo "Step 3: Identifying expensive headers (large, heavily templated)..."
while read count header; do
if [ -f "$header" ]; then
size=$(wc -l < "$header")
echo "$header: $size lines (included in $count files)"
fi
done < /tmp/shared_headers.txt | sort -t: -k2 -rn | head -10
# Step 4: Suggest remediation
echo ""
echo "Step 4: Remediation suggestions:"
echo "- Use forward declarations instead of full includes where possible"
echo "- Consider splitting headers: separate interface (forward decls) from impl (definitions)"
echo "- Use precompiled headers for headers included >50 times"
echo "- Consider using C++20 modules to replace header includes entirely"
# Step 5: Measure impact
echo ""
echo "Step 5: Measuring incremental build impact..."
echo "Touch a single .cpp file and measure rebuild time:"
original_mtime=$(stat -c %Y "$TARGET_SOURCE")
touch "$TARGET_SOURCE"
sleep 0.1
echo " Before optimization: (run benchmark now)"
time make clean && time make -j$(nproc)
# Restore
touch -d "@$original_mtime" "$TARGET_SOURCE"
Usage:
./analyze_header_deps.sh src/main.cpp build/
Output: Lists the 20 most-included headers, their sizes, and suggests which to optimize (precompiled headers, forward declarations, C++20 modules).
-j1, -j2, -j4, -j8. Identify the speedup curve and scaling limits.sha256sum or cmp.-O3, full LTO, and debug symbol stripping trade build time for runtime performance. Choose based on context (CI vs. local dev).-j32 on a 4-core machine wastes resources and can thrash. Use -j$(nproc) or measure your optimal -j value.make to ninja or bazel changes parallelization behavior. Always benchmark before/after.Root Cause: Missing header dependencies or forced rebuilds.
Diagnosis:
# Does every .cpp recompile when you change main.cpp?
touch src/main.cpp
time make -j$(nproc) # Should be <2 seconds for one file change
Solution:
target_include_directories(... PUBLIC). In Make: use -MMD -MP flags.gcc -MM src/main.cpp to dump actual dependencies; compare to build system's understanding.Root Cause: Slow linker (GNU ld) or too many symbols.
Diagnosis:
ninja -d stats # Shows Linking [target] time
# If linking is >30% of total time for a moderately-sized project, optimize.
Solution:
-fuse-ld=mold (recommended), -fuse-ld=lld, or -fuse-ld=gold.-Wl,--threads (GNU gold/mold) or use lld (already parallel).-gsplit-dwarf (separates debug from binary).--whole-archive (forces all symbols linked). Use .a static libraries with selective symbol export.Root Cause: Insufficient task graph parallelism or heavy per-file compilation.
Diagnosis:
# Check task graph width
ninja -d graph build.ninja | grep -c "digraph" # Number of nodes
# If total nodes < 10, not enough parallelism.
# Check per-file compilation time
ninja -d stats | grep "ms" | sort -k3 -rn | head -5 # Slowest files
Solution:
extern template)<iostream>, <regex> includes (use precompiled headers)clang++ -ftime-trace=profile.json shows where time is spent (parsing, codegen, optimization).Root Cause: Cache key is incorrect or includes non-deterministic data.
Diagnosis:
# For ccache
ccache -s # Shows hit/miss ratio
# Expect >50% hit rate on incremental CI builds.
# For Gradle
gradle build --build-cache --info | grep "cache"
Solution:
-D__TIME__ suppression in compiler flags)-ffile-prefix-map in GCC/Clang)make --debug=b (shows all recipes executed)ninja -d stats (outputs timing per target)clang++ -ftime-trace (JSON output) or gcc -ftime-reportgradle build --profile (HTML report with task timing)go build -x (shows all commands); wrap with time for eachtsc --diagnostics --listFilesOnlybazel analyze-profile <profile.json>gcc -MM or clang++ -M| Skill | Purpose |
|---|---|
makefile-best-practices | How to structure Makefiles, declare dependencies correctly, and write portable build recipes |
cicd-build-orchestration | How to cache and parallelize builds across CI/CD jobs, orchestrate build pipelines |