Skip to content

perf: cache builtin alias map and DFS cycle-check to fix CompileMCPWorkflow regression#44996

Closed
pelikhan with Copilot wants to merge 2 commits into
mainfrom
copilot/fix-compile-mcp-workflow-performance
Closed

perf: cache builtin alias map and DFS cycle-check to fix CompileMCPWorkflow regression#44996
pelikhan with Copilot wants to merge 2 commits into
mainfrom
copilot/fix-compile-mcp-workflow-performance

Conversation

Copilot AI commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

BenchmarkCompileMCPWorkflow regressed +14% (432µs vs 379µs). Two hot paths were doing redundant work on every compilation: BuiltinModelAliases() deep-copied the 52-entry builtin map on every call, and detectCircularModelAliases ran a full DFS over all 52 builtin keys even though builtins are immutable and cycle-free.

model_aliases.go

  • Added builtinOnlyAliasMap() backed by sync.Once returning a cached shared reference to the merged builtin map
  • MergeImportedModelAliases(nil, nil) — the common case for MCP workflows with no imports — now returns the cached reference instead of deep-copying

model_alias_validation.go

  • detectCircularModelAliases uses a new isBuiltinOnlyAliasMap helper (key-set equality, not just length) to detect when the input is the builtin-only map
  • When builtin-only, DFS runs once via sync.Once and the result is cached for all subsequent calls
  • loadBuiltinModelAliases() errors are now logged with a fallback to full DFS instead of being silently discarded
  • Cached DFS call passes empty markdownPath to avoid embedding a stale caller path in a shared error

Result: ~291–306µs vs reported 432µs (~30% faster); 80 fewer allocs/op, ~10KB less memory per compilation.

