Skip to content

build: add Chrome scheduler trace - #2243

Open
zhouguangyuan0718 wants to merge 16 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/build-trace
Open

build: add Chrome scheduler trace#2243
zhouguangyuan0718 wants to merge 16 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/build-trace

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Depends on #2182. The trace-specific review range is 21650d77..6314152f.

Architecture proposal: #2284.

Summary

  • add llgo build -debug-trace=<file> with Chrome Trace Event JSON output; this is a build-scheduler trace, not runtime/trace
  • rebase the trace directly on the current build: run LLVM package backends in parallel #2182 package-worker implementation without restoring the superseded SSA/backend pipeline, PackageSummary, syntax delta, or snapshot/overlay designs
  • visualize coordinator preparation plus a stable set of worker lanes bounded by effective -p (GOMAXPROCS by default)
  • record package loading, shared backend-state preparation, parallel Go SSA, serial SSA repair/caller tracking/preflight, patched/coordinator/isolated backend classification, immediate archive/cache publication, and final linking
  • add direct SSA-to-backend flow edges without emitting the transitive import graph
  • keep trace state build-local so concurrent invocations do not share files, lanes, or mutable state

Review feedback addressed

  • a trace flush error is now a warning and does not discard an otherwise successful build
  • trace creation uses O_EXCL and refuses to overwrite any existing file
  • the flag help explicitly says build-scheduler trace
  • lane documentation now describes effective -p/GOMAXPROCS and the no-nested-worker-span invariant
  • coordinator-only backend work stays on the coordinator lane; only real bounded SSA/isolated backend work occupies worker lanes

Actual etcd trace

Command, with package build outputs disabled and every package forced:

LLGO_BUILD_CACHE=off llgo build -a -p=8 \
  -debug-trace=/tmp/etcd-p8.json \
  -o /tmp/etcd ./server

Observed on go.etcd.io/etcd/server/v3 at etcd 3e3b4c181c69:

  • wall time: 52.36 s
  • 592 Go SSA tasks, peak parallelism 8
  • 553 isolated LLVM backend tasks, peak 8 and average 7.19
  • all 8 isolated workers active for 82.3% of the isolated backend window
  • 156.41 task-seconds of isolated backend work completed in a 21.75 s window
  • 11 patched backends (2.58 s serial) and 11 other coordinator backends (0.89 s serial)
  • caller-tracking precomputation: 14.14 s serial
  • final link: 8.37 s serial

Open the generated JSON directly in chrome://tracing or https://ui.perfetto.dev/.

Validation

  • go test ./internal/build ./cmd/internal/build ./cmd/internal/flags -count=1
  • go test -race ./internal/build -run '^TestBuildTrace' -count=1
  • focused trace, SSA worker, package task, and command flag tests
  • traced cache-disabled etcd -a -p=8 build and runnable output
  • JSON parsed and concurrency metrics derived from complete events
  • 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 — build scheduler trace

Reviewed the trace-specific range 475dc124..d917b0ce across code quality, performance/concurrency, security, and documentation. The design is solid: the nil-tracer convention keeps disabled builds allocation-free, span/close double-invocation is guarded by sync.Once, and flow edges are deliberately limited to direct SSA dependencies to avoid quadratic trace size.

Concurrency, verified safe (no change needed): the lanes semaphore never deadlocks — every startWorker caller runs under the same parallelism() bound that sizes the lane channel, and the serial coordinator phases never overlap the parallel worker phases. The node.traceSpan / from.end cross-goroutine reads in flow() have a valid happens-before via the pipeline events channel and ready() gating.

Findings are inline. The most impactful is the build-fails-on-trace-write-error behavior in internal/build/build.go.

Minor note not placed inline: the load-bearing invariant behind the lane semaphore ("never open more than parallelism worker spans concurrently, never nest worker spans on one goroutine") is currently undocumented — worth a doc comment on startWorker/buildTracer to protect future callers, since violating it would deadlink real build work rather than just skew the visualization.

View job run

Comment thread internal/build/build.go Outdated
buildSpan.done()
if closeErr := buildTrace.close(); closeErr != nil && resultErr == nil {
result = nil
resultErr = fmt.Errorf("write build trace: %w", closeErr)

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.

A diagnostic trace-write failure turns a fully successful build into a reported failure: on closeErr != nil the deferred func discards the built packages (result = nil) and returns resultErr. -debug-trace is a debugging aid; failing the whole build because the trace file couldn't be flushed is a heavy, asymmetric policy (a write error mid-build is already silently swallowed via the sticky writeErr, and only surfaces here at close). Consider logging the trace-write error to stderr and preserving the build result instead. If failing hard is intended, a short comment justifying it would help, since the behavior is non-obvious.

Comment thread internal/build/build_trace.go Outdated
path = filepath.Join(dir, path)
}
if filepath.Ext(path) == ".go" {
return nil, fmt.Errorf("refusing to overwrite Go source file %s", path)

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 guard refuses only a .go extension, then os.Create (line 86) truncates any other existing target. So -debug-trace=go.mod, -debug-trace=Makefile, an existing archive, etc. are silently overwritten, and relative paths resolve against the source dir. The message "refusing to overwrite Go source file" implies a general overwrite protection that doesn't exist. Either drop the check (and be honest it's a plain os.Create), or if protection is intended use os.OpenFile with O_CREATE|O_EXCL to refuse overwriting any existing file. Low severity since the path is a locally-supplied flag.

