mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
runtime,runtime/metrics: add memory metrics
This change adds support for a variety of runtime memory metrics and contains the base implementation of Read for the runtime/metrics package, which lives in the runtime. It also adds testing infrastructure for the metrics package, and a bunch of format and documentation tests. For #37112. Change-Id: I16a2c4781eeeb2de0abcb045c15105f1210e2d8a Reviewed-on: https://go-review.googlesource.com/c/go/+/247041 Run-TryBot: Michael Knyszek <mknyszek@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Michael Pratt <mpratt@google.com> Trust: Michael Knyszek <mknyszek@google.com>
This commit is contained in:
parent
79781e8dd3
commit
b08dfbaa43
9 changed files with 781 additions and 6 deletions
|
|
@ -89,7 +89,11 @@ func (gcToolchain) gc(b *Builder, a *Action, archive string, importcfg []byte, s
|
||||||
extFiles := len(p.CgoFiles) + len(p.CFiles) + len(p.CXXFiles) + len(p.MFiles) + len(p.FFiles) + len(p.SFiles) + len(p.SysoFiles) + len(p.SwigFiles) + len(p.SwigCXXFiles)
|
extFiles := len(p.CgoFiles) + len(p.CFiles) + len(p.CXXFiles) + len(p.MFiles) + len(p.FFiles) + len(p.SFiles) + len(p.SysoFiles) + len(p.SwigFiles) + len(p.SwigCXXFiles)
|
||||||
if p.Standard {
|
if p.Standard {
|
||||||
switch p.ImportPath {
|
switch p.ImportPath {
|
||||||
case "bytes", "internal/poll", "net", "os", "runtime/pprof", "runtime/trace", "sync", "syscall", "time":
|
case "bytes", "internal/poll", "net", "os":
|
||||||
|
fallthrough
|
||||||
|
case "runtime/metrics", "runtime/pprof", "runtime/trace":
|
||||||
|
fallthrough
|
||||||
|
case "sync", "syscall", "time":
|
||||||
extFiles++
|
extFiles++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,32 @@ func (p *ProfBuf) Close() {
|
||||||
(*profBuf)(p).close()
|
(*profBuf)(p).close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadMetricsSlow(memStats *MemStats, samplesp unsafe.Pointer, len, cap int) {
|
||||||
|
stopTheWorld("ReadMetricsSlow")
|
||||||
|
|
||||||
|
// Initialize the metrics beforehand because this could
|
||||||
|
// allocate and skew the stats.
|
||||||
|
semacquire(&metricsSema)
|
||||||
|
initMetrics()
|
||||||
|
semrelease(&metricsSema)
|
||||||
|
|
||||||
|
systemstack(func() {
|
||||||
|
// Read memstats first. It's going to flush
|
||||||
|
// the mcaches which readMetrics does not do, so
|
||||||
|
// going the other way around may result in
|
||||||
|
// inconsistent statistics.
|
||||||
|
readmemstats_m(memStats)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Read metrics off the system stack.
|
||||||
|
//
|
||||||
|
// The only part of readMetrics that could allocate
|
||||||
|
// and skew the stats is initMetrics.
|
||||||
|
readMetrics(samplesp, len, cap)
|
||||||
|
|
||||||
|
startTheWorld()
|
||||||
|
}
|
||||||
|
|
||||||
// ReadMemStatsSlow returns both the runtime-computed MemStats and
|
// ReadMemStatsSlow returns both the runtime-computed MemStats and
|
||||||
// MemStats accumulated by scanning the heap.
|
// MemStats accumulated by scanning the heap.
|
||||||
func ReadMemStatsSlow() (base, slow MemStats) {
|
func ReadMemStatsSlow() (base, slow MemStats) {
|
||||||
|
|
|
||||||
367
src/runtime/metrics.go
Normal file
367
src/runtime/metrics.go
Normal file
|
|
@ -0,0 +1,367 @@
|
||||||
|
// Copyright 2020 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 runtime
|
||||||
|
|
||||||
|
// Metrics implementation exported to runtime/metrics.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// metrics is a map of runtime/metrics keys to
|
||||||
|
// data used by the runtime to sample each metric's
|
||||||
|
// value.
|
||||||
|
metricsSema uint32 = 1
|
||||||
|
metricsInit bool
|
||||||
|
metrics map[string]metricData
|
||||||
|
)
|
||||||
|
|
||||||
|
type metricData struct {
|
||||||
|
// deps is the set of runtime statistics that this metric
|
||||||
|
// depends on. Before compute is called, the statAggregate
|
||||||
|
// which will be passed must ensure() these dependencies.
|
||||||
|
deps statDepSet
|
||||||
|
|
||||||
|
// compute is a function that populates a metricValue
|
||||||
|
// given a populated statAggregate structure.
|
||||||
|
compute func(in *statAggregate, out *metricValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initMetrics initializes the metrics map if it hasn't been yet.
|
||||||
|
//
|
||||||
|
// metricsSema must be held.
|
||||||
|
func initMetrics() {
|
||||||
|
if metricsInit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metrics = map[string]metricData{
|
||||||
|
"/memory/classes/heap/free:bytes": {
|
||||||
|
deps: makeStatDepSet(heapStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = uint64(in.heapStats.committed - in.heapStats.inHeap -
|
||||||
|
in.heapStats.inStacks - in.heapStats.inWorkBufs -
|
||||||
|
in.heapStats.inPtrScalarBits)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/heap/objects:bytes": {
|
||||||
|
deps: makeStatDepSet(heapStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = in.heapStats.inObjects
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/heap/released:bytes": {
|
||||||
|
deps: makeStatDepSet(heapStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = uint64(in.heapStats.released)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/heap/stacks:bytes": {
|
||||||
|
deps: makeStatDepSet(heapStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = uint64(in.heapStats.inStacks)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/heap/unused:bytes": {
|
||||||
|
deps: makeStatDepSet(heapStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = uint64(in.heapStats.inHeap) - in.heapStats.inObjects
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/metadata/mcache/free:bytes": {
|
||||||
|
deps: makeStatDepSet(sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = in.sysStats.mCacheSys - in.sysStats.mCacheInUse
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/metadata/mcache/inuse:bytes": {
|
||||||
|
deps: makeStatDepSet(sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = in.sysStats.mCacheInUse
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/metadata/mspan/free:bytes": {
|
||||||
|
deps: makeStatDepSet(sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = in.sysStats.mSpanSys - in.sysStats.mSpanInUse
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/metadata/mspan/inuse:bytes": {
|
||||||
|
deps: makeStatDepSet(sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = in.sysStats.mSpanInUse
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/metadata/other:bytes": {
|
||||||
|
deps: makeStatDepSet(heapStatsDep, sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = uint64(in.heapStats.inWorkBufs+in.heapStats.inPtrScalarBits) + in.sysStats.gcMiscSys
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/os-stacks:bytes": {
|
||||||
|
deps: makeStatDepSet(sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = in.sysStats.stacksSys
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/other:bytes": {
|
||||||
|
deps: makeStatDepSet(sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = in.sysStats.otherSys
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/profiling/buckets:bytes": {
|
||||||
|
deps: makeStatDepSet(sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = in.sysStats.buckHashSys
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/memory/classes/total:bytes": {
|
||||||
|
deps: makeStatDepSet(heapStatsDep, sysStatsDep),
|
||||||
|
compute: func(in *statAggregate, out *metricValue) {
|
||||||
|
out.kind = metricKindUint64
|
||||||
|
out.scalar = uint64(in.heapStats.committed+in.heapStats.released) +
|
||||||
|
in.sysStats.stacksSys + in.sysStats.mSpanSys +
|
||||||
|
in.sysStats.mCacheSys + in.sysStats.buckHashSys +
|
||||||
|
in.sysStats.gcMiscSys + in.sysStats.otherSys
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
metricsInit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// statDep is a dependency on a group of statistics
|
||||||
|
// that a metric might have.
|
||||||
|
type statDep uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
heapStatsDep statDep = iota // corresponds to heapStatsAggregate
|
||||||
|
sysStatsDep // corresponds to sysStatsAggregate
|
||||||
|
numStatsDeps
|
||||||
|
)
|
||||||
|
|
||||||
|
// statDepSet represents a set of statDeps.
|
||||||
|
//
|
||||||
|
// Under the hood, it's a bitmap.
|
||||||
|
type statDepSet [1]uint64
|
||||||
|
|
||||||
|
// makeStatDepSet creates a new statDepSet from a list of statDeps.
|
||||||
|
func makeStatDepSet(deps ...statDep) statDepSet {
|
||||||
|
var s statDepSet
|
||||||
|
for _, d := range deps {
|
||||||
|
s[d/64] |= 1 << (d % 64)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// differennce returns set difference of s from b as a new set.
|
||||||
|
func (s statDepSet) difference(b statDepSet) statDepSet {
|
||||||
|
var c statDepSet
|
||||||
|
for i := range s {
|
||||||
|
c[i] = s[i] &^ b[i]
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// union returns the union of the two sets as a new set.
|
||||||
|
func (s statDepSet) union(b statDepSet) statDepSet {
|
||||||
|
var c statDepSet
|
||||||
|
for i := range s {
|
||||||
|
c[i] = s[i] | b[i]
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty returns true if there are no dependencies in the set.
|
||||||
|
func (s *statDepSet) empty() bool {
|
||||||
|
for _, c := range s {
|
||||||
|
if c != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// has returns true if the set contains a given statDep.
|
||||||
|
func (s *statDepSet) has(d statDep) bool {
|
||||||
|
return s[d/64]&(1<<(d%64)) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// heapStatsAggregate represents memory stats obtained from the
|
||||||
|
// runtime. This set of stats is grouped together because they
|
||||||
|
// depend on each other in some way to make sense of the runtime's
|
||||||
|
// current heap memory use. They're also sharded across Ps, so it
|
||||||
|
// makes sense to grab them all at once.
|
||||||
|
type heapStatsAggregate struct {
|
||||||
|
heapStatsDelta
|
||||||
|
|
||||||
|
// inObjects is the bytes of memory occupied by objects,
|
||||||
|
// derived from other values in heapStats.
|
||||||
|
inObjects uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute populates the heapStatsAggregate with values from the runtime.
|
||||||
|
func (a *heapStatsAggregate) compute() {
|
||||||
|
memstats.heapStats.read(&a.heapStatsDelta)
|
||||||
|
|
||||||
|
// Calculate derived stats.
|
||||||
|
a.inObjects = uint64(a.largeAlloc - a.largeFree)
|
||||||
|
for i := range a.smallAllocCount {
|
||||||
|
a.inObjects += uint64(a.smallAllocCount[i]-a.smallFreeCount[i]) * uint64(class_to_size[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sysStatsAggregate represents system memory stats obtained
|
||||||
|
// from the runtime. This set of stats is grouped together because
|
||||||
|
// they're all relatively cheap to acquire and generally independent
|
||||||
|
// of one another and other runtime memory stats. The fact that they
|
||||||
|
// may be acquired at different times, especially with respect to
|
||||||
|
// heapStatsAggregate, means there could be some skew, but because of
|
||||||
|
// these stats are independent, there's no real consistency issue here.
|
||||||
|
type sysStatsAggregate struct {
|
||||||
|
stacksSys uint64
|
||||||
|
mSpanSys uint64
|
||||||
|
mSpanInUse uint64
|
||||||
|
mCacheSys uint64
|
||||||
|
mCacheInUse uint64
|
||||||
|
buckHashSys uint64
|
||||||
|
gcMiscSys uint64
|
||||||
|
otherSys uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute populates the sysStatsAggregate with values from the runtime.
|
||||||
|
func (a *sysStatsAggregate) compute() {
|
||||||
|
a.stacksSys = memstats.stacks_sys.load()
|
||||||
|
a.buckHashSys = memstats.buckhash_sys.load()
|
||||||
|
a.gcMiscSys = memstats.gcMiscSys.load()
|
||||||
|
a.otherSys = memstats.other_sys.load()
|
||||||
|
|
||||||
|
systemstack(func() {
|
||||||
|
lock(&mheap_.lock)
|
||||||
|
a.mSpanSys = memstats.mspan_sys.load()
|
||||||
|
a.mSpanInUse = uint64(mheap_.spanalloc.inuse)
|
||||||
|
a.mCacheSys = memstats.mcache_sys.load()
|
||||||
|
a.mCacheInUse = uint64(mheap_.cachealloc.inuse)
|
||||||
|
unlock(&mheap_.lock)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// statAggregate is the main driver of the metrics implementation.
|
||||||
|
//
|
||||||
|
// It contains multiple aggregates of runtime statistics, as well
|
||||||
|
// as a set of these aggregates that it has populated. The aggergates
|
||||||
|
// are populated lazily by its ensure method.
|
||||||
|
type statAggregate struct {
|
||||||
|
ensured statDepSet
|
||||||
|
heapStats heapStatsAggregate
|
||||||
|
sysStats sysStatsAggregate
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure populates statistics aggregates determined by deps if they
|
||||||
|
// haven't yet been populated.
|
||||||
|
func (a *statAggregate) ensure(deps *statDepSet) {
|
||||||
|
missing := deps.difference(a.ensured)
|
||||||
|
if missing.empty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := statDep(0); i < numStatsDeps; i++ {
|
||||||
|
if !missing.has(i) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch i {
|
||||||
|
case heapStatsDep:
|
||||||
|
a.heapStats.compute()
|
||||||
|
case sysStatsDep:
|
||||||
|
a.sysStats.compute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.ensured = a.ensured.union(missing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// metricValidKind is a runtime copy of runtime/metrics.ValueKind and
|
||||||
|
// must be kept structurally identical to that type.
|
||||||
|
type metricKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// These values must be kept identical to their corresponding Kind* values
|
||||||
|
// in the runtime/metrics package.
|
||||||
|
metricKindBad metricKind = iota
|
||||||
|
metricKindUint64
|
||||||
|
metricKindFloat64
|
||||||
|
metricKindFloat64Histogram
|
||||||
|
)
|
||||||
|
|
||||||
|
// metricSample is a runtime copy of runtime/metrics.Sample and
|
||||||
|
// must be kept structurally identical to that type.
|
||||||
|
type metricSample struct {
|
||||||
|
name string
|
||||||
|
value metricValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// metricValue is a runtime copy of runtime/metrics.Sample and
|
||||||
|
// must be kept structurally identical to that type.
|
||||||
|
type metricValue struct {
|
||||||
|
kind metricKind
|
||||||
|
scalar uint64 // contains scalar values for scalar Kinds.
|
||||||
|
pointer unsafe.Pointer // contains non-scalar values.
|
||||||
|
}
|
||||||
|
|
||||||
|
// agg is used by readMetrics, and is protected by metricsSema.
|
||||||
|
//
|
||||||
|
// Managed as a global variable because its pointer will be
|
||||||
|
// an argument to a dynamically-defined function, and we'd
|
||||||
|
// like to avoid it escaping to the heap.
|
||||||
|
var agg statAggregate
|
||||||
|
|
||||||
|
// readMetrics is the implementation of runtime/metrics.Read.
|
||||||
|
//
|
||||||
|
//go:linkname readMetrics runtime/metrics.runtime_readMetrics
|
||||||
|
func readMetrics(samplesp unsafe.Pointer, len int, cap int) {
|
||||||
|
// Construct a slice from the args.
|
||||||
|
sl := slice{samplesp, len, cap}
|
||||||
|
samples := *(*[]metricSample)(unsafe.Pointer(&sl))
|
||||||
|
|
||||||
|
// Acquire the metricsSema but with handoff. This operation
|
||||||
|
// is expensive enough that queueing up goroutines and handing
|
||||||
|
// off between them will be noticably better-behaved.
|
||||||
|
semacquire1(&metricsSema, true, 0, 0)
|
||||||
|
|
||||||
|
// Ensure the map is initialized.
|
||||||
|
initMetrics()
|
||||||
|
|
||||||
|
// Clear agg defensively.
|
||||||
|
agg = statAggregate{}
|
||||||
|
|
||||||
|
// Sample.
|
||||||
|
for i := range samples {
|
||||||
|
sample := &samples[i]
|
||||||
|
data, ok := metrics[sample.name]
|
||||||
|
if !ok {
|
||||||
|
sample.value.kind = metricKindBad
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Ensure we have all the stats we need.
|
||||||
|
// agg is populated lazily.
|
||||||
|
agg.ensure(&data.deps)
|
||||||
|
|
||||||
|
// Compute the value based on the stats we have.
|
||||||
|
data.compute(&agg, &sample.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
semrelease(&metricsSema)
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ type Description struct {
|
||||||
//
|
//
|
||||||
// The format of the metric may be described by the following regular expression.
|
// The format of the metric may be described by the following regular expression.
|
||||||
//
|
//
|
||||||
// ^(?P<name>/[^:]+):(?P<unit>[^:*\/]+(?:[*\/][^:*\/]+)*)$
|
// ^(?P<name>/[^:]+):(?P<unit>[^:*/]+(?:[*/][^:*/]+)*)$
|
||||||
//
|
//
|
||||||
// The format splits the name into two components, separated by a colon: a path which always
|
// The format splits the name into two components, separated by a colon: a path which always
|
||||||
// starts with a /, and a machine-parseable unit. The name may contain any valid Unicode
|
// starts with a /, and a machine-parseable unit. The name may contain any valid Unicode
|
||||||
|
|
@ -26,6 +26,9 @@ type Description struct {
|
||||||
// A complete name might look like "/memory/heap/free:bytes".
|
// A complete name might look like "/memory/heap/free:bytes".
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
// Description is an English language sentence describing the metric.
|
||||||
|
Description string
|
||||||
|
|
||||||
// Kind is the kind of value for this metric.
|
// Kind is the kind of value for this metric.
|
||||||
//
|
//
|
||||||
// The purpose of this field is to allow users to filter out metrics whose values are
|
// The purpose of this field is to allow users to filter out metrics whose values are
|
||||||
|
|
@ -44,7 +47,80 @@ type Description struct {
|
||||||
StopTheWorld bool
|
StopTheWorld bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var allDesc = []Description{}
|
// The English language descriptions below must be kept in sync with the
|
||||||
|
// descriptions of each metric in doc.go.
|
||||||
|
var allDesc = []Description{
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/heap/free:bytes",
|
||||||
|
Description: "Memory that is available for allocation, and may be returned to the underlying system.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/heap/objects:bytes",
|
||||||
|
Description: "Memory occupied by live objects and dead objects that have not yet been collected.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/heap/released:bytes",
|
||||||
|
Description: "Memory that has been returned to the underlying system.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/heap/stacks:bytes",
|
||||||
|
Description: "Memory allocated from the heap that is occupied by stacks.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/heap/unused:bytes",
|
||||||
|
Description: "Memory that is unavailable for allocation, but cannot be returned to the underlying system.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/metadata/mcache/free:bytes",
|
||||||
|
Description: "Memory that is reserved for runtime mcache structures, but not in-use.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/metadata/mcache/inuse:bytes",
|
||||||
|
Description: "Memory that is occupied by runtime mcache structures that are currently being used.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/metadata/mspan/free:bytes",
|
||||||
|
Description: "Memory that is reserved for runtime mspan structures, but not in-use.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/metadata/mspan/inuse:bytes",
|
||||||
|
Description: "Memory that is occupied by runtime mspan structures that are currently being used.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/metadata/other:bytes",
|
||||||
|
Description: "Memory that is reserved for or used to hold runtime metadata.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/os-stacks:bytes",
|
||||||
|
Description: "Stack memory allocated by the underlying operating system.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/other:bytes",
|
||||||
|
Description: "Memory used by execution trace buffers, structures for debugging the runtime, finalizer and profiler specials, and more.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/profiling/buckets:bytes",
|
||||||
|
Description: "Memory that is used by the stack trace hash map used for profiling.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "/memory/classes/total:bytes",
|
||||||
|
Description: "All memory mapped by the Go runtime into the current process as read-write. Note that this does not include memory mapped by code called via cgo or via the syscall package. Sum of all metrics in /memory/classes.",
|
||||||
|
Kind: KindUint64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// All returns a slice of containing metric descriptions for all supported metrics.
|
// All returns a slice of containing metric descriptions for all supported metrics.
|
||||||
func All() []Description {
|
func All() []Description {
|
||||||
|
|
|
||||||
125
src/runtime/metrics/description_test.go
Normal file
125
src/runtime/metrics/description_test.go
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
// Copyright 2020 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 metrics_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"runtime/metrics"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDescriptionNameFormat(t *testing.T) {
|
||||||
|
r := regexp.MustCompile("^(?P<name>/[^:]+):(?P<unit>[^:*/]+(?:[*/][^:*/]+)*)$")
|
||||||
|
descriptions := metrics.All()
|
||||||
|
for _, desc := range descriptions {
|
||||||
|
if !r.MatchString(desc.Name) {
|
||||||
|
t.Errorf("metrics %q does not match regexp %s", desc.Name, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMetricDocs(t *testing.T) map[string]string {
|
||||||
|
if runtime.GOOS == "android" {
|
||||||
|
t.Skip("no access to Go source on android")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get doc.go.
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
filename = filepath.Join(filepath.Dir(filename), "doc.go")
|
||||||
|
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const (
|
||||||
|
stateSearch = iota // look for list of metrics
|
||||||
|
stateNextMetric // look for next metric
|
||||||
|
stateNextDescription // build description
|
||||||
|
)
|
||||||
|
state := stateSearch
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
result := make(map[string]string)
|
||||||
|
var metric string
|
||||||
|
var prevMetric string
|
||||||
|
var desc strings.Builder
|
||||||
|
for s.Scan() {
|
||||||
|
line := strings.TrimSpace(s.Text())
|
||||||
|
switch state {
|
||||||
|
case stateSearch:
|
||||||
|
if line == "Supported metrics" {
|
||||||
|
state = stateNextMetric
|
||||||
|
}
|
||||||
|
case stateNextMetric:
|
||||||
|
// Ignore empty lines until we find a non-empty
|
||||||
|
// one. This will be our metric name.
|
||||||
|
if len(line) != 0 {
|
||||||
|
prevMetric = metric
|
||||||
|
metric = line
|
||||||
|
if prevMetric > metric {
|
||||||
|
t.Errorf("metrics %s and %s are out of lexicographical order", prevMetric, metric)
|
||||||
|
}
|
||||||
|
state = stateNextDescription
|
||||||
|
}
|
||||||
|
case stateNextDescription:
|
||||||
|
if len(line) == 0 || line == `*/` {
|
||||||
|
// An empty line means we're done.
|
||||||
|
// Write down the description and look
|
||||||
|
// for a new metric.
|
||||||
|
result[metric] = desc.String()
|
||||||
|
desc.Reset()
|
||||||
|
state = stateNextMetric
|
||||||
|
} else {
|
||||||
|
// As long as we're seeing data, assume that's
|
||||||
|
// part of the description and append it.
|
||||||
|
if desc.Len() != 0 {
|
||||||
|
// Turn previous newlines into spaces.
|
||||||
|
desc.WriteString(" ")
|
||||||
|
}
|
||||||
|
desc.WriteString(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if line == `*/` {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state == stateSearch {
|
||||||
|
t.Fatalf("failed to find supported metrics docs in %s", filename)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDescriptionDocs(t *testing.T) {
|
||||||
|
docs := extractMetricDocs(t)
|
||||||
|
descriptions := metrics.All()
|
||||||
|
for _, d := range descriptions {
|
||||||
|
want := d.Description
|
||||||
|
got, ok := docs[d.Name]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("no docs found for metric %s", d.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("mismatched description and docs for metric %s", d.Name)
|
||||||
|
t.Errorf("want: %q, got %q", want, got)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(docs) > len(descriptions) {
|
||||||
|
docsLoop:
|
||||||
|
for name, _ := range docs {
|
||||||
|
for _, d := range descriptions {
|
||||||
|
if name == d.Name {
|
||||||
|
continue docsLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Errorf("stale documentation for non-existent metric: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,60 @@ the documentation of the Name field of the Description struct.
|
||||||
|
|
||||||
Supported metrics
|
Supported metrics
|
||||||
|
|
||||||
TODO(mknyszek): List them here as they're added.
|
/memory/classes/heap/free:bytes
|
||||||
|
Memory that is available for allocation, and may be returned
|
||||||
|
to the underlying system.
|
||||||
|
|
||||||
|
/memory/classes/heap/objects:bytes
|
||||||
|
Memory occupied by live objects and dead objects that have
|
||||||
|
not yet been collected.
|
||||||
|
|
||||||
|
/memory/classes/heap/released:bytes
|
||||||
|
Memory that has been returned to the underlying system.
|
||||||
|
|
||||||
|
/memory/classes/heap/stacks:bytes
|
||||||
|
Memory allocated from the heap that is occupied by stacks.
|
||||||
|
|
||||||
|
/memory/classes/heap/unused:bytes
|
||||||
|
Memory that is unavailable for allocation, but cannot be
|
||||||
|
returned to the underlying system.
|
||||||
|
|
||||||
|
/memory/classes/metadata/mcache/free:bytes
|
||||||
|
Memory that is reserved for runtime mcache structures, but
|
||||||
|
not in-use.
|
||||||
|
|
||||||
|
/memory/classes/metadata/mcache/inuse:bytes
|
||||||
|
Memory that is occupied by runtime mcache structures that
|
||||||
|
are currently being used.
|
||||||
|
|
||||||
|
/memory/classes/metadata/mspan/free:bytes
|
||||||
|
Memory that is reserved for runtime mspan structures, but
|
||||||
|
not in-use.
|
||||||
|
|
||||||
|
/memory/classes/metadata/mspan/inuse:bytes
|
||||||
|
Memory that is occupied by runtime mspan structures that are
|
||||||
|
currently being used.
|
||||||
|
|
||||||
|
/memory/classes/metadata/other:bytes
|
||||||
|
Memory that is reserved for or used to hold runtime
|
||||||
|
metadata.
|
||||||
|
|
||||||
|
/memory/classes/os-stacks:bytes
|
||||||
|
Stack memory allocated by the underlying operating system.
|
||||||
|
|
||||||
|
/memory/classes/other:bytes
|
||||||
|
Memory used by execution trace buffers, structures for
|
||||||
|
debugging the runtime, finalizer and profiler specials, and
|
||||||
|
more.
|
||||||
|
|
||||||
|
/memory/classes/profiling/buckets:bytes
|
||||||
|
Memory that is used by the stack trace hash map used for
|
||||||
|
profiling.
|
||||||
|
|
||||||
|
/memory/classes/total:bytes
|
||||||
|
All memory mapped by the Go runtime into the current process
|
||||||
|
as read-write. Note that this does not include memory mapped
|
||||||
|
by code called via cgo or via the syscall package.
|
||||||
|
Sum of all metrics in /memory/classes.
|
||||||
*/
|
*/
|
||||||
package metrics
|
package metrics
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
|
|
||||||
package metrics
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "runtime" // depends on the runtime via a linkname'd function
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
// Sample captures a single metric sample.
|
// Sample captures a single metric sample.
|
||||||
type Sample struct {
|
type Sample struct {
|
||||||
// Name is the name of the metric sampled.
|
// Name is the name of the metric sampled.
|
||||||
|
|
@ -16,6 +21,9 @@ type Sample struct {
|
||||||
Value Value
|
Value Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implemented in the runtime.
|
||||||
|
func runtime_readMetrics(unsafe.Pointer, int, int)
|
||||||
|
|
||||||
// Read populates each Value field in the given slice of metric samples.
|
// Read populates each Value field in the given slice of metric samples.
|
||||||
//
|
//
|
||||||
// Desired metrics should be present in the slice with the appropriate name.
|
// Desired metrics should be present in the slice with the appropriate name.
|
||||||
|
|
@ -25,5 +33,5 @@ type Sample struct {
|
||||||
// will have the value populated as KindBad to indicate that the name is
|
// will have the value populated as KindBad to indicate that the name is
|
||||||
// unknown.
|
// unknown.
|
||||||
func Read(m []Sample) {
|
func Read(m []Sample) {
|
||||||
panic("unimplemented")
|
runtime_readMetrics(unsafe.Pointer(&m[0]), len(m), cap(m))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
114
src/runtime/metrics_test.go
Normal file
114
src/runtime/metrics_test.go
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
// Copyright 2020 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 runtime_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"runtime/metrics"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareAllMetricsSamples() (map[string]metrics.Description, []metrics.Sample) {
|
||||||
|
all := metrics.All()
|
||||||
|
samples := make([]metrics.Sample, len(all))
|
||||||
|
descs := make(map[string]metrics.Description)
|
||||||
|
for i := range all {
|
||||||
|
samples[i].Name = all[i].Name
|
||||||
|
descs[all[i].Name] = all[i]
|
||||||
|
}
|
||||||
|
return descs, samples
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadMetrics(t *testing.T) {
|
||||||
|
// Tests whether readMetrics produces values aligning
|
||||||
|
// with ReadMemStats while the world is stopped.
|
||||||
|
var mstats runtime.MemStats
|
||||||
|
_, samples := prepareAllMetricsSamples()
|
||||||
|
runtime.ReadMetricsSlow(&mstats, unsafe.Pointer(&samples[0]), len(samples), cap(samples))
|
||||||
|
|
||||||
|
checkUint64 := func(t *testing.T, m string, got, want uint64) {
|
||||||
|
t.Helper()
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("metric %q: got %d, want %d", m, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to make sure the values we read line up with other values we read.
|
||||||
|
for i := range samples {
|
||||||
|
switch name := samples[i].Name; name {
|
||||||
|
case "/memory/classes/heap/free:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapIdle-mstats.HeapReleased)
|
||||||
|
case "/memory/classes/heap/released:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapReleased)
|
||||||
|
case "/memory/classes/heap/objects:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapAlloc)
|
||||||
|
case "/memory/classes/heap/unused:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.HeapInuse-mstats.HeapAlloc)
|
||||||
|
case "/memory/classes/heap/stacks:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.StackInuse)
|
||||||
|
case "/memory/classes/metadata/mcache/free:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.MCacheSys-mstats.MCacheInuse)
|
||||||
|
case "/memory/classes/metadata/mcache/inuse:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.MCacheInuse)
|
||||||
|
case "/memory/classes/metadata/mspan/free:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.MSpanSys-mstats.MSpanInuse)
|
||||||
|
case "/memory/classes/metadata/mspan/inuse:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.MSpanInuse)
|
||||||
|
case "/memory/classes/metadata/other:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.GCSys)
|
||||||
|
case "/memory/classes/os-stacks:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.StackSys-mstats.StackInuse)
|
||||||
|
case "/memory/classes/other:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.OtherSys)
|
||||||
|
case "/memory/classes/profiling/buckets:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.BuckHashSys)
|
||||||
|
case "/memory/classes/total:bytes":
|
||||||
|
checkUint64(t, name, samples[i].Value.Uint64(), mstats.Sys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadMetricsConsistency(t *testing.T) {
|
||||||
|
// Tests whether readMetrics produces consistent, sensible values.
|
||||||
|
// The values are read concurrently with the runtime doing other
|
||||||
|
// things (e.g. allocating) so what we read can't reasonably compared
|
||||||
|
// to runtime values.
|
||||||
|
|
||||||
|
// Read all the supported metrics through the metrics package.
|
||||||
|
descs, samples := prepareAllMetricsSamples()
|
||||||
|
metrics.Read(samples)
|
||||||
|
|
||||||
|
// Check to make sure the values we read make sense.
|
||||||
|
var totalVirtual struct {
|
||||||
|
got, want uint64
|
||||||
|
}
|
||||||
|
for i := range samples {
|
||||||
|
kind := samples[i].Value.Kind()
|
||||||
|
if want := descs[samples[i].Name].Kind; kind != want {
|
||||||
|
t.Errorf("supported metric %q has unexpected kind: got %d, want %d", samples[i].Name, kind, want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if samples[i].Name != "/memory/classes/total:bytes" && strings.HasPrefix(samples[i].Name, "/memory/classes") {
|
||||||
|
v := samples[i].Value.Uint64()
|
||||||
|
totalVirtual.want += v
|
||||||
|
|
||||||
|
// None of these stats should ever get this big.
|
||||||
|
// If they do, there's probably overflow involved,
|
||||||
|
// usually due to bad accounting.
|
||||||
|
if int64(v) < 0 {
|
||||||
|
t.Errorf("%q has high/negative value: %d", samples[i].Name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch samples[i].Name {
|
||||||
|
case "/memory/classes/total:bytes":
|
||||||
|
totalVirtual.got = samples[i].Value.Uint64()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if totalVirtual.got != totalVirtual.want {
|
||||||
|
t.Errorf(`"/memory/classes/total:bytes" does not match sum of /memory/classes/**: got %d, want %d`, totalVirtual.got, totalVirtual.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -882,7 +882,8 @@ func (m *consistentHeapStats) unsafeClear() {
|
||||||
// heapStatsDelta, the resulting values should be complete and
|
// heapStatsDelta, the resulting values should be complete and
|
||||||
// valid statistic values.
|
// valid statistic values.
|
||||||
//
|
//
|
||||||
// Not safe to call concurrently.
|
// Not safe to call concurrently. The world must be stopped
|
||||||
|
// or metricsSema must be held.
|
||||||
func (m *consistentHeapStats) read(out *heapStatsDelta) {
|
func (m *consistentHeapStats) read(out *heapStatsDelta) {
|
||||||
// Getting preempted after this point is not safe because
|
// Getting preempted after this point is not safe because
|
||||||
// we read allp. We need to make sure a STW can't happen
|
// we read allp. We need to make sure a STW can't happen
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue