171 lines
5.3 KiB
Go
171 lines
5.3 KiB
Go
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
|
|
}
|