Skip to content

perf: fix CompileMemoryUsage regression by optimizing normalizeBlankLines and findRunValue#44995

Open
pelikhan with Copilot wants to merge 11 commits into
mainfrom
copilot/performance-regression-compile-memory-usage
Open

perf: fix CompileMemoryUsage regression by optimizing normalizeBlankLines and findRunValue#44995
pelikhan with Copilot wants to merge 11 commits into
mainfrom
copilot/performance-regression-compile-memory-usage

Conversation

Copilot AI commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

CompileMemoryUsage was running +10.41% slower (6.30ms vs 5.71ms historical average). Profiling identified two hot spots responsible.

Changes

  • normalizeBlankLines (compiler_yaml.go) — replaced strings.Split + strings.Join with a single pre-allocated strings.Builder. The old approach allocated a full []string slice and then a ~90KB copy of the YAML content on every call (~264KB/call). The new implementation scans byte-by-byte with strings.IndexByte and returns a zero-copy slice via b.String()[:lastNonBlankEnd]:

    // Before: ~264KB allocated per call
    lines := strings.Split(yamlContent, "\n")
    for i, line := range lines {
        if strings.TrimSpace(line) == "" { lines[i] = "" }
    }
    return strings.TrimRight(strings.Join(lines, "\n"), "\n") + "\n"
    
    // After: one pre-allocated buffer, no Join copy
    var b strings.Builder
    b.Grow(len(yamlContent))
    // ... scan byte-by-byte, track lastNonBlankEnd ...
    return b.String()[:lastNonBlankEnd]
  • findRunValue (template_injection_validation.go) — added a strings.Contains(keyPart, "run:") pre-check before invoking runKeyPattern.FindStringIndex. The regex was applied to every YAML line in walkRunBlockLines; the fast-path short-circuits the ~99% of lines that can never match, eliminating the regex backtracking cost that showed up as 24% of CPU time in scanRunContentExpressions.

Result

BenchmarkCompileMemoryUsage: ~4.8ms → ~2.5ms (−48%), ~1701KB → ~1568KB per op (−7.6%).


Generated by 👨‍🍳 PR Sous Chef · 11.7 AIC · ⌖ 6.78 AIC · ⊞ 4.2K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · 11.1 AIC · ⌖ 6.4 AIC · ⊞ 7.5K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · 6.24 AIC · ⌖ 5.58 AIC · ⊞ 7.5K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · 10.1 AIC · ⌖ 6.32 AIC · ⊞ 7.5K ·
Comment /souschef to run again

Copilot AI and others added 2 commits July 11, 2026 17:49
…oryUsage regression

- Rewrite normalizeBlankLines to use a single pre-allocated strings.Builder
  instead of strings.Split + strings.Join, reducing ~150KB of allocations per
  compilation call.
- Add a fast strings.Contains pre-check in findRunValue before applying the
  regex, avoiding expensive regexp.FindStringIndex calls on the majority of
  YAML lines that don't contain "run:".

These two changes together reduce CompileMemoryUsage from ~4.8ms to ~2.5ms
(-48%) and reduce per-iteration allocations from ~1701KB to ~1572KB (-7.6%)
on the benchmark machine.

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

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix performance regression in CompileMemoryUsage perf: fix CompileMemoryUsage regression by optimizing normalizeBlankLines and findRunValue Jul 11, 2026
Copilot AI requested a review from pelikhan July 11, 2026 17:52
@pelikhan pelikhan marked this pull request as ready for review July 11, 2026 18:00
Copilot AI review requested due to automatic review settings July 11, 2026 18:00
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 11, 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 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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

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

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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

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 YAML compilation hot paths to reduce runtime and allocations.

Changes:

  • Reworks blank-line normalization using a preallocated builder.
  • Adds a fast pre-check before run-key regex matching.
  • One security issue found: quoted run keys bypass validation.