Comment thread internal/build/build_trace.go Outdated
}

// buildTracer is owned by one Build invocation. The lane semaphore mirrors
// the build's -p limit, so overlapping worker-lane events visualize the same

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 the lane semaphore "mirrors the build's -p limit," but the lane count comes from conf.parallelism(), which returns BuildParallelism only when > 0 and otherwise falls back to GOMAXPROCS. When -p isn't supplied the lanes mirror GOMAXPROCS, not a user -p value. Suggest wording like "the build's effective package parallelism (-p, or GOMAXPROCS by default)."

Comment thread cmd/internal/flags/flags.go Outdated
// output file; test and run may coordinate multiple child invocations.
func AddBuildTraceFlag(fs *flag.FlagSet) {
BuildTrace = ""
fs.StringVar(&BuildTrace, "debug-trace", "", "Write a Chrome/Perfetto build trace to file")

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 PR stresses this is a build scheduler trace, not a runtime/execution trace, and this same file already defines -trace ("Write an execution trace to the specified file"). Neither the flag name -debug-trace nor this usage string conveys "scheduler," so the two are easy to conflate. The doc comment on AddBuildTraceFlag and the internal test both say "scheduler" — consider aligning the user-facing usage string, e.g. "Write a Chrome/Perfetto build-scheduler trace to file".

Comment thread internal/build/package_pipeline.go Outdated
} else {
traceSpan.setArg("class", "isolated")
}
if ctx.buildTrace != 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.

This explicit if ctx.buildTrace != nil is redundant with flow()'s own nil-receiver guard, and inconsistent with every other call site here (startWorker, setArg, done are all called unconditionally on a possibly-nil tracer). If the intent is to skip building the callerNodes loop when tracing is off, a one-line comment would clarify it; otherwise drop it for consistency.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

6314152fcf88 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 18456 B +0.0% 310.073 ms +5.2% (worse) 1.261 ms -0.6% (better)
Linux fmtprintf 1861416 B +1.7% (worse) 2.870 s -3.8% (better) 3.180 ms +1.5% (worse)
Linux println 68008 B +0.0% 295.299 ms +1.5% (worse) 1.617 ms +1.8% (worse)
macOS cprintf 84672 B +0.0% 507.251 ms -17.4% (better) 4.173 ms +18.2% (worse)
macOS fmtprintf 1869328 B +0.0% 3.665 s -2.2% (better) 19.628 ms +29.2% (worse)
macOS println 121200 B +0.0% 470.327 ms +14.8% (worse) 4.859 ms +10.8% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.260 ns/op +0.2% (worse)
Linux BenchmarkMergeCompilerFlags 152.900 ns/op +1.5% (worse)
Linux BenchmarkMergeLinkerFlags 98.690 ns/op +4.8% (worse)
Linux BenchmarkChannelBuffered 35.540 ns/op +5.3% (worse)
Linux BenchmarkChannelHandoff 27579 ns/op +0.9% (worse)
Linux BenchmarkDefer 46.720 ns/op -4.6% (better)
Linux BenchmarkDirectCall 1.561 ns/op +0.3% (worse)
Linux BenchmarkGlobalRead 1.557 ns/op +0.1% (worse)
Linux BenchmarkGlobalWrite 2.489 ns/op +0.2% (worse)
Linux BenchmarkGoroutine 31885 ns/op -1.9% (better)
Linux BenchmarkInterfaceCall 7.786 ns/op -3.9% (better)
Linux BenchmarkRuntimeGetG 2.179 ns/op -0.0% (better)
macOS BenchmarkLookupPCRandom 20.990 ns/op +43.1% (worse)
macOS BenchmarkMergeCompilerFlags 214.300 ns/op +60.9% (worse)
macOS BenchmarkMergeLinkerFlags 114.600 ns/op +37.9% (worse)
macOS BenchmarkChannelBuffered 35.120 ns/op +5.6% (worse)
macOS BenchmarkChannelHandoff 11001 ns/op +3.2% (worse)
macOS BenchmarkDefer 39.410 ns/op +4.9% (worse)
macOS BenchmarkDirectCall 1.275 ns/op -0.8% (better)
macOS BenchmarkGlobalRead 1.714 ns/op +29.3% (worse)
macOS BenchmarkGlobalWrite 1.687 ns/op +5.0% (worse)
macOS BenchmarkGoroutine 69052 ns/op +18.3% (worse)
macOS BenchmarkInterfaceCall 6.117 ns/op +4.7% (worse)
macOS BenchmarkRuntimeGetG 2.874 ns/op +3.9% (worse)

Compared with 6670dae3884d measured in the same runner job.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/build-trace branch 13 times, most recently from 64654f5 to e1fee5d Compare August 3, 2026 02:49
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