Skip to content

build: pipeline package SSA into LLVM backends - #2242

Open
zhouguangyuan0718 wants to merge 23 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/package-pipeline
Open

build: pipeline package SSA into LLVM backends#2242
zhouguangyuan0718 wants to merge 23 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/package-pipeline

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the all-SSA-then-all-LLVM boundary with one package pipeline under the existing -p concurrency budget
  • keep at least one SSA producer active while SSA work remains, then let completed packages enter isolated LLVM backends as soon as their own and effective dependency SSA state is ready
  • preserve the dependency-free LLVM scheduling introduced by build: run LLVM backends in parallel by package #2182; LLVM completion is never used as a package dependency barrier
  • compute immutable caller-tracking summaries once when each SSA package completes and create a package-local frozen tracker for each backend
  • prepare an immutable merged type view for source patches before workers start; keep the legacy patched backend exclusive because it still rewrites shared go/ssa package type pointers
  • retain deterministic error selection, runtime gating, and coordinator-only compatibility paths while each backend task publishes its archive/cache before completing

Dependency and review boundary

Depends on #2182. This remains an optional follow-up; in-memory package archiving is implemented earlier in #2175 and does not depend on this pipeline.

The intended diff is:

Please review only da5685a0..a456e8d7.

Validation

  • focused pipeline scheduler and archive race tests
  • clean -a -p=8 cgo + Plan9 build and runtime check
  • 78 in-process LLVM archives, no external ar, no LLVM package temporary .o
  • git diff --check

The earlier pipeline-only cold-cache measurements remain historical; absolute timings should be refreshed after the new #2175 archive path lands.

@fennoai fennoai 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: build — pipeline package SSA into LLVM backends

Reviewed the incremental range the PR requests, 455fd3e4..6e0a230a (12 files, +1050/-19), across four dimensions (quality, performance, security/concurrency, docs). The change is well-structured and the trickiest invariants (single shared -p budget, patched-package exclusivity, immutable merged type view, frozen per-backend caller-tracking) are documented and hold up. Edge cases I checked are correct: empty input, -p=1 legacy SSA-then-backend ordering, mid-flight SSA error draining, and patched-backend exclusivity.

Concurrency/security: no data-race findings. Shared maps (ctx.patches, ctx.sfilesCache, caller-tracking summaries) are provably populated/frozen before any goroutine launches, panics are recovered on both SSA and backend paths, and no goroutine-leak or blocked-send paths were found (events is buffered to parallelism, active never exceeds it).

Findings below (all non-blocking). Inline comments carry the specifics.

Summary of findings

  • internal/build/package_build.go / scheduler (worth confirming): a non-patched coordinator backend (plan9asm package) runs executeCoordinatorPackageWithCallerTracking via task := *ctx, i.e. on the shared ctx.prog, while serialActive only serializes coordinators against each other — isolated backends can run concurrently. Only patched coordinators get full exclusiveActive exclusivity. Please confirm building a coordinator on shared ctx.prog/ctx.dedup concurrently with isolated sessions is race-free; this is a behavioral change from the legacy strictly-sequential group ordering, and plan9asm packages may not be exercised by the -race targets listed in the PR.
  • Performance (medium): O(n²) scheduler scans — allBackendsDone() and the nextBackend() isolated fallback linearly scan tasks on every scheduling event. A doneCount counter and a dependency-indexed ready-queue would remove it for large dependency graphs.
  • Minor cleanups: dead packagePipelineSSA.order field; duplicate ctx.patches[...] lookup + redundant patched local; SSA-centric stall-error message; a couple of doc-comment wording nits.

No praise-only or low-confidence items are included.

View job run


func (ctx *context) executeCoordinatorPackageWithCallerTracking(spec packageBuildSpec, tracking *cl.CallerTracking, verbose bool) error {
task := *ctx
task.callerTracking = tracking

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.

Please confirm this is race-free — potential behavioral change from the legacy ordering.

executeCoordinatorPackageWithCallerTracking does task := *ctx, so it builds on the shared ctx.prog (and shares ctx.dedup, ctx.pkgs, ...). In buildPackagePipeline, serialActive only serializes coordinators against each other; isolated backends (each with their own session.prog) can still be launched while a coordinator runs. Only patched coordinators get full exclusivity via exclusiveActive/active==0.

A non-patched coordinator arises for plan9asm packages (packageRequiresCoordinator). In the legacy buildPreflightedPackageGroup, the patched→coordinator→isolated groups ran strictly sequentially, so coordinators finished before any isolated backend started. Here they overlap. If building a plan9asm coordinator on the shared ctx.prog concurrently with isolated sessions is safe, a one-line comment stating why would prevent a future regression; plan9asm packages may not be covered by the -race targets in the PR description.

}
return nil
}
allBackendsDone := func() bool {

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.

Performance (medium): O(n^2) scheduler scans. allBackendsDone() here, and the isolated fallback loop in nextBackend (lines 199-203), each do a full linear scan of tasks. allBackendsDone() runs on every outer-loop iteration (line 232) and again at line 285; nextBackend() runs on every inner-loop iteration. With ~n SSA events + ~n backend events, aggregate cost is O(n^2) for a graph of n packages (hundreds–thousands transitively).

Suggested mitigation: keep a running doneCount (bumped where tasks are marked done) and compare against len(tasks) instead of scanning; index dependents by SSA node (you already know which dependents to reconsider when a node finishes at line 305) to feed a ready-queue rather than re-scanning all tasks. advanceSerial is already correctly amortized via the serialPos cursor, so only these two spots are affected.

node.entry.fixOrder = node.entry.fixOrder || entry.fixOrder
continue
}
node := &packagePipelineSSA{entry: entry, order: len(nodes)}

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.

