2.2 KiB
tf-0002: DAG EdgesTo O(E) full-edge scan inside O(V) vertex loop — O(V×E) CBD transform
Severity: MEDIUM CWE: CWE-407 (Algorithmic Complexity — linear membership test in hot loop) Speedup: ~100x at V=200 chain graph (verified by unit test) Target: Terraform (hashicorp/terraform) Files:
internal/dag/graph.go:79-90—EdgesTo()iterates all edges O(E) per callinternal/terraform/transform_destroy_cbd.go:122,136—EdgesTo(v)called insidefor _, v := range g.Vertices()
Description
CBDEdgeTransformer.Transform() iterates all vertices and calls EdgesTo(v)
for each destroyer vertex:
for _, v := range g.Vertices() { // O(V)
...
for _, e := range g.EdgesTo(v) { // O(E) — scans ALL edges
EdgesTo() scans the full edge list filtering by target:
func (g *Graph) EdgesTo(v Vertex) []Edge {
var result []Edge
search := hashcode(v)
for _, e := range g.Edges() { // O(E) linear scan
if hashcode(e.Target()) == search {
result = append(result, e)
}
}
return result
}
Total: O(V × E). For V=500 resources and E=1000 edges, this is 500,000 comparisons per destroy plan instead of ~1,500.
Root Cause
EdgesTo queries the full edge set rather than the upEdges index that
the graph already maintains. upEdgesNoCopy(v) returns the set of source
vertices for target v in O(1) via map lookup.
Fix: rewrite EdgesTo to use upEdgesNoCopy(v):
func (g *Graph) EdgesTo(v Vertex) []Edge {
sources := g.upEdgesNoCopy(v)
result := make([]Edge, 0, sources.Len())
for _, src := range sources {
result = append(result, BasicEdge(src.(Vertex), v))
}
return result
}
Cost becomes O(in-degree(v)) per call; O(E) total across all vertices.
Patch
See patch/tf-0002-dag-graph-edgesto-upedges.patch
Complexity Before
EdgesTo(v) per call: O(E)
CBDEdgeTransformer.Transform(): O(V × E)
Complexity After
EdgesTo(v) per call: O(in-degree(v))
CBDEdgeTransformer.Transform(): O(E) total
Reproduction
cd defects/terraform/unit && javac -d . TerraformDagTest.java && java -ea unit.TerraformDagTest
test3: V=200 chain, defect=39800 ops, fixed=399 ops, ratio≈100x