Skip to content

perf: fix 14% CompileSimpleWorkflow regression by eliminating intermediate YAML buffer allocations#29780

Merged
pelikhan merged 4 commits intomainfrom
copilot/fix-performance-regression-compile-simple-workflow
May 2, 2026
Merged

perf: fix 14% CompileSimpleWorkflow regression by eliminating intermediate YAML buffer allocations#29780
pelikhan merged 4 commits intomainfrom
copilot/fix-performance-regression-compile-simple-workflow

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 2, 2026

CompileSimpleWorkflow regressed 14% (1.47ms → 1.67ms) due to excessive intermediate strings.Builder allocations in the YAML generation path, driving GC to consume ~30% of CPU time.

Root cause

Three compounding allocation sources per compile:

Site Allocation
RenderToYAML() internal builder 96 KB
renderJob() per-job buffer × ~5 jobs 8 KB × N
generateYAML outer yaml + body builders 96 KB × 2
WriteString(line + "\n") in hot loops 1 temp alloc/line

The RenderToYAML and renderJob pattern built strings into their own builders then copied the result into the caller's builder — paying both the allocation and the copy.

Changes

  • jobs.go — Add WriteJobsYAML(*strings.Builder) and renderJobTo(*strings.Builder, *Job) that write directly to the caller's builder. RenderToYAML() / renderJob() become thin wrappers for existing callers; RenderToYAML is pre-sized with Grow(64 KB) so its compatibility builder avoids regrowth on typical outputs.

  • compiler_yaml.go — Use WriteJobsYAML in generateWorkflowBody; retain 96 KB initial capacity for yaml/body builders (covers 70–90 KB Copilot/Claude workflows without reallocation); replace s + "\n\n" concatenations with two WriteString calls.

  • compiler_yaml_ai_execution.go, compiler_yaml_main_job.go — Replace WriteString(line + "\n") hot-loop pattern with WriteString(line) + WriteByte('\n').

  • jobs_test.go — Add TestJobManager_WriteJobsYAML with three table-driven cases that verify WriteJobsYAML appends correctly to an already-populated builder and that its output matches RenderToYAML().

// Before — allocates 96 KB buffer + copies ~50 KB string
yaml.WriteString(c.jobManager.RenderToYAML())

// After — writes directly into the existing builder
c.jobManager.WriteJobsYAML(yaml)

Results

Metric Regressed After
ns/op 1,671,911 ~1,280,000 (−23%)
B/op ~992 KB ~790 KB (−20%)
allocs/op ~4,573 ~4,474

Lands ~13% below the historical baseline (1.47 ms).

Copilot AI and others added 2 commits May 2, 2026 15:47
…rmance regression

Key optimizations:
1. Add JobManager.WriteJobsYAML(*strings.Builder) to write the jobs section
   directly to an existing builder instead of allocating a 96KB intermediate
   buffer + string copy in RenderToYAML.
2. Add JobManager.renderJobTo(*strings.Builder, *Job) that writes each job
   directly to the provided builder, eliminating one 8KB buffer per job.
3. Reduce generateYAML body/yaml builder initial capacity from 96KB to 64KB
   (sufficient for ~60KB compiled output).
4. Replace yaml.WriteString(line + "\n") with two separate WriteString/WriteByte
   calls in generateEngineExecutionSteps and other hot paths to eliminate
   temporary string allocations.
5. Replace string concatenation in generateWorkflowBody (onSection+"\n\n", etc.)
   with two WriteString calls.

Results: -27% in B/op (992KB → 722KB), -2% in allocs/op, ~21% faster wall time
vs the regressed version (1.67ms → 1.32ms).

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e57b41f5-70f8-457a-aa52-7d03cee44cd6

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix performance regression in CompileSimpleWorkflow perf: fix 14% CompileSimpleWorkflow regression by eliminating intermediate YAML buffer allocations May 2, 2026
Copilot AI requested a review from pelikhan May 2, 2026 15:51
@pelikhan pelikhan marked this pull request as ready for review May 2, 2026 15:52
Copilot AI review requested due to automatic review settings May 2, 2026 15:52
@github-actions github-actions Bot mentioned this pull request May 2, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes workflow YAML generation to reduce allocations in CompileSimpleWorkflow, primarily by streaming YAML directly into existing builders instead of building intermediate strings.

