#!/usr/bin/env python3 """Memory / CPU benchmark: Ferowen vs. other browsers on macOS. Method (identical for every browser): 1. Launch the browser as an app via LaunchServices (`open -n`), with a throwaway profile, opening the same five public pages. 2. Wait SETTLE seconds for the pages to load. 3. Memory = sum of `footprint` (physical footprint, the number Activity Monitor shows as "Memory") over the app process and every process macOS records it as responsible for (renderers, GPU, network, WebKit WebContent helpers...). 4. CPU (load) = total CPU seconds consumed by that process family so far. CPU (idle) = CPU seconds consumed over the next IDLE seconds with the pages open and untouched. 5. Quit the browser. Each browser runs with its default settings (Ferowen: Shield on). Usage: python3 scripts/bench_macos.py path/to/Ferowen.app [--runs N] [--settle SECONDS] [--only Ferowen] --settle 660 measures "after 11 minutes", when background tabs have had time to be managed by each browser's default memory policy. """ import ctypes import json import os import re import signal import subprocess import sys import tempfile import time URLS = [ "https://en.wikipedia.org/wiki/Rust_(programming_language)", "https://github.com/servo/servo", "https://www.theverge.com/", "https://www.youtube.com/", "https://news.ycombinator.com/", ] SETTLE = 60 IDLE = 60 _libc = ctypes.CDLL("/usr/lib/libSystem.B.dylib") _resp = _libc.responsibility_get_pid_responsible_for_pid _resp.argtypes = [ctypes.c_int] _resp.restype = ctypes.c_int def ps_table(): out = subprocess.check_output(["ps", "-axo", "pid=,time=,command="], text=True) rows = {} for line in out.splitlines(): m = re.match(r"\s*(\d+)\s+(\S+)\s+(.*)", line) if m: rows[int(m.group(1))] = (cpu_secs(m.group(2)), m.group(3)) return rows def cpu_secs(t): parts = [float(p) for p in t.replace("-", ":").split(":")] secs = 0.0 for p in parts: secs = secs * 60 + p return secs def family(main_pid, table): return [pid for pid in table if pid == main_pid or _resp(pid) == main_pid] def footprint_bytes(pid): try: out = subprocess.check_output( ["footprint", "-f", "bytes", "--noCategories", "-p", str(pid)], text=True, stderr=subprocess.DEVNULL, timeout=30, ) except Exception: return 0 m = re.search(r"Footprint:\s*(\d+)\s*B", out) return int(m.group(1)) if m else 0 def wait_main(match, timeout=30): end = time.time() + timeout while time.time() < end: for pid, (_, cmd) in ps_table().items(): if match(cmd): return pid time.sleep(0.5) raise RuntimeError("browser did not start") def measure(name, launch, match): subprocess.run(launch, check=True) main = wait_main(match) time.sleep(SETTLE) table = ps_table() fam = family(main, table) mem = sum(footprint_bytes(p) for p in fam) cpu_load = sum(table[p][0] for p in fam) time.sleep(IDLE) table2 = ps_table() fam2 = family(main, table2) cpu_after = sum(table2[p][0] for p in fam2) for p in sorted(set(fam2), reverse=True): try: os.kill(p, signal.SIGTERM) except OSError: pass time.sleep(5) return { "browser": name, "processes": len(fam), "memory_mb": round(mem / 1048576, 1), "cpu_load_s": round(cpu_load, 2), "cpu_idle_s_per_min": round((cpu_after - cpu_load) * 60 / IDLE, 2), } def main(): app = os.path.abspath(sys.argv[1]) global SETTLE runs = int(sys.argv[sys.argv.index("--runs") + 1]) if "--runs" in sys.argv else 1 only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else None if "--settle" in sys.argv: SETTLE = int(sys.argv[sys.argv.index("--settle") + 1]) tmp = tempfile.mkdtemp(prefix="ferowen-bench-") results = [] for r in range(runs): home = os.path.join(tmp, f"ferowen-home-{r}") os.makedirs(home) results.append(measure( "Ferowen", ["open", "-n", "--env", f"HOME={home}", app, "--args", *URLS], lambda c: c.startswith(os.path.join(app, "Contents/MacOS/Ferowen")), )) if only == "Ferowen": continue chrome_dir = os.path.join(tmp, f"chrome-{r}") if os.path.exists("/Applications/Google Chrome.app"): results.append(measure( "Google Chrome", ["open", "-n", "-a", "Google Chrome", "--args", f"--user-data-dir={chrome_dir}", "--no-first-run", "--no-default-browser-check", *URLS], lambda c: "Google Chrome.app/Contents/MacOS/Google Chrome" in c and f"--user-data-dir={chrome_dir}" in c, )) ff_dir = os.path.join(tmp, f"firefox-{r}") if os.path.exists("/Applications/Firefox.app"): os.makedirs(ff_dir) results.append(measure( "Firefox", ["open", "-n", "-a", "Firefox", "--args", "-no-remote", "-profile", ff_dir, *URLS], lambda c: "Firefox.app/Contents/MacOS/firefox" in c and ff_dir in c and "-contentproc" not in c, )) meta = { "date": time.strftime("%Y-%m-%d"), "machine": subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"], text=True).strip(), "macos": subprocess.check_output(["sw_vers", "-productVersion"], text=True).strip(), "urls": URLS, "settle_s": SETTLE, "idle_s": IDLE, } print(json.dumps({"meta": meta, "results": results}, indent=2)) if __name__ == "__main__": main()