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>
108 lines
4.9 KiB
Python
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())
|