Skip to content

build: add Chrome scheduler trace - #2243

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

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

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Depends on #2242. The trace-specific review range is a456e8d7..a8402aea.

Summary

  • add llgo build -debug-trace=<file> with Chrome Trace Event JSON output (this is a build scheduler trace, not runtime/trace)
  • visualize coordinator work plus a stable set of worker lanes bounded by the effective -p value
  • record package load, cache preflight, SSA, isolated/coordinator/patched backend tasks through archive/cache completion, and linking
  • add direct SSA-to-backend dependency flows without emitting quadratic transitive edges
  • keep trace state build-local so concurrent invocations do not share files, lanes, or mutable state

Validation

  • focused trace tests in internal/build and cmd/llgo
  • inherited clean -p=8 mixed cgo/Plan9 archive build
  • git diff --check

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

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

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.

}

// 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)."

// 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".

} 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

ebc13d3a1976 | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18544 B new 289.557 ms new 1.293 ms new
Linux fmtprintf 2217544 B new 3.474 s new 2.427 ms new
Linux println 71504 B new 291.082 ms new 1.636 ms new
macOS cprintf 84672 B new 598.793 ms new 5.427 ms new
macOS fmtprintf 2361520 B new 4.116 s new 19.188 ms new
macOS println 125712 B new 462.216 ms new 4.995 ms new
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 13.220 ns/op new
Linux BenchmarkMergeCompilerFlags 152.400 ns/op new
Linux BenchmarkMergeLinkerFlags 94.570 ns/op new
Linux BenchmarkChannelBuffered 34.510 ns/op new
Linux BenchmarkChannelHandoff 25871 ns/op new
Linux BenchmarkDefer 44.060 ns/op new
Linux BenchmarkDirectCall 1.556 ns/op new
Linux BenchmarkGlobalRead 1.868 ns/op new
Linux BenchmarkGlobalWrite 2.491 ns/op new
Linux BenchmarkGoroutine 30943 ns/op new
Linux BenchmarkInterfaceCall 8.096 ns/op new
Linux BenchmarkRuntimeGetG 1.868 ns/op new
macOS BenchmarkLookupPCRandom 11.890 ns/op new
macOS BenchmarkMergeCompilerFlags 110.700 ns/op new
macOS BenchmarkMergeLinkerFlags 69.890 ns/op new
macOS BenchmarkChannelBuffered 30.740 ns/op new
macOS BenchmarkChannelHandoff 8551 ns/op new
macOS BenchmarkDefer 28.630 ns/op new
macOS BenchmarkDirectCall 1.036 ns/op new
macOS BenchmarkGlobalRead 1.086 ns/op new
macOS BenchmarkGlobalWrite 1.055 ns/op new
macOS BenchmarkGoroutine 27805 ns/op new
macOS BenchmarkInterfaceCall 5.201 ns/op new
macOS BenchmarkRuntimeGetG 2.405 ns/op new

No main baseline exists yet; all metrics are marked new.

Warning

  • The rendered benchmark data could not be pushed.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/build-trace branch 8 times, most recently from 31f9df9 to 2381d97 Compare August 1, 2026 23:22
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