Skip to content

build: isolate package backend sessions - #2180

Open
zhouguangyuan0718 wants to merge 13 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/parallel-build-pr6
Open

build: isolate package backend sessions#2180
zhouguangyuan0718 wants to merge 13 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/parallel-build-pr6

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • centralize backend Program construction in an immutable build-local template
  • create independent backend sessions with their own llssa.Program, LLVM context, TargetMachine, and C ABI transformer
  • replay runtime, Python, source-patch, linkname, no-interface, and frontend syntax state into new Programs
  • precompute caller tracking before it becomes read-only worker input
  • keep package backend execution serial on the coordinator ctx.prog in this layer; newSession is infrastructure-only until build: run LLVM backends in parallel by package #2182
  • copy Go GlobalDCE and deadcode-drop configuration into the session template, while leaving cross-Program ABI type/locality transfer to the layer that first activates those sessions

Dependency and review boundary

Depends on #2179.

The intended diff for this PR is:

Please review only 3a677a12..8d7ec246.

Tests

  • focused backend template/session and caller-tracking tests
  • Dev GlobalDCE unit suite across ssa, internal/build, internal/crosscompile, cmd/internal/flags, and internal/cabi
  • inherited archive tests
  • git diff --check

@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: template isolated backend programs

A careful, well-structured refactor. The preflight/execute/finalize staging, the immutable PackageSummary link-time boundary, and the bounded worker pools are cleanly separated and well-tested. The added synchronization (builtMu, sfilesMu, llvmVersionMu, cacheManagerMu, llssaInitOnce) makes the new concurrent phases race-safe, and collectFingerprint correctly stopped sharing context.fingerprinting by threading a per-call map.

One finding is worth confirming before merge (potential cold-cache cross-compile link failure); the rest are cleanup and can follow. Inline comments carry the concrete diff-line findings.

Correctness — please verify before merge

Skipped runtime packages can trip the new nil-Summary link error. linkMainPkg now requires aPkg.Summary != nil for every entry in linkedOrder, and linkedOrder includes any package with a non-empty ExportFile (build.go:1286). ExportFile comes from go/packages at load time and is non-empty for compiled runtime packages. But buildAllPkgs skips building runtime packages when !needRuntime && !needPyInit && Target != "" (build.go:949-955), so on a cold cache those packages never reach finalizePackageBuild and keep Summary == nil. Preflight does run over the full plan and tryLoadFromCache can populate Summary, so a warm cache masks this — but a clean cross-compile build where the runtime is genuinely not needed appears to hit fmt.Errorf("package %s has no linker summary"). The previous code tolerated this because it only read aPkg.LinkArgs/ArchiveFile for runtime packages and never dereferenced LPkg/Summary. Suggest exempting isRuntimePkg from the up-front nil check and guarding the per-summary reads, or clearing ExportFile on skipped runtime packages. See inline comment on build.go:1294.

Concurrency (not blocking)

  • Panic inside a preflight worker goroutine. appendExternalLinkArgs (build.go:1066 and build.go:1083) panics when a library cannot be located or CheckLinkArgs fails. It is now invoked from a preflight worker (preflightPackageBuild) for source-less PkgLinkExtern packages, so a failure crashes the process instead of propagating as the error the pipeline otherwise returns, and the crash is now non-deterministic across workers. Prefer returning an error from this path now that it runs concurrently.
  • Fingerprint invariant is load-bearing. The parallel-preflight safety relies on dependencyFingerprint never concurrently mutating a shared aPackage: it holds only because the level barrier (wg.Wait() per level) guarantees in-plan deps are fully fingerprinted in an earlier level, and the plan's edges (newPackageBuildPlan) and the fingerprint recursion (collectDependencyInputs) both derive from effectiveDependencies. If a future change lets the fingerprint recursion traverse an edge not modeled in the plan, this becomes a real data race on unsynchronized aPackage fields. Worth a comment documenting the invariant at preflightPackageBuilds / newPackageBuildPlan.

