"""_probe_chromium_footprint.py — measure Chromium's footprint for one page. Compares the flag profiles so the memory trade-off is measured rather than guessed. Run: .venv/bin/python linux/_probe_chromium_footprint.py [url] Reports process count and total resident memory for the browser tree, using the same flags the player uses (so the numbers match production). """ import os import subprocess import sys import time from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from linux_display import ensure_session_environment # noqa: E402 ensure_session_environment() import linux_browser # noqa: E402 URL = sys.argv[1] if len(sys.argv) > 1 else 'about:blank' def tree_memory(root_pid): """(PSS kB, process count) for root_pid and its descendants. PSS (proportional set size) from ``/proc//smaps_rollup`` is the right measure here, **not** RSS. Chromium forks many processes that share the same libraries and file-backed pages; summing RSS counts every shared page once per process, which inflated earlier measurements by roughly 2x and even made a smaller configuration look larger. """ try: out = subprocess.run( ['ps', '-eo', 'pid,ppid'], capture_output=True, text=True, timeout=10, check=False, ).stdout except Exception: return 0, 0 parents = {} for line in out.splitlines()[1:]: parts = line.split() if len(parts) >= 2 and parts[0].isdigit(): parents[int(parts[0])] = int(parts[1]) family = [root_pid] changed = True while changed: changed = False for pid, parent in parents.items(): if parent in family and pid not in family: family.append(pid) changed = True total_kb = 0 for pid in family: try: with open(f'/proc/{pid}/smaps_rollup') as fh: for line in fh: if line.startswith('Pss:'): total_kb += int(line.split()[1]) break except OSError: # Process exited between listing and reading. continue return total_kb, len(family) def measure(mode): os.environ['KIWY_CHROMIUM_MODE'] = mode profile = f'/tmp/kiwy-footprint-{mode}' subprocess.run(['rm', '-rf', profile], check=False) adapter = linux_browser.LinuxChromiumAdapter( browser_path=linux_browser.find_linux_browser(), kiosk=True, ) adapter._profile_dir = profile ok = adapter.launch(URL, 1280, 720) if not ok or adapter._proc is None: return None try: # Let Chromium finish spawning helpers before sampling. time.sleep(8) if adapter._proc.poll() is not None: return {'crashed': True, 'rc': adapter._proc.returncode} pss_kb, procs = tree_memory(adapter._proc.pid) return {"pss_mb": pss_kb / 1024.0, "procs": procs} finally: adapter.teardown() time.sleep(1.5) print(f'url = {URL}\n') print(f'{"mode":<10} {"procs":>6} {"PSS (MB)":>10}') print('-' * 30) results = {} for mode in ('safe', 'light', 'minimal'): result = measure(mode) results[mode] = result if result is None: note = 'launch failed' elif result.get('crashed'): note = f'CRASHED rc={result["rc"]}' else: note = '' if note: print(f'{mode:<10} {"-":>6} {note:>10}') else: print(f'{mode:<10} {result["procs"]:>6} {result["pss_mb"]:>10.0f}') print() base = results.get('safe', {}) or {} light = results.get('light', {}) or {} if base.get('pss_mb') and light.get('pss_mb'): saved = base['pss_mb'] - light['pss_mb'] pct = 100.0 * saved / base['pss_mb'] print(f'light mode saves {saved:.0f} MB ({pct:.0f}%) vs the default flag set') print(f' processes: {base["procs"]} -> {light["procs"]}')