blob: feb9d9ad32335898250fee36fd97402d5ca05b87 [file] [log] [blame]
// 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 memory provides functions to collect memory information from a
// variety of places.
package memory
import (
"bufio"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
// ReadRuntimeStats is a convenience for runtime.ReadMemStats.
func ReadRuntimeStats() runtime.MemStats {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
return ms
}
// SystemStats holds values from the /proc/meminfo
// file, which describes the total system memory.
// All values are in bytes.
type SystemStats struct {
Total uint64
Free uint64
Available uint64
Used uint64
Buffers uint64
Cached uint64
}
// ReadSystemStats reads the /proc/meminfo file to get information about the
// machine.
func ReadSystemStats() (SystemStats, error) {
f, err := os.Open("/proc/meminfo")
if err != nil {
return SystemStats{}, err
}
defer f.Close()
// Read the number, convert kibibytes to bytes, and set p.
set := func(p *uint64, words []string) {
if len(words) != 3 || words[2] != "kB" {
err = fmt.Errorf("got %+v, want 3 words, third is 'kB'", words)
return
}
var ki uint64
ki, err = strconv.ParseUint(words[1], 10, 64)
if err == nil {
*p = ki * 1024
}
}
scan := bufio.NewScanner(f)
var sms SystemStats
for scan.Scan() {
words := strings.Fields(scan.Text())
switch words[0] {
case "MemTotal:":
set(&sms.Total, words)
case "MemFree:":
set(&sms.Free, words)
case "MemAvailable:":
set(&sms.Available, words)
case "Buffers:":
set(&sms.Buffers, words)
case "Cached:":
set(&sms.Cached, words)
}
}
if err == nil {
err = scan.Err()
}
if err != nil {
return SystemStats{}, err
}
sms.Used = sms.Total - sms.Free - sms.Buffers - sms.Cached // see `man free`
return sms, nil
}
// ProcessStats holds values that describe the current process's memory.
// All values are in bytes.
type ProcessStats struct {
VSize uint64 // virtual memory size
RSS uint64 // resident set size (physical memory in use)
}
// ReadProcessStats reads memory stats for the process.
func ReadProcessStats() (ProcessStats, error) {
f, err := os.Open("/proc/self/stat")
if err != nil {
return ProcessStats{}, err
}
defer f.Close()
// Values from `man proc`.
var (
d int
s string
c byte
vsize, rss uint64
)
_, err = fmt.Fscanf(f, "%d %s %c %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d",
&d, &s, &c, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &vsize, &rss)
if err != nil {
return ProcessStats{}, err
}
const pageSize = 4 * 1024 // Linux page size, from `getconf PAGESIZE`
return ProcessStats{
VSize: vsize,
RSS: rss * pageSize,
}, nil
}
// ReadCgroupStats reads memory information for the current cgroup.
// (A cgroup is the sandbox in which a docker container runs.)
// All values are in bytes.
func ReadCgroupStats() (map[string]uint64, error) {
m, err := getCgroupStats()
if err != nil {
return nil, err
}
// k8s's definition of container memory, as shown by `kubectl top pods`.
// See https://www.magalix.com/blog/memory_working_set-vs-memory_rss.
workingSet := m["usage"]
tif := m["total_inactive_file"]
if tif > workingSet {
workingSet = 0
} else {
workingSet -= tif
}
m["workingSet"] = workingSet
// True RSS. See note on https://lwn.net/Articles/432224.
m["trueRSS"] = m["rss"] + m["mapped_file"]
return m, nil
}
func getCgroupStats() (map[string]uint64, error) {
const cgroupMemDir = "/sys/fs/cgroup/memory"
readUintFile := func(filename string) (uint64, error) {
data, err := os.ReadFile(filepath.Join(cgroupMemDir, filename))
if err != nil {
return 0, err
}
u, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
if err != nil {
return 0, err
}
return u, nil
}
m := map[string]uint64{}
var err error
m["limit"], err = readUintFile("memory.limit_in_bytes")
if err != nil {
return nil, err
}
m["usage"], err = readUintFile("memory.usage_in_bytes")
if err != nil {
return nil, err
}
f, err := os.Open(filepath.Join(cgroupMemDir, "memory.stat"))
if err != nil {
return nil, err
}
defer f.Close()
scan := bufio.NewScanner(f)
for scan.Scan() {
fs := strings.Fields(scan.Text())
if len(fs) != 2 {
return nil, fmt.Errorf("memory.stat: %q: not two fields", scan.Text())
}
u, err := strconv.ParseUint(fs[1], 10, 64)
if err != nil {
return nil, err
}
m[fs[0]] = u
}
if err := scan.Err(); err != nil {
return nil, err
}
return m, nil
}
// Format formats a memory value for humans. It uses a B, K, M or G suffix and
// rounds to two decimal places.
func Format(m uint64) string {
const Ki = 1024
if m < Ki {
return fmt.Sprintf("%d B", m)
}
if m < Ki*Ki {
return fmt.Sprintf("%.2f K", float64(m)/Ki)
}
if m < Ki*Ki*Ki {
return fmt.Sprintf("%.2f M", float64(m)/(Ki*Ki))
}
return fmt.Sprintf("%.2f G", float64(m)/(Ki*Ki*Ki))
}