runtime/metrics: add metric for total goroutines created

For #15490.

Change-Id: Ic587dda1f42d613ea131a6b53ce6ba6e6cadf4c7
Reviewed-on: https://go-review.googlesource.com/c/go/+/690398
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Michael Knyszek <mknyszek@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Michael Anthony Knyszek 2025-07-23 18:41:56 +00:00 committed by Gopher Robot
parent 13df972f68
commit ab8121a407
6 changed files with 49 additions and 5 deletions

View file

@ -500,6 +500,13 @@ func initMetrics() {
out.scalar = uint64(in.schedStats.gWaiting) out.scalar = uint64(in.schedStats.gWaiting)
}, },
}, },
"/sched/goroutines-created:goroutines": {
deps: makeStatDepSet(schedStatsDep),
compute: func(in *statAggregate, out *metricValue) {
out.kind = metricKindUint64
out.scalar = uint64(in.schedStats.gCreated)
},
},
"/sched/latencies:seconds": { "/sched/latencies:seconds": {
compute: func(_ *statAggregate, out *metricValue) { compute: func(_ *statAggregate, out *metricValue) {
sched.timeToRun.write(out) sched.timeToRun.write(out)
@ -779,6 +786,7 @@ type schedStatsAggregate struct {
gRunnable uint64 gRunnable uint64
gNonGo uint64 gNonGo uint64
gWaiting uint64 gWaiting uint64
gCreated uint64
} }
// compute populates the schedStatsAggregate with values from the runtime. // compute populates the schedStatsAggregate with values from the runtime.
@ -790,10 +798,12 @@ func (a *schedStatsAggregate) compute() {
lock(&sched.lock) lock(&sched.lock)
// Collect running/runnable from per-P run queues. // Collect running/runnable from per-P run queues.
a.gCreated += sched.goroutinesCreated.Load()
for _, p := range allp { for _, p := range allp {
if p == nil || p.status == _Pdead { if p == nil || p.status == _Pdead {
break break
} }
a.gCreated += p.goroutinesCreated
switch p.status { switch p.status {
case _Prunning: case _Prunning:
a.gRunning++ a.gRunning++

View file

@ -437,6 +437,12 @@ var allDesc = []Description{
Description: "The current runtime.GOMAXPROCS setting, or the number of operating system threads that can execute user-level Go code simultaneously.", Description: "The current runtime.GOMAXPROCS setting, or the number of operating system threads that can execute user-level Go code simultaneously.",
Kind: KindUint64, Kind: KindUint64,
}, },
{
Name: "/sched/goroutines-created:goroutines",
Description: "Count of goroutines created since program start.",
Cumulative: true,
Kind: KindUint64,
},
{ {
Name: "/sched/goroutines/not-in-go:goroutines", Name: "/sched/goroutines/not-in-go:goroutines",
Description: "Approximate count of goroutines running or blocked in a system call or cgo call. Not guaranteed to add up to /sched/goroutines:goroutines with other goroutine metrics.", Description: "Approximate count of goroutines running or blocked in a system call or cgo call. Not guaranteed to add up to /sched/goroutines:goroutines with other goroutine metrics.",

View file

@ -509,6 +509,9 @@ Below is the full list of supported metrics, ordered lexicographically.
operating system threads that can execute user-level Go code operating system threads that can execute user-level Go code
simultaneously. simultaneously.
/sched/goroutines-created:goroutines
Count of goroutines created since program start.
/sched/goroutines/not-in-go:goroutines /sched/goroutines/not-in-go:goroutines
Approximate count of goroutines running or blocked in Approximate count of goroutines running or blocked in
a system call or cgo call. Not guaranteed to add up to a system call or cgo call. Not guaranteed to add up to

View file

@ -1583,12 +1583,14 @@ func TestReadMetricsSched(t *testing.T) {
runnable runnable
running running
waiting waiting
created
) )
var s [4]metrics.Sample var s [5]metrics.Sample
s[0].Name = "/sched/goroutines/not-in-go:goroutines" s[notInGo].Name = "/sched/goroutines/not-in-go:goroutines"
s[1].Name = "/sched/goroutines/runnable:goroutines" s[runnable].Name = "/sched/goroutines/runnable:goroutines"
s[2].Name = "/sched/goroutines/running:goroutines" s[running].Name = "/sched/goroutines/running:goroutines"
s[3].Name = "/sched/goroutines/waiting:goroutines" s[waiting].Name = "/sched/goroutines/waiting:goroutines"
s[created].Name = "/sched/goroutines-created:goroutines"
logMetrics := func(t *testing.T, s []metrics.Sample) { logMetrics := func(t *testing.T, s []metrics.Sample) {
for i := range s { for i := range s {
@ -1645,6 +1647,9 @@ func TestReadMetricsSched(t *testing.T) {
check(t, &s[waiting], 0, waitingSlack) check(t, &s[waiting], 0, waitingSlack)
}) })
metrics.Read(s[:])
createdAfterBase := s[created].Value.Uint64()
// Force Running count to be high. We'll use these goroutines // Force Running count to be high. We'll use these goroutines
// for Runnable, too. // for Runnable, too.
const count = 10 const count = 10
@ -1673,6 +1678,11 @@ func TestReadMetricsSched(t *testing.T) {
// of runnable goroutines all spinning. We cannot write anything // of runnable goroutines all spinning. We cannot write anything
// out. // out.
if testenv.HasParallelism() { if testenv.HasParallelism() {
t.Run("created", func(t *testing.T) {
metrics.Read(s[:])
logMetrics(t, s[:])
checkEq(t, &s[created], createdAfterBase+count)
})
t.Run("running", func(t *testing.T) { t.Run("running", func(t *testing.T) {
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(count + 4)) defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(count + 4))
// It can take a little bit for the scheduler to // It can take a little bit for the scheduler to
@ -1706,6 +1716,11 @@ func TestReadMetricsSched(t *testing.T) {
exit.Store(1) exit.Store(1)
// Now we can check our invariants. // Now we can check our invariants.
t.Run("created", func(t *testing.T) {
// Look for count-1 goroutines because we read metrics
// *before* t.Run goroutine was created for this sub-test.
checkEq(t, &s[created], createdAfterBase+count-1)
})
t.Run("running", func(t *testing.T) { t.Run("running", func(t *testing.T) {
logMetrics(t, s[:]) logMetrics(t, s[:])
checkEq(t, &s[running], 1) checkEq(t, &s[running], 1)

View file

@ -5262,6 +5262,7 @@ func newproc1(fn *funcval, callergp *g, callerpc uintptr, parked bool, waitreaso
racereleasemergeg(newg, unsafe.Pointer(&labelSync)) racereleasemergeg(newg, unsafe.Pointer(&labelSync))
} }
} }
pp.goroutinesCreated++
releasem(mp) releasem(mp)
return newg return newg
@ -5841,6 +5842,8 @@ func (pp *p) destroy() {
pp.gcAssistTime = 0 pp.gcAssistTime = 0
gcCleanups.queued += pp.cleanupsQueued gcCleanups.queued += pp.cleanupsQueued
pp.cleanupsQueued = 0 pp.cleanupsQueued = 0
sched.goroutinesCreated.Add(int64(pp.goroutinesCreated))
pp.goroutinesCreated = 0
pp.xRegs.free() pp.xRegs.free()
pp.status = _Pdead pp.status = _Pdead
} }

View file

@ -764,6 +764,9 @@ type p struct {
// gcStopTime is the nanotime timestamp that this P last entered _Pgcstop. // gcStopTime is the nanotime timestamp that this P last entered _Pgcstop.
gcStopTime int64 gcStopTime int64
// goroutinesCreated is the total count of goroutines created by this P.
goroutinesCreated uint64
// xRegs is the per-P extended register state used by asynchronous // xRegs is the per-P extended register state used by asynchronous
// preemption. This is an empty struct on platforms that don't use extended // preemption. This is an empty struct on platforms that don't use extended
// register state. // register state.
@ -892,6 +895,10 @@ type schedt struct {
// M, but waiting for locks within the runtime. This field stores the value // M, but waiting for locks within the runtime. This field stores the value
// for Ms that have exited. // for Ms that have exited.
totalRuntimeLockWaitTime atomic.Int64 totalRuntimeLockWaitTime atomic.Int64
// goroutinesCreated (plus the value of goroutinesCreated on each P in allp)
// is the sum of all goroutines created by the program.
goroutinesCreated atomic.Uint64
} }
// Values for the flags field of a sigTabT. // Values for the flags field of a sigTabT.