Skip to content

Commit 5742d01

Browse files
akoclaude
andcommitted
perf: pre-warm name lookup maps to eliminate O(n²) BSON parsing in catalog source
Each describeMicroflow() call was re-parsing ALL domain models, microflows, and pages from BSON to build name lookup maps. With 925 microflows × 8 workers × 3 list calls, that's ~2,775 full BSON parsing passes. Pre-warm entityNames, microflowNames, and pageNames maps once in PreWarmCache() and share across all parallel describe goroutines. Reduces source generation from O(n²) to O(n). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f7b2879 commit 5742d01

File tree

3 files changed

+87
-24
lines changed

3 files changed

+87
-24
lines changed

mdl/executor/cmd_catalog.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/mendixlabs/mxcli/mdl/ast"
1515
"github.com/mendixlabs/mxcli/mdl/catalog"
16+
"github.com/mendixlabs/mxcli/model"
1617
)
1718

1819
// execShowCatalogTables handles SHOW CATALOG TABLES.
@@ -788,10 +789,39 @@ func (e *Executor) captureDescribeParallel(objectType string, qualifiedName stri
788789
return buf.String(), nil
789790
}
790791

791-
// PreWarmCache ensures the hierarchy cache is populated before parallel operations.
792+
// PreWarmCache ensures all caches are populated before parallel operations.
792793
// Must be called from the main goroutine before using captureDescribeParallel.
794+
// This avoids O(n²) re-parsing in describe functions by building name lookup
795+
// maps once and sharing them across all goroutines.
793796
func (e *Executor) PreWarmCache() {
794-
e.getHierarchy()
797+
h, _ := e.getHierarchy()
798+
if h == nil || e.cache == nil {
799+
return
800+
}
801+
802+
// Build entity name lookup
803+
e.cache.entityNames = make(map[model.ID]string)
804+
dms, _ := e.reader.ListDomainModels()
805+
for _, dm := range dms {
806+
modName := h.GetModuleName(dm.ContainerID)
807+
for _, ent := range dm.Entities {
808+
e.cache.entityNames[ent.ID] = modName + "." + ent.Name
809+
}
810+
}
811+
812+
// Build microflow name lookup
813+
e.cache.microflowNames = make(map[model.ID]string)
814+
mfs, _ := e.reader.ListMicroflows()
815+
for _, mf := range mfs {
816+
e.cache.microflowNames[mf.ID] = h.GetQualifiedName(mf.ContainerID, mf.Name)
817+
}
818+
819+
// Build page name lookup
820+
e.cache.pageNames = make(map[model.ID]string)
821+
pgs, _ := e.reader.ListPages()
822+
for _, pg := range pgs {
823+
e.cache.pageNames[pg.ID] = h.GetQualifiedName(pg.ContainerID, pg.Name)
824+
}
795825
}
796826

797827
// execSearch handles SEARCH 'query' command.

mdl/executor/cmd_microflows_show.go

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -267,35 +267,21 @@ func (e *Executor) describeMicroflow(name ast.QualifiedName) error {
267267
return fmt.Errorf("failed to build hierarchy: %w", err)
268268
}
269269

270-
// Build entity name lookup for resolving entity IDs
271-
entityNames := make(map[model.ID]string)
272-
domainModels, _ := e.reader.ListDomainModels()
273-
for _, dm := range domainModels {
274-
modName := h.GetModuleName(dm.ContainerID)
275-
for _, entity := range dm.Entities {
276-
entityNames[entity.ID] = modName + "." + entity.Name
277-
}
278-
}
279-
280-
// Build microflow name lookup for resolving microflow IDs
281-
microflowNames := make(map[model.ID]string)
282-
283-
// Build page name lookup for resolving page IDs
284-
pageNames := make(map[model.ID]string)
285-
allPages, _ := e.reader.ListPages()
286-
for _, p := range allPages {
287-
pageNames[p.ID] = h.GetQualifiedName(p.ContainerID, p.Name)
288-
}
270+
// Use pre-warmed cache if available (from PreWarmCache), otherwise build on demand
271+
entityNames := e.getEntityNames(h)
272+
microflowNames := e.getMicroflowNames(h)
289273