Changes:

  • Added direct-write helpers in JobManager so jobs can be rendered into an existing strings.Builder.
  • Updated workflow body generation to use the direct-write job path and reduced initial YAML builder capacity from 96 KiB to 64 KiB.
  • Replaced several WriteString(line + "\n") hot-loop patterns with WriteString(line) plus WriteByte('\n').
Show a summary per file
File Description
pkg/workflow/jobs.go Adds direct-to-builder job rendering helpers and keeps string-returning wrappers for compatibility.
pkg/workflow/compiler_yaml.go Switches workflow body generation to the new direct-write jobs API and retunes builder sizing/newline writes.
pkg/workflow/compiler_yaml_ai_execution.go Removes temporary string concatenation in AI execution step emission loops.
pkg/workflow/compiler_yaml_main_job.go Applies the same newline-write optimization across several main-job step generation loops.

Copilot's findings

Tip

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

  • Files reviewed: 4/4 changed files
  • Comments generated: 3

Comment thread pkg/workflow/compiler_yaml.go Outdated
Comment on lines +342 to +346
// 64 KB covers the common simple-workflow case without a reallocation;
// larger workflows will trigger a single grow (to ~128 KB) which is
// acceptable given the reduction in baseline allocation compared to the
// previous 96 KB constant.
const initialBuilderCapacity = 64 * 1024
Comment thread pkg/workflow/jobs.go
Comment on lines 175 to +180

