Skip to content

cl: prepare caller tracking inputs once - #2286

Merged
cpunion merged 3 commits into
xgo-dev:mainfrom
zhouguangyuan0718:codex/caller-precompute-inputs
Aug 11, 2026
Merged

cl: prepare caller tracking inputs once#2286
cpunion merged 3 commits into
xgo-dev:mainfrom
zhouguangyuan0718:codex/caller-precompute-inputs

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Based directly on the current main, which already contains #2182. The PR now has two reviewable commits:

  • a4acd4c98 — prepare caller-tracking inputs once
  • e15a47dc6 — remove process-wide frontend options

Caller-precompute optimization

  • snapshot ssa.Program.RuntimeTypes() once per Program and partition it by declaring package
  • materialize method wrappers on the coordinator before backend workers start
  • sort receiver types before MethodValue calls so shared SSA mutation has a stable order
  • reuse the same per-package function collection for caller base and extended sets instead of traversing the Program twice
  • preserve the frozen read-only CallerTracking contract used by isolated package backends
  • retain the merged recover-facts precompute before caller analysis

Frontend-state cleanup

  • remove the process-wide EnableDebug, EnableDbgSyms, EnableTrace, and EnableExportRename compatibility entry points
  • remove legacyOptions and the context.optionsSet fallback; lowering reads only the package context's explicit Options
  • keep deprecated one-shot package APIs deterministic by using zero-value Options
  • update cltest, debug/export/locality, and shadow-stack tests to pass options explicitly
  • remove the never-assigned disableInline flag and its dead branches

The normal build path already constructs one cl.Options value per invocation and copies it into each isolated package task. This cleanup removes the remaining test/one-shot paths that could change frontend semantics through process-global state.

DAG experiment

I also tested a bounded two-level DAG: independent per-package base nodes followed by extended nodes. It reduced the already-optimized caller phase only from about 0.13s to 0.06s, but changed generated code between serial and parallel analysis because caller analysis can still trigger lazy shared SSA method resolution. One observed difference swapped the two calls in internal/sync.init. The DAG scheduler is therefore intentionally not included.

Measurements

Original forced-cold etcd server measurement (LLGO_BUILD_CACHE=off, -a, -p=8):

After rebasing onto current main, the final two-commit version built etcd server in 37.98s wall (user 208.45s, sys 10.09s) with the LLGo build cache disabled and -a -p=8.

Validation

  • go test ./cl -count=1
  • go test ./internal/build ./ssa ./internal/dcepass -count=1
  • go test -race ./cl -run '^TestCallerTrackingPrecompute' -count=1
  • go test -race ./internal/build -run '^TestConcurrentInvocationsIsolateFrontendOptions$' -count=1
  • focused caller-set parity, debug metadata, export rename, locality, and shadow-stack tests
  • forced-cold etcd server build with LLGO_BUILD_CACHE=off -a -p=8

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

3fd9bda13854 | 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% 297.327 ms +1.3% (worse) 1.316 ms +4.3% (worse)
Linux fmtprintf 1881664 B +0.0% 2.685 s -9.6% (better) 3.351 ms -3.0% (better)
Linux println 68512 B +0.0% 304.251 ms +2.5% (worse) 1.596 ms -3.2% (better)
macOS cprintf 84672 B +0.0% 480.170 ms -19.6% (better) 3.517 ms -26.2% (better)
macOS fmtprintf 1889248 B +0.0% 2.635 s -34.6% (better) 13.981 ms -6.6% (better)
macOS println 121216 B +0.0% 514.546 ms -13.3% (better) 6.145 ms +0.1% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.440 ns/op +0.1% (worse)
Linux BenchmarkMergeCompilerFlags 153.900 ns/op +2.0% (worse)
Linux BenchmarkMergeLinkerFlags 100.200 ns/op +5.9% (worse)
Linux BenchmarkChannelBuffered 34.850 ns/op +0.0%
Linux BenchmarkChannelHandoff 26898 ns/op +0.8% (worse)
Linux BenchmarkDefer 49.410 ns/op -0.2% (better)
Linux BenchmarkDirectCall 1.556 ns/op +0.0%
Linux BenchmarkGlobalRead 1.557 ns/op -0.1% (better)
Linux BenchmarkGlobalWrite 2.487 ns/op +0.0%
Linux BenchmarkGoroutine 32480 ns/op +0.1% (worse)
Linux BenchmarkInterfaceCall 7.793 ns/op +0.1% (worse)
Linux BenchmarkRuntimeGetG 2.183 ns/op +0.1% (worse)
macOS BenchmarkLookupPCRandom 12.230 ns/op -15.3% (better)
macOS BenchmarkMergeCompilerFlags 175.200 ns/op +1.8% (worse)
macOS BenchmarkMergeLinkerFlags 88.940 ns/op -44.6% (better)
macOS BenchmarkChannelBuffered 23.980 ns/op -9.4% (better)
macOS BenchmarkChannelHandoff 7255 ns/op +3.9% (worse)
macOS BenchmarkDefer 30.210 ns/op -18.3% (better)
macOS BenchmarkDirectCall 1.081 ns/op -7.6% (better)
macOS BenchmarkGlobalRead 1.098 ns/op -17.4% (better)
macOS BenchmarkGlobalWrite 1.081 ns/op -17.8% (better)
macOS BenchmarkGoroutine 46562 ns/op -22.0% (better)
macOS BenchmarkInterfaceCall 5.500 ns/op -10.8% (better)
macOS BenchmarkRuntimeGetG 2.081 ns/op -15.9% (better)

