makepad/libs/relax/compiler/tests/shutdown/run.py
Admin b22a714840 makepad run: the Rust JIT (libs/relax) and makepad-remote
The JIT that runs makepad apps from source: lexer, front end, type checker,
lazy borrow checker (Gandalf; `--check-first` opts in), codegen, session and
warm/cold caches keyed by compiler stamp, provided crates bound late,
startup images, live patching. User-facing name is `makepad run` (binary
`makepad`, MAKEPAD_RUN_* env, ~/.makepad-run); libs/relax stays the internal
library name. `makepad check` reports rustc-style diagnostics with rustc
error codes and checks a whole workspace (`--workspace`). Runs on macOS,
Linux, Windows and Android. makepad-remote: one tool schema for apps and the
WM (app/1): grabs, captures, traces, frame profiles and tweaks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

108 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Cold startup and SIGINT shutdown regression; run on the test node via x64run.
Pass --makepad, --workspace, --out and the package to run. MAKEPAD_RUN_STD and the
headless display environment come from the caller. Each launch gets a new cache.
Optional --gdb captures only this harness's child, before forcibly stopping it.
"""
import argparse
import ctypes
import json
import os
from pathlib import Path
import re
import signal
import subprocess
import sys
import tempfile
import time
def permit_debugger():
# Yama permits the named tracer and its descendants: the harness and its gdb.
libc = ctypes.CDLL(None, use_errno=True)
if libc.prctl(0x59616D61, os.getppid(), 0, 0, 0) != 0:
raise OSError(ctypes.get_errno(), "PR_SET_PTRACER")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--makepad", required=True, type=Path)
parser.add_argument("--workspace", required=True, type=Path)
parser.add_argument("--out", required=True, type=Path)
parser.add_argument("--runs", type=int, default=10)
parser.add_argument("--after-present", type=float, default=4)
parser.add_argument("--startup-timeout", type=float, default=240)
parser.add_argument("--shutdown-timeout", type=float, default=15)
parser.add_argument("--gdb", action="store_true")
parser.add_argument("package")
args = parser.parse_args()
if args.runs < 1 or min(args.startup_timeout, args.shutdown_timeout) <= 0 or args.after_present < 0:
parser.error("runs and timeouts must be positive; after-present must be nonnegative")
if args.gdb and sys.platform != "linux":
parser.error("--gdb requires Linux")
args.out.mkdir(parents=True, exist_ok=True)
out = Path(tempfile.mkdtemp(prefix="shutdown-", dir=args.out.resolve()))
results = []
for cycle in range(1, args.runs + 1):
log = out / f"{cycle}.log"
env = dict(os.environ, MAKEPAD_RUN_CACHE="on", MAKEPAD_RUN_CACHE_DIR=str(out / f"cache-{cycle}"))
with log.open("w") as stream:
child = subprocess.Popen(
[str(args.makepad.resolve()), "run", "--verbose", args.package],
cwd=args.workspace, env=env, stdout=stream, stderr=subprocess.STDOUT,
start_new_session=True, preexec_fn=permit_debugger if args.gdb else None,
)
print(f"START {cycle} pid={child.pid} log={log}", flush=True)
started = time.monotonic()
present = None
shutdown = None
forced = False
def backtrace(phase):
if args.gdb:
with (out / f"{cycle}-{phase}-gdb.log").open("w") as trace:
subprocess.run(
["gdb", "-q", "-batch", "-ex", "set pagination off",
"-ex", "thread apply all bt", "-ex", "detach", "-p", str(child.pid)],
stdout=trace, stderr=subprocess.STDOUT, timeout=60,
)
try:
while child.poll() is None:
now = time.monotonic()
if present is None and " first frame at " in log.read_text(errors="replace"):
present = now
if (present is not None and now - present >= args.after_present) or now - started >= args.startup_timeout:
if present is None:
backtrace("startup")
stopping = time.monotonic()
os.killpg(child.pid, signal.SIGINT)
try:
child.wait(timeout=args.shutdown_timeout)
except subprocess.TimeoutExpired:
forced = True
backtrace("shutdown")
shutdown = time.monotonic() - stopping
break
time.sleep(0.1)
finally:
if child.poll() is None:
os.killpg(child.pid, signal.SIGKILL)
child.wait()
text = log.read_text(errors="replace")
first = re.search(r"first frame at ([\d.]+) s", text)
row = dict(cycle=cycle, pid=child.pid, exit=child.returncode, sigkill=forced,
first_frame_s=float(first[1]) if first else None, shutdown_s=shutdown,
panics=text.count("panicked at"), compile_errors=text.count("compile error in"), log=str(log))
results.append(row)
(out / "results.json").write_text(json.dumps(results, indent=2) + "\n")
print(json.dumps(row), flush=True)
if not first or shutdown is None or forced or child.returncode != 0 or row["compile_errors"]:
return 1
print(f"PASS: {args.runs} cold launches and SIGINT shutdowns; evidence {out}", flush=True)
return 0
if __name__ == "__main__":
sys.exit(main())