// RenderToYAML generates the jobs section of a GitHub Actions workflow.
// Prefer WriteJobsYAML when an existing *strings.Builder is available to avoid
// an extra allocation.
func (jm *JobManager) RenderToYAML() string {
var yaml strings.Builder
Comment on lines +297 to +299
// Generate jobs section using JobManager — write directly to avoid an
// intermediate string allocation.
c.jobManager.WriteJobsYAML(yaml)
@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 2, 2026

@copilot review all comments

…AML, add WriteJobsYAML test

- Restore initialBuilderCapacity to 96KB in generateYAML to avoid regressions
  on Copilot/Claude workflows (70-90KB). The perf win is from eliminating copies,
  not from reducing capacity.
- Pre-size the RenderToYAML compatibility builder with Grow(64KB) so existing
  callers don't regress vs the old pre-sized buffer.
- Add TestJobManager_WriteJobsYAML with three table cases that verify WriteJobsYAML
  appends correctly to an already-populated builder and that output matches
  RenderToYAML().

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6befe686-4a16-4b17-877f-0b92cceca11e

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 2, 2026

@copilot review all comments

Addressed all three review points in commit 8bb6a80:

  1. Capacity regression on large workflows — restored initialBuilderCapacity to 96 KB in generateYAML. The performance win comes entirely from eliminating the intermediate copies (RenderToYAML + WriteString), not from reducing capacity.

  2. RenderToYAML starts at zero capacity — added yaml.Grow(64 * 1024) so the compatibility wrapper pre-sizes its buffer for the ~48–60 KB all-jobs output.

  3. Missing integration test — added TestJobManager_WriteJobsYAML with three table-driven cases that write a header into a builder first, then call WriteJobsYAML, and assert both the full output content and that the jobs section is identical to RenderToYAML().

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw pull.rebase sv git 1/x6�� ithub-script/git/ref/tags/v9 git (http block)
  • https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1
    • Triggering command: /usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq [.object.sha, .object.type] | @tsv /ref/tags/v9 (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq [.object.sha, .object.type] | @tsv /ref/tags/v9 committer.name sv (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq [.object.sha, .object.type] | @tsv --show-toplevel (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq [.object.sha, .object.type] | @tsv (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq [.object.sha, .object.type] | @tsv --show-toplevel (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq [.object.sha, .object.type] | @tsv xterm-color git /usr/bin/infocmp ithub-script/gitgit (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v6
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq [.object.sha, .object.type] | @tsv th .prettierignore --log-level=error (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq [.object.sha, .object.type] | @tsv (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq [.object.sha, .object.type] | @tsv */*.json&#39; &#39;!../../../pkg/workflow/js/**/*.json&#39; --ignore-path ../../../.prettierignore (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq [.object.sha, .object.type] | @tsv /ref/tags/v9 git sv ormance-regressiinfocmp --local bin/node git rev-�� --show-toplevel git /usr/bin/git get --local m/_npx/b38865467--show-toplevel git (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v9
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v9 --jq [.object.sha, .object.type] | @tsv (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v9 --jq [.object.sha, .object.type] | @tsv ignore-path ../../../.prettierignore (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v9 --jq [.object.sha, .object.type] | @tsv it} (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v9.0.0
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v9.0.0 --jq [.object.sha, .object.type] | @tsv --show-toplevel (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v9.0.0 --jq [.object.sha, .object.type] | @tsv /repos/actions/github-script/git/ref/tags/v9 --jq /usr/bin/git h ../../../.pretgh (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v9.0.0 --jq [.object.sha, .object.type] | @tsv --show-toplevel sh /usr/bin/infocmp npx prettier --wgit (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq [.object.sha, .object.type] | @tsv --show-toplevel (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq [.object.sha, .object.type] | @tsv /ref/tags/v9 --local sv user.name (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq [.object.sha, .object.type] | @tsv xterm-color (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v6
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v6 --jq [.object.sha, .object.type] | @tsv th .prettierignore --log-level=error (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v6 --jq [.object.sha, .object.type] | @tsv (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v6 --jq [.object.sha, .object.type] | @tsv */*.json&#39; &#39;!../../../pkg/workflow/js/**/*.json&#39; --ignore-path ../../../.prettierignore (http block)
  • https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v4 --jq [.object.sha, .object.type] | @tsv --get-regexp ormance-regression-compile-simple-workflow son ignore (http block)
  • https://api.github.com/repos/aws-actions/configure-aws-credentials/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/aws-actions/configure-aws-credentials/git/ref/tags/v4 --jq [.object.sha, .object.type] | @tsv --show-toplevel (http block)
    • Triggering command: /usr/bin/gh gh api /repos/aws-actions/configure-aws-credentials/git/ref/tags/v4 --jq [.object.sha, .object.type] | @tsv ithub-script/git/ref/tags/v9 (http block)
  • https://api.github.com/repos/azure/login/git/ref/tags/v2
    • Triggering command: /usr/bin/gh gh api /repos/azure/login/git/ref/tags/v2 --jq [.object.sha, .object.type] | @tsv --show-toplevel 64/pkg/tool/linux_amd64/link /usr/bin/git (http block)
  • https://api.github.com/repos/docker/login-action/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/docker/login-action/git/ref/tags/v3 --jq [.object.sha, .object.type] | @tsv --show-toplevel (http block)
  • https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v0.1.2
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v0.1.2 --jq [.object.sha, .object.type] | @tsv --show-toplevel (http block)
  • https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v1.0.0 --jq [.object.sha, .object.type] | @tsv --get-regexp --local ache/node/24.14.1/x64/lib/node_modules/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin/sh (http block)
  • https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.2.3
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v1.2.3 --jq [.object.sha, .object.type] | @tsv --get-regexp --local k/node_modules/.bin/sh (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v0.47.4
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v0.47.4 --jq [.object.sha, .object.type] | @tsv /ref/tags/v9 (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq [.object.sha, .object.type] | @tsv ignore-path ../../../.prettierignore (http block)
  • https://api.github.com/repos/google-github-actions/auth/git/ref/tags/v2
    • Triggering command: /usr/bin/gh gh api /repos/google-github-actions/auth/git/ref/tags/v2 --jq [.object.sha, .object.type] | @tsv ithub-script/git/ref/tags/v9 (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq [.object.sha, .object.type] | @tsv ignore-path ../../../.prettierignore (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan pelikhan merged commit e1296d9 into main May 2, 2026
@pelikhan pelikhan deleted the copilot/fix-performance-regression-compile-simple-workflow branch May 2, 2026 16:36
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 CompileSimpleWorkflow: 14.0% slower

3 participants