Skip to content

perf: fix ParseWorkflow regression (+29% → -51%) via targeted caching#44994

Merged
pelikhan merged 6 commits into
mainfrom
copilot/fix-parseworkflow-performance
Jul 13, 2026
Merged

perf: fix ParseWorkflow regression (+29% → -51%) via targeted caching#44994
pelikhan merged 6 commits into
mainfrom
copilot/fix-parseworkflow-performance

Conversation

Copilot AI commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

ParseWorkflow regressed from 371μs to 479μs (+29.2%) due to three per-call allocations that were unnecessary for the common case (no imports, no custom model aliases).

Root causes & fixes

MergeImportedModelAliases — 5KB deep copy on every call (model_aliases.go)

  • 52-entry builtin map (154 string slices) was deep-copied on every ParseWorkflowFile invocation, even when importedModels and frontmatterModels were both empty (the overwhelmingly common case)
  • Added getBuiltinOnlyAliasMap() returning a shared read-only singleton; MergeImportedModelAliases returns it directly when both inputs are empty
  • Added mapHeaderPointer() via unsafe.Pointer + isBuiltinOnlyAliasMap() for zero-overhead map identity comparison (Go disallows map == map)

detectCircularModelAliases — 52-node DFS on every call (model_alias_validation.go)

  • Full DFS + sorted-key allocation over all 52 builtin aliases ran unconditionally on every parse, even though builtins never change
  • Cached via builtinCycleCheckOnce; runs at most once per process lifetime when the map is the shared builtin

NewTools — 14-entry map literal allocated per call (tools_parser.go)

  • knownTools map literal was re-created inside NewTools() on every invocation
  • Promoted to a package-level variable

Template validation — regex executed on all markdown (template_validation.go)

  • Three validation functions ran FindAllStringSubmatch unconditionally
  • Added strings.Contains("{{#if") / strings.Contains("{{#") fast-paths to skip regex when no template blocks are present

Result

ns/op
Historical baseline 371,023
Regression (reported) 479,440
After this PR (local) ~240,000

~50% faster than the regression; ~35% faster than the historical baseline.

Copilot AI and others added 3 commits July 11, 2026 18:00
- model_aliases.go: add isBuiltinOnlyAliasMap() using reflect.ValueOf().Pointer()
  for map identity check, plus builtinOnlyAliasMapID uintptr cache variable;
  MergeImportedModelAliases returns shared builtin map (no deep copy) when no
  imports/overrides present - avoids 154 string slice allocations per call

- model_alias_validation.go: detectCircularModelAliases() fast-path caches DFS
  result for builtin-only map via builtinCycleCheckOnce (fixes invalid map==map
  comparison by using isBuiltinOnlyAliasMap() instead)

- tools_parser.go: promote knownTools from local map literal to package-level
  variable to avoid 14-entry map allocation on every NewTools() call

- template_validation.go: add strings.Contains fast-paths to three validation
  functions so they skip regex entirely for markdown with no template blocks

Benchmark: 479μs (regression) → 235μs (local, -51%)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
… cached error path

- model_aliases.go: replace reflect.ValueOf().Pointer() with mapHeaderPointer()
  using unsafe.Pointer extraction; avoids reflection overhead on every call;
  remove redundant getBuiltinOnlyAliasMap() call from isBuiltinOnlyAliasMap()

- model_alias_validation.go: pass empty path to builtin cycle check so a
  hypothetical cached error does not expose a caller's workflow path

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- model_aliases.go: expand mapHeaderPointer comment with Go version stability
  note; expand MergeImportedModelAliases doc with prominent immutability warning
  using ⚠ symbol

- model_alias_validation.go: use sentinel '<builtin-aliases>' path in builtin
  cycle check instead of empty string for clearer hypothetical error messages

