Skip to content

runtime: stabilize goroutine hooks and GOROOT classifications - #2421

Draft
cpunion wants to merge 6 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-runtime-runner-fixes-main-20260826
Draft

runtime: stabilize goroutine hooks and GOROOT classifications#2421
cpunion wants to merge 6 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-runtime-runner-fixes-main-20260826

Conversation

@cpunion

@cpunion cpunion commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

This is the implementation half split from #2419. It is stacked on #2419 so that #2419 remains a one-file, deletion-only PR. The commits after #2419:

  • make runtime.NumGoroutine report LLGo's live runtime contexts;
  • move the Darwin/Linux procPin hooks into the core runtime so both sync.Pool and sync/atomic.Value use the same serialized implementation;
  • delete the directly corresponding flake entries for goprint.go and fixedbugs/issue52612.go;
  • isolate baseline Go commands from the repository module and force GOTOOLCHAIN=local;
  • treat notapplicable.yaml as a global design classification and accept either successful or ordinary failed execution while keeping resource-guard failures fatal;
  • move stack.go from four platform/version flake selectors to one global not-applicable entry;
  • run each hosted GOROOT job with GOMAXPROCS=1, in addition to the existing build/run timeouts and RSS guard;
  • classify full-matrix semantic failures as ordinary xfails and host-unsafe thread-exhaustion cases as global host skips.

Direct expectation removals

  • goprint.go: 2 flake selectors removed after runtime.NumGoroutine learned to track live workers;
  • fixedbugs/issue52612.go: 2 flake selectors removed after sync/atomic.Value gained the shared core-runtime procPin hooks;
  • stack.go: 4 flake selectors removed and replaced by one global notapplicable.yaml design classification.

The large set of already-passing hosted expectations is not deleted by these commits; that cleanup is isolated in #2419.

Full-matrix findings

  • fixedbugs/issue16016.go, chan/goroutines.go, and chanlinear.go intentionally create or repeatedly double enough goroutines to exhaust LLGo's current one-thread-per-goroutine host limit, so their host skips are global; the old chanlinear.go timeout and flake selectors are removed to keep classifications disjoint;
  • index0.go reproducibly crashes LLVM 19 object emission below the 3 GiB RSS guard even with GOMAXPROCS=1;
  • fixedbugs/issue34123.go, Go 1.26 Linux convert5.go, and heapsampling.go have deterministic semantic mismatches and remain executed as ordinary xfails;
  • typeparam/chans.go is genuinely flaky across platforms: on Go 1.26 macOS, bounded single-case repetitions produced one 61-second channel/select panic followed by two passes, so its former Linux-only flake selector is global.

Relationship to other PRs

Validation

The complete 16-shard Darwin/Linux × Go 1.25.0/1.26.5 integration run received every report and observed all 4,170 selected cases. Its only failures were the now-reproduced and globally classified typeparam/chans.go flake and the now-global host-unsafe chanlinear.go; handling details and the run link are recorded in the PR comments.

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@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

Solid, carefully-commented PR. The procPin/procUnpin move to procpin.go preserves the prior logic (renamed identifiers; both sync and sync/atomic hooks now route through one shared procPin() helper), the build tags are consistent, and the new tests (runtime_g_state_test.go, TestBaselineEnvironmentsForceLocalToolchain, TestWriteStdlibImportCfgIgnoresRepositoryModule, TestStackIsGloballyNotApplicable) lock in the intended behavior. The test-harness changes (GOTOOLCHAIN=local, go list std in t.TempDir()) are good hardening and were checked for shell-injection/TOCTOU — none found.

Findings below are all minor / non-blocking — mostly maintainability and documentation notes. No correctness defects in current behavior.

Additional note (not inline): LLGO_GOROOT_GOMAXPROCS is added to .github/workflows/goroot.yml (line 23) alongside the existing LLGO_GOROOT_HEARTBEAT_SECONDS / LLGO_GOROOT_VERBOSE, but like those two it is not read anywhere in the repo at these SHAs — presumably consumed by an out-of-repo harness. Consistent with existing convention; worth confirming the consumer exists.

Config consistency (test/goroot/notapplicable.yaml): since notApplicableConfig.Match now ignores version:/platform: selectors, the several version/platform-scoped entries in this file (e.g. the rangegen.go variants around lines 51-64) are functionally global. Consider adding a one-line note to the file header stating those selectors are inert for not-applicable entries, or validating them at load time, so a future maintainer isn't misled into thinking a version:-scoped entry actually scopes classification.

procPinOnce.Do(initProcPinMu)
procPinMu.Lock()
return 0
}

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.

