Skip to content

build: isolate package backend sessions - #2180

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

build: isolate package backend sessions#2180
zhouguangyuan0718 wants to merge 4 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 a build-local template
  • create independent backend sessions with their own llssa.Program, LLVM Context, TargetMachine, and C ABI transformer
  • replay runtime, Python, syntax/directive, and patched-package state into each new Program
  • freeze prepared locality declaration/layout state from the main Program after preflight and share it directly with backend Programs, without per-session map copies
  • keep package-syntax parse markers Program-local, so workers record their own parsing without mutating the shared locality state
  • permit only idempotent re-declaration of an already prepared name/owner pair after freeze; reject genuinely new locality mutations
  • precompute caller tracking before it becomes read-only worker input
  • preserve the current serial package backend execution in this layer; sessions are infrastructure only until build: run LLVM backends in parallel by package #2182

This layer intentionally has no PackageSummary and does not enable package workers. It only makes a complete package-local backend session reproducible without process-global state or shared LLVM-owned objects. The shared locality state contains no LLVM objects and becomes immutable before backend execution.

Dependency and review boundary

Based directly on current main; it no longer depends on #2179.

  • base: ca0d4199c
  • head: bfdcaae86
  • 7 files, +514 / -34
  • 3 commits

Please review ca0d4199c..bfdcaae86.

Tests

  • go test ./internal/build ./cl ./ssa
  • focused backend-session, concurrent Invocation, package test, caller-tracking, and frozen-locality -race tests
  • repeated frozen shared-locality race test
  • 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 Outdated
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.

funcIndex uint32
}

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.97659% with 21 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/build/build.go 86.31% 8 Missing and 5 partials ⚠️
cl/import.go 79.31% 3 Missing and 3 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

a30cc34f2514 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 18344 B +0.0% 359.789 ms -10.0% (better) 1.456 ms -2.5% (better)
Linux fmtprintf 1862640 B +1.7% (worse) 4.017 s +4.8% (worse) 3.558 ms +3.6% (worse)
Linux println 67720 B +0.0% 356.550 ms -6.2% (better) 1.700 ms -7.4% (better)
macOS cprintf 84672 B +0.0% 430.667 ms -10.4% (better) 6.903 ms +45.0% (worse)
macOS fmtprintf 1869328 B +0.0% 2.884 s -39.8% (better) 12.493 ms -53.7% (better)
macOS println 121200 B +0.0% 476.994 ms +6.9% (worse) 8.403 ms +36.9% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.950 ns/op -2.0% (better)
Linux BenchmarkMergeCompilerFlags 164.900 ns/op -2.3% (better)
Linux BenchmarkMergeLinkerFlags 104.100 ns/op -2.5% (better)
Linux BenchmarkChannelBuffered 37.970 ns/op +3.5% (worse)
Linux BenchmarkChannelHandoff 28088 ns/op +1.4% (worse)
Linux BenchmarkDefer 56.530 ns/op +1.1% (worse)
Linux BenchmarkDirectCall 1.687 ns/op +1.4% (worse)
Linux BenchmarkGlobalRead 1.689 ns/op +1.9% (worse)
Linux BenchmarkGlobalWrite 2.678 ns/op +1.2% (worse)
Linux BenchmarkGoroutine 44321 ns/op +26.5% (worse)
Linux BenchmarkInterfaceCall 8.428 ns/op -2.5% (better)
Linux BenchmarkRuntimeGetG 2.365 ns/op +1.5% (worse)
macOS BenchmarkLookupPCRandom 12.660 ns/op -30.9% (better)
macOS BenchmarkMergeCompilerFlags 115.600 ns/op -66.8% (better)
macOS BenchmarkMergeLinkerFlags 82.990 ns/op -25.8% (better)
macOS BenchmarkChannelBuffered 22.600 ns/op -34.1% (better)
macOS BenchmarkChannelHandoff 7346 ns/op -43.8% (better)
macOS BenchmarkDefer 26.720 ns/op -22.0% (better)
macOS BenchmarkDirectCall 0.993 ns/op -10.1% (better)
macOS BenchmarkGlobalRead 1.103 ns/op -2.8% (better)
macOS BenchmarkGlobalWrite 1.084 ns/op -4.0% (better)
macOS BenchmarkGoroutine 36465 ns/op +17.1% (worse)
macOS BenchmarkInterfaceCall 4.354 ns/op -10.7% (better)
macOS BenchmarkRuntimeGetG 1.893 ns/op -36.3% (better)

Compared with ca0d4199c5d9 measured in the same runner job.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch 14 times, most recently from a79d6f7 to 8d227f7 Compare August 3, 2026 02:49
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch 4 times, most recently from 5672eef to 4622a4c Compare August 3, 2026 22:20
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch from 4622a4c to bfdcaae Compare August 3, 2026 22:34
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