#!/usr/bin/env python3 """Measure Flectar Mail startup milestones and idle Linux process resources. The application emits structured startup events only while this harness sets FLECTAR_STARTUP_METRICS. Each run uses an isolated copy of the supplied local profile, disables provider sync, waits for a real rendered core-ready frame, then samples /proc after a configurable idle interval. """ from __future__ import annotations import argparse import hashlib import json import os import platform import queue import re import shutil import statistics import subprocess import sys import tempfile import threading import time from pathlib import Path from typing import Any METRIC_PREFIX = "FLECTAR_STARTUP_METRIC " RENDERER_PREFIX = "FLECTAR_RENDERER " RESOURCE_KEYS = ( "Rss", "Pss", "Pss_Anon", "Pss_File", "Pss_Shmem", "Private_Clean", "Private_Dirty", "Shared_Clean", "Shared_Dirty", "Swap", "SwapPss", ) SMAPS_HEADER = re.compile(r"^[0-9a-f]+-[0-9a-f]+\s") MAPPING_KEYS = ( "Size", "Rss", "Pss", "Anonymous", "Private_Clean", "Private_Dirty", "Shared_Clean", "Shared_Dirty", "Swap", "SwapPss", ) def command_output(*command: str) -> str: try: return subprocess.check_output(command, text=True, stderr=subprocess.DEVNULL).strip() except (OSError, subprocess.CalledProcessError): return "unavailable" def file_sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as source: for chunk in iter(lambda: source.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def copy_profile(source: Path | None, destination: Path) -> None: if source is None: destination.mkdir(parents=True, exist_ok=True) return shutil.copytree(source, destination, symlinks=True) def read_proc_resources(pid: int) -> dict[str, int]: resources: dict[str, int] = {} smaps = Path(f"/proc/{pid}/smaps_rollup").read_text(encoding="utf-8") for line in smaps.splitlines(): name, separator, remainder = line.partition(":") if separator and name in RESOURCE_KEYS: field = "swap_pss" if name == "SwapPss" else name.lower() resources[f"{field}_kib"] = int(remainder.split()[0]) for line in Path(f"/proc/{pid}/status").read_text(encoding="utf-8").splitlines(): if line.startswith("Threads:"): resources["threads"] = int(line.split()[1]) break resources["file_descriptors"] = len(list(Path(f"/proc/{pid}/fd").iterdir())) return resources def mapping_kind(path: str, executable: Path) -> str: path = path.removesuffix(" (deleted)") if path == str(executable): return "executable" if path == "[heap]": return "heap" if path.startswith("[stack"): return "stack" if path.startswith("/SYSV") or ( (path.startswith("/dev/shm/") or "memfd:" in path) and any(name in path.lower() for name in ("wayland", "x11", "slint", "softbuffer")) ): return "shared_surface_candidate" if path.startswith("/dev/shm/") or "memfd:" in path or path.startswith("[anon_shmem"): return "shared_memory" if not path or path.startswith("[anon"): return "anonymous" if path.startswith("["): return "kernel_mapping" name = path.rsplit("/", 1)[-1].lower() if name.endswith((".ttf", ".ttc", ".otf", ".otc")): return "font_file" if name.endswith((".db", ".sqlite", ".sqlite3", "-wal", "-shm")): return "database_file" if ".so" in name and (name.endswith(".so") or ".so." in name): return "shared_library" return "other_file" def parse_smaps(contents: str, executable: Path) -> dict[str, Any]: """Return per-mapping counters without retaining process or profile paths.""" mappings: list[dict[str, Any]] = [] current: dict[str, Any] | None = None for line in contents.splitlines(): if SMAPS_HEADER.match(line): fields = line.split(maxsplit=5) if len(fields) < 5: raise ValueError("invalid smaps header") if current is not None: mappings.append(current) current = { "kind": mapping_kind(fields[5] if len(fields) == 6 else "", executable), "permissions": fields[1], } continue if current is None: continue name, separator, value = line.partition(":") if separator and name in MAPPING_KEYS: field = "swap_pss" if name == "SwapPss" else name.lower() current[f"{field}_kib"] = int(value.split()[0]) if current is not None: mappings.append(current) by_kind: dict[str, dict[str, int]] = {} for mapping in mappings: totals = by_kind.setdefault(mapping["kind"], {"mapping_count": 0}) totals["mapping_count"] += 1 for key, value in mapping.items(): if key.endswith("_kib"): totals[key] = totals.get(key, 0) + value return {"mapping_count": len(mappings), "by_kind": by_kind, "mappings": mappings} def read_proc_mappings(pid: int, executable: Path) -> dict[str, Any]: return parse_smaps( Path(f"/proc/{pid}/smaps").read_text(encoding="utf-8"), executable ) def runtime_executable(pid: int) -> Path: """Find the ELF after a launcher script has replaced itself with the app.""" return Path(os.readlink(f"/proc/{pid}/exe").removesuffix(" (deleted)")) def read_cpu_seconds(pid: int) -> float: fields = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8").split() ticks = int(fields[13]) + int(fields[14]) return ticks / os.sysconf(os.sysconf_names["SC_CLK_TCK"]) def stderr_reader(stream: Any, messages: queue.Queue[str]) -> None: for line in iter(stream.readline, ""): messages.put(line.rstrip("\n")) messages.put("") def record_stderr_line( line: str, events: dict[str, dict[str, Any]], stderr_tail: list[str] ) -> None: for prefix, name in ((METRIC_PREFIX, None), (RENDERER_PREFIX, "renderer_selected")): if not line.startswith(prefix): continue try: payload = json.loads(line[len(prefix) :]) if not isinstance(payload, dict): break if name is None: events[payload["event"]] = payload return if payload.get("event") == "selected": events[name] = payload return except (json.JSONDecodeError, KeyError): break if line: stderr_tail.append(line) del stderr_tail[:-40] def wait_for_metric( process: subprocess.Popen[str], messages: queue.Queue[str], events: dict[str, dict[str, Any]], stderr_tail: list[str], wanted: str, timeout: float, ) -> None: deadline = time.monotonic() + timeout while time.monotonic() < deadline: if wanted in events: return if process.poll() is not None and messages.empty(): break try: line = messages.get(timeout=min(0.1, max(0.01, deadline - time.monotonic()))) except queue.Empty: continue if not line: continue record_stderr_line(line, events, stderr_tail) if wanted not in events: tail = "\n".join(stderr_tail[-12:]) raise RuntimeError(f"timed out waiting for {wanted!r}; recent stderr:\n{tail}") def drain_messages( messages: queue.Queue[str], events: dict[str, dict[str, Any]], stderr_tail: list[str], ) -> None: while True: try: line = messages.get_nowait() except queue.Empty: return record_stderr_line(line, events, stderr_tail) def run_once( binary: Path, profile_data: Path | None, profile_cache: Path | None, settle_seconds: float, idle_seconds: float, timeout_seconds: float, round_number: int, include_mappings: bool = False, ) -> dict[str, Any]: with tempfile.TemporaryDirectory(prefix="flectar-startup-benchmark-") as temporary: root = Path(temporary) data_home = root / "data-home" cache_home = root / "cache-home" copy_profile(profile_data, data_home / "flectar-mail") copy_profile(profile_cache, cache_home / "flectar-mail") environment = os.environ.copy() environment.update( { "XDG_DATA_HOME": str(data_home), "XDG_CACHE_HOME": str(cache_home), "FLECTAR_STARTUP_METRICS": "1", "FLECTAR_RENDERER": "cpu", "FLECTAR_BENCHMARK_DISABLE_SYNC": "1", "FLECTAR_BENCHMARK_EXIT_AFTER_MS": str( int((settle_seconds + idle_seconds) * 1000) + 1500 ), } ) process = subprocess.Popen( [str(binary)], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, bufsize=1, env=environment, ) assert process.stderr is not None messages: queue.Queue[str] = queue.Queue() reader = threading.Thread( target=stderr_reader, args=(process.stderr, messages), daemon=True, ) reader.start() events: dict[str, dict[str, Any]] = {} stderr_tail: list[str] = [] try: wait_for_metric( process, messages, events, stderr_tail, "core_ready_frame", timeout_seconds, ) time.sleep(settle_seconds) if process.poll() is not None: raise RuntimeError("application exited during the idle-settle interval") initial_cpu = read_cpu_seconds(process.pid) idle_started = time.monotonic() time.sleep(idle_seconds) idle_elapsed = time.monotonic() - idle_started final_cpu = read_cpu_seconds(process.pid) resources = read_proc_resources(process.pid) mapping_report = ( read_proc_mappings(process.pid, runtime_executable(process.pid)) if include_mappings else None ) resources["idle_cpu_percent"] = ( max(0.0, final_cpu - initial_cpu) / idle_elapsed * 100.0 ) resources["idle_sample_seconds"] = idle_elapsed resources["idle_settle_seconds"] = settle_seconds process.wait(timeout=5) drain_messages(messages, events, stderr_tail) renderer = events.get("renderer_selected", {}) if renderer.get("active") != "cpu" or renderer.get("wgpu_initialized") is not False: raise RuntimeError(f"expected CPU renderer without WGPU, got {renderer}") if process.returncode != 0: raise RuntimeError( f"application exited with {process.returncode}:\n" + "\n".join(stderr_tail[-12:]) ) finally: if process.poll() is None: process.terminate() try: process.wait(timeout=3) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=3) milestones = { name: payload.get("elapsed_ms") for name, payload in events.items() if "elapsed_ms" in payload } warm_event = events.get("warm_cache_loaded", {}) result = { "round": round_number, "pid": process.pid, "warm_cache_hit": bool(warm_event.get("hit", False)), "milestones_ms": milestones, "idle": resources, "events": events, } if mapping_report is not None: result["mapping_report"] = mapping_report return result def median(values: list[float | int]) -> float | None: return statistics.median(values) if values else None def summarize(runs: list[dict[str, Any]]) -> dict[str, Any]: milestone_names = sorted( {name for run in runs for name in run["milestones_ms"].keys()} ) resource_names = sorted({name for run in runs for name in run["idle"].keys()}) return { "rounds": len(runs), "warm_cache_hits": sum(run["warm_cache_hit"] for run in runs), "median_milestones_ms": { name: median( [ run["milestones_ms"][name] for run in runs if run["milestones_ms"].get(name) is not None ] ) for name in milestone_names }, "median_idle": { name: median( [run["idle"][name] for run in runs if run["idle"].get(name) is not None] ) for name in resource_names }, } def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--binary", type=Path, required=True) parser.add_argument( "--profile-data-dir", type=Path, help="Optional flectar-mail data directory copied into every isolated run.", ) parser.add_argument( "--profile-cache-dir", type=Path, help="Optional flectar-mail cache directory containing the warm projection.", ) parser.add_argument("--rounds", type=int, default=5) parser.add_argument( "--settle-seconds", type=float, default=2.0, help="Time after the ready frame before idle CPU sampling begins.", ) parser.add_argument("--idle-seconds", type=float, default=5.0) parser.add_argument("--timeout-seconds", type=float, default=45.0) parser.add_argument( "--include-mappings", action="store_true", help="Include anonymized per-mapping /proc smaps counters and category totals.", ) parser.add_argument( "--output", type=Path, default=Path("target/startup-benchmark/startup-benchmark.json"), ) args = parser.parse_args() if sys.platform != "linux": parser.error("this harness currently requires Linux /proc resource counters") binary = args.binary.resolve() if not binary.is_file() or not os.access(binary, os.X_OK): parser.error(f"binary is not executable: {binary}") if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): parser.error( "no display is available; run under xvfb-run, for example: " "xvfb-run -a ./scripts/benchmark-startup.py --binary …" ) if ( args.rounds < 1 or args.settle_seconds < 0 or args.idle_seconds <= 0 or args.timeout_seconds <= 0 ): parser.error("rounds and sample timings must be positive; settle may be zero") for label, source in ( ("profile data", args.profile_data_dir), ("profile cache", args.profile_cache_dir), ): if source is not None and not source.is_dir(): parser.error(f"{label} directory does not exist: {source}") git_status = command_output("git", "status", "--porcelain") identity = { "git_commit": command_output("git", "rev-parse", "HEAD"), "git_dirty": git_status not in ("", "unavailable"), "rustc": command_output("rustc", "--version", "--verbose"), "binary": str(binary), "binary_sha256": file_sha256(binary), "platform": platform.platform(), "python": platform.python_version(), "display": os.environ.get("DISPLAY"), "wayland_display": os.environ.get("WAYLAND_DISPLAY"), "renderer_requested": "cpu", } runs = [] for round_number in range(1, args.rounds + 1): result = run_once( binary, args.profile_data_dir, args.profile_cache_dir, args.settle_seconds, args.idle_seconds, args.timeout_seconds, round_number, args.include_mappings, ) runs.append(result) print( f"round={round_number} " f"first_frame_ms={result['milestones_ms'].get('first_frame')} " f"warm_frame_ms={result['milestones_ms'].get('warm_cache_frame')} " f"core_frame_ms={result['milestones_ms'].get('core_ready_frame')} " f"pss_kib={result['idle'].get('pss_kib')} " f"idle_cpu_percent={result['idle'].get('idle_cpu_percent'):.3f}" ) report = { "schema_version": 2, "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "identity": identity, "configuration": { "rounds": args.rounds, "settle_seconds": args.settle_seconds, "idle_seconds": args.idle_seconds, "timeout_seconds": args.timeout_seconds, "include_mappings": args.include_mappings, "profile_data_supplied": args.profile_data_dir is not None, "profile_cache_supplied": args.profile_cache_dir is not None, }, "summary": summarize(runs), "runs": runs, } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n") print(f"report={args.output.resolve()}") if __name__ == "__main__": main()