SystemUsage
Samples (cpu_percent, mem_total_bytes, mem_used_bytes) for the dashboards' gauges.
psutil shows a container every processor and byte of the host, though its
cgroup allows it a share of them, and the host's figures count every other
tenant too. Where the cgroup the process runs in limits CPU or memory, the
sampler reads that cgroup: on the unified hierarchy cpu.stat and
memory.current against cpu.max and memory.max, on the legacy one the
cpuacct and memory controllers against the CFS quota and the memory limit.
Memory in use leaves out the page cache the kernel can drop (the inactive file
pages), the way container tools report it, and CPU is the usage counter's
growth between two samples over the cores the cgroup may use: its quota, else
the processors it is allowed. A limit the cgroup does not set is the host's
total. A cgroup that limits nothing is not the environment, and neither is one
that cannot be read: the node is, through psutil's system-wide figures.
Attributes
attributeLIMITS= ('cpu.max', 'memory.max', 'cpu.cfs_quota_us', 'memory.limit_in_bytes')Functions
constructor__init__(root=CGROUP_ROOT, proc_cgroup=PROC_CGROUP, clock=time.monotonic) -> NoneSource Code
def __init__(self, root: str = CGROUP_ROOT, proc_cgroup: str = PROC_CGROUP,
clock: Callable[[], float] = time.monotonic) -> None:
self._clock = clock
self._unified = False
self._cpu_dir: Optional[str] = None
self._mem_dir: Optional[str] = None
self._last: Optional[Tuple[float, float]] = None
for line in (_read(proc_cgroup) or "").splitlines():
parts = line.split(":", 2)
if len(parts) != 3:
continue
controllers, path = parts[1], parts[2].lstrip("/")
if controllers == "":
own = os.path.join(root, path)
if os.path.isfile(os.path.join(own, "cpu.stat")):
self._unified, self._cpu_dir, self._mem_dir = True, own, own
continue
names = controllers.split(",")
# A container that keeps the host's cgroup names sees only its own
# cgroup at the controller's mount, so that is the fallback.
for mount in (os.path.join(root, controllers, path), os.path.join(root, controllers)):
if not os.path.isdir(mount):
continue
if ("cpuacct" in names or "cpu" in names) and self._cpu_dir is None:
self._cpu_dir = mount
if "memory" in names and self._mem_dir is None:
self._mem_dir = mount
break
if not self._limited():
self._cpu_dir = self._mem_dir = Noneparamrootstr= CGROUP_ROOTparamproc_cgroupstr= PROC_CGROUPparamclockCallable[[], float]= time.monotonicReturns
Nonefunc_limited() -> boolWhether the cgroup caps CPU or memory below the host.
Source Code
def _limited(self) -> bool:
"""Whether the cgroup caps CPU or memory below the host."""
host = psutil.virtual_memory().total
for d in (self._cpu_dir, self._mem_dir):
for name in self.LIMITS:
text = (_read(os.path.join(d, name)) or "") if d else ""
if text.split() and text.split()[0].isdigit():
if name.startswith("cpu") or int(text.split()[0]) < host:
return True
return FalseReturns
boolfunc_cpu() -> Optional[Tuple[float, float]]The cgroup's CPU time in seconds and the cores it may use.
Source Code
def _cpu(self) -> Optional[Tuple[float, float]]:
"""The cgroup's CPU time in seconds and the cores it may use."""
d = self._cpu_dir
if d is None:
return None
if self._unified:
usec = _stat(os.path.join(d, "cpu.stat"), "usage_usec")
if usec is None:
return None
quota = (_read(os.path.join(d, "cpu.max")) or "").split()
cores = int(quota[0]) / int(quota[1]) if len(quota) == 2 and quota[0].isdigit() else _allowed_cores()
return usec / 1e6, cores
nsec = _read(os.path.join(d, "cpuacct.usage"))
if nsec is None or not nsec.isdigit():
return None
quota, period = _read(os.path.join(d, "cpu.cfs_quota_us")), _read(os.path.join(d, "cpu.cfs_period_us"))
limited = quota is not None and period is not None and quota.isdigit() and int(quota) > 0 and int(period) > 0
return int(nsec) / 1e9, int(quota) / int(period) if limited else _allowed_cores()Returns
typing.Optional[typing.Tuple[float, float]]func_memory() -> Optional[Tuple[int, int]]The cgroup's memory limit, the host's total without one, and its use.
Source Code
def _memory(self) -> Optional[Tuple[int, int]]:
"""The cgroup's memory limit, the host's total without one, and its use."""
d = self._mem_dir
if d is None:
return None
if self._unified:
current, limit = _read(os.path.join(d, "memory.current")), _read(os.path.join(d, "memory.max"))
cache = _stat(os.path.join(d, "memory.stat"), "inactive_file")
else:
current, limit = _read(os.path.join(d, "memory.usage_in_bytes")), _read(os.path.join(d, "memory.limit_in_bytes"))
cache = _stat(os.path.join(d, "memory.stat"), "total_inactive_file")
if current is None or not current.isdigit():
return None
host = psutil.virtual_memory().total
total = int(limit) if limit is not None and limit.isdigit() and int(limit) < host else host
return total, max(0, int(current) - (cache or 0))Returns
typing.Optional[typing.Tuple[int, int]]funcsample() -> Tuple[float, int, int]Source Code
def sample(self) -> Tuple[float, int, int]:
cpu, mem, now = self._cpu(), self._memory(), self._clock()
if cpu is None:
percent = psutil.cpu_percent()
else:
used, cores = cpu
percent = 0.0
if self._last is not None and now > self._last[1]:
percent = min(100.0, max(0.0, (used - self._last[0]) / (now - self._last[1]) / max(cores, 1e-9) * 100))
self._last = (used, now)
if mem is None:
vm = psutil.virtual_memory()
mem = (vm.total, vm.used)
return round(percent, 1), mem[0], mem[1]Returns
typing.Tuple[float, int, int]