Skip to content

Commit 3170fe3

Browse files
committed
perf(graph_builder): release dedup maps after Snapshot (Task A4)
GraphBuilder.Snapshot extracted deduped nodes/edges into sorted slices but left builder.nodes and builder.edges maps holding references to the same objects. With the slices and maps coexisting for the rest of the enrich pipeline (~30 sec wall time on ~/projects/), ~280 MB of duplicate references stayed live needlessly. Clear the maps inside Snapshot before returning. Snapshot is now single-shot — calling it twice on the same builder returns an empty snapshot (acceptable; the only caller is analyzer.Enrich which calls once). Plan: docs/superpowers/plans/2026-05-13-enrich-oom-fix.md Task A4. Verification: - New TestSnapshotReleasesDedupMaps asserts both nodes + edges maps are nilled after Snapshot returns. - go test ./... -count=1: 876 pass (no regressions).
1 parent e311d99 commit 3170fe3

2 files changed

Lines changed: 30 additions & 1 deletion

File tree

go/internal/analyzer/graph_builder.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ type Snapshot struct {
8585

8686
// Snapshot returns the current state as a sorted, dangling-edge-free
8787
// Snapshot with surfaced dedup/drop counts.
88+
//
89+
// After this call returns, the builder's internal dedup maps are cleared
90+
// (set to nil). This releases ~280 MB of reference pressure at ~/projects/
91+
// scale where the downstream enrich pipeline holds the returned Snapshot
92+
// slices for the lifetime of the function — coexisting with the dedup
93+
// maps was the largest in-memory duplication in the pipeline. Snapshot
94+
// is therefore single-shot: subsequent calls to Snapshot or Add on the
95+
// same builder are not supported.
8896
func (b *GraphBuilder) Snapshot() Snapshot {
8997
b.mu.Lock()
9098
defer b.mu.Unlock()
@@ -109,11 +117,17 @@ func (b *GraphBuilder) Snapshot() Snapshot {
109117
}
110118
sort.Slice(edges, func(i, j int) bool { return edges[i].ID < edges[j].ID })
111119

112-
return Snapshot{
120+
snap := Snapshot{
113121
Nodes: nodes,
114122
Edges: edges,
115123
DedupedNodes: b.dedupedNodes,
116124
DedupedEdges: b.dedupedEdges,
117125
DroppedEdges: dropped,
118126
}
127+
// Release dedup maps so Go GC can collect them while downstream
128+
// enrich stages run. The maps held references to every node and
129+
// edge already projected into the returned slices.
130+
b.nodes = nil
131+
b.edges = nil
132+
return snap
119133
}

go/internal/analyzer/graph_builder_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ import (
77
"github.com/randomcodespace/codeiq/go/internal/model"
88
)
99

10+
func TestSnapshotReleasesDedupMaps(t *testing.T) {
11+
gb := NewGraphBuilder()
12+
gb.Add(&detector.Result{
13+
Nodes: []*model.CodeNode{model.NewCodeNode("x", model.NodeClass, "X")},
14+
Edges: []*model.CodeEdge{{ID: "e:x:x", SourceID: "x", TargetID: "x", Kind: model.EdgeContains}},
15+
})
16+
_ = gb.Snapshot()
17+
if gb.nodes != nil {
18+
t.Errorf("Snapshot must nil GraphBuilder.nodes to allow GC; got len=%d", len(gb.nodes))
19+
}
20+
if gb.edges != nil {
21+
t.Errorf("Snapshot must nil GraphBuilder.edges to allow GC; got len=%d", len(gb.edges))
22+
}
23+
}
24+
1025
func TestGraphBuilderDeduplicatesByID(t *testing.T) {
1126
gb := NewGraphBuilder()
1227
n1 := model.NewCodeNode("a", model.NodeClass, "A")

0 commit comments

Comments
 (0)