- template_validation.go: add comment explaining why '{{#if' is the correct
  fast-path substring (templateRegionPattern exclusively matches {{#if blocks)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

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

Draft PR — largest of the perf batch (132 additions, 4 files). Unmark draft and ensure CI passes before merging.

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 50/100 (impact:25 + urgency:15 + quality:10)
Action 📦 batch_review
Batch pr-batch:perf-regressions (with #44995, #44996)

Rationale: Draft. Fixes ParseWorkflow +29% regression via targeted caching. Good approach; batch with other perf PRs for efficient review.

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, #44996)
Action batch_review

Draft. ParseWorkflow caching improvement. +132/-20. Review in 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:31
Copilot AI review requested due to automatic review settings July 13, 2026 06:31

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 workflow parsing through targeted caching and template-validation fast paths.

Changes:

  • Shares and caches built-in model aliases and cycle validation.
  • Reuses the built-in tool-name set.
  • Skips template regex scans when conditions are absent.
Show a summary per file
File Description
pkg/workflow/tools_parser.go Reuses the known-tools map.
pkg/workflow/template_validation.go Adds regex fast paths.
pkg/workflow/model_aliases.go Introduces shared built-in aliases.
pkg/workflow/model_alias_validation.go Caches built-in cycle checks.

Review details

Tip

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

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

Comment on lines +173 to +175
// returns the shared, read-only builtin alias map directly (no allocation).
// Callers MUST NOT mutate the returned map; it is shared across all concurrent
// parse calls. Use isBuiltinOnlyAliasMap() to detect this case if needed.
Comment on lines +112 to +113
id := builtinOnlyAliasMapID
return id != 0 && mapHeaderPointer(m) == id
templateValidationLog.Print("Validating that pre-expanded experiment placeholders are not used in template conditions")

// Fast path: skip expensive regex if the markdown contains no template conditions.
if !strings.Contains(markdown, "{{#") {
templateValidationLog.Print("Checking for double-quoted experiment comparison expressions")

// Fast path: skip expensive regex if the markdown contains no template conditions.
if !strings.Contains(markdown, "{{#") {
Comment on lines +90 to +91
// knownTools is the set of built-in tool names that NewTools handles explicitly.
// It is a package-level variable to avoid re-allocating this map on every call.
@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

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel analysis is not applicable. The PR modifies only production code (model_alias_validation.go, model_aliases.go, template_validation.go, tools_parser.go).

@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.

@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 summary

The performance wins are real and the approach is sound overall, but there are two blocking correctness issues that should be fixed before merge.

Blocking

  1. Data race on builtinOnlyAliasMapID (model_aliases.go:112) — the field is read without synchronization outside the sync.Once, which is a data race under the Go memory model. Fix: use atomic.Uintptr.

  2. unsafe.Pointer map-header extraction (model_aliases.go:87) — relies on undocumented Go runtime internals. reflect.ValueOf(m).Pointer() is the documented, spec-stable equivalent with identical performance.

Non-blocking

  1. Shared singleton mutation risk (model_aliases.go:185) — downstream code could silently corrupt the builtin singleton. The doc comment is a soft guard; consider a concurrent ParseWorkflowFile test run with -race to enforce the invariant.

The template_validation.go fast-paths and tools_parser.go package-level promotion are clean and unambiguously correct.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 42.2 AIC · ⌖ 4.74 AIC · ⊞ 4.8K

// in that case builtinOnlyAliasMapID is 0 and the function returns false, which is
// correct because no caller can hold a reference to the shared map before it exists.
func isBuiltinOnlyAliasMap(m map[string][]string) bool {
id := builtinOnlyAliasMapID

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.

Data race on builtinOnlyAliasMapID

isBuiltinOnlyAliasMap reads builtinOnlyAliasMapID on this line without any synchronization. The write happens inside builtinOnlyAliasMapOnce.Do (line 99), but the Go memory model only guarantees visibility to goroutines that participate in the Once; concurrent goroutines reading the variable directly can observe a stale (zero) value or, on weakly-ordered hardware, a torn read.

Fix: replace the plain uintptr with atomic.Uintptr:

// var block
builtinOnlyAliasMapID atomic.Uintptr

// getBuiltinOnlyAliasMap Once.Do:
builtinOnlyAliasMapID.Store(mapHeaderPointer(data))

// isBuiltinOnlyAliasMap:
id := builtinOnlyAliasMapID.Load()

@copilot please address this.

// consistent across all supported Go versions (see runtime/map.go). It is
// the same technique used internally by reflect.Value.Pointer() for maps.
func mapHeaderPointer(m map[string][]string) uintptr {
return *(*uintptr)(unsafe.Pointer(&m))

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.

Undocumented runtime internals in mapHeaderPointer

This function deferences the map variable as a *uintptr to extract the internal *runtime.hmap pointer. While this layout has been stable in practice, it is not part of the Go language spec or any exported ABI contract, and the comment's claim that reflect.Value.Pointer() uses "the same technique" is misleading — reflect is part of the runtime and has privileged access.

Risks:

  • A future Go version (especially with the register-based calling convention or potential map rewrite in Go 1.24+) could change map value layout, causing a silent wrong result or crash.
  • GC-compaction (if ever introduced) could move the hmap, making the stored uintptr stale.

Prefer using reflect.ValueOf(m).Pointer() — it provides the same identity comparison, is in the standard library, and is explicitly documented to return the hash-table pointer for maps:

import "reflect"

func mapHeaderPointer(m map[string][]string) uintptr {
    return reflect.ValueOf(m).Pointer()
}

This is zero additional allocation (reflect.Value is stack-allocated for non-interface values) and removes the dependency on undocumented layout.

@copilot please address this.

// aliases. Avoid deep-copying the 52-entry builtin map (154 string slices) on every
// ParseWorkflowFile call by returning the shared read-only builtin map directly.
if len(importedModels) == 0 && len(frontmatterModels) == 0 {
result := getBuiltinOnlyAliasMap()

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 singleton returned to callers who may mutate it

When both inputs are empty, MergeImportedModelAliases now returns the shared builtinOnlyAliasMap singleton directly. The doc comment warns callers not to mutate it, but the current call site in workflow_builder.go immediately assigns it to workflowData.ModelMappings, which is later assigned to apiProxy.Models (awf_config.go:530). If any code downstream of that ever writes to the map (e.g., apiProxy.Models[key] = value), it will silently corrupt the shared singleton for all subsequent parses.

The doc comment alone is an insufficient guard against this class of bug in a growing codebase. Consider one of:

  1. Returning a thin read-only wrapper (if the interface allows), or
  2. Documenting the invariant at both the assignment sites, or
  3. Adding a -race-compatible test that calls ParseWorkflowFile concurrently to catch future violations.

@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 correctness and safety issues.

📋 Key Themes & Highlights

Key Issues

  • Data race (builtinOnlyAliasMapID is a plain uintptr written under sync.Once but read without synchronisation — use atomic.Uintptr)
  • Panic on error (getBuiltinOnlyAliasMap panics rather than propagating; should cache and return the error)
  • unsafe map identity (mapHeaderPointer extracts *runtime.hmap; same goal achievable more safely with reflect.Value.Pointer() or an atomic.Bool sentinel)
  • Mutation contract unenforced (shared builtin map returned to callers with no runtime guard; needs at least a test asserting the fast-path identity)
  • No regression benchmark guard (the fixed regression has no automated threshold assertion to prevent a future repeat)

Positive Highlights

  • ✅ Excellent root-cause analysis and clear PR description with benchmark numbers
  • ✅ Template fast-paths (strings.Contains) are clean and obviously correct
  • knownTools promotion to package-level is a straightforward, safe win
  • builtinCycleCheckOnce for DFS caching is well-structured and easy to follow

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

builtinOnlyAliasMapOnce.Do(func() {
data, err := loadBuiltinModelAliases()
if err != nil {
panic(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.

[/diagnosing-bugs] panic in a hot-path singleton hides the error from callers and kills the whole process — even if the caller could recover gracefully.

💡 Suggested fix: return the error instead

Change the signature to (map[string][]string, error) and cache the error:

func getBuiltinOnlyAliasMap() (map[string][]string, error) {
    builtinOnlyAliasMapOnce.Do(func() {
        data, err := loadBuiltinModelAliases()
        if err != nil {
            builtinOnlyAliasMapErr = err
            return
        }
        builtinOnlyAliasMap = data
        builtinOnlyAliasMapID = mapHeaderPointer(data)
    })
    return builtinOnlyAliasMap, builtinOnlyAliasMapErr
}

This keeps error surfacing consistent with how loadBuiltinModelAliases errors are handled elsewhere.

@copilot please address this.

// consistent across all supported Go versions (see runtime/map.go). It is
// the same technique used internally by reflect.Value.Pointer() for maps.
func mapHeaderPointer(m map[string][]string) uintptr {
return *(*uintptr)(unsafe.Pointer(&m))

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] mapHeaderPointer relies on an undocumented Go runtime internal (map values as single-word *hmap pointers). While stable since Go 1.0, this is fragile — a future runtime change (or GC move in a hypothetical moving GC) could silently break the identity check.

💡 Simpler alternative without unsafe

Since getBuiltinOnlyAliasMap is the only place that sets builtinOnlyAliasMap, you can replace mapHeaderPointer + builtinOnlyAliasMapID with an atomic bool:

var builtinOnlyAliasMapSet atomic.Bool

func getBuiltinOnlyAliasMap() map[string][]string {
    builtinOnlyAliasMapOnce.Do(func() {
        data, _ := loadBuiltinModelAliases() // handle error appropriately
        builtinOnlyAliasMap = data
        builtinOnlyAliasMapSet.Store(true)
    })
    return builtinOnlyAliasMap
}

func isBuiltinOnlyAliasMap(m map[string][]string) bool {
    // A caller can only hold the shared map after getBuiltinOnlyAliasMap returns it.
    // If the set flag is false, no caller can hold it yet.
    return builtinOnlyAliasMapSet.Load() && m != nil && len(m) == len(builtinOnlyAliasMap) &&
        // cheap pointer check via reflect for read-only assertion
        reflect.ValueOf(m).Pointer() == reflect.ValueOf(builtinOnlyAliasMap).Pointer()
}

Or more simply, just keep a bool sentinel inside the Once and use an explicit pointer stored as unsafe.Pointer via atomic.Pointer[map[string][]string] — all safer than raw uintptr arithmetic.

@copilot please address this.

// in that case builtinOnlyAliasMapID is 0 and the function returns false, which is
// correct because no caller can hold a reference to the shared map before it exists.
func isBuiltinOnlyAliasMap(m map[string][]string) bool {
id := builtinOnlyAliasMapID

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] builtinOnlyAliasMapID is a plain uintptr read outside the sync.Once — this is a data race on first concurrent use.

The write on line 99 happens inside builtinOnlyAliasMapOnce.Do, but the read on line 112 (id := builtinOnlyAliasMapID) happens without any synchronisation. The sync.Once guarantees the write is ordered before subsequent Do callers unblock, but isBuiltinOnlyAliasMap can be called concurrently before Do has run — leading to a torn read on 32-bit platforms or under the race detector.

💡 Fix: use atomic.Uintptr
import "sync/atomic"

var builtinOnlyAliasMapID atomic.Uintptr

// in Once.Do:
builtinOnlyAliasMapID.Store(mapHeaderPointer(data))

// in isBuiltinOnlyAliasMap:
id := builtinOnlyAliasMapID.Load()
return id != 0 && mapHeaderPointer(m) == id

@copilot please address this.

// aliases. Avoid deep-copying the 52-entry builtin map (154 string slices) on every
// ParseWorkflowFile call by returning the shared read-only builtin map directly.
if len(importedModels) == 0 && len(frontmatterModels) == 0 {
result := getBuiltinOnlyAliasMap()

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] The returned shared map has no mutation guard — any caller that accidentally writes to it (e.g., a test doing result["my-alias"] = ...) will silently corrupt state for all concurrent parsers.

💡 Suggested safeguard

Add a test that verifies mutation attempts on the returned map are caught early:

func TestMergeImportedModelAliases_NoMutationOfBuiltinMap(t *testing.T) {
    result := MergeImportedModelAliases(nil, nil)
    before := len(result)
    // Deliberately mutate — should be safe only because callers won't do this,
    // but document that it would corrupt state:
    // result["should-not-be-here"] = []string{"x"} // this would break other tests
    _ = before
    // At minimum, assert the returned map equals the builtin map by reference
    assert.True(t, isBuiltinOnlyAliasMap(result), "fast-path should return the shared builtin map")
}

If enforcing immutability at runtime is critical, consider wrapping in a type that panics on write rather than relying on documentation alone.

@copilot please address this.

// Pass a sentinel path so that if a cycle is ever introduced into the
// builtin data (a bug) the error message clearly identifies the source
// rather than referencing a caller's workflow path.
builtinCycleCheckErr = detectCircularModelAliasesUnoptimized(aliasMap, "<builtin-aliases>")

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] The PR fixes a performance regression but adds no benchmark assertions or regression guard. The existing BenchmarkParseWorkflow will detect future regressions only if someone remembers to run it with -benchtime and compare — it's a passive check.

💡 Suggested addition

Add a sub-benchmark or TestMain-style baseline guard that fails if ParseWorkflow exceeds a threshold, e.g.:

func BenchmarkParseWorkflow_NoRegression(b *testing.B) {
    // Fails CI if median ns/op exceeds the pre-regression baseline (479k ns/op)
    // giving a 20% headroom above the new ~240k target.
    const maxNsPerOp = 380_000
    // ... run benchmark and assert b.Elapsed()/N < maxNsPerOp
}

Alternatively, document the expected ns/op threshold in a comment on BenchmarkParseWorkflow so reviewers have context when they see benchmark output in CI.

@copilot please address this.

@github-actions

Copy link
Copy Markdown
Contributor

Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (132 new lines in pkg/workflow/) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/44994-parseworkflow-hot-path-caching-via-singletons.md — review and complete it before merging.

This PR cannot merge until an ADR is linked in the PR body.

What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:
    ADR: ADR-44994: ParseWorkflow Hot-Path Caching via Process-Lifetime Singletons

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 56.2 AIC · ⌖ 7.71 AIC · ⊞ 8.5K ·
Comment /review to run again

@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 — correctness and safety issues in the unsafe/singleton optimization

The perf goal is valid and the template/knownTools fast-paths are fine. The model_aliases.go optimizations introduce four interconnected issues that need to be resolved before merging.

Blocking issues (4)
  1. Data race on builtinOnlyAliasMapID — plain uintptr read/write without atomic, races under concurrent ParseWorkflowFile calls (-race will flag it)
  2. unsafe map header pointer — relies on undocumented Go runtime internals that are not guaranteed stable; Go 1.24+ map redesign (Swiss tables) makes this an active fragility risk
  3. panic inside sync.Once poisons the singleton — after a panic Do is marked done but builtinOnlyAliasMap is nil; future callers return nil silently with no error
  4. Shared mutable map returned to callers — comment-only MUST NOT mutate contract is invisible at call sites; any caller mutation causes a data race and corrupts all future parses

All four stem from the same root: using unsafe pointer identity instead of a wrapper type. The fix is straightforward — wrap the singleton in a named struct so identity is just pointer equality with no unsafe needed.

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

Comments that could not be inline-anchored

pkg/workflow/model_aliases.go:113

Data race on builtinOnlyAliasMapID: the plain uintptr read in isBuiltinOnlyAliasMap races with the write inside builtinOnlyAliasMapOnce.Do.

<details>
<summary>💡 Details and suggested fix</summary>

isBuiltinOnlyAliasMap reads builtinOnlyAliasMapID via a plain load (line 112) with no synchronization. The write happens inside builtinOnlyAliasMapOnce.Do. sync.Once guarantees happens-before only for goroutines that pass through Do — a goroutine that calls `isBuiltinOnlyAlias…

pkg/workflow/model_aliases.go:91

unsafe map pointer relies on undocumented Go runtime internals: *(*uintptr)(unsafe.Pointer(&amp;m)) deferences the internal map header, which is not part of the Go specification and can break silently on any toolchain upgrade.

<details>
<summary>💡 Why this is fragile and a safer alternative</summary>

The comment says "this layout has been stable since Go 1.0" but that is not a guarantee — it is historical observation. The Go team has discussed changing the map implementation (e.g. Swiss …

pkg/workflow/model_aliases.go:100

panic inside sync.Once permanently poisons the singleton: if loadBuiltinModelAliases() ever returns an error, the panic unwinds through Do, the Once is marked complete, and all future calls to getBuiltinOnlyAliasMap return nil map — silently, with no error.

<details>
<summary>💡 Why this matters</summary>

From the Go docs: if f passed to Once.Do panics, Do considers it to have returned. Future calls to Do return immediately. So after the panic:

  • `builtinOnlyAliasMa…
pkg/workflow/model_aliases.go:145

Shared singleton returned to callers with no mutation guard: MergeImportedModelAliases returns the internal builtinOnlyAliasMap directly, but the comment-only contract MUST NOT mutate is unenforceable and not visible at all call sites.

<details>
<summary>💡 Risk and mitigation</summary>

Any caller today or in the future that does:

aliases := MergeImportedModelAliases(nil, nil)
aliases[&quot;my-model&quot;] = []string{&quot;gpt-4&quot;} // silently corrupts the shared singleton

...will corru…

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

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

Rationale: Fixes ParseWorkflow perf regression (+29% → -51%) via singleton caching. 5 files including ADR doc. Multiple CHANGES_REQUESTED — higher priority than #44996 as regression is more severe. Author should address reviewer feedback; batch with PR #44996 for unified perf review.

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

@pelikhan
pelikhan merged commit 52463fe into main Jul 13, 2026
23 checks passed
@pelikhan
pelikhan deleted the copilot/fix-parseworkflow-performance branch July 13, 2026 14:31
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.9

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 ParseWorkflow: +29.2% slower

3 participants