Skip to content

Commit 8b1ed6b

Browse files
committed
perf: batch node persistence during builds
1 parent 701b3bc commit 8b1ed6b

2 files changed

Lines changed: 119 additions & 17 deletions

File tree

internal/app/ingest/workflow/build.go

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,19 @@ func (b *buildPersistBatch) reset() {
107107
b.bytes = 0
108108
}
109109

110-
// bindAndReleaseNodeBatch upserts a parsed file's nodes and binds its comment annotations within a transaction-scoped store.
111-
// @intent persist nodes and their annotation bindings atomically per file before releasing comment buffers.
112-
// @sideEffect writes graph nodes and annotation rows via the transaction-scoped store.
113-
// @mutates graph nodes and annotations
114-
func (s *Service) bindAndReleaseNodeBatch(ctx context.Context, txStore ingestapp.GraphStore, batches []parsedBuildNodeBatch, idx int) error {
110+
// bindAndReleaseNodeBatch binds a parsed file's comments after its nodes have persisted, then releases comment buffers.
111+
// @intent preserve per-file annotation binding and release behavior after the enclosing flush persists all nodes together.
112+
// @sideEffect writes annotation rows via the transaction-scoped store.
113+
// @mutates graph annotations
114+
func (s *Service) bindAndReleaseNodeBatch(ctx context.Context, txStore ingestapp.GraphStore, storedNodesByFile map[string][]graph.Node, batches []parsedBuildNodeBatch, idx int) error {
115115
parsed := &batches[idx]
116116

117-
if err := txStore.UpsertNodes(ctx, parsed.nodes); err != nil {
118-
return trace.Wrap(err, "upsert nodes for "+parsed.relPath)
119-
}
120-
121117
if len(parsed.tsComments) > 0 {
122118
binderComments := toBinderComments(parsed.tsComments)
123119
binder := binding.NewBinder()
124120
bindings := binder.Bind(binderComments, parsed.nodes, parsed.language, parsed.sourceLines)
125121

126-
storedNodes, err := txStore.GetNodesByFile(ctx, parsed.relPath)
127-
if err != nil {
128-
return trace.Wrap(err, "get stored nodes for annotations")
129-
}
122+
storedNodes := storedNodesByFile[parsed.relPath]
130123
storedMap := make(map[string]*graph.Node, len(storedNodes))
131124
for i := range storedNodes {
132125
key := storedNodes[i].QualifiedName + ":" + strconv.Itoa(storedNodes[i].StartLine)
@@ -602,19 +595,50 @@ func (s *Service) packageSemanticEdgeBatches(batches []parsedBuildNodeBatch) []p
602595
return out
603596
}
604597

605-
// flushBuildBatch persists the buffered nodes for the current batch.
606-
// @intent persist nodes before all edges so foreign-key style references can resolve.
598+
// flushBuildBatch persists the buffered nodes for the current bounded batch.
599+
// @intent persist all batch nodes before annotations and all edges so references can resolve with fewer store operations.
607600
// @sideEffect upserts graph nodes and annotations through the transaction-scoped store.
608601
// @mutates graph nodes and annotations
609602
func (s *Service) flushBuildBatch(ctx context.Context, txStore ingestapp.GraphStore, batch *buildPersistBatch) error {
610603
if batch.files == 0 {
611604
return nil
612605
}
606+
if err := ctx.Err(); err != nil {
607+
return err
608+
}
609+
610+
nodeCount := 0
611+
annotationFilePaths := make([]string, 0, len(batch.nodeBatches))
612+
for _, parsed := range batch.nodeBatches {
613+
nodeCount += len(parsed.nodes)
614+
if len(parsed.tsComments) > 0 {
615+
annotationFilePaths = append(annotationFilePaths, parsed.relPath)
616+
}
617+
}
618+
nodes := make([]graph.Node, 0, nodeCount)
619+
for _, parsed := range batch.nodeBatches {
620+
nodes = append(nodes, parsed.nodes...)
621+
}
622+
if len(nodes) > 0 {
623+
if err := txStore.UpsertNodes(ctx, nodes); err != nil {
624+
return trace.Wrap(err, "upsert batch nodes")
625+
}
626+
}
627+
628+
var storedNodesByFile map[string][]graph.Node
629+
if len(annotationFilePaths) > 0 {
630+
stored, err := txStore.GetNodesByFiles(ctx, annotationFilePaths)
631+
if err != nil {
632+
return trace.Wrap(err, "get stored nodes for annotations")
633+
}
634+
storedNodesByFile = stored
635+
}
636+
613637
for i := range batch.nodeBatches {
614638
if err := ctx.Err(); err != nil {
615639
return err
616640
}
617-
if err := s.bindAndReleaseNodeBatch(ctx, txStore, batch.nodeBatches, i); err != nil {
641+
if err := s.bindAndReleaseNodeBatch(ctx, txStore, storedNodesByFile, batch.nodeBatches, i); err != nil {
618642
return err
619643
}
620644
}

internal/app/ingest/workflow/indexer_test.go

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ type recordingGraphStore struct {
116116
nextID uint
117117
nodesByFP map[string][]graph.Node
118118
edges []graph.Edge
119+
upsertedNodeBatches [][]graph.Node
119120
upsertedEdges [][]graph.Edge
120121
fileSuffixLookupCalls int
121122
importFileNodeCalls int
@@ -137,6 +138,7 @@ func (r *recordingGraphStore) DeleteGraph(ctx context.Context) error {
137138

138139
func (r *recordingGraphStore) UpsertNodes(ctx context.Context, nodes []graph.Node) error {
139140
r.record("UpsertNodes")
141+
r.upsertedNodeBatches = append(r.upsertedNodeBatches, append([]graph.Node(nil), nodes...))
140142
for i := range nodes {
141143
r.nextID++
142144
nodes[i].ID = r.nextID
@@ -207,6 +209,7 @@ func (r *recordingGraphStore) GetNodesByQualifiedNames(ctx context.Context, name
207209
}
208210

209211
func (r *recordingGraphStore) GetNodesByFiles(ctx context.Context, filePaths []string) (map[string][]graph.Node, error) {
212+
r.record("GetNodesByFiles")
210213
set := make(map[string]bool, len(filePaths))
211214
for _, fp := range filePaths {
212215
set[fp] = true
@@ -2334,7 +2337,7 @@ func Keep() {}
23342337
t.Fatalf("Build: %v", err)
23352338
}
23362339

2337-
want := []string{"DeleteGraph", "UpsertNodes", "GetNodesByFile", "UpsertAnnotation", "UpsertEdges"}
2340+
want := []string{"DeleteGraph", "UpsertNodes", "GetNodesByFiles", "UpsertAnnotation", "UpsertEdges"}
23382341
for i, op := range want[:4] {
23392342
if len(fakeStore.ops) <= i {
23402343
t.Fatalf("ops too short: got %v", fakeStore.ops)
@@ -2359,6 +2362,81 @@ func Keep() {}
23592362
}
23602363
}
23612364

2365+
func TestFlushBuildBatch_BatchesNodesAndAnnotationLookups(t *testing.T) {
2366+
fakeStore := newRecordingGraphStore(t)
2367+
released := 0
2368+
svc := &Service{
2369+
onBatchRelease: func([]parsedBuildNodeBatch, int) {
2370+
released++
2371+
},
2372+
}
2373+
batch := buildPersistBatch{
2374+
files: 2,
2375+
nodeBatches: []parsedBuildNodeBatch{
2376+
{
2377+
relPath: "first.go",
2378+
nodes: []graph.Node{{
2379+
QualifiedName: "sample.First",
2380+
FilePath: "first.go",
2381+
Kind: graph.NodeKindFunction,
2382+
StartLine: 2,
2383+
EndLine: 2,
2384+
}},
2385+
tsComments: []ingest.CommentBlock{{StartLine: 1, EndLine: 1, Text: "@intent first"}},
2386+
language: "go",
2387+
sourceLines: []string{"// @intent first", "func First() {}"},
2388+
},
2389+
{
2390+
relPath: "second.go",
2391+
nodes: []graph.Node{{
2392+
QualifiedName: "sample.Second",
2393+
FilePath: "second.go",
2394+
Kind: graph.NodeKindFunction,
2395+
StartLine: 2,
2396+
EndLine: 2,
2397+
}},
2398+
tsComments: []ingest.CommentBlock{{StartLine: 1, EndLine: 1, Text: "@intent second"}},
2399+
language: "go",
2400+
sourceLines: []string{"// @intent second", "func Second() {}"},
2401+
},
2402+
},
2403+
}
2404+
2405+
if err := svc.flushBuildBatch(context.Background(), fakeStore, &batch); err != nil {
2406+
t.Fatalf("flushBuildBatch: %v", err)
2407+
}
2408+
if got := len(fakeStore.upsertedNodeBatches); got != 1 {
2409+
t.Fatalf("node upsert calls = %d, want 1 (ops=%v)", got, fakeStore.ops)
2410+
}
2411+
if got := len(fakeStore.upsertedNodeBatches[0]); got != 2 {
2412+
t.Fatalf("batched node count = %d, want 2", got)
2413+
}
2414+
countOperation := func(want string) int {
2415+
count := 0
2416+
for _, op := range fakeStore.ops {
2417+
if op == want {
2418+
count++
2419+
}
2420+
}
2421+
return count
2422+
}
2423+
if got := countOperation("GetNodesByFiles"); got != 1 {
2424+
t.Fatalf("bulk annotation node lookups = %d, want 1 (ops=%v)", got, fakeStore.ops)
2425+
}
2426+
if got := countOperation("GetNodesByFile"); got != 0 {
2427+
t.Fatalf("per-file annotation node lookups = %d, want 0 (ops=%v)", got, fakeStore.ops)
2428+
}
2429+
if got := countOperation("UpsertAnnotation"); got != 2 {
2430+
t.Fatalf("annotation upserts = %d, want 2 (ops=%v)", got, fakeStore.ops)
2431+
}
2432+
if released != 2 {
2433+
t.Fatalf("released batches = %d, want 2", released)
2434+
}
2435+
if batch.files != 0 || len(batch.nodeBatches) != 0 {
2436+
t.Fatalf("batch was not reset: %+v", batch)
2437+
}
2438+
}
2439+
23622440
func TestBuild_FlushesLargeBuildInBoundedBatches(t *testing.T) {
23632441
fakeStore := newRecordingGraphStore(t)
23642442
svc := &Service{

0 commit comments

Comments
 (0)