Show a summary per file
File Description
pkg/workflow/compiler_yaml.go Optimizes blank-line normalization.
pkg/workflow/template_injection_validation.go Adds regex fast-path filtering.

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: 1
  • Review effort level: Medium

Comment on lines +83 to +87
// Fast pre-check: the regex requires "run:" to be present, so skip the
// (more expensive) regex entirely when the substring is absent.
if !strings.Contains(keyPart, "run:") {
return "", false
}
@github-actions github-actions Bot mentioned this pull request Jul 11, 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.

Review: perf: fix CompileMemoryUsage regression

The normalizeBlankLines rewrite is well-crafted — pre-allocated builder, zero-copy slice return, correct handling of empty/all-whitespace inputs, and good coverage of the edge cases.

One blocking issue in the findRunValue fast-path that needs fixing before merge.

Blocking: The strings.Contains(keyPart, "run:") pre-check misses quoted YAML key forms ("run":, 'run':) that the regex does match. Lines with those forms bypass the template-injection scan entirely — a silent security regression. Changing the check to strings.Contains(keyPart, "run") preserves the performance win while closing the gap.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 26.6 AIC · ⌖ 5.32 AIC · ⊞ 4.8K

}

func findRunValue(keyPart string) (string, bool) {
// Fast pre-check: the regex requires "run:" to be present, so skip the

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 pre-check strings.Contains(keyPart, "run:") has a security gap.

The regex (?:^|[\s{,])(?:run|["'run["']): also matches quoted YAML keys like "run": and 'run':. But strings.Contains(keyPart, "run:") returns false for those — e.g. {"run": "cmd"} contains "run":, not run:.

This means any YAML line with a quoted run key silently bypasses the template-injection scan.

Fix — broaden to just check for the word run:

if !strings.Contains(keyPart, "run") {
    return "", false
}

This is still a huge fast-path win (only lines containing run anywhere reach the regex), with no false-negative risk.

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

REQUEST_CHANGES — one high-severity security issue must be fixed

The performance improvements are real and the normalizeBlankLines rewrite is correct. However, the findRunValue pre-check introduces a security regression that must be fixed before merge.

Blocking issue

findRunValue pre-check silently bypasses injection validation for quoted-key run blocks (line 85 in template_injection_validation.go). runKeyPattern matches "run": and 'run':, but strings.Contains(keyPart, "run:") returns false for both — the colon is not adjacent to n in the quoted forms. Any template injection inside a quoted-key run block will be missed entirely.

Non-blocking note

The comment on return b.String()[:lastNonBlankEnd] overstates the optimization by calling it a "zero-copy operation" — b.String() itself copies the builder bytes. The slice avoids a second copy, but the description should be corrected to avoid misleading future readers.

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

func findRunValue(keyPart string) (string, bool) {
// Fast pre-check: the regex requires "run:" to be present, so skip the
// (more expensive) regex entirely when the substring is absent.
if !strings.Contains(keyPart, "run:") {

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.

Security bypass: pre-check rejects quoted-key YAML forms that the regex would match, silently skipping injection validation.

💡 Details and fix

runKeyPattern matches three forms of a run key:

Form Example
Unquoted run:
Double-quoted "run":
Single-quoted 'run':

The pre-check strings.Contains(keyPart, "run:") only matches the unquoted form. In "run":, the characters after n are " then : — the substring run: is absent, so strings.Contains returns false and the function short-circuits before the regex runs. Any template expression injected via a quoted-key run block ("run": |) will silently bypass detection.

Fix — widen the pre-check or remove it entirely:

// Option A: widen
if !strings.Contains(keyPart, "run:") &&
   !strings.Contains(keyPart, `"run":`) &&
   !strings.Contains(keyPart, "'run':") {
    return "", false
}

// Option B: just remove the pre-check — the compiled regex is already fast

Comment thread pkg/workflow/compiler_yaml.go Outdated
}
return strings.TrimRight(strings.Join(lines, "\n"), "\n") + "\n"
// Slice the builder string to drop trailing blank lines. Because Go string
// slicing shares the underlying bytes array, this is a zero-copy operation.

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.

Misleading comment claims zero-copy, but b.String() already copied the builder's bytes.

💡 Detail

The comment reads:

Because Go string slicing shares the underlying bytes array, this is a zero-copy operation.

The slice b.String()[:lastNonBlankEnd] is indeed zero-copy, but b.String() itself copies the builder's internal []byte into a new immutable string. The overall return b.String()[:lastNonBlankEnd] is therefore a copy — not zero-copy. Future maintainers reasoning about allocation budgets will be misled.

Fix: reword the comment to say the

@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 /codebase-design — requesting changes on missing regression coverage.

📋 Key Themes & Highlights

Key Themes

  • Missing regression tests: Both hot-spot fixes are non-trivial rewrites with no unit tests. The normalizeBlankLines rewrite has 7+ edge cases documented in comments but none verified by tests. The findRunValue fast-path has no test confirming it produces identical results to the original regex-only path.
  • Inaccurate comment: The "zero-copy" description for b.String()[:lastNonBlankEnd] is incorrect — strings.Builder.String() always copies the buffer; the slice only avoids a second copy.

Positive Highlights

  • ✅ Impressive benchmark improvement: −48% latency and −7.6% memory per op
  • ✅ The strings.Contains fast-path in findRunValue is a clean, minimal change
  • ✅ Thorough inline comments explain the intent of lastNonBlankEnd and the early-return edge case
  • b.Grow(len(yamlContent)) pre-allocation is exactly the right approach to eliminate incremental growth

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

return strings.TrimRight(strings.Join(lines, "\n"), "\n") + "\n"
// Slice the builder string to drop trailing blank lines. Because Go string
// slicing shares the underlying bytes array, this is a zero-copy operation.
return b.String()[:lastNonBlankEnd]

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] No regression tests for the rewritten normalizeBlankLines — a non-trivial rewrite with subtle edge-case handling (empty input, all-whitespace, no trailing newline) is exactly where a unit test suite pays off.

💡 Suggested test cases
func TestNormalizeBlankLines(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  string
    }{
        {"empty string", "", "\n"},
        {"single blank line", "\n", "\n"},
        {"all whitespace line", "   \n", "\n"},
        {"no trailing newline", "hello", "hello\n"},
        {"trailing blank lines stripped", "a\n\nb\n\n\n", "a\n\nb\n"},
        {"blank lines in middle preserved", "a\n\nb\n", "a\n\nb\n"},
        {"whitespace-only lines cleared", "a\n   \nb\n", "a\n\nb\n"},
    }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            got := normalizeBlankLines(tc.input)
            if got != tc.want {
                t.Errorf("got %q, want %q", got, tc.want)
            }
        })
    }
}

