This guide explains bv's performance characteristics, how to diagnose slow startup, and available tuning options.
bv computes 9 graph-theoretic metrics on startup. Their computational complexity varies significantly:
| Metric | Complexity | 100 nodes | 500 nodes | 1000 nodes | 2000 nodes |
|---|---|---|---|---|---|
| Degree | O(V) | <1ms | <1ms | <5ms | <10ms |
| TopologicalSort | O(V+E) | <1ms | <5ms | <5ms | <10ms |
| Critical Path | O(V+E) | <1ms | <5ms | <5ms | <10ms |
| PageRank | O(iter×E) | <5ms | ~20ms | ~40ms | ~100ms |
| Eigenvector | O(iter×E) | <5ms | ~15ms | ~30ms | ~70ms |
| HITS | O(iter×E) | <5ms | ~5ms | ~10ms | ~30ms |
| Betweenness | O(V×E) | ~10ms | ~300ms | ~1.3s | ~4.6s |
| Cycles | O((V+E)×C) | varies | varies | varies | varies |
Key Insight: Betweenness centrality and cycle detection are the primary performance bottlenecks for large graphs.
bv uses a two-phase startup to ensure responsive UI:
Computes metrics needed for initial render:
- Degree centrality (blocking indicators)
- Topological sort (execution order)
- Basic stats (counts, density)
Result: You see the issue list immediately.
Computes expensive metrics in a background goroutine:
- PageRank
- Betweenness (with timeout)
- Eigenvector
- HITS
- Cycle detection
- Critical path scoring
Result: Insights dashboard shows "Computing..." until Phase 2 completes.
- Linear algorithms (degree, topo sort) scale with V+E
- Betweenness scales quadratically: O(V×E)
- For 2000+ nodes, betweenness can take 5+ seconds
density = edges / (nodes × (nodes - 1))
| Density | Classification | Impact |
|---|---|---|
| <0.01 | Sparse | Fast - most real projects |
| 0.01-0.05 | Normal | Standard performance |
| 0.05-0.15 | Dense | Betweenness may timeout |
| >0.15 | Very Dense | Consider simplifying deps |
Cycle detection uses Johnson's algorithm which enumerates ALL elementary cycles:
- Acyclic graphs: Fast (SCC pre-check returns immediately)
- Few cycles: Fast
- Many overlapping cycles: Can be exponential
- Complete graph: O((n-1)!) cycles - pathological case
A complete graph with 20 nodes has 19! ≈ 10^17 cycles. This is why cycle detection has strict timeouts.
bv automatically adjusts algorithm selection based on graph size:
- All metrics computed with exact algorithms
- Generous timeouts (2 seconds)
- Full cycle enumeration (up to 1000)
- All metrics computed with exact algorithms
- Standard timeouts (500ms)
- Cycle limit: 100
- Approximate betweenness for sparse graphs (density < 0.01)
- Betweenness skipped for dense graphs
- Shorter timeouts (200-300ms)
- Cycle limit: 50
- Approximate betweenness (sampling-based)
- Cycle detection skipped
- HITS skipped if density > 0.001
- Minimal timeouts
For large graphs (500+ nodes), bv uses a sampling-based approximation of betweenness centrality instead of the exact O(V×E) algorithm:
Instead of computing shortest paths from ALL nodes, we sample k pivot nodes and extrapolate:
- Randomly select k pivot nodes
- Compute betweenness contribution from each pivot
- Scale up by (n/k) to estimate full betweenness
| Sample Size | Approximate Error |
|---|---|
| k=50 | ~14% |
| k=100 | ~10% |
| k=200 | ~7% |
| Graph Size | Sample Size |
|---|---|
| <100 nodes | Exact (use full algorithm) |
| 100-500 nodes | min(50, 20% of nodes) |
| 500-2000 nodes | 100 |
| >2000 nodes | 200 |
For a 1000-node graph:
- Exact: ~1.3 seconds
- Approximate (k=100): ~130ms (10x faster)
The approximation is sufficient for ranking purposes (identifying which nodes are most central) while dramatically improving startup time.
# Show detailed startup timing breakdown
bv --profile-startup
# Machine-readable timing (JSON)
bv --profile-startup --profile-jsonSample --profile-startup output:
Startup Profile for /path/to/.beads/beads.jsonl
================================================
Data: 847 issues, 2341 dependencies, density=0.003
Phase 1 (blocking):
Build graph: 12ms
Degree: 3ms
TopoSort: 5ms
Total Phase 1: 20ms
Phase 2 (async):
PageRank: 45ms
Betweenness: 312ms (timeout: NO)
Eigenvector: 28ms
HITS: 19ms
Cycles: 67ms (found: 3)
Critical Path: 11ms
Total Phase 2: 482ms
Total startup: 502ms
Recommendations:
✓ Startup within acceptable range (<1s)
⚠ Betweenness taking 60% of Phase 2 time
Consider: --force-full-analysis only when needed
# Force compute ALL metrics regardless of graph size
# (May be slow for large graphs - use sparingly)
bv --force-full-analysisbv --profile-startupIdentify which phase/metric is slow.
bv --robot-insights | jq '.stats | {nodeCount, edgeCount, density}'For large graphs (>500 nodes), some metrics are automatically skipped.
bv --robot-insights | jq '.cycles'Many cycles can cause slowdowns even with timeouts due to memory pressure.
If betweenness is the bottleneck:
- Check if your graph is >500 nodes - it should auto-skip
- If not, consider if you need betweenness metrics
If cycles are the bottleneck:
- Review your dependencies for circular patterns
- Use
bdto break cycles:bd unblock A --from B
If startup is slow and profiling shows unexpected behavior:
bv --profile-startup --profile-json > profile.jsonInclude profile.json in your bug report.
| Graph Size | Target Startup |
|---|---|
| <100 nodes | <100ms |
| 100-500 nodes | <300ms |
| 500-1000 nodes | <500ms |
| 1000-2000 nodes | <1s |
| >2000 nodes | <2s |
These targets assume Phase 1 (blocking) startup. Phase 2 completes asynchronously.
-
Keep dependency graphs sparse
- Only create blocking dependencies where truly needed
- Use
relatedtype for informational links
-
Avoid circular dependencies
- Cycles indicate design issues
- Break cycles before they accumulate
-
Monitor graph density
- Healthy: <0.05
- Warning: >0.15
-
Use robot flags for programmatic access
--robot-insightsfor metrics--robot-planfor actionable items
-
Check for timeouts in robot output
- Timeout flags indicate metrics may be incomplete
- Design agents to handle partial data gracefully
-
For large repositories
- Use
--robot-planfor immediate actionable items - Avoid forcing full analysis unless needed
- Use
All expensive algorithms have configurable timeouts:
| Algorithm | Default Timeout | Rationale |
|---|---|---|
| Betweenness | 500ms | O(V×E) can be seconds |
| PageRank | 500ms | Usually fast, defensive |
| HITS | 500ms | Usually fast, defensive |
| Cycle Detection | 500ms | Can be exponential |
When a timeout triggers:
- The metric is skipped or returns partial results
- A warning appears in the profile output
- The UI marks the metric as unavailable
For very large graphs:
-
Cycle enumeration is memory-hungry
- Each cycle stores full path
- Limited to
MaxCyclesToStore(default: 100)
-
Graph structure uses gonum's sparse representation
- Efficient for sparse graphs
- ~100 bytes per node + ~50 bytes per edge
-
Typical memory usage
- 1000 issues, 3000 deps: ~5MB
- 5000 issues, 15000 deps: ~25MB
- 10000 issues, 30000 deps: ~50MB
Run the benchmark suite to measure performance on your hardware:
# Run all benchmarks
./scripts/benchmark.sh
# Save baseline
./scripts/benchmark.sh baseline
# Compare after changes
./scripts/benchmark.sh compare
# Quick benchmarks (CI mode)
./scripts/benchmark.sh quickSee benchmarks/ directory for detailed results.