Skip to content

build: template isolated backend programs#2180

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

build: template isolated backend programs#2180
zhouguangyuan0718 wants to merge 14 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/parallel-build-pr6

Conversation

@zhouguangyuan0718

Copy link
Copy Markdown
Contributor

Stacked on #2179 (and therefore includes #2174-#2179).\n\nCentralizes backend Program and C ABI Transformer construction in an immutable template. It preserves target, Go GlobalDCE, pthread, LTO, funcinfo, runtime/python lazy getters, and runtime linkname registration; each template session gets an independent LLVM context. The existing build path remains serial and now uses the same template, preparing worker-local backend sessions for the following PR.

@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.

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
// 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.

@@ -23,6 +23,18 @@ import (
)

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
}

// 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

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6 branch from 752707f to 768c0d5 Compare July 25, 2026 05:36
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