Without tests, a future refactor or Go version change could silently break the behaviour that was carefully preserved in the comments.

@copilot please address this.

Comment thread pkg/workflow/compiler_yaml.go Outdated
}
return strings.TrimRight(strings.Join(lines, "\n"), "\n") + "\n"
// Slice the builder string to drop trailing blank lines. Because Go string
// slicing shares the underlying bytes array, this is a zero-copy operation.

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.

[/codebase-design] The comment on line 367 says b.String()[:lastNonBlankEnd] is "zero-copy", but strings.Builder.String() always copies the internal byte buffer into a new string — the slice just avoids a second copy. The comment is technically incorrect and may mislead future readers.

💡 Suggested fix

Change the comment from:

// Slice the builder string to drop trailing blank lines. Because Go string
// slicing shares the underlying bytes array, this is a zero-copy operation.

to something like:

// Slice the builder string to drop trailing blank lines. The slice avoids
// an extra allocation that a second strings.Builder write would incur, but
// b.String() itself still copies the internal buffer once.

This keeps the documentation accurate for future maintainers.

@copilot please address this.

if !strings.Contains(keyPart, "run:") {
return "", false
}
loc := runKeyPattern.FindStringIndex(keyPart)

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 pre-check strings.Contains(keyPart, "run:") is sound for the known regex forms, but there are no tests that assert the fast-path produces identical results to the regex alone. A regression test covering the quoted ("run":) and flow-style ({run:) forms would lock in correctness.

💡 Suggested test
func TestFindRunValueFastPath(t *testing.T) {
    cases := []struct {
        input  string
        wantOk bool
    }{
        {`run: echo hello`, true},
        {`"run": echo hello`, true},
        {`{run: echo hello}`, true},
        {`step: build`, false},
        {`runner: ubuntu`, false}, // "run" present but not "run:"
    }
    for _, c := range cases {
        _, ok := findRunValue(c.input)
        if ok != c.wantOk {
            t.Errorf("findRunValue(%q) ok=%v, want %v", c.input, ok, c.wantOk)
        }
    }
}

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

pr-sous-chef
@copilot please run the pr-finisher skill, make sure this branch is up to date with its base branch, address any remaining review feedback, and rerun checks when done.

Run: https://github.com/github/gh-aw/actions/runs/29163434766

Generated by 👨‍🍳 PR Sous Chef · 11.7 AIC · ⌖ 6.78 AIC · ⊞ 4.2K ·
Comment /souschef to run again

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor (perf)
Risk 🟡 Medium
Score 72/100 (impact: 35, urgency: 22, quality: 15)
Batch perf-regressions (with #44996, #44994)
Action fast_track

Memory regression fix. +155/-4. CI passed ✅. High priority — fast track for expedited review.

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

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address these unresolved review comments:

All seven threads were addressed in the prior commits on this branch:

  • r3564752585 / r3564762742 / r3564760647 (security: quoted run keys bypass validation) — fixed in commit 59dbe90: findRunValue now checks run:, run":, and run':, covering all alternatives of runKeyPattern. Tests for double-quoted, single-quoted, and mixed-quote forms are in TestScanRunContentExpressions and TestFindRunValueFastPath.
  • r3564762744 / r3564762956 (misleading "zero-copy" comment) — corrected in commit 4db4e87: comment now accurately states b.String() copies once and the slice avoids a second copy.
  • r3564762954 (TestNormalizeBlankLines missing) — added in commit 4db4e87: 10 cases covering empty input, all-whitespace, no trailing newline, mid-blank preservation, trailing-blank stripping.
  • r3564762959 (TestFindRunValueFastPath missing) — added in commit 4db4e87: 11 cases covering all unquoted, double-quoted, single-quoted, and mixed-quote forms.

Branch is current with main. All tests pass locally. Ready for human CI re-trigger before merge.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Run: https://github.com/github/gh-aw/actions/runs/29199165277

Generated by 👨‍🍳 PR Sous Chef · 4.72 AIC · ⌖ 4.27 AIC · ⊞ 4.2K ·
Comment /souschef to run again

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Run: https://github.com/github/gh-aw/actions/runs/29199165277...

Branch is current with main and all review threads were addressed in prior commits. Local validation passes. Ready for human CI re-trigger before merge.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the unresolved review feedback on this PR, and rerun checks when done. Unresolved threads to handle first (newest first):

  • reviewer github-actions...

All code changes addressing these threads are already on the branch:

  • r3564752585 / r3564762742 / r3564760647 (security: quoted run keys) — fixed in commit 59dbe90: findRunValue checks run:, run":, and run':. All 5 threads in this group are marked is_outdated: true, meaning they're on code that no longer exists in the PR.
  • r3564762744 / r3564762956 (misleading "zero-copy" comment) — corrected in commit 4db4e87. Also is_outdated: true.
  • r3564762954 (TestNormalizeBlankLines) — 10-case test added in commit 4db4e87. Passes locally.
  • r3564762959 (TestFindRunValueFastPath) — 11-case test added in commit 4db4e87. Passes locally.

The threads remain marked unresolved in the GitHub UI because this agent doesn't have the resolveReviewThread GraphQL mutation available. A maintainer needs to resolve them manually in the PR UI before merge.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

\n@copilot please run the skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Generated by 👨‍🍳 PR Sous Chef · 6.24 AIC · ⌖ 5.58 AIC · ⊞ 7.5K ·
Comment /souschef to run again

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address unresolved review feedback, and rerun checks once the branch is up to date.

Generated by 👨‍🍳 PR Sous Chef · 10.1 AIC · ⌖ 6.32 AIC · ⊞ 7.5K ·
Comment /souschef to run again

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

\n@copilot please run the skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

...

All review feedback was addressed in prior commits on this branch. Current state:

  • Security fix (quoted run keys): findRunValue pre-checks run:, run":, and run':, covering all runKeyPattern alternatives including double-quoted and single-quoted YAML key forms.
  • Misleading zero-copy comment: Corrected in compiler_yaml.go — comment now accurately states b.String() copies once and the slice avoids a second copy.
  • TestNormalizeBlankLines: 10 cases added covering empty input, all-whitespace, no trailing newline, mid-blank preservation, trailing-blank stripping.
  • TestFindRunValueFastPath: 11 cases added covering unquoted, double-quoted, single-quoted, and mixed-quote forms, plus non-matching patterns.

Branch is current with main (HEAD be2ad55). Local validation (make fmt, make lint, make test-unit, make agent-report-progress) all pass. Review threads remain unresolved in GitHub UI because the agent cannot call the resolveReviewThread GraphQL mutation — a maintainer needs to resolve them and re-trigger CI before merge.

@copilot please run the pr-finisher skill, address unresolved review feedback, and rerun checks once the branch is up to date.

...

Same state as above — all review feedback was addressed in prior commits. Branch is current with main, local validation passes, and there are no remaining code changes needed. The threads remain marked unresolved in the GitHub UI because the agent lacks the resolveReviewThread mutation. A maintainer needs to resolve the threads and re-trigger CI before merge.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Generated by PR Sous Chef run https://github.com/github/gh-aw/actions/runs/29204847475

Generated by 👨‍🍳 PR Sous Chef · 13.1 AIC · ⌖ 6.58 AIC · ⊞ 5.1K ·
Comment /souschef to run again

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Generated by PR Sous Chef run https://github.com/github/gh-aw/ac...

Branch is current with main (both at c0b1a3b). All review feedback was addressed in prior commits — the security fix for quoted run keys, corrected zero-copy comment, TestNormalizeBlankLines, and TestFindRunValueFastPath are all on the branch. Local validation (make fmt, make lint, make test-unit, make agent-report-progress) passes. The remaining blocker is the blocked merge state pending human CI re-trigger: a maintainer needs to resolve the review threads in the GitHub UI and re-trigger CI (close/reopen PR or workflow_dispatch) before merge.

…egression-compile-memory-usage

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the unresolved review feedback, and rerun checks once the branch is up to date.

Unresolved review threads (newest first):...

Branch is now current with main (merge commit 7e49b7a). All review threads were addressed in prior commits:

  • r3564752585 / r3564760647 / r3564762742findRunValue pre-checks run:, run":, and run':, covering all runKeyPattern alternatives (commit 59dbe90).
  • r3564762744 / r3564762956 — corrected misleading zero-copy comment (commit 4db4e87).
  • r3564762954TestNormalizeBlankLines with 10 cases (commit 4db4e87).
  • r3564762959TestFindRunValueFastPath with 11 cases (commit 4db4e87).

Local validation (make fmt, make lint, make test-unit, make agent-report-progress) all pass. A maintainer needs to resolve the threads in the GitHub UI and re-trigger CI before merge.

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 CompileMemoryUsage: +10.4% slower

4 participants