0.2.78
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package runtimestats
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Process is what the operating system knows about the container that the Go runtime does
|
||||
// not. Every field of it is optional: this is read from /proc, which exists in the
|
||||
// container the gateway is deployed in and does not exist on a developer's machine, so an
|
||||
// unavailable figure is omitted rather than reported as zero — nought open sockets and
|
||||
// "could not look" are different answers and the console must not print the first for the
|
||||
// second.
|
||||
type Process struct {
|
||||
PID int `json:"pid"`
|
||||
// CPUSeconds is cumulative processor time, and CPUPercent is the share of one
|
||||
// processor used since the previous read. A hundred per cent is one core saturated,
|
||||
// not the machine: with GOMAXPROCS processors available the ceiling is that times a
|
||||
// hundred, which is why the console prints both.
|
||||
CPUSeconds float64 `json:"cpuSeconds,omitempty"`
|
||||
CPUPercent float64 `json:"cpuPercent,omitempty"`
|
||||
CPUKnown bool `json:"cpuKnown"`
|
||||
OpenFiles int `json:"openFiles,omitempty"`
|
||||
OpenSockets int `json:"openSockets,omitempty"`
|
||||
FileLimit int `json:"fileLimit,omitempty"`
|
||||
FilesKnown bool `json:"filesKnown"`
|
||||
}
|
||||
|
||||
// threadCount is operating-system threads, which is a different number from goroutines and
|
||||
// is the one that matters when goroutines are blocked in syscalls: the runtime creates a
|
||||
// thread per blocked call, and a thread costs far more than a goroutine does.
|
||||
func threadCount() int {
|
||||
profile := pprof.Lookup("threadcreate")
|
||||
if profile == nil {
|
||||
return 0
|
||||
}
|
||||
return profile.Count()
|
||||
}
|
||||
|
||||
// configuredMemoryLimit reports what the container was started with rather than what the
|
||||
// runtime currently holds, so the console can say whether the limit is deliberate.
|
||||
func configuredMemoryLimit() string {
|
||||
return strings.TrimSpace(os.Getenv("GOMEMLIMIT"))
|
||||
}
|
||||
|
||||
var (
|
||||
cpuMu sync.Mutex
|
||||
lastCPUAt time.Time
|
||||
lastCPUSecs float64
|
||||
lastPercent float64
|
||||
)
|
||||
|
||||
func readProcess() Process {
|
||||
process := Process{PID: os.Getpid()}
|
||||
if seconds, ok := processCPUSeconds(); ok {
|
||||
process.CPUSeconds = seconds
|
||||
process.CPUKnown = true
|
||||
process.CPUPercent = cpuPercentSince(seconds, time.Now())
|
||||
}
|
||||
if files, sockets, ok := openDescriptors(); ok {
|
||||
process.OpenFiles = files
|
||||
process.OpenSockets = sockets
|
||||
process.FilesKnown = true
|
||||
process.FileLimit = descriptorLimit()
|
||||
}
|
||||
return process
|
||||
}
|
||||
|
||||
// cpuPercentSince needs two readings, so the first call after start-up reports nothing
|
||||
// rather than dividing by the life of the process — an average over four hours would hide
|
||||
// exactly the spike somebody opened the page to find. A read that arrives too soon after
|
||||
// the last one repeats the previous answer instead of amplifying rounding into a spike.
|
||||
func cpuPercentSince(seconds float64, now time.Time) float64 {
|
||||
const minInterval = 5 * time.Second
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
if lastCPUAt.IsZero() {
|
||||
lastCPUAt, lastCPUSecs = now, seconds
|
||||
return 0
|
||||
}
|
||||
elapsed := now.Sub(lastCPUAt)
|
||||
if elapsed < minInterval {
|
||||
return lastPercent
|
||||
}
|
||||
used := seconds - lastCPUSecs
|
||||
lastCPUAt, lastCPUSecs = now, seconds
|
||||
if used < 0 {
|
||||
lastPercent = 0
|
||||
return 0
|
||||
}
|
||||
lastPercent = used / elapsed.Seconds() * 100
|
||||
return lastPercent
|
||||
}
|
||||
|
||||
// processCPUSeconds prefers /proc/self/schedstat, whose first field is cumulative time on
|
||||
// a processor in nanoseconds — exact, where /proc/self/stat is in clock ticks whose length
|
||||
// cannot be read without cgo and has to be assumed to be the usual hundredth of a second.
|
||||
func processCPUSeconds() (float64, bool) {
|
||||
if raw, err := os.ReadFile("/proc/self/schedstat"); err == nil {
|
||||
fields := strings.Fields(string(raw))
|
||||
if len(fields) > 0 {
|
||||
if nanos, err := strconv.ParseFloat(fields[0], 64); err == nil {
|
||||
return nanos / 1e9, true
|
||||
}
|
||||
}
|
||||
}
|
||||
raw, err := os.ReadFile("/proc/self/stat")
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
// The second field is the command name in brackets and may itself contain spaces, so
|
||||
// the fields after it are counted from the closing bracket rather than from the start.
|
||||
shut := strings.LastIndex(string(raw), ")")
|
||||
if shut < 0 {
|
||||
return 0, false
|
||||
}
|
||||
fields := strings.Fields(string(raw)[shut+1:])
|
||||
// After the bracket, field 1 is the state; utime and stime are fields 12 and 13.
|
||||
if len(fields) < 13 {
|
||||
return 0, false
|
||||
}
|
||||
user, userErr := strconv.ParseFloat(fields[11], 64)
|
||||
system, systemErr := strconv.ParseFloat(fields[12], 64)
|
||||
if userErr != nil || systemErr != nil {
|
||||
return 0, false
|
||||
}
|
||||
const ticksPerSecond = 100
|
||||
return (user + system) / ticksPerSecond, true
|
||||
}
|
||||
|
||||
func openDescriptors() (files, sockets int, ok bool) {
|
||||
entries, err := os.ReadDir("/proc/self/fd")
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
for _, entry := range entries {
|
||||
files++
|
||||
target, err := os.Readlink("/proc/self/fd/" + entry.Name())
|
||||
if err == nil && strings.HasPrefix(target, "socket:") {
|
||||
sockets++
|
||||
}
|
||||
}
|
||||
return files, sockets, true
|
||||
}
|
||||
|
||||
func descriptorLimit() int {
|
||||
raw, err := os.ReadFile("/proc/self/limits")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
if !strings.HasPrefix(line, "Max open files") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(strings.TrimPrefix(line, "Max open files"))
|
||||
if len(fields) == 0 {
|
||||
return 0
|
||||
}
|
||||
limit, err := strconv.Atoi(fields[0])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return limit
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user