Files
beszel/agent/memory_linux.go
henrygd 4fe1a0a7a4 refactor(agent): share cgroup path vars and v2 mount lookup
Drop the cpu prefix from the cgroup path vars now that memory
accounting uses them too, and extract cgroupV2Dir() for the v2 mount
lookup previously repeated in CPU, memory and the root cgroup check.
2026-10-05 14:54:29 -04:00

83 lines
2.6 KiB
Go

//go:build linux
package agent
import (
"path/filepath"
"strconv"
"github.com/henrygd/beszel/agent/utils"
)
// containerMemoryMetrics is only used when USE_CGROUP is explicitly enabled.
// Unlike /proc/stat, lxcfs already serves a container-scoped /proc/meminfo,
// so LXC keeps the existing memory path by default.
func containerMemoryMetrics(hostTotal uint64, forceUseCgroup bool) (memoryMetrics, bool) {
if !forceUseCgroup {
return memoryMetrics{}, false
}
if dir, ok := cgroupV2Dir(); ok {
if metrics, ok := readCgroupMemoryMetrics(dir, hostTotal, true); ok {
return metrics, true
}
}
if dir := utils.CgroupMountPoint(cgroupMountinfo, "cgroup", "memory"); dir != "" {
return readCgroupMemoryMetrics(dir, hostTotal, false)
}
return memoryMetrics{}, false
}
// Read the hierarchy mount root to include sibling services and descendants,
// rather than the agent's own service cgroup. A v1 unlimited limit is a
// page-aligned LONG_MAX sentinel; the threshold also covers 32-bit kernels.
func readCgroupMemoryMetrics(dir string, hostTotal uint64, v2 bool) (memoryMetrics, bool) {
usageFile, limitFile, cacheKey, shmemKey := "memory.usage_in_bytes", "memory.limit_in_bytes", "total_cache", "total_shmem"
if v2 {
usageFile, limitFile, cacheKey, shmemKey = "memory.current", "memory.max", "file", "shmem"
}
usage, ok := utils.ReadUintFile(filepath.Join(dir, usageFile))
if !ok {
return memoryMetrics{}, false
}
rawLimit, ok := utils.ReadStringFileOK(filepath.Join(dir, limitFile))
if !ok {
return memoryMetrics{}, false
}
var limit uint64
if !(v2 && rawLimit == "max") {
var err error
limit, err = strconv.ParseUint(rawLimit, 10, 64)
if err != nil {
return memoryMetrics{}, false
}
if !v2 && (limit >= (uint64(1)<<63)-(1<<20) || limit == 2147479552) {
limit = 0 // v1 unlimited sentinel
} else if limit == 0 {
return memoryMetrics{}, false
}
}
total := limit
if hostTotal > 0 && (total == 0 || hostTotal < total) {
total = hostTotal
}
if total == 0 {
return memoryMetrics{}, false // unlimited and no usable host total
}
stat, err := utils.ReadCgroupStat(filepath.Join(dir, "memory.stat"))
if err != nil {
return memoryMetrics{}, false
}
cache, ok := stat[cacheKey]
if !ok {
return memoryMetrics{}, false
}
shmem, ok := stat[shmemKey]
if !ok {
return memoryMetrics{}, false
}
// Both cache (v1) and file (v2) include shmem/tmpfs, which must remain
// charged as used memory rather than being classified as file cache.
cache = min(saturatingSub(cache, shmem), usage)
return memoryMetrics{Total: total, Used: usage - cache, BuffCache: cache}, true
}