Minor: packagePipelineSSA.order is effectively dead in production — it is assigned here (order: len(nodes)) but only read by package_pipeline_test.go. The real ordering is the topological sort in orderPackagePipelineSSA, which never reads order. Consider dropping the field (adjusting the test) or adding a comment that it exists solely as a stable first-seen index for tests.

}

patched := false
if _, ok := ctx.patches[spec.pkg.PkgPath]; ok {

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.

Minor cleanup: ctx.patches[spec.pkg.PkgPath] is looked up twice (here and at line 441 as patch, ok), and the patched local (line 449) duplicates the task.patched field set just below. You can reuse the line-441 ok/patch and read task.patched directly, removing the second lookup and the shadow variable.

if allBackendsDone() && ssaDone == len(ssaNodes) {
break
}
return nil, fmt.Errorf("package pipeline stalled with %d/%d SSA packages complete", ssaDone, len(ssaNodes))

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.

Minor: this stall error is reachable only on an internal invariant break, but with ssaDone == len(ssaNodes) the remaining stall cause is backend-side (an unschedulable ready()/ssaDeps case), while the message reports only SSA completion counts. Including remaining undone backend-task counts would make a future stall easier to diagnose.

Comment thread cl/instr.go

// NewPackageCallerTrackingForPackages is NewPackageCallerTracking for a
// backend that compiles more than one SSA package, such as an alternate-package
// patch.

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.

Doc nit: NewPackageCallerTrackingForPackages is described as being NewPackageCallerTracking "for a backend that compiles more than one SSA package," but it is actually the general/primary form — NewPackageCallerTracking (line 1050) is the single-package wrapper that delegates here. Consider "generalizes NewPackageCallerTracking to a backend that may compile more than one SSA package" to avoid implying the singular form is primary.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

e389cd89dce9 | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18544 B new 319.990 ms new 1.388 ms new
Linux fmtprintf 2217544 B new 3.819 s new 2.815 ms new
Linux println 71504 B new 320.400 ms new 1.780 ms new
macOS cprintf 84672 B new 702.017 ms new 7.040 ms new
macOS fmtprintf 2361520 B new 4.147 s new 28.412 ms new
macOS println 125712 B new 564.998 ms new 6.894 ms new
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 12.290 ns/op new
Linux BenchmarkMergeCompilerFlags 143.800 ns/op new
Linux BenchmarkMergeLinkerFlags 93.840 ns/op new
Linux BenchmarkChannelBuffered 36.290 ns/op new
Linux BenchmarkChannelHandoff 26053 ns/op new
Linux BenchmarkDefer 43 ns/op new
Linux BenchmarkDirectCall 1.757 ns/op new
Linux BenchmarkGlobalRead 1.786 ns/op new
Linux BenchmarkGlobalWrite 2.812 ns/op new
Linux BenchmarkGoroutine 31321 ns/op new
Linux BenchmarkInterfaceCall 8.788 ns/op new
Linux BenchmarkRuntimeGetG 1.758 ns/op new
macOS BenchmarkLookupPCRandom 14.260 ns/op new
macOS BenchmarkMergeCompilerFlags 173.200 ns/op new
macOS BenchmarkMergeLinkerFlags 110.500 ns/op new
macOS BenchmarkChannelBuffered 29.340 ns/op new
macOS BenchmarkChannelHandoff 8938 ns/op new
macOS BenchmarkDefer 29.150 ns/op new
macOS BenchmarkDirectCall 1.196 ns/op new
macOS BenchmarkGlobalRead 1.220 ns/op new
macOS BenchmarkGlobalWrite 1.075 ns/op new
macOS BenchmarkGoroutine 27731 ns/op new
macOS BenchmarkInterfaceCall 5.274 ns/op new
macOS BenchmarkRuntimeGetG 2.729 ns/op new

No main baseline exists yet; all metrics are marked new.

Warning

  • The rendered benchmark data could not be pushed.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/package-pipeline branch 3 times, most recently from efc18f2 to c81529c Compare July 31, 2026 23:32
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/package-pipeline branch 5 times, most recently from 3dd516d to fbe1ed3 Compare August 1, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant