Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

221 lines
6.6 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Small psutil-compatible memory helpers for macOS VM stat churn.
macOS 27 changed the HOST_VM_INFO64 tail layout before psutil had a matching
adapter in some bundled environments. oMLX only needs a small subset of
``psutil.virtual_memory()`` for memory guard and dashboard decisions, so keep a
local implementation for macOS and use psutil on other platforms.
"""
from __future__ import annotations
import ctypes
import ctypes.util
import logging
import os
import re
import subprocess
import sys
import time
from collections import namedtuple
from typing import Any
logger = logging.getLogger(__name__)
_HOST_VM_INFO64 = 4
_HOST_INFO64_MAX_COUNT = 256
_VM_STATS_MIN_COUNT = 4
_VM_PAGE_SIZE = 16384
_SYSCTL = "/usr/sbin/sysctl"
_VM_STAT = "/usr/bin/vm_stat"
_SLOW_FALLBACK_TTL_S = 1.0
svmem = namedtuple(
"svmem",
["total", "available", "percent", "used", "free", "active", "inactive", "wired"],
)
_cached_total_memory: int | None = None
_cached_slow_virtual_memory: Any | None = None
_cached_slow_virtual_memory_at = 0.0
if sys.platform == "darwin":
try:
_libc = ctypes.CDLL(ctypes.util.find_library("c"))
_libc.mach_host_self.restype = ctypes.c_uint
_MACH_HOST = _libc.mach_host_self()
_ps = ctypes.c_uint(0)
_libc.host_page_size(_MACH_HOST, ctypes.byref(_ps))
if _ps.value > 0:
_VM_PAGE_SIZE = int(_ps.value)
except Exception: # noqa: BLE001
_libc = None
_MACH_HOST = None
else:
_libc = None
_MACH_HOST = None
def get_total_memory() -> int:
"""Return total physical memory in bytes."""
global _cached_total_memory
if _cached_total_memory is not None:
return _cached_total_memory
try:
pages = os.sysconf("SC_PHYS_PAGES")
page_size = os.sysconf("SC_PAGE_SIZE")
total = int(pages) * int(page_size)
if total > 0:
_cached_total_memory = total
return total
except (AttributeError, ValueError, OSError):
pass
if sys.platform == "darwin":
try:
out = subprocess.check_output([_SYSCTL, "-n", "hw.memsize"], text=True)
total = int(out.strip())
if total > 0:
_cached_total_memory = total
return total
except (subprocess.SubprocessError, ValueError, OSError):
pass
try:
import psutil # type: ignore
total = int(psutil.virtual_memory().total)
if total > 0:
_cached_total_memory = total
return total
except Exception as exc: # noqa: BLE001
logger.debug("psutil total memory fallback failed: %s", exc)
return 0
def get_macos_vm_stats() -> dict[str, int] | None:
"""Return macOS vm_statistics64 page counters in bytes.
The first four counters are stable across SDK versions. The oversized
host_info64_t buffer avoids binding oMLX to an SDK-specific struct tail.
"""
if _libc is None or _MACH_HOST is None:
return None
try:
stats = (ctypes.c_int * _HOST_INFO64_MAX_COUNT)()
count = ctypes.c_uint(_HOST_INFO64_MAX_COUNT)
rc = _libc.host_statistics64(
_MACH_HOST, _HOST_VM_INFO64, stats, ctypes.byref(count)
)
if rc != 0 or count.value < _VM_STATS_MIN_COUNT:
return None
ps = _VM_PAGE_SIZE
return {
"free": int(stats[0]) * ps,
"active": int(stats[1]) * ps,
"inactive": int(stats[2]) * ps,
"wired": int(stats[3]) * ps,
}
except Exception: # noqa: BLE001
return None
def _build_svmem(stats: dict[str, int], total: int | None = None) -> Any:
free = max(0, int(stats.get("free", 0) or 0))
active = max(0, int(stats.get("active", 0) or 0))
inactive = max(0, int(stats.get("inactive", 0) or 0))
wired = max(0, int(stats.get("wired", 0) or 0))
available = max(0, free + inactive)
total_bytes = int(total or get_total_memory() or 0)
if total_bytes <= 0:
total_bytes = max(available + active + wired, free + active + inactive + wired)
used = max(0, total_bytes - available)
percent = (used / total_bytes * 100.0) if total_bytes > 0 else 0.0
return svmem(
total=total_bytes,
available=available,
percent=percent,
used=used,
free=free,
active=active,
inactive=inactive,
wired=wired,
)
def _parse_vm_stat_output(output: str) -> dict[str, int] | None:
page_size = _VM_PAGE_SIZE
first_line = output.splitlines()[0] if output else ""
match = re.search(r"page size of (\d+) bytes", first_line)
if match is not None:
page_size = int(match.group(1))
pages: dict[str, int] = {}
for line in output.splitlines():
if ":" not in line:
continue
key, raw_value = line.split(":", 1)
value = raw_value.strip().rstrip(".").replace(" ", "")
if not value.isdigit():
continue
pages[key.strip()] = int(value)
if not pages:
return None
return {
"free": pages.get("Pages free", 0) * page_size,
"active": pages.get("Pages active", 0) * page_size,
"inactive": pages.get("Pages inactive", 0) * page_size,
"wired": pages.get("Pages wired down", 0) * page_size,
}
def _virtual_memory_from_vm_stat_cached() -> Any | None:
global _cached_slow_virtual_memory, _cached_slow_virtual_memory_at
now = time.monotonic()
if (
_cached_slow_virtual_memory is not None
and now - _cached_slow_virtual_memory_at < _SLOW_FALLBACK_TTL_S
):
return _cached_slow_virtual_memory
try:
out = subprocess.check_output([_VM_STAT], text=True)
except (subprocess.SubprocessError, OSError) as exc:
logger.debug("vm_stat fallback failed: %s", exc)
return None
stats = _parse_vm_stat_output(out)
if stats is None:
return None
vm = _build_svmem(stats)
_cached_slow_virtual_memory = vm
_cached_slow_virtual_memory_at = now
return vm
def virtual_memory(*, allow_slow_fallback: bool = True) -> Any:
"""Return a psutil-like virtual memory snapshot.
On macOS this avoids psutil's HOST_VM_INFO64 adapter. On other platforms it
delegates to psutil.
"""
if sys.platform == "darwin":
stats = get_macos_vm_stats()
if stats is not None:
return _build_svmem(stats)
if allow_slow_fallback:
vm = _virtual_memory_from_vm_stat_cached()
if vm is not None:
return vm
raise RuntimeError("macOS VM statistics unavailable")
import psutil # type: ignore
return psutil.virtual_memory()