290274
// Find the microflow
291275
allMicroflows, err := e.reader.ListMicroflows()
292276
if err != nil {
293277
return fmt.Errorf("failed to list microflows: %w", err)
294278
}
295279

296-
// Build microflow name lookup
297-
for _, mf := range allMicroflows {
298-
microflowNames[mf.ID] = h.GetQualifiedName(mf.ContainerID, mf.Name)
280+
// Supplement microflow name lookup if not pre-warmed
281+
if len(microflowNames) == 0 {
282+
for _, mf := range allMicroflows {
283+
microflowNames[mf.ID] = h.GetQualifiedName(mf.ContainerID, mf.Name)
284+
}
299285
}
300286

301287
var targetMf *microflows.Microflow

mdl/executor/executor.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type executorCache struct {
3434

3535
// Track domain models modified during this session for finalization
3636
modifiedDomainModels map[model.ID]string // domain model unit ID -> module name
37+
38+
// Pre-warmed name lookup maps for parallel describe (goroutine-safe after init)
39+
entityNames map[model.ID]string // entity ID -> "Module.EntityName"
40+
microflowNames map[model.ID]string // microflow ID -> "Module.MicroflowName"
41+
pageNames map[model.ID]string // page ID -> "Module.PageName"
3742
}
3843

3944
// createdMicroflowInfo tracks a microflow created during this session.
@@ -61,6 +66,48 @@ type createdSnippetInfo struct {
6166
ContainerID model.ID
6267
}
6368

69+
// getEntityNames returns the entity name lookup map, using the pre-warmed cache if available.
70+
func (e *Executor) getEntityNames(h *ContainerHierarchy) map[model.ID]string {
71+
if e.cache != nil && len(e.cache.entityNames) > 0 {
72+
return e.cache.entityNames
73+
}
74+
entityNames := make(map[model.ID]string)
75+
dms, _ := e.reader.ListDomainModels()
76+
for _, dm := range dms {
77+
modName := h.GetModuleName(dm.ContainerID)
78+
for _, ent := range dm.Entities {
79+
entityNames[ent.ID] = modName + "." + ent.Name
80+
}
81+
}
82+
return entityNames
83+
}
84+
85+
// getMicroflowNames returns the microflow name lookup map, using the pre-warmed cache if available.
86+
func (e *Executor) getMicroflowNames(h *ContainerHierarchy) map[model.ID]string {
87+
if e.cache != nil && len(e.cache.microflowNames) > 0 {
88+
return e.cache.microflowNames
89+
}
90+
microflowNames := make(map[model.ID]string)
91+
mfs, _ := e.reader.ListMicroflows()
92+
for _, mf := range mfs {
93+
microflowNames[mf.ID] = h.GetQualifiedName(mf.ContainerID, mf.Name)
94+
}
95+
return microflowNames
96+
}
97+
98+
// getPageNames returns the page name lookup map, using the pre-warmed cache if available.
99+
func (e *Executor) getPageNames(h *ContainerHierarchy) map[model.ID]string {
100+
if e.cache != nil && len(e.cache.pageNames) > 0 {
101+
return e.cache.pageNames
102+
}
103+
pageNames := make(map[model.ID]string)
104+
pgs, _ := e.reader.ListPages()
105+
for _, pg := range pgs {
106+
pageNames[pg.ID] = h.GetQualifiedName(pg.ContainerID, pg.Name)
107+
}
108+
return pageNames
109+
}
110+
64111
const (
65112
// maxOutputLines is the per-statement line limit. Statements that produce more
66113
// lines than this are aborted to prevent runaway output from infinite loops.

0 commit comments

Comments
 (0)