chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
DEFAULT_SOCKET = f"/run/user/{os.getuid()}/jcode.sock"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcMem:
|
||||
pid: int
|
||||
role: str
|
||||
cmd: str
|
||||
session_id: str | None
|
||||
socket_path: str | None
|
||||
rss_mb: float
|
||||
pss_mb: float
|
||||
anon_mb: float
|
||||
shared_clean_mb: float
|
||||
private_clean_mb: float
|
||||
private_dirty_mb: float
|
||||
swap_mb: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class Totals:
|
||||
count: int
|
||||
rss_mb: float
|
||||
pss_mb: float
|
||||
anon_mb: float
|
||||
private_dirty_mb: float
|
||||
shared_clean_mb: float
|
||||
swap_mb: float
|
||||
|
||||
|
||||
SMAPS_KEYS = {
|
||||
"Rss": "rss_mb",
|
||||
"Pss": "pss_mb",
|
||||
"Anonymous": "anon_mb",
|
||||
"Shared_Clean": "shared_clean_mb",
|
||||
"Private_Clean": "private_clean_mb",
|
||||
"Private_Dirty": "private_dirty_mb",
|
||||
"Swap": "swap_mb",
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Summarize jcode server/client process memory using smaps_rollup")
|
||||
p.add_argument("--json", action="store_true", help="Print JSON instead of a human summary")
|
||||
p.add_argument(
|
||||
"--include-aux",
|
||||
action="store_true",
|
||||
help="Include non-default-socket helper/test jcode processes in the output",
|
||||
)
|
||||
p.add_argument(
|
||||
"--socket",
|
||||
default=DEFAULT_SOCKET,
|
||||
help=f"Main jcode socket to treat as the primary instance (default: {DEFAULT_SOCKET})",
|
||||
)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def read_text(path: Path, binary: bool = False) -> str | None:
|
||||
try:
|
||||
if binary:
|
||||
return path.read_bytes().replace(b"\x00", b" ").decode("utf-8", "ignore").strip()
|
||||
return path.read_text().strip()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def read_argv(path: Path) -> list[str] | None:
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except Exception:
|
||||
return None
|
||||
if not raw:
|
||||
return None
|
||||
return [part.decode("utf-8", "ignore") for part in raw.split(b"\x00") if part]
|
||||
|
||||
|
||||
def parse_socket_path(cmd: str) -> str | None:
|
||||
m = re.search(r"(?:^| )--socket\s+(\S+)", cmd)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def parse_session_id(cmd: str) -> str | None:
|
||||
m = re.search(r"--resume\s+(session_[^\s]+)", cmd)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def first_non_option(argv: list[str]) -> str | None:
|
||||
skip_next = False
|
||||
for arg in argv[1:]:
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if arg in {"--socket", "--resume", "-C", "--chdir", "--model", "--provider"}:
|
||||
skip_next = True
|
||||
continue
|
||||
if arg.startswith("-"):
|
||||
continue
|
||||
return arg
|
||||
return None
|
||||
|
||||
|
||||
def classify_process(argv: list[str], cmd: str, main_socket: str) -> tuple[str | None, bool]:
|
||||
argv0 = Path(argv[0]).name if argv else ""
|
||||
if not argv0.startswith("jcode"):
|
||||
return None, False
|
||||
|
||||
if "jcode serve" in cmd:
|
||||
socket_path = parse_socket_path(cmd) or main_socket
|
||||
if socket_path == main_socket:
|
||||
return "server", True
|
||||
return "server_aux", False
|
||||
|
||||
socket_path = parse_socket_path(cmd) or main_socket
|
||||
is_main = socket_path == main_socket
|
||||
|
||||
if "cargo build" in cmd or "rustc --crate-name jcode" in cmd or "handle_resume_session" in cmd:
|
||||
return None, False
|
||||
|
||||
subcommand = first_non_option(argv)
|
||||
if subcommand in {"auth", "login", "logout", "serve", "self-dev"} and "--resume session_" not in cmd:
|
||||
if subcommand == "self-dev" and "--resume session_" in cmd:
|
||||
pass
|
||||
else:
|
||||
return None, False
|
||||
|
||||
if "--resume session_" in cmd or " --fresh-spawn " in cmd:
|
||||
return ("client_session" if is_main else "client_aux"), is_main
|
||||
|
||||
return ("client_interactive" if is_main else "client_aux"), is_main
|
||||
|
||||
|
||||
def parse_smaps_rollup(pid: int) -> dict[str, float] | None:
|
||||
path = Path(f"/proc/{pid}/smaps_rollup")
|
||||
txt = read_text(path)
|
||||
if not txt:
|
||||
return None
|
||||
out = {value: 0.0 for value in SMAPS_KEYS.values()}
|
||||
for line in txt.splitlines():
|
||||
for key, out_key in SMAPS_KEYS.items():
|
||||
if line.startswith(f"{key}:"):
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
out[out_key] = round(int(parts[1]) / 1024.0, 1)
|
||||
return out
|
||||
|
||||
|
||||
def iter_jcode_processes(main_socket: str, include_aux: bool) -> Iterable[ProcMem]:
|
||||
for pid_dir in Path("/proc").iterdir():
|
||||
if not pid_dir.name.isdigit():
|
||||
continue
|
||||
pid = int(pid_dir.name)
|
||||
argv = read_argv(pid_dir / "cmdline")
|
||||
if not argv:
|
||||
continue
|
||||
cmd = " ".join(argv)
|
||||
if "jcode" not in cmd:
|
||||
continue
|
||||
role, is_main = classify_process(argv, cmd, main_socket)
|
||||
if role is None:
|
||||
continue
|
||||
if not include_aux and not is_main and role not in {"server"}:
|
||||
continue
|
||||
smaps = parse_smaps_rollup(pid)
|
||||
if not smaps:
|
||||
continue
|
||||
yield ProcMem(
|
||||
pid=pid,
|
||||
role=role,
|
||||
cmd=cmd,
|
||||
session_id=parse_session_id(cmd),
|
||||
socket_path=parse_socket_path(cmd),
|
||||
**smaps,
|
||||
)
|
||||
|
||||
|
||||
def sum_totals(procs: list[ProcMem]) -> Totals:
|
||||
return Totals(
|
||||
count=len(procs),
|
||||
rss_mb=round(sum(p.rss_mb for p in procs), 1),
|
||||
pss_mb=round(sum(p.pss_mb for p in procs), 1),
|
||||
anon_mb=round(sum(p.anon_mb for p in procs), 1),
|
||||
private_dirty_mb=round(sum(p.private_dirty_mb for p in procs), 1),
|
||||
shared_clean_mb=round(sum(p.shared_clean_mb for p in procs), 1),
|
||||
swap_mb=round(sum(p.swap_mb for p in procs), 1),
|
||||
)
|
||||
|
||||
|
||||
def print_human(server: list[ProcMem], clients: list[ProcMem], aux: list[ProcMem]) -> None:
|
||||
def print_group(name: str, procs: list[ProcMem]) -> None:
|
||||
totals = sum_totals(procs)
|
||||
print(f"\n{name} ({totals.count})")
|
||||
print("-" * len(f"{name} ({totals.count})"))
|
||||
print(
|
||||
f"total: RSS {totals.rss_mb:.1f} MB | PSS {totals.pss_mb:.1f} MB | "
|
||||
f"Anon {totals.anon_mb:.1f} MB | Private dirty {totals.private_dirty_mb:.1f} MB"
|
||||
)
|
||||
for p in sorted(procs, key=lambda x: (x.role, -x.pss_mb, x.pid)):
|
||||
label = p.session_id or p.role
|
||||
print(
|
||||
f" pid {p.pid:<7} {label:<48} RSS {p.rss_mb:>6.1f} | PSS {p.pss_mb:>6.1f} | "
|
||||
f"Anon {p.anon_mb:>6.1f} | PrivDirty {p.private_dirty_mb:>6.1f}"
|
||||
)
|
||||
|
||||
print_group("Server", server)
|
||||
print_group("Clients", clients)
|
||||
if aux:
|
||||
print_group("Auxiliary", aux)
|
||||
|
||||
grand = sum_totals(server + clients)
|
||||
print("\nPrimary total (server + clients)")
|
||||
print("--------------------------------")
|
||||
print(
|
||||
f"RSS {grand.rss_mb:.1f} MB | PSS {grand.pss_mb:.1f} MB | "
|
||||
f"Anon {grand.anon_mb:.1f} MB | Private dirty {grand.private_dirty_mb:.1f} MB | "
|
||||
f"Shared clean {grand.shared_clean_mb:.1f} MB"
|
||||
)
|
||||
print(
|
||||
f"Overcount if you sum RSS instead of PSS: {round(grand.rss_mb - grand.pss_mb, 1):.1f} MB"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
procs = list(iter_jcode_processes(args.socket, args.include_aux))
|
||||
server = [p for p in procs if p.role == "server"]
|
||||
clients = [p for p in procs if p.role.startswith("client_") and p.role != "client_aux"]
|
||||
aux = [p for p in procs if p.role.endswith("aux")]
|
||||
|
||||
if args.json:
|
||||
payload = {
|
||||
"socket": args.socket,
|
||||
"server": [asdict(p) for p in sorted(server, key=lambda x: x.pid)],
|
||||
"clients": [asdict(p) for p in sorted(clients, key=lambda x: x.pid)],
|
||||
"auxiliary": [asdict(p) for p in sorted(aux, key=lambda x: x.pid)],
|
||||
"totals": {
|
||||
"server": asdict(sum_totals(server)),
|
||||
"clients": asdict(sum_totals(clients)),
|
||||
"primary": asdict(sum_totals(server + clients)),
|
||||
"auxiliary": asdict(sum_totals(aux)),
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, indent=2))
|
||||
else:
|
||||
print_human(server, clients, aux)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user