Compared with c454c0e65b71 measured in the same runner job.

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

This is a large, well-tested PR that (1) precomputes caller-tracking data serially before launching parallel package backend builds on isolated LLVM Programs, (2) makes recursive named-type conversion order-independent, and (3) adds cross-context LLVM type/constant cloning in dcepass. The concurrency model is largely sound: backend Programs share only read-only packageSyntaxData/localities under an RWMutex, per-Program typs/cvtneed/abiSymbol are not shared, and retained Programs correctly outlive the deadcode/ABI consumers before disposal.

I found one latent correctness regression (inline) plus a few fragility/accuracy notes. Details are inline; additional items below.

Additional findings

  • xtool/env/env.go:110 (lookPathInEnvironment) — cwd/relative executable resolution. The custom PATH resolver rewrites an empty PATH entry to . and joins relative PATH entries with dir, then executes any matching executable regular file. This bypasses the stdlib LookPath ErrDot protection (Go 1.19+) that refuses cwd-relative execution. The subcommand strings feeding ExpandEnvToArgsWith come from #cgo/cflags directives in the compiled packages (appendExternalLinkArgs, clFiles); when building untrusted third-party code, a pkg-config/llvm-config binary placed in the build dir (or a relative PATH entry) could be executed. Consider skipping empty/relative PATH entries or refusing candidates resolved inside the working directory, matching ErrDot semantics. There is also a os.Statexec.Command TOCTOU window (env.go:129/:81), elevated by the above.

  • Memory scaling (informational). executeIsolatedPackage transfers every successful package's Program to ctx.retained, disposed only after all links complete (build.go / package_build.go:305). Peak memory therefore scales with total package count rather than in-flight workers. The code comment acknowledges this is intentional pending a future PackageSummary; noting as the main scalability trade-off.

  • runBoundedPackageJobs (package_build.go:206) — no sibling cancellation on first error (informational). Workers drain the full job queue even after a job fails; the first submitted error is returned after wg.Wait(). Deterministic error ordering is preserved, but a failing build does more work than necessary.

Comment thread cl/import.go
if item.Args != inPkgName && !options.ExportRename {
return false, fmt.Errorf("export comment has wrong name %q", item.Args)
}
prog.SetLinkname(fullName, item.Args)

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 new "export" case does not handle the two-token //export Name Rename form that the legacy initLink path supports (cl/import.go initLink splits text on the first space into inPkgName and link).

For //export Name Rename, directive.ParseGroup yields item.Args = "Name Rename" as a single string. This code then:

  • compares the whole "Name Rename" against inPkgName, so the wrong-name check fires incorrectly, and
  • calls prog.SetLinkname(fullName, item.Args) with the literal "Name Rename" instead of "Rename".

Currently latent (no two-token //export exists in the repo, and single-token //export foo works), but under ExportRename (target builds) this would silently produce a malformed linkname. Suggest splitting item.Args on whitespace to recover inPkgName/link, or explicitly rejecting the two-token form here.

// `go list` (PkgPath not in any module, and Dir/Standard/Goroot unset).
// In that case, treat the package as having no selected .s files.
if pkg.Dir == "" {
ctx.sfilesCache[pkg.ID] = 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.

When sfilesFrozen is true, these fast-path branches still write ctx.sfilesCache[pkg.ID] = nil (also at lines 422 and 483) before the frozen guard is reached. In backend workers sfilesCache is the shared coordinator map (newBackendTask passes it directly) and workers run concurrently with no lock on this map.

It is not hit today because preparePackageSFiles pre-populates every task's entry (so the cache-hit returns first) and plan9-asm packages are routed to the serial coordinator. But the safety relies entirely on those invariants holding for every package a worker can reach. Consider checking the frozen guard before any fast-path write so a frozen context never mutates the shared map, or at least asserting the invariant at these write sites.

Comment thread internal/build/build.go Outdated
c.retained.programs = append(c.retained.programs, retainedBackendProgram{
pkg: pkg,
prog: prog,
abiTypes: prog.AbiTypes(),

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.

prog.AbiTypes() allocates and sorts the package's ABI-symbol name list, and it runs inside the retained.mu critical section. This serializes every worker's publish step on that sort. Compute abiTypes := prog.AbiTypes() before taking the lock so the critical section is just the slice append. Minor, but it is on the join path of the parallel phase.

Comment thread internal/build/package_build.go Outdated
task.pkg.setNeedRuntimeOrPyInit(task.pkg.LPkg.NeedRuntime, task.pkg.LPkg.NeedPyInit)
}
// Linking still consumes live package state: method tables, globals,
// funcinfo/PCLN, C exports, and DCE source modules. Cache hits intentionally

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 "Cache hits intentionally follow the serial path" is inaccurate. partitionPackageExecutions / packageRequiresCoordinator route isolated-vs-coordinator purely from canUseIsolatedBackend() and Plan9 asm usage — cache-hit status is never consulted. A cache-hit package in a normal exe build runs on this isolated path, not the serial/coordinator path. The described behavior (rebuild frontend, skip backend, keep module alive via retention) is correct; only the "serial path" attribution is wrong.

@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.34177% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cl/instr.go 87.23% 12 Missing and 6 partials ⚠️
cl/compile.go 80.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@cpunion
cpunion merged commit 43cf22e 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