…Workflow speedup

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix regression in CompileMCPWorkflow performance perf: cache builtin alias map and DFS cycle-check to fix CompileMCPWorkflow regression Jul 11, 2026
Copilot AI requested a review from pelikhan July 11, 2026 18:09
@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor (perf)
Risk 🟡 Medium
Score 58/100 (Impact 28 + Urgency 15 + Quality 15)
Action 🔄 batch_review
CI ⚪ Unknown (draft, no checks yet)
Batch perf-regressions (with #44994, #44995)

Draft PR — part of perf regression batch. Review together with #44994 and #44995.

Generated by 🔧 PR Triage Agent · 180.8 AIC · ⌖ 7.62 AIC · ⊞ 5.6K ·

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor / perf
Risk 🟡 medium
Score 45/100 (impact:22 + urgency:13 + quality:10)
Action 📦 batch_review
Batch pr-batch:perf-regressions (with #44994, #44995)

Rationale: Draft. Caches builtin alias map and DFS cycle-check to fix CompileMCPWorkflow +14% regression. Small change. Batch with other perf PRs.

Triage run §29183606049

Generated by 🔧 PR Triage Agent · 171.6 AIC · ⌖ 5.63 AIC · ⊞ 5.6K ·

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor (perf)
Risk 🟡 Medium
Score 55/100 (impact: 25, urgency: 16, quality: 14)
Batch perf-regressions (with #44995, #44994)
Action batch_review

Draft. Caches builtin alias map and DFS cycle-check. +72/-1. Review as part of perf-regressions batch.

Generated by 🔧 PR Triage Agent · 48.3 AIC · ⌖ 7.99 AIC · ⊞ 5.6K ·

@pelikhan pelikhan marked this pull request as ready for review July 13, 2026 06:25
Copilot AI review requested due to automatic review settings July 13, 2026 06:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes model-alias compilation by caching immutable builtin data and cycle-check results.

Changes:

  • Caches the builtin-only alias map.
  • Adds a cached DFS fast path for builtin aliases.
  • Logs builtin-loading failures before fallback validation.
Show a summary per file
File Description
pkg/workflow/model_aliases.go Adds builtin alias-map caching.
pkg/workflow/model_alias_validation.go Adds cached builtin cycle validation.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment on lines +272 to +285
// isBuiltinOnlyAliasMap reports whether aliasMap has exactly the same set of keys as
// rawBuiltins. Because MergeImportedModelAliases never modifies existing builtin keys
// (it only appends new ones), equal key sets mean equal content.
func isBuiltinOnlyAliasMap(aliasMap, rawBuiltins map[string][]string) bool {
if len(aliasMap) != len(rawBuiltins) {
return false
}
for k := range aliasMap {
if _, exists := rawBuiltins[k]; !exists {
return false
}
}
return true
}
Comment on lines +146 to +149
if len(importedModels) == 0 && len(frontmatterModels) == 0 {
result := builtinOnlyAliasMap()
modelAliasesLog.Printf("Fast path: returning shared builtin alias map (%d entries)", len(result))
return result
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #44996 does not have the 'implementation' label and has only 72 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped.

@github-actions github-actions Bot mentioned this pull request Jul 13, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — two correctness issues must be fixed before merge

The performance goal is valid, but the implementation introduces two correctness bugs that need addressing:

  1. Shared mutable map (model_aliases.go): MergeImportedModelAliases(nil, nil) now returns a package-level singleton with no compile-time mutation guard. Any future caller that writes to the result will silently corrupt all subsequent compilations. The fix is trivial: return BuiltinModelAliases() (a copy) in the nil/nil fast path — the allocation cost is negligible for 52 entries.

  2. Key-set equality heuristic for cycle-check caching (model_alias_validation.go): isBuiltinOnlyAliasMap uses key-count + key-membership to infer the map is the builtin singleton. This produces a wrong result for any map built with the same key set but different values (e.g., hand-constructed in tests). Pointer identity (aliasMap == builtinOnlyAliasMap()) is O(1), guaranteed-correct, and eliminates both the heuristic and the separate loadBuiltinModelAliases() call for non-builtin callers.

Non-blocking observation

The overhead comment (comment 3) is informational: the O(n) key scan on every non-builtin call is a minor regression for custom-model workflows, but fixable as a side effect of adopting pointer identity.

🔎 Code quality review by PR Code Quality Reviewer · 49.7 AIC · ⌖ 4.97 AIC · ⊞ 5.4K
Comment /review to run again

if len(importedModels) == 0 && len(frontmatterModels) == 0 {
result := builtinOnlyAliasMap()
modelAliasesLog.Printf("Fast path: returning shared builtin alias map (%d entries)", len(result))
return result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shared map with no mutation enforcement: MergeImportedModelAliases now returns a cached singleton in the fast path; any caller that writes to the map will silently corrupt all future compilations.

💡 Details and suggested fix

The fast path at lines 143–151 returns builtinOnlyAliasMap(), which is the package-level singleton mergedBuiltinOnlyMap. Go maps are reference types — assigning the returned value to a struct field and then writing to it (even transitively, e.g. via maps.Copy) mutates the singleton.

Current callers look safe:

  • workflow_builder.go:154 stores the result in WorkflowData.ModelMappings, which is then read-only by awf_config.go:530.

But the contract is enforced only by a comment, and MergeImportedModelAliases is a public function. Any new caller that does:

result := MergeImportedModelAliases(nil, nil)
result["my-model"] = []string{"acme/model"}  // silently corrupts the singleton

will corrupt every subsequent compilation without any test catching it.

Fix options (in order of preference):

  1. Return a copy always in the nil/nil case (cheap: 52 entries, already the pre-diff behaviour).
  2. Introduce a typed read-only wrapper so the compiler enforces immutability at the call site.
  3. At minimum, add a test that calls MergeImportedModelAliases(nil, nil) twice and verifies modifications to one result do not affect the other.

Option 1 is one line and eliminates the whole class of bug without a fragile API contract.

}

// isBuiltinOnlyAliasMap reports whether aliasMap has exactly the same set of keys as
// rawBuiltins. Because MergeImportedModelAliases never modifies existing builtin keys

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragile key-equality assumption can return a wrong cached cycle-check result: the comment claims equal key sets imply identical content, but this is only true as long as callers go through MergeImportedModelAliases. Any direct construction of an alias map with the same keys but different values will hit the cached builtin DFS result instead of running a fresh check.

💡 Details and suggested fix

The logic is:

  1. Load the raw builtins.
  2. If aliasMap has the same key set → it must be the builtin map → return cached DFS result.

The invariant relies on MergeImportedModelAliases never overriding existing builtin keys. That's documented in the current code, but:

  • It is a cross-function invariant with no compile-time enforcement.
  • Any test or future code path that builds an alias map directly (e.g. in unit tests constructing WorkflowData by hand) with the same key count as the builtins will silently skip cycle detection.
  • If a bug in MergeImportedModelAliases ever does overwrite a key, the cycle check becomes wrong with no visible failure.

Safer approach: Instead of a key-set equality heuristic, use pointer identity. Cache a pointer to the builtin map from builtinOnlyAliasMap() and compare:

if aliasMap == builtinOnlyAliasMap() {  // pointer equality — exact same map
    ...
}

This is O(1), guaranteed-correct, and eliminates the indirect reasoning about key sets entirely. The fast path in MergeImportedModelAliases already returns the same singleton, so this will fire exactly when intended.

// are immutable, so we run the DFS once and cache the result for all future calls.
rawBuiltins, err := loadBuiltinModelAliases()
if err != nil {
modelAliasValidationLog.Printf("Warning: could not load builtin model aliases for cycle-check fast path, falling back to full validation: %v", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadBuiltinModelAliases() + O(n) key scan runs on every call even for non-builtin maps: the fast-path check itself adds overhead for the common case of workflows with custom models.

💡 Details

For every compilation that has custom models (i.e., non-nil importedModels or frontmatterModels), detectCircularModelAliases now does:

  1. loadBuiltinModelAliases() — returns cached data, cheap but not free (sync.Once unlock, two pointer reads).
  2. isBuiltinOnlyAliasMap() — iterates all N keys of aliasMap checking map membership in rawBuiltins.

For the builtin-only fast path this is a net win. For the customModels path, it is pure overhead added before the actual DFS.

If this function is on the hot path for custom-model compilations too, the scan should be gated behind the pointer-identity check suggested in the other comment (which is O(1) and requires no iteration):

if aliasMap == builtinOnlyAliasMap() {
    // run/return cached DFS
}
// fall through to full DFS — no loadBuiltinModelAliases call at all

This eliminates both the loadBuiltinModelAliases() call and the O(n) key scan for all non-builtin callers.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: perf — cache builtin alias map and DFS cycle-check

Good performance win with clear motivation. Two non-blocking suggestions surfaced (see inline comments).

Key observations

model_aliases.go fast path — The MergeImportedModelAliases(nil,nil) shortcut is sound. The risk is that the read-only contract is enforced only by convention. Any future caller that appends to a slice value (e.g. aliases["sonnet"] = append(...)) will silently corrupt the shared singleton.

isBuiltinOnlyAliasMap O(n) key comparison — The equal-key-sets-imply-equal-values assumption is correct today but fragile. Since MergeImportedModelAliases(nil,nil) returns the exact singleton from builtinOnlyAliasMap(), a pointer-identity check would be O(1) and structurally correct.

builtinCycleCheckErr race safety — The write inside sync.Once.Do and the read after are correctly ordered. No race.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 46.3 AIC · ⌖ 4.53 AIC · ⊞ 4.8K

}
}
return true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}
return true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key-set equality check in isBuiltinOnlyAliasMap is O(n) and relies on the invariant that equal key-sets imply equal values. This is a fragile assumption that will silently break if MergeImportedModelAliases ever allows a caller to shadow a builtin key without adding a new key. Since MergeImportedModelAliases(nil,nil) now returns the exact singleton produced by builtinOnlyAliasMap(), a simpler and more reliable identity check is possible using a pointer sentinel stored when the once fires, removing both the O(n) iteration and the brittleness. @copilot please address this.

// from avoiding the O(n) deep copy on every compilation.
if len(importedModels) == 0 && len(frontmatterModels) == 0 {
result := builtinOnlyAliasMap()
modelAliasesLog.Printf("Fast path: returning shared builtin alias map (%d entries)", len(result))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fast path returns a shared map, but the doc-comment change only adds a sentence at the top — the function signature still returns map[string][]string with no indication to users of the public API that it can return a read-only reference. Callers outside this package (e.g., model_aliases_import_test.go) receive this map and the read-only contract is only communicated via the updated comment. Consider returning the map wrapped in a defensive copy check in tests, or adding a // go:noinline sentinel comment to at least surface the caveat in godoc prominently. This is low severity but could silently corrupt the singleton if an external caller ever ranges over the map and appends to a slice value. @copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on two correctness/safety issues and one missing regression guard.

📋 Key Themes & Highlights

Key Themes

  • Shared-map mutation risk (highest priority): MergeImportedModelAliases(nil, nil) now returns a shared reference with no write protection. Any caller writing into workflowData.ModelMappings will silently corrupt the cache for all subsequent compilations in the same process.
  • Missing regression tests: The two sync.Once caching paths have no dedicated tests. Removing or breaking either cache causes a silent regression with nothing to catch it.
  • Residual overhead in DFS fast path: loadBuiltinModelAliases() plus an O(n) key-set scan still run on every detectCircularModelAliases call. Pointer-identity comparison would eliminate this.

Positive Highlights

  • ✅ Clean two-layer sync.Once pattern — alias-map cache and DFS-result cache are nicely separated.
  • ✅ Fallback logging on loadBuiltinModelAliases error is a real improvement over the previous silent discard.
  • detectCircularModelAliasesImpl extraction is a well-named seam that makes fast paths testable.
  • ✅ Benchmark results are compelling (~30% improvement, 80 fewer allocs/op).

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 55 AIC · ⌖ 4.86 AIC · ⊞ 6.6K
Comment /matt to run again

// Callers that only read the result (WorkflowData.ModelMappings consumers) benefit
// from avoiding the O(n) deep copy on every compilation.
if len(importedModels) == 0 && len(frontmatterModels) == 0 {
result := builtinOnlyAliasMap()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] MergeImportedModelAliases(nil, nil) now returns a shared cached map, but nothing in Go prevents a caller from mutating workflowData.ModelMappings and corrupting the cache for all subsequent compilations in the same process.

💡 Suggested mitigation

Add a test that verifies mutating the returned map does not affect subsequent calls:

func TestMergeImportedModelAliases_CachedMapIsolation(t *testing.T) {
    m1 := MergeImportedModelAliases(nil, nil)
    originalLen := len(m1)
    // Simulate an accidental write by a caller
    for k := range m1 { delete(m1, k); break }
    m2 := MergeImportedModelAliases(nil, nil)
    require.Len(t, m2, originalLen, "mutating returned map must not corrupt the shared cache")
}

This test will currently fail — making the risk concrete. The fix could be returning a read-only wrapper, or defensive copying in builtinOnlyAliasMap for all write-capable callers.

@copilot please address this.

// IS the builtin map (MergeImportedModelAliases only ever adds new keys — it never
// removes or overrides them — so equal key sets imply identical content). Builtins
// are immutable, so we run the DFS once and cache the result for all future calls.
rawBuiltins, err := loadBuiltinModelAliases()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] loadBuiltinModelAliases() is still called on every invocation of detectCircularModelAliases, even when the fast path fires. The DFS is cached, but the key-set comparison and the loadBuiltinModelAliases call itself add overhead on each compilation.

💡 Suggested optimisation

Use pointer identity instead of key-set equality: since builtinOnlyAliasMap() always returns the same pointer, you can compare map headers:

// At package level (model_alias_validation.go)
var builtinAliasMapPtr map[string][]string

func init() { builtinAliasMapPtr = builtinOnlyAliasMap() }

func detectCircularModelAliases(aliasMap map[string][]string, markdownPath string) error {
    if isBuiltinOnly := reflect.ValueOf(aliasMap).Pointer() == reflect.ValueOf(builtinAliasMapPtr).Pointer(); isBuiltinOnly {
        // ... cached DFS
    }
    return detectCircularModelAliasesImpl(aliasMap, markdownPath)
}

Or more idiomatically, pass a bool isBuiltinOnly flag from the call site (validateModelAliasMap) to avoid loading builtins altogether in the fast path.

@copilot please address this.

// Pass an empty markdownPath: the cached result is shared across all
// compilations, so any path embedded in an error message would be
// misleading for callers other than the first one.
builtinCycleCheckErr = detectCircularModelAliasesImpl(rawBuiltins, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] No regression test exists for the builtinCycleCheckOnce caching path. If the sync.Once wrapper is accidentally removed or the fast path condition breaks, compilation will silently regress without any failing test.

💡 Suggested test
func TestDetectCircularModelAliases_BuiltinFastPath(t *testing.T) {
    builtins := BuiltinModelAliases()
    // First call — populates the cache
    err1 := detectCircularModelAliases(builtins, "/path/a.md")
    require.NoError(t, err1)
    // Second call — must use cache (same result, no panic)
    err2 := detectCircularModelAliases(builtins, "/path/b.md")
    require.NoError(t, err2)
    // Non-builtin map — must NOT use cache
    custom := map[string][]string{"custom": {"gpt-4o"}}
    for k, v := range builtins { custom[k] = v }
    err3 := detectCircularModelAliases(custom, "/path/c.md")
    require.NoError(t, err3)
}

This guards the fast-path branch and ensures the key-set equality check behaves correctly for both the hit and miss cases.

@copilot please address this.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor
Risk 🟡 Medium
Score 55/100 (impact:30 urgency:15 quality:10)
Action 📦 batch_review
Batch pr-batch:perf-regressions

Rationale: Caches builtin alias map and DFS cycle-check to fix CompileMCPWorkflow perf regression (+14%). 2 hot-path Go files changed. CHANGES_REQUESTED from bot reviewer — author should address feedback. Batch with PR #44994 (same perf-regression cluster) for unified review.

Generated by 🔧 PR Triage Agent · 181 AIC · ⌖ 5.14 AIC · ⊞ 5.6K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[performance] Regression in CompileMCPWorkflow: +14.0% slower

3 participants