| // 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)) |
| } |