Skip to content

build: run LLVM package backends in parallel - #2182

Merged
xushiwei merged 1 commit into
xgo-dev:mainfrom
zhouguangyuan0718:agent/parallel-build-pr8
Aug 11, 2026
Merged

build: run LLVM package backends in parallel#2182
xushiwei merged 1 commit into
xgo-dev:mainfrom
zhouguangyuan0718:agent/parallel-build-pr8

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Stack

  1. ssa: make recursive type conversion order-independent #2280 makes recursive type conversion order-independent.
  2. build: prepare read-only package backend state #2282 prepares Program-side package metadata once and shares it read-only with fresh backend Programs.
  3. This PR adds the bounded package workers and link integration.

Summary

  • classify normal packages for isolated LLVM lowering and run them with bounded workers; -p controls the bound and the default is GOMAXPROCS
  • keep patched packages, Plan 9 assembly packages, unsupported build modes, and ModuleHook builds on the coordinator path
  • publish each archive/cache result immediately after that package backend completes, avoiding a separate concentrated publication phase
  • give every isolated package a fresh LLVM Context, TargetMachine, lowering caches, ABI state, and C ABI transformer
  • retain every successful worker Program and LPkg until all links, dead-code analysis, strong ABI type overrides, and other whole-program consumers complete; clear LPkg and explicitly dispose all Programs on normal, error, and panic paths
  • recreate cross-context ABI declarations from Go-owned type identities so LLVM values never cross Context boundaries
  • rebuild the frontend module on cache hits while skipping backend emission, preserving the same live link state as the serial implementation

There is no PackageSummary dependency in this implementation. A future summary can reduce retained peak memory, but it is not required to enable package-level LLVM parallelism safely.

Validation

  • go test ./ssa ./cl ./internal/build ./internal/dcepass ./xtool/env -count=1
  • full TestBuildAndCheckSymbolsFromTestdrop through the cl suite
  • p=1 and p=8 cold/hot package-cache tests
  • concurrent build-session and deadcode worker-context tests
  • targeted race tests for parallel scheduling, retained Programs, preloaded syntax, caller tracking, and cross-context DCE cloning
  • go test ./... -run ^$ compiled all packages except a local Go 1.26.5 vet panic in cmd/vendor/x/tools/refactor/satisfy for test/go; the package compiles with -vet=off

@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: run isolated LLVM backends by package

Reviewed the changes in this PR's own commit (build: run isolated LLVM backends by package) across internal/build/{build,collect,package_build,plan9asm}.go and the added tests. The isolation design (per-worker backendSession, immutable template, snapshot-before-dispose, serialized Plan9 asm path) is coherent and the level scheduling / cache-hit finalization reuse are handled carefully. The findings below are ordered by severity.

Correctness (concurrency)

  • Cache-hit packages still compile on the shared ctx.prog while a level runs concurrently. In buildPreflightedPackageLevel, when canUseIsolatedBackend() && len(level) > 1, every spec in the level runs buildPreflightedPackage(ctx, ...) on the shared coordinator ctx from multiple goroutines. Inside executePackageBuild, the isolated-backend branch is gated on !aPkg.CacheHit, so a cache-hit package falls through to buildPkg(ctx, ...)preparePackageModulecl.NewPackageExWithEmbed(ctx.prog, ...) (the early return for cache hits happens after that call). Two or more cache-hit packages in the same level therefore register into the same llssa.Program concurrently — a data race. This is the common incremental-build case (warm cache). See inline note at internal/build/build.go. Worth confirming with go test -race ./internal/build on a build whose ready level contains ≥2 cache-hit packages.

Robustness

  • Panics in worker goroutines are not recovered. preparePackageModule (and other frontend paths) use check(err) which panics. On the serial path this unwound to a returned error; on the new pooled path (buildPreflightedPackageLevel, preflightPackageBuilds) a compile error panics inside a worker goroutine with no recover, crashing the process and hanging wg.Wait() instead of being aggregated into firstErr. Consider a defer recover() in each worker that converts the panic into a result{err: ...}.

