build: split package compilation stages - #2175
Conversation
There was a problem hiding this comment.
Review: build — model package build units explicitly
Overall a clean, well-documented refactor that stages the build pipeline for bounded parallel compilation. The register*/build* split is sensible, the buildSSAPkgs worker pool is correctly bounded (min(parallelism(), len(unique))), dedupes by *ssa.Package, and runs fixSSAOrder serially afterward, and Package.Build() is documented as thread-safe — so the concurrency introduced here is race-free. Behavior parity with the old buildOne switch looks preserved. Findings below are correctness/maintainability nits; none are blocking.
Inline findings
internal/goflags/flagfile.go:30— adding-ptoargumentListFlagNamesalso changes flag-file line parsing (wholeLineValueFlag), a likely regression.internal/build/collect.go:214—llvmVersionis only eagerly initialized when cache is enabled, leaving the lazy first-use write the doc comment claims to remove.internal/build/build.go:926— inconsistent secondpackageBuildResultFor(spec)snapshot.
Additional (non-inline) notes
internal/build/package_build.go:60-66:packageBuildResult.spec,cacheHit, andarchiveFileare populated but never read by production code (only by the test). They read as dead fields; consider trimming until the scheduler PR needs them, or add a note that they are intentional forward-looking scaffolding.internal/build/build.go:2040(jobs := make(chan ssaBuildEntry)): the dispatch channel is unbuffered, so the producer hands off one entry at a time. Sinceuniqueis fully materialized before dispatch, a buffered channel (make(chan ssaBuildEntry, len(unique))) would let the producer enqueue without blocking and reduce handoff overhead. Minor.internal/build/collect.go:241-246(dependencyFingerprint): for a dep with no module version that is not inpkgByID, a throwawaytempaPackage is fingerprinted with no memoization, so the same dep is recomputed once per parent. Worth acontext-level fingerprint cache keyed bydep.IDif fan-in is high. (Largely pre-existing; noting becauseeffectiveDependenciesnow feeds this path.)
A -race CI run over a multi-package build would be worthwhile to lock in the new concurrency guarantee.
| "gcflags", | ||
| "gccgoflags", | ||
| "ldflags", | ||
| "p", |
There was a problem hiding this comment.
Adding "p" to argumentListFlagNames affects two consumers with different semantics. normalizeBuildFlags correctly turns -p 4 into -p=4, but wholeLineValueFlag (this same file) treats an argument-list flag as consuming the entire remainder of the line unquoted. -ldflags/-gcflags want that (space-separated list values), but -p takes a single integer.
A flags.txt line like -p=4 -v will now be captured whole as the flag -p=4 -v instead of split into -p=4 and -v. That reaches parseBuildParallel, where strconv.Atoi("4 -v") fails and ApplyBuildFlags returns an error, and -v is lost. Consider keeping -p out of the wholeLineValueFlag set (e.g. split the list into "normalize-only" vs "whole-line-value" names), and adding a test for -p alongside other flags on one flag-file line.
| if c.sfilesCache == nil { | ||
| c.sfilesCache = make(map[string][]string) | ||
| } | ||
| if !cacheEnabled() { |
There was a problem hiding this comment.
initializePackageBuildState documents that it "avoids lazy first-use writes becoming data races later," but it only initializes llvmVersion/llvmVersionReady when cacheEnabled() is true (this early return). When the cache is disabled, getLLVMVersion still performs a lazy first-use write. Since getLLVMVersion is used for fingerprinting regardless of cache state, this is exactly the latent race the comment sets out to prevent once package building is parallelized. Consider initializing llvmVersion/llvmVersionReady unconditionally, before the cacheEnabled() check.
| if err := ctx.saveToCache(aPkg); err != nil && verbose { | ||
| fmt.Fprintf(os.Stderr, "warning: failed to save cache for %s: %v\n", pkg.PkgPath, err) | ||
| } | ||
| return packageBuildResultFor(spec), nil |
There was a problem hiding this comment.
result was already captured at line 913 (and returned for the cache-hit path at 915), but the success path recomputes packageBuildResultFor(spec) here instead of returning that value. Because normalizeToArchive mutates aPkg.ArchiveFile, the two snapshots differ in archiveFile. Harmless today only because archiveFile is unused, but it's a latent inconsistency if that field becomes load-bearing. Recompute once after normalizeToArchive/appendExternalLinkArgs and return that single value.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
225a82b to
1bfd648
Compare
1bfd648 to
6cead60
Compare
LLGo baseline benchmarks
Program measurements
Core language and compiler benchmarks
No main baseline exists yet; all metrics are marked Warning
|
7f754a8 to
93f7d8a
Compare
9cc93a5 to
e98e124
Compare
e98e124 to
dcf4cca
Compare
Summary
MemoryBuffertogether with file-backed cgo/Plan9 members.ofilesDependency and review boundary
Based on current
main, which includes #2212, #2221, and #1736. In-memory archive writing additionally depends on xgo-dev/llvm#47. The temporary module replacement will be removed after that binding PR merges.The intended diff for this PR is:
2310ff87(current main with compiler: isolate per-invocation frontend state #2221)dcf4cca4Please review only
2310ff87..dcf4cca4.Tests
go test ./internal/build ./cl ./ssago test -race ./internal/build -run TestNormalizeToArchive -count=1arand creates no temporary LLVM package.ogit diff --check