Panic-safety of the global serialization (minor / latent). procPin/procUnpin are not defer-paired, so if a caller ever panics between procPin() and procUnpin(), procPinMu stays locked and every subsequent procPin on any thread deadlocks. Today's darwin/linux callers (sync/atomic.Value.Store; the repo's patched sync.Pool uses a TLS slot and doesn't call runtime_procPin) don't panic while pinned, so this is latent — but the invariants it relies on are load-bearing and unstated:

  • procUnpin must run on the same OS thread that called procPin (the mutex is PTHREAD_MUTEX_NORMAL via Init(nil); cross-thread unlock is UB), and
  • procPin regions must never nest (non-recursive mutex would self-deadlock).

Both hold under the current 1:1 goroutine↔OS-thread backend, but a future M:N scheduler change could silently break them. Consider a short comment recording these two invariants (and a "must not panic while pinned" note).

// LLGo has no Go P to pin a goroutine to. Serialize procPin regions instead,
// preserving the exclusion that sync.Pool and sync/atomic.Value require.
func procPin() int {
procPinOnce.Do(initProcPinMu)

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.

Per-call pthread_once on a hot path (minor perf). psync.Once.Do links to libc pthread_once, so every sync.Pool / sync/atomic.Value operation that hits runtime_procPin now pays a pthread_once call in addition to the mutex lock. pthread_once is cheap after first completion, but it's avoidable: pthread_mutex_init(nil) is equivalent to PTHREAD_MUTEX_INITIALIZER, so procPinMu could be eagerly/statically initialized (or initialized once in an init/first-getg) and the Once guard dropped entirely.

Separately worth flagging in a comment or tracking issue: the single process-wide procPinMu turns what is a contention-free per-P op in upstream Go into a globally serialized critical section, which becomes a scalability ceiling for sync.Pool-heavy concurrent workloads. This is the deliberate correctness tradeoff noted in the comment above — just calling out the ceiling explicitly.

// Applicability is an LLGo design property, not an observation tied to
// one hosted platform or Go release. Keep accepting legacy selectors in
// the YAML, but intentionally do not use them when classifying a case.
if !matchEntry("", "", entry.Directive, entry.Case, goVersion, platform, tc) {

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.

Ignored version/platform selectors are a maintainability trap (minor). Passing "", "" here intentionally makes not-applicable classification global, and the comment explains why. But because xfailEntry still parses version:/platform:, a maintainer can add a platform:-scoped not-applicable entry expecting it to be honored, and it will instead apply everywhere. Consider failing config load if a not-applicable entry sets version/platform, or documenting in notapplicable.yaml that those selectors are inert for not-applicable entries. (TestNotApplicableMatch and TestStackIsGloballyNotApplicable do assert the global behavior.)

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

6d766f9f3c5f | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Text size vs base Build vs base Run vs base
Linux cprintf 19384 B +128 B / +0.7% (worse) 387 B 0 B / +0.0% 338.730 ms +10.24 ms / +3.1% (worse) 1.245 ms +29.87 us / +2.5% (worse)
Linux cprintf-lto 19216 B +128 B / +0.7% (worse) 368 B 0 B / +0.0% 337.196 ms +11.77 ms / +3.6% (worse) 1.229 ms -8.8 us / -0.7% (better)
Linux fmtprintf 1653408 B +184 B / +0.01113% (worse) 501083 B 0 B / +0.0% 2.821 s -36.36 ms / -1.3% (better) 3.641 ms +403.7 us / +12.5% (worse)
Linux fmtprintf-lto 1526000 B +176 B / +0.01153% (worse) 459700 B 0 B / +0.0% 8.879 s -83.33 ms / -0.9% (better) 3.081 ms +30.98 us / +1.0% (worse)
Linux println 62160 B +128 B / +0.2% (worse) 15261 B 0 B / +0.0% 333.810 ms -290.6 us / -0.1% (better) 1.572 ms +13.18 us / +0.8% (worse)
Linux println-lto 53936 B +96 B / +0.2% (worse) 12882 B 0 B / +0.0% 512.604 ms -6.757 ms / -1.3% (better) 1.551 ms +10.35 us / +0.7% (worse)
macOS cprintf 84480 B 0 B / +0.0% 16621 B +128 B / +0.8% (worse) 743.381 ms +198 ms / +36.3% (worse) 6.839 ms +67.79 us / +1.0% (worse)
macOS cprintf-lto 100704 B 0 B / +0.0% 16601 B +128 B / +0.8% (worse) 737.448 ms +212.5 ms / +40.5% (worse) 4.377 ms +965.3 us / +28.3% (worse)
macOS fmtprintf 1514672 B +16416 B / +1.1% (worse) 882309 B +176 B / +0.01995% (worse) 3.868 s -207.7 ms / -5.1% (better) 7.965 ms +196.7 us / +2.5% (worse)
macOS fmtprintf-lto 1208976 B 0 B / +0.0% 875333 B +204 B / +0.02331% (worse) 8.229 s +698.3 ms / +9.3% (worse) 8.439 ms +784.7 us / +10.3% (worse)
macOS println 114832 B 0 B / +0.0% 34977 B +128 B / +0.4% (worse) 612.897 ms +185.6 ms / +43.4% (worse) 5.643 ms +919.2 us / +19.5% (worse)
macOS println-lto 118656 B 0 B / +0.0% 32617 B +128 B / +0.4% (worse) 614.721 ms -87.52 ms / -12.5% (better) 4.321 ms +105.7 us / +2.5% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.330 ns/op +0.05 ns/op / +0.4% (worse)
Linux BenchmarkMergeCompilerFlags 151.700 ns/op +0.2 ns/op / +0.1% (worse)
Linux BenchmarkMergeLinkerFlags 95.200 ns/op 0 ns/op / +0.0%
Linux BenchmarkChannelBuffered 35.830 ns/op +0.25 ns/op / +0.7% (worse)
Linux BenchmarkChannelHandoff 26464 ns/op -123 ns/op / -0.5% (better)
Linux BenchmarkDefer 48.450 ns/op -0.81 ns/op / -1.6% (better)
Linux BenchmarkDirectCall 1.558 ns/op +0.002 ns/op / +0.1% (worse)
Linux BenchmarkGlobalRead 1.556 ns/op -0.314 ns/op / -16.8% (better)
Linux BenchmarkGlobalWrite 2.487 ns/op +0.006 ns/op / +0.2% (worse)
Linux BenchmarkGoroutine 30313 ns/op -555 ns/op / -1.8% (better)
Linux BenchmarkInterfaceCall 8.409 ns/op 0 ns/op / +0.0%
Linux BenchmarkRuntimeGetG 2.179 ns/op 0 ns/op / +0.0%
macOS BenchmarkLookupPCRandom 12.180 ns/op -6.74 ns/op / -35.6% (better)
macOS BenchmarkMergeCompilerFlags 137.400 ns/op -79.3 ns/op / -36.6% (better)
macOS BenchmarkMergeLinkerFlags 112.400 ns/op -10.1 ns/op / -8.2% (better)
macOS BenchmarkChannelBuffered 27.970 ns/op -2.51 ns/op / -8.2% (better)
macOS BenchmarkChannelHandoff 7533 ns/op -2596 ns/op / -25.6% (better)
macOS BenchmarkDefer 47.040 ns/op +5.22 ns/op / +12.5% (worse)
macOS BenchmarkDirectCall 1.077 ns/op -0.118 ns/op / -9.9% (better)
macOS BenchmarkGlobalRead 1.072 ns/op -0.11 ns/op / -9.3% (better)
macOS BenchmarkGlobalWrite 1.078 ns/op -0.185 ns/op / -14.6% (better)
macOS BenchmarkGoroutine 34240 ns/op -24203 ns/op / -41.4% (better)
macOS BenchmarkInterfaceCall 4.812 ns/op -0.883 ns/op / -15.5% (better)
macOS BenchmarkRuntimeGetG 2.264 ns/op -0.272 ns/op / -10.7% (better)

Compared with 6fe8babd468c measured in the same runner job.

@cpunion

cpunion commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Full-matrix result collected from the former combined tree:

https://github.com/cpunion/llgo/actions/runs/32947330715

  • received all 16/16 shard reports;
  • selected and observed all 4,170 cases;
  • 4,159 classified passes, 2 failures, 9 host skips;
  • the only failures were darwin/arm64 go1.26.5 typeparam/chans.go and darwin/arm64 go1.25.0 chanlinear.go.

Both failures are addressed on the current #2421 head:

  • typeparam/chans.go was reproduced under the same single-P bound as one channel/select panic followed by two passes, so its existing Linux-only flake classification is now global;
  • chanlinear.go repeatedly doubles a goroutine-per-channel workload and exhausted LLGo's one-thread-per-goroutine host limit, so it is now a global host skip and its old timeout/flake selectors are removed.

All other 4,168 observed cases were classified successfully. Standard CI is rerunning on the corrected head.

@cpunion

cpunion commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Post-split full GOROOT validation for the current implementation head 6d766f9f3 (16 Darwin/Linux × Go 1.25.0/1.26.5 shards, GOMAXPROCS=1, bounded build/run timeouts, and 3 GiB RSS guard): https://github.com/cpunion/llgo/actions/runs/32958755080

@cpunion
cpunion marked this pull request as draft August 27, 2026 10:21
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