Files
mail/scripts/benchmark-tray.py

100 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Measure CPU-rendered welcome -> tray -> restored memory on isolated profiles.
Requires Linux /proc and a display. Unset WAYLAND_DISPLAY to force X11.
Uses the application's opt-in metrics and production tray lifecycle helpers.
"""
import argparse
import importlib.util
import json
import os
from pathlib import Path
import queue
import statistics
import subprocess
import tempfile
import threading
import time
spec = importlib.util.spec_from_file_location("startup", Path(__file__).with_name("benchmark-startup.py"))
startup = importlib.util.module_from_spec(spec)
spec.loader.exec_module(startup)
def run(binary, screenshots, include_mappings=False):
with tempfile.TemporaryDirectory(prefix="flectar-tray-benchmark-") as root:
env = dict(os.environ, XDG_DATA_HOME=root + "/data", XDG_CACHE_HOME=root + "/cache",
FLECTAR_RENDERER="cpu", FLECTAR_STARTUP_METRICS="1",
FLECTAR_BENCHMARK_DISABLE_SYNC="1", FLECTAR_BENCHMARK_TRAY_INTERVAL_MS="6000",
FLECTAR_BENCHMARK_EXIT_AFTER_MS="18000")
if screenshots:
env["FLECTAR_BENCHMARK_SCREENSHOT_DIR"] = str(screenshots.resolve())
process = subprocess.Popen([str(binary)], env=env, stderr=subprocess.PIPE,
stdout=subprocess.DEVNULL, text=True)
messages = queue.Queue()
threading.Thread(target=startup.stderr_reader, args=(process.stderr, messages), daemon=True).start()
events, tail, phases, phase_mappings = {}, [], {}, {}
try:
for phase, event in [("visible", "core_ready_frame"), ("tray", "tray_hidden"),
("restored", "tray_restored_frame")]:
startup.wait_for_metric(process, messages, events, tail, event, 45)
if "render_error" in events[event]:
raise RuntimeError(events[event])
time.sleep(2)
cpu, began = startup.read_cpu_seconds(process.pid), time.monotonic()
time.sleep(2)
resources = startup.read_proc_resources(process.pid)
resources["idle_cpu_percent"] = (startup.read_cpu_seconds(process.pid) - cpu) / (time.monotonic() - began) * 100
phases[phase] = resources
if include_mappings:
phase_mappings[phase] = startup.read_proc_mappings(
process.pid, startup.runtime_executable(process.pid)
)
process.wait(timeout=8)
startup.drain_messages(messages, events, tail)
if process.returncode:
raise RuntimeError(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}")
finally:
if process.poll() is None:
process.terminate()
process.wait(timeout=5)
result = dict(phases=phases, events=events)
if include_mappings:
result["phase_mappings"] = phase_mappings
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--binary", type=Path, required=True)
parser.add_argument("--rounds", type=int, default=3)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--screenshots", type=Path)
parser.add_argument("--include-mappings", action="store_true",
help="Include anonymized per-mapping /proc smaps counters for each phase.")
args = parser.parse_args()
if args.rounds < 1:
parser.error("rounds must be positive")
binary = args.binary.resolve()
runs = []
for index in range(args.rounds):
result = run(binary, args.screenshots / str(index + 1) if args.screenshots else None,
args.include_mappings)
runs.append(result)
print(json.dumps(result["phases"]), flush=True)
report = dict(binary=str(binary), binary_sha256=startup.file_sha256(binary),
include_mappings=args.include_mappings,
display=os.environ.get("DISPLAY"), wayland_display=os.environ.get("WAYLAND_DISPLAY"),
platform=startup.platform.platform(), screenshots=bool(args.screenshots), runs=runs,
medians={phase: {key: statistics.median(r["phases"][phase][key] for r in runs)
for key in runs[0]["phases"][phase]} for phase in runs[0]["phases"]})
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(report, indent=2) + "\n")
if __name__ == "__main__":
main()