Performance (not blocking)

  • Serial backend caps the realized speedup. Preflight (cache lookup + fingerprint) runs in parallel, but the dominant per-package cost — executePackageBuild -> buildPkg -> LLVM RunPasses + object emission — stays serial in buildAllPkgs (build.go:939-955). On cold builds the wall-clock win is bounded to the preflight/SSA-build phases; the headline "parallel build pipeline" delivers little on codegen-bound builds. This is acknowledged in the comments as intentional staging, flagging only so the limitation is explicit.
  • Per-level barrier + per-level pool re-creation over-serialize preflight. preflightPackageBuilds drains and wg.Wait()s each dependency level and re-creates the jobs/results channels and goroutines per level (package_build.go:48-73). A single straggler in an intermediate level stalls the whole next level, and deep-but-narrow graphs pay repeated pool setup/teardown. A single persistent pool fed by an in-degree ready-queue would remove both the barrier stall and the churn.

Maintainability (not blocking)

See inline comments on package_build.go:114 (packageBuildResult largely unused), build.go:964 (result built on discarded error paths), funcinfo_table.go:88 (production-dead []Package shims / awkward *Summaries naming), package_build.go:153 (hard error on duplicate spec vs prior silent skip), flagfile.go:25 (argumentListFlagNames now includes scalar -p), and build.go:973 (preflight skip-comment omits the already-built case).

Note: I could not run a cold-cache cross-compile build in this environment, so the correctness finding above is reasoned from the code paths rather than reproduced — please confirm.

View job run

