| // Copyright 2025 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package main | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "internal/testenv" | |
| "log" | |
| "os" | |
| "runtime" | |
| "runtime/debug" | |
| "runtime/metrics" | |
| "strings" | |
| "sync/atomic" | |
| "syscall" | |
| "time" | |
| ) | |
| func init() { | |
| register("SchedMetrics", SchedMetrics) | |
| } | |
| // Tests runtime/metrics.Read for various scheduler metrics. | |
| // | |
| // Implemented in testprog to prevent other tests from polluting | |
| // the metrics. | |
| func SchedMetrics() { | |
| const ( | |
| notInGo = iota | |
| runnable | |
| running | |
| waiting | |
| created | |
| threads | |
| numSamples | |
| ) | |
| var s [numSamples]metrics.Sample | |
| s[notInGo].Name = "/sched/goroutines/not-in-go:goroutines" | |
| s[runnable].Name = "/sched/goroutines/runnable:goroutines" | |
| s[running].Name = "/sched/goroutines/running:goroutines" | |
| s[waiting].Name = "/sched/goroutines/waiting:goroutines" | |
| s[created].Name = "/sched/goroutines-created:goroutines" | |
| s[threads].Name = "/sched/threads/total:threads" | |
| var failed bool | |
| var out bytes.Buffer | |
| logger := log.New(&out, "", 0) | |
| indent := 0 | |
| logf := func(s string, a ...any) { | |
| var prefix strings.Builder | |
| for range indent { | |
| prefix.WriteString("\t") | |
| } | |
| logger.Printf(prefix.String()+s, a...) | |
| } | |
| errorf := func(s string, a ...any) { | |
| logf(s, a...) | |
| failed = true | |
| } | |
| run := func(name string, f func()) { | |
| logf("=== Checking %q", name) | |
| indent++ | |
| f() | |
| indent-- | |
| } | |
| logMetrics := func(s []metrics.Sample) { | |
| for i := range s { | |
| logf("%s: %d", s[i].Name, s[i].Value.Uint64()) | |
| } | |
| } | |
| initialGMP := runtime.GOMAXPROCS(-1) | |
| logf("Initial GOMAXPROCS=%d", initialGMP) | |
| // generalSlack is the amount of goroutines we allow ourselves to be | |
| // off by in any given category, either due to background system | |
| // goroutines. This excludes GC goroutines. | |
| generalSlack := uint64(4) | |
| // waitingSlack is the max number of blocked goroutines controlled | |
| // by the runtime that we'll allow for. This includes GC goroutines | |
| // as well as finalizer and cleanup goroutines. | |
| waitingSlack := generalSlack + uint64(2*initialGMP) | |
| // threadsSlack is the maximum number of threads left over | |
| // from the runtime (sysmon, the template thread, etc.) | |
| // Certain build modes may also cause the creation of additional | |
| // threads through frequent scheduling, like mayMoreStackPreempt. | |
| // A slack of 5 is arbitrary but appears to be enough to cover | |
| // the leftovers plus any inflation from scheduling-heavy build | |
| // modes. We then also add initialGMP to this slack, since we're | |
| // about to call runtime.GC, and in the worst case this will | |
| // spin up GOMAXPROCS new threads to run those workers. | |
| threadsSlack := 5 + uint64(initialGMP) | |
| // Make sure GC isn't running, since GC workers interfere with | |
| // expected counts. | |
| defer debug.SetGCPercent(debug.SetGCPercent(-1)) | |
| runtime.GC() | |
| check := func(s *metrics.Sample, min, max uint64) { | |
| val := s.Value.Uint64() | |
| if val < min { | |
| errorf("%s too low; %d < %d", s.Name, val, min) | |
| } | |
| if val > max { | |
| errorf("%s too high; %d > %d", s.Name, val, max) | |
| } | |
| } | |
| checkEq := func(s *metrics.Sample, value uint64) { | |
| check(s, value, value) | |
| } | |
| spinUntil := func(f func() bool) bool { | |
| for { | |
| if f() { | |
| return true | |
| } | |
| time.Sleep(50 * time.Millisecond) | |
| } | |
| } | |
| // Check base values. | |
| run("base", func() { | |
| defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) | |
| metrics.Read(s[:]) | |
| logMetrics(s[:]) | |
| check(&s[notInGo], 0, generalSlack) | |
| check(&s[runnable], 0, generalSlack) | |
| checkEq(&s[running], 1) | |
| check(&s[waiting], 0, waitingSlack) | |
| }) | |
| metrics.Read(s[:]) | |
| createdAfterBase := s[created].Value.Uint64() | |
| // Force Running count to be high. We'll use these goroutines | |
| // for Runnable, too. | |
| const count = 10 | |
| var ready, exit atomic.Uint32 | |
| for range count { | |
| go func() { | |
| ready.Add(1) | |
| for exit.Load() == 0 { | |
| // Spin to get us and keep us running, but check | |
| // the exit condition so we exit out early if we're | |
| // done. | |
| start := time.Now() | |
| for time.Since(start) < 10*time.Millisecond && exit.Load() == 0 { | |
| } | |
| runtime.Gosched() | |
| } | |
| }() | |
| } | |
| for ready.Load() < count { | |
| runtime.Gosched() | |
| } | |
| // Be careful. We've entered a dangerous state for platforms | |
| // that do not return back to the underlying system unless all | |
| // goroutines are blocked, like js/wasm, since we have a bunch | |
| // of runnable goroutines all spinning. We cannot write anything | |
| // out. | |
| if testenv.HasParallelism() { | |
| run("created", func() { | |
| metrics.Read(s[:]) | |
| logMetrics(s[:]) | |
| checkEq(&s[created], createdAfterBase+count) | |
| }) | |
| run("running", func() { | |
| defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(count + 4)) | |
| // It can take a little bit for the scheduler to | |
| // distribute the goroutines to Ps, so retry until | |
| // we see the count we expect or the test times out. | |
| spinUntil(func() bool { | |
| metrics.Read(s[:]) | |
| return s[running].Value.Uint64() >= count | |
| }) | |
| logMetrics(s[:]) | |
| check(&s[running], count, count+4) | |
| check(&s[threads], count, count+4+threadsSlack) | |
| }) | |
| // Force runnable count to be high. | |
| run("runnable", func() { | |
| defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) | |
| metrics.Read(s[:]) | |
| logMetrics(s[:]) | |
| checkEq(&s[running], 1) | |
| check(&s[runnable], count-1, count+generalSlack) | |
| }) | |
| // Done with the running/runnable goroutines. | |
| exit.Store(1) | |
| } else { | |
| // Read metrics and then exit all the other goroutines, | |
| // so that system calls may proceed. | |
| metrics.Read(s[:]) | |
| // Done with the running/runnable goroutines. | |
| exit.Store(1) | |
| // Now we can check our invariants. | |
| run("created", func() { | |
| // Look for count-1 goroutines because we read metrics | |
| // *before* run goroutine was created for this sub-test. | |
| checkEq(&s[created], createdAfterBase+count-1) | |
| }) | |
| run("running", func() { | |
| logMetrics(s[:]) | |
| checkEq(&s[running], 1) | |
| checkEq(&s[threads], 1) | |
| }) | |
| run("runnable", func() { | |
| logMetrics(s[:]) | |
| check(&s[runnable], count-1, count+generalSlack) | |
| }) | |
| } | |
| // Force not-in-go count to be high. This is a little tricky since | |
| // we try really hard not to let things block in system calls. | |
| // We have to drop to the syscall package to do this reliably. | |
| run("not-in-go", func() { | |
| // Block a bunch of goroutines on an OS pipe. | |
| pr, pw, err := pipe() | |
| if err != nil { | |
| switch runtime.GOOS { | |
| case "js", "wasip1": | |
| logf("creating pipe: %v", err) | |
| return | |
| } | |
| panic(fmt.Sprintf("creating pipe: %v", err)) | |
| } | |
| for i := 0; i < count; i++ { | |
| go syscall.Read(pr, make([]byte, 1)) | |
| } | |
| // Let the goroutines block. | |
| spinUntil(func() bool { | |
| metrics.Read(s[:]) | |
| return s[notInGo].Value.Uint64() >= count | |
| }) | |
| logMetrics(s[:]) | |
| check(&s[notInGo], count, count+generalSlack) | |
| syscall.Close(pw) | |
| syscall.Close(pr) | |
| }) | |
| run("waiting", func() { | |
| // Force waiting count to be high. | |
| const waitingCount = 1000 | |
| stop := make(chan bool) | |
| for i := 0; i < waitingCount; i++ { | |
| go func() { <-stop }() | |
| } | |
| // Let the goroutines block. | |
| spinUntil(func() bool { | |
| metrics.Read(s[:]) | |
| return s[waiting].Value.Uint64() >= waitingCount | |
| }) | |
| logMetrics(s[:]) | |
| check(&s[waiting], waitingCount, waitingCount+waitingSlack) | |
| close(stop) | |
| }) | |
| if failed { | |
| fmt.Fprintln(os.Stderr, out.String()) | |
| os.Exit(1) | |
| } else { | |
| fmt.Fprintln(os.Stderr, "OK") | |
| } | |
| } |