Performance

  • Each worker Program replays the entire program's syntax metadata. newProgram() iterates the full immutable syntaxInputs set (ParsePkgSyntax + PreCollectLinknames over every file of every package) plus preCollectRuntimeLinknames for every per-package session. Since a session is created per compiled package, this is O(packages²) frontend replay and each concurrently-live Program holds its own copy of the program-wide maps (peak memory ≈ parallelism × program-metadata). Consider building this metadata once and sharing it read-only, or pooling one session per worker goroutine rather than one per package. Inline note at internal/build/build.go.
  • plan9asmGlobalContextMu is held across module transforms and mod.String(). Only the llvm.GlobalContext-based TranslateSourceModuleForPkg call needs the global lock; holding it across LowerLargeAggregates, TransformModule, and the (potentially expensive) mod.String() serializes that work across all workers for asm-heavy packages (runtime, syscall, internal/*). The sibling plan9asmSigsForPkg already scopes its lock narrowly to just the translate call. Inline note at internal/build/plan9asm.go.

Maintainability

  • Unsynchronized lazy nil-init of shared ctx.sfiles / ctx.plan9asm. plan9asmEnabled, getCachedSFiles, and cacheSFiles do if ctx.X == nil { ctx.X = &...{} } as an unguarded write on a shared ctx, reachable from the concurrent worker path. It is currently benign because Do()/initializePackageBuildState always pre-populate these fields, but the dead nil branches imply a safety that isn't there. Recommend removing them and relying on eager init (and consolidating the three sfilesState construction sites, two of which leave cache nil).
  • Stale comments. preparePackageModule still says it "remains serial because it updates Program-wide registration state" (internal/build/build.go:1825-1826) — it now runs concurrently against a per-worker Program on the isolated path. compilePackageModule's comment "so later PRs can give it a worker-local backend context" (internal/build/build.go:1873-1875) is now stale since this PR already provides that worker-local context. Please update both.

Comment thread internal/build/build.go Outdated
// each package, so packageBuildPlan can schedule ready transactions under -p.
func executePackageBuild(ctx *context, spec packageBuildSpec, verbose bool) error {
aPkg := spec.pkg
if !aPkg.CacheHit && ctx.canUseIsolatedBackend() {

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.

Data race on the shared ctx.prog for cache-hit packages. This gate uses the isolated backend only when !aPkg.CacheHit. A cache-hit package falls through to buildPkg(ctx, ...) (the shared ctx passed by buildPreflightedPackageLevel) → preparePackageModulecl.NewPackageExWithEmbed(ctx.prog, ...), which runs before the cache-hit early return. When canUseIsolatedBackend() && len(level) > 1, workers run concurrently on the shared ctx, so ≥2 cache-hit packages in one level register into the same llssa.Program simultaneously — a race. Route cache-hit frontend registration through an isolated/serialized path too.

Comment thread internal/build/package_build.go Outdated
for index := range jobs {
preflight := preflights[level[index].pkg.ID]
value, err := buildPreflightedPackage(ctx, preflight, verbose)
completed <- result{index: index, value: value, err: err}

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.

No panic recovery in the worker. buildPreflightedPackage reaches check(err) (which panics) via preparePackageModule/clFile/appendExternalLinkArgs. On the old serial path this surfaced as a returned error; here a panic in a worker goroutine has no recover, so it crashes the process and wg.Wait() never returns instead of being aggregated into firstErr. Add a defer func(){ if r:=recover(); r!=nil { completed <- result{index:index, err: fmt.Errorf("%v", r)} } }() (or return errors instead of panicking on this path).

Comment thread internal/build/build.go Outdated
if t.pythonPackage != nil {
prog.SetPython(t.pythonPackage)
}
for _, input := range t.syntaxInputs {

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.

O(packages²) frontend replay + duplicated memory. Because a session is created per compiled package, this loop re-walks the full immutable syntaxInputs set (ParsePkgSyntax + PreCollectLinknames over every file of every package), and preCollectRuntimeLinknames runs again below, for every worker Program. Each concurrently-live Program also keeps its own copy of the program-wide linkname/type-background maps, so peak memory scales with parallelism × program-metadata. Consider building this metadata once and sharing it read-only, or pooling one session per worker goroutine instead of one per package.

Comment thread internal/build/plan9asm.go Outdated
if shouldSkipDarwinDynimportTrampolineAsm(skipDarwinDynimportTrampolines, sfile, src) {
continue
}
plan9asmGlobalContextMu.Lock()

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.

Lock scope is wider than the GlobalContext dependency requires. Only TranslateSourceModuleForPkg touches llvm.GlobalContext; holding plan9asmGlobalContextMu across LowerLargeAggregates, TransformModule, and mod.String() (below) serializes those across all workers for asm-heavy packages. plan9asmSigsForPkg already scopes its lock to just the translate call — matching that here would recover most of the parallelism for runtime/stdlib builds.

Comment thread internal/build/plan9asm.go Outdated

func (ctx *context) plan9asmEnabled(pkgPath string) bool {
ctx.plan9asmOnce.Do(func() {
if ctx.plan9asm == 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.

Unsynchronized write to a shared ctx field. if ctx.plan9asm == nil { ctx.plan9asm = &plan9asmState{} } (and the equivalent for ctx.sfiles in getCachedSFiles/cacheSFiles) mutates the shared ctx without a lock, and this path is reachable from concurrent workers. It's currently benign only because Do()/initializePackageBuildState always pre-populate these fields — so the nil branch is dead code that implies a safety it doesn't provide. Recommend removing the lazy init and relying on eager initialization.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch from 70052aa to 8aeb8a1 Compare July 25, 2026 05:36
@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-pr8 branch 9 times, most recently from e1dbc60 to c175b7f Compare July 27, 2026 10:13
@zhouguangyuan0718 zhouguangyuan0718 changed the title build: run isolated LLVM backends by package build: run LLVM backends in parallel by package Jul 27, 2026
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch 7 times, most recently from 0a057c6 to 4454cde Compare July 30, 2026 15:18
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

1273b8ab1af0 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 18656 B +0.0% 310.421 ms -0.3% (better) 1.249 ms -2.3% (better)
Linux fmtprintf 1881664 B +0.0% 3.058 s -3.7% (better) 3.317 ms -2.5% (better)
Linux println 68512 B +0.0% 306.305 ms +0.9% (worse) 1.635 ms -0.7% (better)
macOS cprintf 84672 B +0.0% 479.283 ms +29.8% (worse) 4.915 ms -20.0% (better)
macOS fmtprintf 1889248 B +0.0% 2.469 s -11.2% (better) 11.808 ms -2.6% (better)
macOS println 121216 B +0.0% 567.438 ms +29.6% (worse) 7.160 ms +91.3% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.400 ns/op +0.3% (worse)
Linux BenchmarkMergeCompilerFlags 152.200 ns/op +0.0%
Linux BenchmarkMergeLinkerFlags 95.250 ns/op -0.2% (better)
Linux BenchmarkChannelBuffered 34.940 ns/op +0.2% (worse)
Linux BenchmarkChannelHandoff 27444 ns/op +2.0% (worse)
Linux BenchmarkDefer 50.700 ns/op +2.1% (worse)
Linux BenchmarkDirectCall 1.557 ns/op +0.1% (worse)
Linux BenchmarkGlobalRead 1.556 ns/op -0.1% (better)
Linux BenchmarkGlobalWrite 2.487 ns/op +0.0% (worse)
Linux BenchmarkGoroutine 34309 ns/op -0.8% (better)
Linux BenchmarkInterfaceCall 7.776 ns/op -0.1% (better)
Linux BenchmarkRuntimeGetG 2.183 ns/op +0.1% (worse)
macOS BenchmarkLookupPCRandom 12.520 ns/op +8.7% (worse)
macOS BenchmarkMergeCompilerFlags 110.500 ns/op -5.9% (better)
macOS BenchmarkMergeLinkerFlags 73.700 ns/op +2.2% (worse)
macOS BenchmarkChannelBuffered 24.840 ns/op +7.5% (worse)
macOS BenchmarkChannelHandoff 8383 ns/op +19.1% (worse)
macOS BenchmarkDefer 32.330 ns/op +6.8% (worse)
macOS BenchmarkDirectCall 1.088 ns/op +5.8% (worse)
macOS BenchmarkGlobalRead 1.175 ns/op +11.0% (worse)
macOS BenchmarkGlobalWrite 1.278 ns/op +11.7% (worse)
macOS BenchmarkGoroutine 47329 ns/op +48.7% (worse)
macOS BenchmarkInterfaceCall 5.154 ns/op +12.1% (worse)
macOS BenchmarkRuntimeGetG 2.025 ns/op -3.6% (better)

Compared with 6bc422655d08 measured in the same runner job.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch 2 times, most recently from d49a547 to 356136d Compare July 31, 2026 01:51
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch 3 times, most recently from 4501ff4 to 7a66581 Compare July 31, 2026 23:04
@zhouguangyuan0718

Copy link
Copy Markdown
Contributor Author

The recursive type-conversion fix has been split into prerequisite PR #2280.

This branch was reordered to use the exact #2280 commit as its first commit, followed by the package-parallel commits. The resulting source tree is byte-for-byte unchanged from the previously validated head. After #2280 merges, rebasing this PR onto main will naturally remove that prerequisite commit.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch 2 times, most recently from 3a58b7e to daf714b Compare August 4, 2026 13:56
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch 4 times, most recently from 1730ef1 to 342e9e2 Compare August 5, 2026 13:41
@zhouguangyuan0718 zhouguangyuan0718 changed the title build: run LLVM backends in parallel by package build: run LLVM package backends in parallel Aug 5, 2026
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch from 342e9e2 to 21650d7 Compare August 5, 2026 13:45
@zhouguangyuan0718

Copy link
Copy Markdown
Contributor Author

@xgopilot please review the current PR head (21650d7) from scratch. Focus on concurrency safety, shared mutable state, deterministic output, LLVM context/module lifetime, cache-hit behavior, error and panic cleanup, and bounded worker scheduling. Please ignore findings that only apply to earlier revisions.

@fennoai

fennoai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

An internal error occurred. Please try again later.

View job run

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch from 21650d7 to ea7bd9a Compare August 5, 2026 16:29
@zhouguangyuan0718

Copy link
Copy Markdown
Contributor Author

@xgopilot review current head ea7bd9a. Focus on bounded package scheduling, worker-local LLVM state, shared-state races, cache-hit behavior, deterministic output, Program/Module lifetime, cross-context DCE, and error/panic cleanup.

@fennoai

fennoai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review complete — parallel LLVM package backends (ea7bd9a)

I reviewed the fixed range 6670dae...ea7bd9a across all requested axes and submitted a PR review with 2 inline notes.

Verdict: No blocking correctness bug found. The design is careful and the concurrency reasoning holds up.

Covered:

  • Bounded scheduling — worker pool bounded by -p (default GOMAXPROCS); per-job panics recovered into errors; errors returned lowest-index-first (deterministic); distinct results[index] writes.
  • Worker-local LLVM state — each package gets a fresh Context/TargetMachine/gocvt/ABI/C-ABI transformer; shared metadata is read-only after preload and mutating setters are bypassed on the PreloadedSyntax path.
  • Shared-state racespackageSyntax is RWMutex-guarded; sfilesCache/plan9asmPkgs are pre-populated then frozen for read-only worker access.
  • Cross-context DCEcloneType correctly re-interns types into the destination Context (opaque pointers, identified vs literal structs, ConstNamedStruct-only); fail-fast panics on unsupported kinds.
  • Cache-hit behavior — cache hits follow the serial path (rebuild frontend module, skip backend emission, keep module alive for links), matching serial semantics.
  • Deterministic output — link order from allPkgs/linkedOrder; abitype dedup is order-independent (same RuntimeName ⇒ same Go type identity).
  • Program/Module lifetime — retained Programs kept alive through links + DCE + strong-ABI overrides; LPkg cleared before Dispose(); disposal on normal/error/panic paths; ownership transfer via the owned flag is correct.

Main note (fragility, not a live bug): CallerTracking.base/extended are unsynchronized maps that runtimeCallerFuncSet writes on a miss. The parallel path is safe only because Precompute(AllPackages()) fully populates them before workers start — but nothing enforces this at the read sites. A future lazy SSA-package creation during lowering would silently reintroduce a data race. Suggested a guard or doc-contract inline.

Smaller notes: pkgSFiles frozen-cache error is a good defensive belt (confirm no dependency-only package queries SFiles); env.lookPathInEnvironment lacks Windows PATHEXT handling (fine for current host-only use).

Not verified here: ssa/cl/internal/build/internal/dcepass link against LLVM, which is absent in this sandbox, so I could not compile or run their tests — findings are from static review. xtool/env builds and its tests pass. I recommend CI confirm the -race suite exercises the p=8 multi-package caller-tracking path.

Review · View job run

@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 — parallel LLVM package backends (ea7bd9a)

I reviewed the requested areas against the fixed range 6670dae...ea7bd9a. Overall this is a careful, well-reasoned design: worker-local LLVM state is genuinely isolated, cross-Context boundaries are handled correctly, and the retained-Program lifetime is disposed on normal/error/panic paths. I found no blocking correctness bug. The notes below are one fragility risk worth guarding and a few smaller observations.

Note: ssa, cl, internal/build, and internal/dcepass link against LLVM, which is not installed in this review sandbox, so I could not compile or run their tests here. xtool/env builds and its tests pass. The findings below are from static review.

What holds up well

  • Bounded scheduling (runBoundedPackageJobs): fixed worker pool bounded by min(max(1,-p), len), -p defaulting to GOMAXPROCS. Per-job panics are recovered into errors; errors are collected by index and returned lowest-index-first, so the surfaced error is deterministic regardless of completion order. Workers write only to distinct results[index] slots — no aliasing.
  • Worker-local LLVM state: newBackendSession/NewBackendProgram give each package a fresh Context, TargetMachine, gocvt (owns typs/cvtneed), ABI state, and C ABI transformer. Shared packageSyntax/localities/callerTracking are read-only after preload, and the mutating setters on packageSyntax are all bypassed on the PreloadedSyntax lowering path (initFiles/importPkg early-return) and additionally guarded by its RWMutex.
  • Cross-context DCE (dcepass.cloneType): re-interning every type into the destination Context, opaque-pointer handling, identified-vs-literal struct re-interning, and the ConstNamedStruct-only path are correct and the rationale in the comments matches the LLVM verifier's constraints. panic on unsupported constant/expr/type kinds is a reasonable fail-fast.
  • Deterministic output: link order derives from allPkgs/linkedOrder, not completion order. AbiTypes() sorts by name; retainedBackendAbiTypes() concatenation order only affects which duplicate "wins" in RegisterAbiTypes, and duplicates share the same Go type identity (RuntimeName(t)), so the result is order-independent. Immediate per-package publication doesn't affect final artifact contents.
  • Program/Module lifetime: retained Programs are kept alive through links + DCE + strong-ABI override emission, then LPkg cleared before any Dispose(), with disposal wired on the success path (build.go:750) and via defer for error/panic. executeIsolatedPackage's owned flag correctly transfers ownership only after retainBackendProgram.

Fragility risk worth guarding (not a live bug)

The correctness of the parallel path rests on an unstated invariant: every *ssa.Package a worker can query — and every callee package it reaches — must already be memoized before workers start. CallerTracking.base/extended are plain maps with no mutex; runtimeCallerFuncSet writes on a miss. Today Precompute(progSSA.AllPackages()) runs after all SSA packages are created (build.go:645-646), so every worker lookup is a pure read and there is no race. But nothing enforces this at the point of use — a future change that lazily creates an SSA package during lowering, or queries a package outside AllPackages(), would silently reintroduce an unsynchronized concurrent map write. Consider one of: a race-detector-friendly guard/assertion on miss during the parallel phase, or a short doc-contract at the runtimeCallerFuncSet read sites. Inline notes below.

Smaller observations

  • pkgSFiles returns a "not prepared before backend execution" error when a frozen cache misses. Since preparePackageSFiles covers pkg.Package and pkg.AltPkg for every task in prePackageBuilds, this should never fire in normal flow — good defensive belt. Worth confirming no backend path queries SFiles for a package that wasn't a build task (e.g. a dependency-only package).
  • xtool/env.lookPathInEnvironment does executable-bit resolution but no Windows PATHEXT/.exe handling. Fine for the current host-only helper-command use; flagging only in case cross-platform host builds are in scope.

Verification I could not run here

  • go test ./ssa ./cl ./internal/build ./internal/dcepass and the -race scheduling/retained-Program/cross-context-DCE tests (LLVM absent). The PR description lists these as passing; I recommend CI confirm the -race suite specifically exercises the p=8 path with packages whose callee tracking spans multiple packages.

Comment thread cl/instr.go Outdated
// Precompute before workers start and then share the plain maps read-only.
type CallerTracking struct {
base map[*ssa.Package]map[*ssa.Function]bool
extended map[*ssa.Package]map[*ssa.Function]bool

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.

base/extended are unsynchronized plain maps, and runtimeCallerFuncSet/runtimeCallerBaseSet write into them on a miss (lines 972, 1037). The parallel path is safe only because Precompute(progSSA.AllPackages()) fully populates both maps before any worker runs, making every worker lookup a pure read.

This invariant is load-bearing but unenforced. If a future change lazily creates an *ssa.Package during lowering, or a worker queries a package/callee not in AllPackages() at precompute time, this becomes an unsynchronized concurrent map write (data race + possible corruption) that the -race suite may not catch unless it hits that exact path.

Suggestion: add a guard so a miss during the parallel phase is loud rather than silent — e.g. a flag set after Precompute that makes runtimeCallerFuncSet panic on a miss instead of writing, or at minimum a doc-contract at the read sites (compile.go:2281-2282, instr.go:894/904) stating that all lookups must be precomputed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in fa52bf7: CallerTracking is sealed after Precompute, and any later base/extended miss now panics instead of mutating the shared maps. Added focused tests for both miss paths and ran them under -race.

commands: ctx.commands,
frontendOptions: ctx.frontendOptions,
cTransformer: session.transformer,
sfilesCache: ctx.sfilesCache,

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.

Backend contexts share the coordinator's sfilesCache map by reference with sfilesFrozen: true. This is safe because preparePackageSFiles pre-populates every task's pkg.Package/pkg.AltPkg keys during prePackageBuilds, so worker reads hit the early if v, ok := ctx.sfilesCache[pkg.ID]; ok return before any write path. Worth a one-line comment here making the "pre-populated + read-only under freeze" contract explicit, since a concurrent write into this shared map (e.g. a backend querying an unprepared package) would be an unguarded data race rather than the intended sfilesFrozen error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented the pre-populated/read-only contract in fa52bf7. A frozen miss returns before the write path, so worker contexts cannot mutate the shared map.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch 7 times, most recently from 47ddcce to 276f715 Compare August 11, 2026 02:09
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr8 branch from 276f715 to 1273b8a Compare August 11, 2026 03:18
@xushiwei
xushiwei merged commit c454c0e into xgo-dev:main Aug 11, 2026
42 checks passed
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.

2 participants