05_02_02_path_analysis - Análisis de Caminos y Rutas Críticas¶
🛤️ Propósito¶
El Path Analysis Engine identifica caminos específicos a través del grafo, calculando métricas agregadas y detectando puntos problemáticos. Responde preguntas como: - "¿Cuál es el camino crítico que determina el tiempo de build?" - "Si modifico este módulo, qué más se afecta?" - "¿Cómo se conectan dos módulos específicos?" - "¿Cuáles son los módulos de mayor impacto?"
🏗️ Arquitectura¶
Componentes Principales¶
05_02_02_path_analysis/
├── path_analyzer.hpp # Analizadores de caminos
├── test_path_analysis.cpp # Tests unitarios
└── README.md # Documentación
Clases Implementadas¶
- CriticalPathAnalyzer - Caminos críticos (build time, latency)
- DependencyChainAnalyzer - Cadenas de dependencias (transitive closure)
- ImpactAnalyzer - Blast radius de cambios
- ShortestPathFinder - Rutas más cortas entre módulos
- PathMetricsCalculator - Métricas agregadas de paths
🔍 Tipos de Análisis¶
1. Critical Path Analysis¶
Objetivo: Identificar la secuencia más larga de dependencias que determina el tiempo mínimo de build/procesamiento.
Algoritmo: 1. Topological sort del grafo (DFS) 2. Dynamic programming: calcular longest path desde fuentes 3. Path crítico = path con máximo peso acumulado
Uso:
#include "path_analyzer.hpp"
using namespace audio_lab::dependency_graph::analysis;
Graph graph = /* ... */;
CriticalPathAnalyzer analyzer(graph);
// Critical path por build time (CPU cycles)
auto build_path = analyzer.find_critical_path_build_time();
std::cout << "Build critical path: " << build_path.to_string() << "\n";
std::cout << "Total CPU: " << build_path.total_weight << " cycles\n";
// Critical path por latencia
auto latency_path = analyzer.find_critical_path_latency();
std::cout << "Latency critical path: " << latency_path.to_string() << "\n";
std::cout << "Total latency: " << latency_path.total_weight << " samples\n";
// Custom weight function
auto custom_path = analyzer.find_critical_path([](const NodePtr& node) {
// Peso = CPU * latency (productos con alta latencia y CPU)
return node->cpu_cycles * (node->latency_samples + 1);
});
Output Ejemplo:
Build critical path: add_kernel → svf_filter → synth_voice → poly_synth
Total CPU: 2585 cycles
Latency critical path: svf_filter → synth_voice → poly_synth
Total latency: 6 samples
Complejidad: O(V + E) donde V = nodos, E = aristas
2. Dependency Chain Analysis¶
Objetivo: Calcular transitive closure (todas las dependencias directas e indirectas).
Uso:
DependencyChainAnalyzer chain_analyzer(graph);
// Todas las dependencias de un módulo
auto all_deps = chain_analyzer.get_all_dependencies("poly_synth");
std::cout << "poly_synth needs " << all_deps.size() << " modules:\n";
for (const auto& dep : all_deps) {
std::cout << " - " << dep << "\n";
}
// Todos los dependientes (quién usa este módulo)
auto all_dependents = chain_analyzer.get_all_dependents("add_kernel");
std::cout << "add_kernel is used by " << all_dependents.size() << " modules\n";
// Árbol de dependencias completo
auto tree = chain_analyzer.build_dependency_tree("poly_synth");
// tree es estructura jerárquica navegable
Output Ejemplo:
poly_synth needs 7 modules:
- synth_voice
- sine_osc
- svf_filter
- envelope_generator
- add_kernel
- mul_kernel
- clamp_kernel
add_kernel is used by 5 modules
Complejidad: O(V + E) con BFS
3. Impact Analysis (Blast Radius)¶
Objetivo: Determinar el impacto de modificar un módulo.
Uso:
ImpactAnalyzer impact_analyzer(graph);
// Analizar impacto de cambiar un módulo
auto report = impact_analyzer.analyze_change_impact("svf_filter");
std::cout << report.summary();
// Encontrar módulos de mayor impacto
auto high_impact = impact_analyzer.find_high_impact_modules(5);
std::cout << "Top 5 high-impact modules:\n";
for (const auto& [module, count] : high_impact) {
std::cout << " " << module << ": affects " << count << " modules\n";
}
Output Ejemplo:
Impact of changing 'svf_filter':
Affected modules: 3
Total CPU impact: 2450 cycles
Max latency impact: 3 samples
Max depth: 2
Top 5 high-impact modules:
add_kernel: affects 7 modules
mul_kernel: affects 6 modules
svf_filter: affects 3 modules
sine_osc: affects 2 modules
envelope_generator: affects 2 modules
Aplicación: - Refactoring: Saber cuántos módulos se afectan antes de cambiar - Testing: Priorizar tests de módulos de alto impacto - Code review: Revisar más cuidadosamente cambios de alto impacto
Complejidad: O(V + E) por módulo
4. Shortest Path Queries¶
Objetivo: Encontrar cómo se conectan dos módulos (mínimo saltos).
Uso:
ShortestPathFinder path_finder(graph);
// Camino más corto (bidireccional)
auto path = path_finder.find_shortest_path("poly_synth", "add_kernel");
if (!path.empty()) {
std::cout << "Shortest path: " << path.to_string() << "\n";
std::cout << "Distance: " << path.total_weight << " hops\n";
}
// Todos los caminos (hasta 10)
auto all_paths = path_finder.find_all_paths("poly_synth", "add_kernel", 10);
std::cout << "Found " << all_paths.size() << " paths:\n";
for (const auto& p : all_paths) {
std::cout << " " << p.to_string() << "\n";
}
Output Ejemplo:
Shortest path: poly_synth → synth_voice → svf_filter → add_kernel
Distance: 3 hops
Found 2 paths:
poly_synth → synth_voice → svf_filter → add_kernel
poly_synth → synth_voice → sine_osc → add_kernel
Complejidad: - Shortest: O(V + E) con BFS - All paths: O(V!) en peor caso (con límite configurable)
5. Path Metrics¶
Objetivo: Calcular métricas agregadas de un path.
Uso:
PathMetricsCalculator metrics_calc(graph);
// Calcular métricas de critical path
auto build_path = analyzer.find_critical_path_build_time();
auto metrics = metrics_calc.calculate_metrics(build_path);
std::cout << "Depth: " << metrics.depth << " modules\n";
std::cout << "Total CPU: " << metrics.total_cpu_cycles << " cycles\n";
std::cout << "Total latency: " << metrics.total_latency_samples << " samples\n";
std::cout << "Max latency: " << metrics.max_latency_samples << " samples\n";
std::cout << "Avg CPU/level: " << metrics.avg_cpu_per_level << " cycles\n";
// O usar formato automático
std::cout << metrics_calc.format_metrics(build_path);
Output Ejemplo:
Path: add_kernel → svf_filter → synth_voice → poly_synth
Depth: 4 modules
Total CPU: 2585 cycles
Total Latency: 6 samples
Max Latency: 3 samples
Avg CPU/Level: 646.25 cycles
📊 Casos de Uso Prácticos¶
Caso 1: Optimizar Build Time¶
// 1. Encontrar critical path
auto critical = analyzer.find_critical_path_build_time();
std::cout << "Build bottleneck: " << critical.to_string() << "\n";
// 2. Calcular métricas
auto metrics = metrics_calc.calculate_metrics(critical);
std::cout << "Total build CPU: " << metrics.total_cpu_cycles << " cycles\n";
// 3. Identificar módulo más costoso en path
UUID most_expensive;
uint32_t max_cpu = 0;
for (const auto& node_id : critical.nodes) {
auto node = graph.get_node(node_id);
if (node->cpu_cycles > max_cpu) {
max_cpu = node->cpu_cycles;
most_expensive = node_id;
}
}
std::cout << "Optimize this module first: " << most_expensive
<< " (" << max_cpu << " cycles)\n";
Caso 2: Safe Refactoring¶
// Antes de refactorizar un módulo
std::string module_to_change = "svf_filter";
auto impact = impact_analyzer.analyze_change_impact(module_to_change);
std::cout << "Refactoring safety analysis:\n";
std::cout << " Modules affected: " << impact.affected_modules.size() << "\n";
std::cout << " CPU to rebuild: " << impact.total_cpu_impact << " cycles\n";
if (impact.affected_modules.size() > 10) {
std::cout << " ⚠️ HIGH IMPACT - Review carefully!\n";
} else if (impact.affected_modules.size() > 5) {
std::cout << " ⚡ MEDIUM IMPACT - Test thoroughly\n";
} else {
std::cout << " ✅ LOW IMPACT - Safe to change\n";
}
// Listar módulos afectados
std::cout << "\nModules requiring rebuild:\n";
for (const auto& mod : impact.affected_modules) {
std::cout << " - " << mod << "\n";
}
Caso 3: Understanding Architecture¶
// ¿Cómo se relacionan dos módulos?
auto path = path_finder.find_shortest_path("poly_synth", "add_kernel");
if (path.empty()) {
std::cout << "No connection between modules\n";
} else {
std::cout << "Connection path:\n";
std::cout << path.to_string() << "\n";
std::cout << "\nInterpretation:\n";
for (size_t i = 0; i < path.nodes.size() - 1; ++i) {
auto source = graph.get_node(path.nodes[i]);
auto target = graph.get_node(path.nodes[i + 1]);
std::cout << " " << source->label << " (" << to_string(source->level) << ")\n";
std::cout << " ↓ depends on\n";
}
auto last = graph.get_node(path.nodes.back());
std::cout << " " << last->label << " (" << to_string(last->level) << ")\n";
}
Caso 4: Prioritize Testing¶
// Encontrar módulos críticos para testing
auto high_impact = impact_analyzer.find_high_impact_modules(10);
std::cout << "Test Priority List (by impact):\n";
for (size_t i = 0; i < high_impact.size(); ++i) {
const auto& [module, impact_count] = high_impact[i];
auto node = graph.get_node(module);
std::cout << (i + 1) << ". " << node->label << "\n";
std::cout << " Affects: " << impact_count << " modules\n";
std::cout << " Level: " << to_string(node->level) << "\n";
std::cout << " CPU: " << node->cpu_cycles << " cycles\n\n";
}
🧪 Testing¶
Test Cases¶
TEST_CASE("Critical path finds longest weighted path") {
auto graph = create_linear_graph(); // A→B→C→D
CriticalPathAnalyzer analyzer(graph);
auto path = analyzer.find_critical_path_build_time();
REQUIRE(path.nodes.size() == 4);
REQUIRE(path.nodes[0] == "A");
REQUIRE(path.nodes[3] == "D");
}
TEST_CASE("Dependency chain computes transitive closure") {
auto graph = create_diamond_graph(); // A→B,C ; B,C→D
DependencyChainAnalyzer analyzer(graph);
auto deps = analyzer.get_all_dependencies("D");
REQUIRE(deps.size() == 3);
REQUIRE(deps.count("A") == 1);
REQUIRE(deps.count("B") == 1);
REQUIRE(deps.count("C") == 1);
}
TEST_CASE("Impact analysis calculates blast radius") {
auto graph = create_simple_graph();
ImpactAnalyzer analyzer(graph);
auto report = analyzer.analyze_change_impact("svf_filter");
REQUIRE(report.affected_modules.size() > 0);
REQUIRE(report.total_cpu_impact > 0);
}
TEST_CASE("Shortest path finds minimal connection") {
auto graph = create_simple_graph();
ShortestPathFinder finder(graph);
auto path = finder.find_shortest_path("poly_synth", "add_kernel");
REQUIRE(!path.empty());
REQUIRE(path.nodes.front() == "poly_synth");
REQUIRE(path.nodes.back() == "add_kernel");
}
📈 Complejidad Algorítmica¶
| Operación | Algoritmo | Complejidad Tiempo | Complejidad Espacio |
|---|---|---|---|
| Critical Path | Topological Sort + DP | O(V + E) | O(V) |
| All Dependencies | BFS | O(V + E) | O(V) |
| All Dependents | Reverse BFS | O(V + E) | O(V) |
| Impact Analysis | BFS + Metrics | O(V + E) | O(V) |
| Shortest Path | BFS | O(V + E) | O(V) |
| All Paths | DFS with backtrack | O(V!) peor caso | O(V) |
| Path Metrics | Linear scan | O(k) donde k = path length | O(1) |
🔍 Interpretación de Resultados¶
Critical Path¶
Interpretación: - El build NO puede completar en menos de 2585 cycles - Optimizar módulos en este path tiene máximo impacto - Paralelizar otros paths no reduce tiempo totalImpact Report¶
Interpretación: - Cambiarsvf_filter requiere recompilar 3 módulos
- Total CPU para rebuild: 2450 cycles
- Planning: reservar tiempo para testing downstream
Shortest Path¶
Interpretación: - Acoplamiento indirecto entre poly_synth y add_kernel - Cambios en add_kernel eventualmente afectan poly_synth - 3 niveles de indirección = bajo acoplamiento directo🚀 Próximos Pasos¶
✅ Implementado: - Critical path analysis - Dependency chains (transitive closure) - Impact analysis (blast radius) - Shortest path queries - Path metrics calculation
⏳ Futuras Mejoras: - Dijkstra's algorithm para weighted shortest path - A* para pathfinding con heurísticas - Parallel path computation para grafos grandes - Path caching para queries frecuentes - Temporal analysis (cómo cambian paths entre versiones)
Status: ✅ Path Analysis Completo Analizadores: 5 tipos implementados Complejidad: O(V + E) para mayoría de operaciones