Source file src/runtime/testdata/testprog/schedmetrics.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"internal/testenv"
    11  	"log"
    12  	"os"
    13  	"runtime"
    14  	"runtime/debug"
    15  	"runtime/metrics"
    16  	"strings"
    17  	"sync/atomic"
    18  	"syscall"
    19  	"time"
    20  )
    21  
    22  func init() {
    23  	register("SchedMetrics", SchedMetrics)
    24  }
    25  
    26  // Tests runtime/metrics.Read for various scheduler metrics.
    27  //
    28  // Implemented in testprog to prevent other tests from polluting
    29  // the metrics.
    30  func SchedMetrics() {
    31  	const (
    32  		notInGo = iota
    33  		runnable
    34  		running
    35  		waiting
    36  		created
    37  		threads
    38  		numSamples
    39  	)
    40  	var s [numSamples]metrics.Sample
    41  	s[notInGo].Name = "/sched/goroutines/not-in-go:goroutines"
    42  	s[runnable].Name = "/sched/goroutines/runnable:goroutines"
    43  	s[running].Name = "/sched/goroutines/running:goroutines"
    44  	s[waiting].Name = "/sched/goroutines/waiting:goroutines"
    45  	s[created].Name = "/sched/goroutines-created:goroutines"
    46  	s[threads].Name = "/sched/threads/total:threads"
    47  
    48  	var failed bool
    49  	var out bytes.Buffer
    50  	logger := log.New(&out, "", 0)
    51  	indent := 0
    52  	logf := func(s string, a ...any) {
    53  		var prefix strings.Builder
    54  		for range indent {
    55  			prefix.WriteString("\t")
    56  		}
    57  		logger.Printf(prefix.String()+s, a...)
    58  	}
    59  	errorf := func(s string, a ...any) {
    60  		logf(s, a...)
    61  		failed = true
    62  	}
    63  	run := func(name string, f func()) {
    64  		logf("=== Checking %q", name)
    65  		indent++
    66  		f()
    67  		indent--
    68  	}
    69  	logMetrics := func(s []metrics.Sample) {
    70  		for i := range s {
    71  			logf("%s: %d", s[i].Name, s[i].Value.Uint64())
    72  		}
    73  	}
    74  
    75  	// generalSlack is the amount of goroutines we allow ourselves to be
    76  	// off by in any given category, either due to background system
    77  	// goroutines. This excludes GC goroutines.
    78  	generalSlack := uint64(4)
    79  
    80  	// waitingSlack is the max number of blocked goroutines controlled
    81  	// by the runtime that we'll allow for. This includes GC goroutines
    82  	// as well as finalizer and cleanup goroutines.
    83  	waitingSlack := generalSlack + uint64(2*runtime.GOMAXPROCS(-1))
    84  
    85  	// threadsSlack is the maximum number of threads left over
    86  	// from the runtime (sysmon, the template thread, etc.)
    87  	// Certain build modes may also cause the creation of additional
    88  	// threads through frequent scheduling, like mayMoreStackPreempt.
    89  	// A slack of 5 is arbitrary but appears to be enough to cover
    90  	// the leftovers plus any inflation from scheduling-heavy build
    91  	// modes.
    92  	const threadsSlack = 5
    93  
    94  	// Make sure GC isn't running, since GC workers interfere with
    95  	// expected counts.
    96  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
    97  	runtime.GC()
    98  
    99  	check := func(s *metrics.Sample, min, max uint64) {
   100  		val := s.Value.Uint64()
   101  		if val < min {
   102  			errorf("%s too low; %d < %d", s.Name, val, min)
   103  		}
   104  		if val > max {
   105  			errorf("%s too high; %d > %d", s.Name, val, max)
   106  		}
   107  	}
   108  	checkEq := func(s *metrics.Sample, value uint64) {
   109  		check(s, value, value)
   110  	}
   111  	spinUntil := func(f func() bool) bool {
   112  		for {
   113  			if f() {
   114  				return true
   115  			}
   116  			time.Sleep(50 * time.Millisecond)
   117  		}
   118  	}
   119  
   120  	// Check base values.
   121  	run("base", func() {
   122  		defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
   123  		metrics.Read(s[:])
   124  		logMetrics(s[:])
   125  		check(&s[notInGo], 0, generalSlack)
   126  		check(&s[runnable], 0, generalSlack)
   127  		checkEq(&s[running], 1)
   128  		check(&s[waiting], 0, waitingSlack)
   129  	})
   130  
   131  	metrics.Read(s[:])
   132  	createdAfterBase := s[created].Value.Uint64()
   133  
   134  	// Force Running count to be high. We'll use these goroutines
   135  	// for Runnable, too.
   136  	const count = 10
   137  	var ready, exit atomic.Uint32
   138  	for range count {
   139  		go func() {
   140  			ready.Add(1)
   141  			for exit.Load() == 0 {
   142  				// Spin to get us and keep us running, but check
   143  				// the exit condition so we exit out early if we're
   144  				// done.
   145  				start := time.Now()
   146  				for time.Since(start) < 10*time.Millisecond && exit.Load() == 0 {
   147  				}
   148  				runtime.Gosched()
   149  			}
   150  		}()
   151  	}
   152  	for ready.Load() < count {
   153  		runtime.Gosched()
   154  	}
   155  
   156  	// Be careful. We've entered a dangerous state for platforms
   157  	// that do not return back to the underlying system unless all
   158  	// goroutines are blocked, like js/wasm, since we have a bunch
   159  	// of runnable goroutines all spinning. We cannot write anything
   160  	// out.
   161  	if testenv.HasParallelism() {
   162  		run("created", func() {
   163  			metrics.Read(s[:])
   164  			logMetrics(s[:])
   165  			checkEq(&s[created], createdAfterBase+count)
   166  		})
   167  		run("running", func() {
   168  			defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(count + 4))
   169  			// It can take a little bit for the scheduler to
   170  			// distribute the goroutines to Ps, so retry until
   171  			// we see the count we expect or the test times out.
   172  			spinUntil(func() bool {
   173  				metrics.Read(s[:])
   174  				return s[running].Value.Uint64() >= count
   175  			})
   176  			logMetrics(s[:])
   177  			check(&s[running], count, count+4)
   178  			check(&s[threads], count, count+4+threadsSlack)
   179  		})
   180  
   181  		// Force runnable count to be high.
   182  		run("runnable", func() {
   183  			defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
   184  			metrics.Read(s[:])
   185  			logMetrics(s[:])
   186  			checkEq(&s[running], 1)
   187  			check(&s[runnable], count-1, count+generalSlack)
   188  		})
   189  
   190  		// Done with the running/runnable goroutines.
   191  		exit.Store(1)
   192  	} else {
   193  		// Read metrics and then exit all the other goroutines,
   194  		// so that system calls may proceed.
   195  		metrics.Read(s[:])
   196  
   197  		// Done with the running/runnable goroutines.
   198  		exit.Store(1)
   199  
   200  		// Now we can check our invariants.
   201  		run("created", func() {
   202  			// Look for count-1 goroutines because we read metrics
   203  			// *before* run goroutine was created for this sub-test.
   204  			checkEq(&s[created], createdAfterBase+count-1)
   205  		})
   206  		run("running", func() {
   207  			logMetrics(s[:])
   208  			checkEq(&s[running], 1)
   209  			checkEq(&s[threads], 1)
   210  		})
   211  		run("runnable", func() {
   212  			logMetrics(s[:])
   213  			check(&s[runnable], count-1, count+generalSlack)
   214  		})
   215  	}
   216  
   217  	// Force not-in-go count to be high. This is a little tricky since
   218  	// we try really hard not to let things block in system calls.
   219  	// We have to drop to the syscall package to do this reliably.
   220  	run("not-in-go", func() {
   221  		// Block a bunch of goroutines on an OS pipe.
   222  		pr, pw, err := pipe()
   223  		if err != nil {
   224  			switch runtime.GOOS {
   225  			case "js", "wasip1":
   226  				logf("creating pipe: %v", err)
   227  				return
   228  			}
   229  			panic(fmt.Sprintf("creating pipe: %v", err))
   230  		}
   231  		for i := 0; i < count; i++ {
   232  			go syscall.Read(pr, make([]byte, 1))
   233  		}
   234  
   235  		// Let the goroutines block.
   236  		spinUntil(func() bool {
   237  			metrics.Read(s[:])
   238  			return s[notInGo].Value.Uint64() >= count
   239  		})
   240  		logMetrics(s[:])
   241  		check(&s[notInGo], count, count+generalSlack)
   242  
   243  		syscall.Close(pw)
   244  		syscall.Close(pr)
   245  	})
   246  
   247  	run("waiting", func() {
   248  		// Force waiting count to be high.
   249  		const waitingCount = 1000
   250  		stop := make(chan bool)
   251  		for i := 0; i < waitingCount; i++ {
   252  			go func() { <-stop }()
   253  		}
   254  
   255  		// Let the goroutines block.
   256  		spinUntil(func() bool {
   257  			metrics.Read(s[:])
   258  			return s[waiting].Value.Uint64() >= waitingCount
   259  		})
   260  		logMetrics(s[:])
   261  		check(&s[waiting], waitingCount, waitingCount+waitingSlack)
   262  
   263  		close(stop)
   264  	})
   265  
   266  	if failed {
   267  		fmt.Fprintln(os.Stderr, out.String())
   268  		os.Exit(1)
   269  	} else {
   270  		fmt.Fprintln(os.Stderr, "OK")
   271  	}
   272  }
   273  

View as plain text