Skip to content

build: snapshot package linker state - #2179

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

build: snapshot package linker state#2179
zhouguangyuan0718 wants to merge 2 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/parallel-build-pr5

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • introduce LLVM-free PackageSummary as the linker-facing package result
  • snapshot link arguments, runtime requirements, ABI state, globals, funcinfo/PCLN, stubs, and C-shared exports before package LLVM state is released
  • make final linking and metadata generation consume summaries instead of live worker packages
  • persist summary metadata in cache manifests and safely rebuild legacy cache entries
  • retain the serial live-package compatibility path for C archive/shared header generation
  • keep backend execution serial in this layer

Dependency and review boundary

Depends on #2175.

The intended diff for this PR is:

Please review only dcf4cca4..3a677a12.

Tests

  • focused PackageSummary, linker metadata, cache, funcinfo/PCLN, archive error, fingerprint cycle, link-only preflight, and SSA scheduling tests
  • inherited build: split package compilation stages #2175 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.

FennoAI Review — parallel build pipeline

Solid, well-structured refactor. The register/build split, the packageBuildPlan/dependency-level algorithm, and the PackageSummary decoupling (linking no longer reads live LLVM modules) are coherent, and the concurrency-sensitive paths are guarded (builtMu, sfilesMu, cacheManagerMu, llvmVersionMu). Channel/waitgroup usage in both worker pools is correct (no deadlock or goroutine leak). The -p flag parsing, atomic-on-error config application, and the flagfile whole-line-value split are all covered by tests. No blocking issues.

A few concrete points, mostly inline. Two non-inline items worth flagging:

1. -p speedup is bounded by a serial backend (performance). Only two stages run in parallel: buildSSAPkgs (ssa.Package.Build) and the cache/fingerprint preflight (preflightPackageBuilds). The dominant cost — buildPreflightedPackageexecutePackageBuildbuildPkg (frontend + LLVM RunPasses/object emission) — still runs in the strictly serial for _, spec := range normalPkgs loop in buildAllPkgs. The comments acknowledge this is intentional ("backend remains deliberately serial until it owns an isolated LLVM context"), so this is a heads-up rather than a defect: on cold builds -p mainly speeds up type-check/SSA and cache-stat work, not the backend. Consider noting this expectation wherever -p is user-documented.

2. Cache manifest is trusted without re-verifying it against the fingerprint (pre-existing, not a regression). tryLoadFromCache uses the fingerprint only to derive the on-disk filename, then trusts meta.LinkArgs and the new meta.Summary (which now includes LinkArgs flowing onto the linker command line) without recomputing/comparing the fingerprint of the stored manifest. This PR widens the trusted payload but does not change the model (the cache dir is local). Not blocking; recomputing the fingerprint on load would make entries self-authenticating and close this for both old and new fields.

Automated review; treat as suggestions. The concurrency note below (shared fingerprint state) is the item I'd most want confirmed before merge.