Comment thread internal/build/build.go
linkedSummaries := make([]*PackageSummary, len(linkedOrder))
for i, aPkg := range linkedOrder {
if aPkg.Summary == nil {
return fmt.Errorf("package %s has no linker summary", aPkg.PkgPath)

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.

This up-front check requires Summary != nil for every package in linkedOrder, which includes runtime packages (non-empty ExportFile). When buildAllPkgs skips runtime packages (!needRuntime && !needPyInit && Target != "", build.go:949-955) on a cold cache, those packages never reach finalizePackageBuild and keep Summary == nil, so this returns "package %s has no linker summary". The old loop only read LinkArgs/ArchiveFile for runtime packages and never touched LPkg. Consider exempting isRuntimePkg(aPkg.PkgPath) from the nil check (and guarding the per-summary reads) or clearing ExportFile on skipped runtime packages. Please verify with a cold-cache cross-compile build that does not need the runtime.

Comment thread internal/build/package_build.go Outdated
spec := newPackageBuildSpec(pkg)
id := spec.pkg.ID
if _, exists := plan.byID[id]; exists {
return nil, fmt.Errorf("duplicate package build spec for %s", id)

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.

This now hard-fails on a duplicate package ID, whereas the old buildOne used the built map to silently skip already-seen IDs. Correctness now depends on registerSSAPkgs de-duplicating across the initial/dep passes via ctx.pkgByID. That invariant holds today but is load-bearing here — if any future change lets a package appear in both pkgs and depPkgs, the whole build fails instead of degrading. Worth a doc comment stating callers must pass a de-duplicated set.

// packageBuildResult carries the observable output of a serial package build.
// Subsequent scheduler PRs can pass this value between worker and finalization
// stages without exposing the mutable aPackage implementation details.
type packageBuildResult struct {

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.

packageBuildResult is largely speculative: of its five fields only needRuntime/needPyInit are ever read (build.go:944-945). spec, cacheHit, and archiveFile are populated by packageBuildResultFor but never consumed in this PR. Consider trimming to the two used fields (or returning (needRuntime, needPyInit bool)) until the follow-up scheduler PR actually needs the rest.

Comment thread internal/build/build.go Outdated
// after parallel preflight has completed for the package's dependency level.
func buildPreflightedPackage(ctx *context, preflight packagePreflight, verbose bool) (packageBuildResult, error) {
if preflight.skip {
return packageBuildResultFor(preflight.spec), nil

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.

buildPreflightedPackage constructs a full packageBuildResult on both error returns (here and line 967), but callers discard the result whenever err != nil (build.go:940-943, 951). Returning a zero packageBuildResult{} on error paths would be clearer and avoid the wasted work.

@@ -86,12 +86,16 @@ type funcInfoSymbolIndexRecord struct {
}

func collectFuncInfo(pkgs []Package) []funcInfoRecord {

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.

collectFuncInfo (and collectPCLineInfo, collectFuncInfoStubRecords, plus linkedModuleGlobals in build.go) are now only reachable from tests — all production call sites use the ...Summaries variants. This leaves two parallel APIs where the shorter name exists only to keep old tests compiling, and collectFuncInfoStubRecordsSummaries reads awkwardly. Consider migrating the tests to build []*PackageSummary directly and deleting the []Package shims, or making the summary-based function the unsuffixed canonical name.

"unicode"
)

var argumentListFlagNames = [...]string{

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.

argumentListFlagNames now includes "p", which is a scalar flag rather than an argument list, so the name is inaccurate for at least one entry (the new wholeLineValueFlagNames comment acknowledges the distinction). Consider renaming to something like normalizedValueFlagNames to reflect that it drives value normalization, not argument-list semantics.

Comment thread internal/build/build.go Outdated
}

// preflightPackageBuild performs package classification, cache lookup, and
// other work that does not create or transform an LLVM module. It returns

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 comment says skip is returned for "packages with no executable build stage," but the first and most common skip path is the already-built dedup check at build.go:979-981, which returns skip=true for a package that may well have a real build stage (e.g. a shared dep seen in both the initial and dep sets). Suggest amending the comment to also mention already-built packages.

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.95533% with 41 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/build/build.go 89.34% 20 Missing and 14 partials ⚠️
internal/build/package_archive.go 88.09% 3 Missing and 2 partials ⚠️
cl/instr.go 93.54% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch 7 times, most recently from b78b244 to fe5c0bb Compare July 27, 2026 10:13
@zhouguangyuan0718 zhouguangyuan0718 changed the title build: template isolated backend programs build: isolate backend program state Jul 27, 2026
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch from fe5c0bb to 985c3ef Compare July 30, 2026 15:18
@zhouguangyuan0718 zhouguangyuan0718 changed the title build: isolate backend program state build: isolate package backend sessions Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

160c3d0de90b | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18544 B new 323.688 ms new 1.328 ms new
Linux fmtprintf 2249000 B new 3.348 s new 2.441 ms new
Linux println 71504 B new 315.263 ms new 1.573 ms new
macOS cprintf 84672 B new 475.440 ms new 3.516 ms new
macOS fmtprintf 2377936 B new 3.433 s new 21.774 ms new
macOS println 125712 B new 340.862 ms new 4.111 ms new
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 13.350 ns/op new
Linux BenchmarkMergeCompilerFlags 151.500 ns/op new
Linux BenchmarkMergeLinkerFlags 95.160 ns/op new
Linux BenchmarkChannelBuffered 34.630 ns/op new
Linux BenchmarkChannelHandoff 28217 ns/op new
Linux BenchmarkDefer 45.840 ns/op new
Linux BenchmarkDirectCall 1.558 ns/op new
Linux BenchmarkGlobalRead 1.558 ns/op new
Linux BenchmarkGlobalWrite 2.480 ns/op new
Linux BenchmarkGoroutine 33691 ns/op new
Linux BenchmarkInterfaceCall 8.091 ns/op new
Linux BenchmarkRuntimeGetG 2.183 ns/op new
macOS BenchmarkLookupPCRandom 15.280 ns/op new
macOS BenchmarkMergeCompilerFlags 156 ns/op new
macOS BenchmarkMergeLinkerFlags 96.640 ns/op new
macOS BenchmarkChannelBuffered 26.980 ns/op new
macOS BenchmarkChannelHandoff 6078 ns/op new
macOS BenchmarkDefer 34.880 ns/op new
macOS BenchmarkDirectCall 1.126 ns/op new
macOS BenchmarkGlobalRead 1.129 ns/op new
macOS BenchmarkGlobalWrite 1.126 ns/op new
macOS BenchmarkGoroutine 30446 ns/op new
macOS BenchmarkInterfaceCall 5.095 ns/op new
macOS BenchmarkRuntimeGetG 2.257 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/parallel-build-pr6 branch 9 times, most recently from 383f214 to 6d9f76a Compare August 1, 2026 23:22
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch from 6d9f76a to 8d7ec24 Compare August 2, 2026 01:33
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch from 8d7ec24 to 142fd93 Compare August 2, 2026 03:21
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch from 142fd93 to 160c3d0 Compare August 2, 2026 05:09
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