[repository-quality] 🎯 Repository Quality Improvement Report - Performance (2026-09-23) #62961
Closed
Replies: 1 comment
|
This discussion has been marked as outdated by Repository Quality Improvement Agent. A newer discussion is available at Discussion #63184. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
🎯 Repository Quality Improvement Report - Performance
Analysis Date: 2026-09-23
Focus Area: Performance
Strategy Type: Standard
Custom Area: No
Executive Summary
gh aw compile(invoked viamake recompileon every workflow-touching push/PR throughcgo.yml) is a fully sequential, single-goroutine pipeline: it iteratesconfig.MarkdownFilesin a plainforloop incompileSpecificFiles(pkg/cli/compile_pipeline.go) and callscompileWorkflowFileonce per file against one shared*workflow.Compiler. Locally, compiling all 299 top-level workflow.mdfiles (431 including shared imports) takes ~10.6–11.9s wall time regardless ofGOMAXPROCS(16.3s atGOMAXPROCS=1vs 10.7s atGOMAXPROCS=4, i.e. ~35% headroom left entirely unused), confirming the hot path never fans out across the machine's 4 available cores. TheCompilerstruct's own doc comment onownerTypeCache(line 72 ofcompiler_types.go) explicitly states the struct is "not goroutine-safe (Compiler is used sequentially)", so parallelizing today would require either per-file compiler cloning or targeted mutex/sync-map guards around the handful of shared caches (actionCache,actionResolver,importCache,ownerTypeCache,actionPinWarnings,permissionWarningShown,allowedDomainsCache,featureUsage).At the micro-benchmark level,
BenchmarkCompileWorkflowinpkg/workflow/compiler_benchmark_test.goreuses a single*Compileracrossb.Loop()iterations without resetting caches, so its first iteration is a ~5–8x outlier (83ms/497K allocs at-benchtime=1xvs 16.8ms/105K allocs at-benchtime=5x) purely from cold-cache effects rather than genuine per-call cost — this understates true steady-state variance and could mask real regressions in thebenchstat-gated CI performance job (cgo.ymlbenchjob, main-branch only, 10% regression threshold). Given the project already invests inmake bench+benchstatCI regression gating and a growing (431-file) workflow corpus compiled on every relevant push, parallelizing the compile-all loop is a concrete, low-risk win: even a conservative 3x speedup (using ~3 of 4 available cores, leaving headroom for actionlint/zizmor/yamllint post-processing) would cutmake recompilefrom ~11s to under 4s, and CI compile-validation steps proportionally, at zero cost to compile correctness once cache access is synchronized.Full Analysis Report
Focus Area: Performance
Current State Assessment
The compile pipeline (
gh aw compile, backingmake recompile) is the single most frequently re-run expensive operation in this repository — it fires on essentially every PR that touches.github/workflows/*.mdviacgo.yml. It was measured as fully single-threaded:compileSpecificFiles(pkg/cli/compile_pipeline.go:63) drives a plainfor _, markdownFile := range config.MarkdownFilesloop with no goroutines,sync.WaitGroup, orerrgroup.Group— unlike other CLI subsystems (pkg/cli/audit_analysis_fanout.go,pkg/cli/shellcheck.go,pkg/cli/logs_multi.go,pkg/cli/forecast_compute.go) that already useerrgroup/worker-pool patterns for fan-out work.GOMAXPROCS=1vsGOMAXPROCS=4full-repocompile --no-emitruns measured 16.29s vs 10.68s — the only speedup came from background GC and I/O scheduling, not from the compile loop itself using multiple cores.Compilerstruct (pkg/workflow/compiler_types.go:18-90) is explicitly documented as sequential-only via theownerTypeCachefield comment, confirming this is a known, intentional (if unaddressed) design constraint rather than an oversight.BenchmarkCompileWorkflowreuses one*Compilerinstance across the wholetesting.Bloop with no per-iteration reset, so allocation/timing numbers reported tobenchstatmix a cold-start outlier with N-1 warm-cache runs, understating variance the regression gate is meant to catch.Metrics Collected:
compile --no-emitwall time (299 top-level workflows)GOMAXPROCS=1vsGOMAXPROCS=4wall-time deltacompileSpecificFiles,pkg/cli/compile_pipeline.go).mdfiles (incl. shared imports)--parallel/--workers/-jCLI flag oncompilepkg/cli(errgroup/WaitGroup)audit_analysis_fanout.go,shellcheck.go,logs_multi.go,forecast_compute.go,drain3_train.go,audit_cache.go,logs_orchestrator_render.go)BenchmarkCompileWorkflowiteration variance (-benchtime=1xvs-benchtime=5x)cgo.ymlbenchjob)benchstat, 10% threshold, main-branch onlyFindings
Strengths
errgroup.Group, boundedsync.WaitGroup+ semaphore channels) used consistently inpkg/clifor I/O-bound fan-out (audit analyses, log processing, shellcheck), so introducing a bounded worker pool for compilation follows an established, reviewable pattern rather than a novel one.benchjob withbenchstat-based automatic regression detection (>10% fails the build), meaning any performance work here has an existing safety net to prove it doesn't regress hot paths.ownerTypeCachecomment) is refreshingly honest about the sequential-only constraint, making the exact synchronization boundary self-documenting for whoever picks this up.Areas for Improvement
compileSpecificFiles's per-file loop has zero parallelism despite 4 idle cores being available in the benchmark environment; this directly inflates the wall-clock cost of everymake recompileinvocation and any CI job gating on it (cgo.yml).*Compiler(ownerTypeCache,actionPinWarnings,permissionWarningShown,allowedDomainsCache,featureUsage,copilotRequestsTipShown,scheduleWarnings) are read/written without any synchronization primitive, meaning naive parallelization would introduce data races detectable bygo test -race— this needs to be solved deliberately, not accidentally.BenchmarkCompileWorkflow(and likely its siblings incompiler_performance_benchmark_test.go) reuse one*Compilerfor all iterations, producing benchmark numbers dominated by first-iteration cold-cache cost; this weakens the trustworthiness of thebenchstat10%-regression CI gate for exactly the metric it's supposed to protect.--parallel/-jflag exists ongh aw compile, so even users who want to opt into concurrent compilation locally have no lever to pull.Detailed Analysis
Root cause:
pkg/cli/compile_pipeline.go'scompileSpecificFilesfunction was written for correctness and clear sequential error-aggregation (viastats *CompilationStats,*validationResults, and multiplelockFilesFor*slices), and the*workflow.Compilerit drives accumulates cross-file state intentionally (shared action-pin cache, shared import cache) to avoid redundant network/parsing work across the batch. This shared-state design is exactly why it was never parallelized — but the shared caches are also exactly what should let per-file compilation be embarrassingly parallel once access is either (a) mutex-guarded, (b) migrated tosync.Map, or (c) pre-warmed once up front and then treated as read-only during a parallel phase.Concrete remediation path: Introduce a bounded worker pool (e.g.,
errgroup.GroupwithSetLimit(runtime.GOMAXPROCS(0)), mirroring the exact pattern already used inpkg/cli/audit_analysis_fanout.go) around the body of thefor _, markdownFile := range config.MarkdownFilesloop incompileSpecificFiles. Aggregation slices (lockFilesForActionlint,lockFilesForZizmor, etc.) andstats/validationResultswrites need a mutex or per-worker-then-merge pattern. The seven identified*Compilermaps need equivalent guarding (or migration tosync.Map/mutex-wrapped accessors) before this is safe under-race.🤖 Tasks for Copilot Agent
NOTE TO PLANNER AGENT: Split the following tasks into individual work items.
Improvement Tasks
Task 1: Parallelize the full-workflow compile loop with a bounded worker pool
Priority: High
Estimated Effort: Large
Focus Area: Performance
Description: Replace the sequential
for _, markdownFile := range config.MarkdownFilesloop incompileSpecificFiles(pkg/cli/compile_pipeline.go) with a boundederrgroup.Group(usingSetLimit(runtime.GOMAXPROCS(0))), following the existing fan-out pattern inpkg/cli/audit_analysis_fanout.go. Each per-filecompileWorkflowFilecall currently mutates sharedstats *CompilationStats,*validationResults []ValidationResult, and multiplelockFilesFor*slices directly; these must be collected per-worker (or guarded with async.Mutex) and merged deterministically after the group completes so output ordering (summary printing, JSON output) remains stable and reproducible across runs.Acceptance Criteria:
compileSpecificFilescompiles files using a bounded worker pool instead of a single goroutinego test -race ./pkg/cli/...passes with the new parallel path exercisedmake recompilewall time on the full 299-workflow corpus is measured before/after and shows measurable improvement (document in PR description)Code Region:
pkg/cli/compile_pipeline.go(compileSpecificFiles, lines ~63-160+)Task 2: Add mutex/sync.Map guards to shared Compiler caches to make parallel compilation safe
Priority: High
Estimated Effort: Medium
Focus Area: Performance
Description: The
*workflow.Compilerstruct (pkg/workflow/compiler_types.go:18-90) has at least seven mutable maps/slices (ownerTypeCache,actionPinWarnings,permissionWarningShown,allowedDomainsCache,featureUsage,copilotRequestsTipShown,scheduleWarnings) accessed without synchronization, explicitly documented as safe only because "Compiler is used sequentially." This task is a prerequisite/companion to Task 1: add appropriate mutex-guarded accessor methods (or migrate tosync.Mapwhere iteration order doesn't matter) for each of these fields so the compiler can be safely shared across goroutines during parallel compilation.Acceptance Criteria:
sync.Map)go test -race ./pkg/workflow/...passesCode Region:
pkg/workflow/compiler_types.go(lines 18-90,Compilerstruct)Task 3: Fix cold-cache bias in BenchmarkCompileWorkflow that undermines the CI benchstat regression gate
Priority: Medium
Estimated Effort: Small
Focus Area: Performance
Description:
BenchmarkCompileWorkflowinpkg/workflow/compiler_benchmark_test.go(lines 12-52) creates a single*CompilerviaNewCompiler()outside theb.Loop()body and reuses it across all iterations without resetting per-run caches. This causes the first iteration to be a cold-cache outlier (measured 83.2ms/497,214 allocs at-benchtime=1x) versus subsequent warm-cache iterations (measured 16.8ms/105,311 allocs at-benchtime=5x), which distorts the ns/op and allocs/op numbers thatcgo.yml'sbenchjob feeds intobenchstatfor its 10%-regression CI gate. Either reset the compiler's mutable caches each iteration (to measure true steady-state per-call cost consistently) or explicitly document why cache-reuse across iterations is the intended behavior, and consider splitting into aBenchmarkCompileWorkflow_ColdCache(freshNewCompiler()per iteration) andBenchmarkCompileWorkflow_WarmCache(current behavior) so both cost profiles are tracked separately and comparably bybenchstatacross commits.Acceptance Criteria:
BenchmarkCompileWorkflow's iteration-to-iteration variance is either eliminated (fresh compiler per iteration) or split into explicitly named cold/warm variantsgo test -bench=BenchmarkCompileWorkflow -benchmem -run=^$ ./pkg/workflow/make benchoutput remains parseable by the existingbenchstatstep incgo.ymlCode Region:
pkg/workflow/compiler_benchmark_test.go(lines 12-52,BenchmarkCompileWorkflow)Task 4: Add a
--parallel/-jflag togh aw compilefor explicit concurrency controlPriority: Low
Estimated Effort: Small
Focus Area: Performance
Description: Once Tasks 1 and 2 land, expose an opt-in/opt-out lever for the new parallel compile behavior via a
--parallel <N>(or-j <N>) flag ongh aw compile, defaulting toruntime.GOMAXPROCS(0)when unset and to1(fully sequential) when explicitly set to1, matching common CLI tool conventions (e.g.make -j,go build -p). This gives users and CI a way to control resource usage in constrained environments (e.g. small CI runners) without hardcoding worker-pool size in the codebase.Acceptance Criteria:
gh aw compile --helpdocuments the new flag with a clear description and default valuegh aw compile --parallel 1produces identical output to today's sequential behaviorCompileConfig(pkg/cli/compile_orchestrator.go) into the worker-pool limit added in Task 1gh aw compileinvocations without the flag continue to work unchanged (default behavior should not silently break existing scripts/CI expecting current output ordering/timing assumptions)Code Region:
pkg/cli/compile_compiler_setup.go(flag registration),pkg/cli/compile_orchestrator.go(CompileConfig,CompileWorkflows)📊 Historical Context
Previous Focus Areas
pkg/workflow/js/references across 4 skills🎯 Recommendations
Immediate Actions (This Week)
make recompile/CI compile runShort-term Actions (This Month)
benchstatCI regression gatecgo.ymland report the delta in a follow-up runLong-term Actions (This Quarter)
--parallelflag) — Priority: Low, gives operators explicit control once the underlying mechanism is proven stablepkg/cli/compile_pipeline.golines 343 and 809) if they show similar single-threaded bottlenecks📈 Success Metrics
make recompilewall time (299 workflows): ~10.6–11.9s → target <5s (≥2x speedup via bounded parallelism)go test -race ./pkg/workflow/... ./pkg/cli/...pass rate on parallel compile path: N/A (not yet implemented) → 100% cleanBenchmarkCompileWorkflow: ~5x (cold vs warm) → documented/eliminated via explicit cold/warm split or per-iteration resetNext Steps
Generated by Repository Quality Improvement Agent
Next analysis: 2026-09-24 — Focus area selected by diversity algorithm
Warning
Firewall blocked 4 domains
The following domains were blocked by the firewall during workflow execution:
o205451.ingest.us.sentry.ioproxy.golang.orgstorage.googleapis.comsum.golang.orgTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.
All reactions