Comment thread internal/build/collect.go Outdated
if aDep, ok := c.pkgByID[dep.ID]; ok {
if aDep.Fingerprint == "" {
if err := c.collectFingerprint(aDep); err != nil {
if err := c.collectFingerprintWithStack(aDep, fingerprinting); err != 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.

Parallel preflight correctness here rests on a DAG-ordering invariant that is worth confirming and testing. dependencyFingerprint reaches into the shared ctx.pkgByID[dep.ID] aPackage and, when its Fingerprint == "", writes aDep.Fingerprint/aDep.Manifest on that shared object. Within a dependency level, workers run concurrently, so this is only race-free if every in-plan dependency was already fully fingerprinted in an earlier level.

That holds today because newPackageBuildPlan and collectDependencyInputs both derive edges from effectiveDependencies, and pkgByID/plan.byID come from the same allPkgs set — so an in-plan dep is always an earlier level, and out-of-plan deps use fresh local temp objects. But it's fragile: any future case where a fingerprinted dependency is reachable via pkgByID yet not represented as a plan edge (e.g. a dep filtered from the plan but still traversed here, or a plan/fingerprint classification mismatch) would let two same-level workers write the same shared aDep concurrently — a data race producing corrupt manifests.

Coverage is thin: only module_hook_test.go sets Parallel = 2, and there's no -race run over a real multi-package dependency graph. Recommend adding a go test -race case with Parallel >= 2 across a non-trivial import graph, and/or guarding the shared aDep fingerprint write rather than relying solely on level ordering.

Comment thread internal/build/build.go Outdated
PCLNModeSet bool
AllowNoBody bool // allow declarations without bodies, as go tool compile does
// Parallel is the maximum number of LLGo's internally parallel build tasks.
// Zero uses GOMAXPROCS; command-line -p, like go build -p, must be >= 1.

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 field's own contract differs from this comment. The comment describes the CLI rule ("-p ... must be >= 1"), but the Parallel field itself accepts 0 programmatically (parallelism() maps 0GOMAXPROCS, and Do rejects only negative values). For a direct API caller, 0 is valid and means "use GOMAXPROCS". Consider rewording so the field contract (0 = default/GOMAXPROCS, negative = error) is stated for the field, with the >= 1 note scoped to the -p flag.

Comment thread internal/build/build.go
}
// Snapshot every linker-facing LLVM fact before cache publication. The
// summary is the hand-off point to a future worker-local Program.
aPkg.Summary = summarizePackage(aPkg)

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.

summarizePackage currently always returns non-nil on this path, so this is fine today. As defensive robustness: if saveToCache were ever reached with pkg.Summary == nil, metadata() returns nil and the entry is written with summary: absent — which tryLoadFromCache then treats as a permanent cache miss (meta.Summary == nil guard). That is a silently non-cacheable entry rather than an error. A short assertion/comment here that Summary must be set before saveToCache would make the invariant explicit.

Comment thread internal/goflags/gobuild.go Outdated
if err != nil || parallel <= 0 {
return 0, false, fmt.Errorf("-p must be a positive integer, got %q", value)
}
present = true

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: when -p= appears multiple times in the normalized flags, this silently uses the last occurrence. That matches go build's last-wins semantics, so it's defensible, but it's undocumented — a one-line comment noting the intentional last-wins behavior would help future readers.

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.86096% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/build/build.go 91.66% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr5 branch 7 times, most recently from 68b3b55 to b818839 Compare July 31, 2026 01:50
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

ebd698853b7c | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18544 B +0.0% 302.018 ms -7.4% (better) 1.258 ms -20.3% (better)
Linux fmtprintf 2217536 B -0.0% (better) 3.315 s +1.7% (worse) 2.440 ms -0.5% (better)
Linux println 71504 B +0.0% 307.039 ms -1.5% (better) 1.687 ms -1.1% (better)
macOS cprintf 84672 B +0.0% 318.091 ms -28.8% (better) 2.436 ms -57.7% (better)
macOS fmtprintf 2361520 B +0.0% 3.197 s +6.3% (worse) 18.714 ms -1.5% (better)
macOS println 125712 B +0.0% 314.440 ms -24.6% (better) 3.704 ms -15.8% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 13.330 ns/op -0.7% (better)
Linux BenchmarkMergeCompilerFlags 151 ns/op -0.3% (better)
Linux BenchmarkMergeLinkerFlags 94.100 ns/op -0.9% (better)
Linux BenchmarkChannelBuffered 34.020 ns/op -1.6% (better)
Linux BenchmarkChannelHandoff 26753 ns/op +1.6% (worse)
Linux BenchmarkDefer 43.520 ns/op -6.1% (better)
Linux BenchmarkDirectCall 1.560 ns/op +0.2% (worse)
Linux BenchmarkGlobalRead 1.868 ns/op -0.2% (better)
Linux BenchmarkGlobalWrite 2.489 ns/op +0.0% (worse)
Linux BenchmarkGoroutine 33218 ns/op +0.3% (worse)
Linux BenchmarkInterfaceCall 8.097 ns/op +0.0% (worse)
Linux BenchmarkRuntimeGetG 1.867 ns/op -0.2% (better)
macOS BenchmarkLookupPCRandom 12.400 ns/op +4.4% (worse)
macOS BenchmarkMergeCompilerFlags 106.800 ns/op -16.9% (better)
macOS BenchmarkMergeLinkerFlags 72.370 ns/op -14.9% (better)
macOS BenchmarkChannelBuffered 27.740 ns/op +23.8% (worse)
macOS BenchmarkChannelHandoff 6412 ns/op -7.7% (better)
macOS BenchmarkDefer 44.790 ns/op +56.4% (worse)
macOS BenchmarkDirectCall 1.152 ns/op +11.6% (worse)
macOS BenchmarkGlobalRead 1.320 ns/op +28.3% (worse)
macOS BenchmarkGlobalWrite 1.193 ns/op +15.4% (worse)
macOS BenchmarkGoroutine 31577 ns/op +17.8% (worse)
macOS BenchmarkInterfaceCall 5.055 ns/op +11.9% (worse)
macOS BenchmarkRuntimeGetG 5.135 ns/op +146.9% (worse)

Compared only with the latest matching platform in the main series.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr5 branch 11 times, most recently from c72f759 to 942bc28 Compare August 2, 2026 14:37
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