makepad/libs/relax/compiler/tests/exit_persist/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

159 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""The exit never waits, and persistence does not depend on it: run.py /absolute/path/to/makepad.
1. kill: a program idles after its start; once the run has written its cache in the background
(the mid-run write, MAKEPAD_RUN_CACHE_TRACE) it is SIGKILLed -- no exit work at all. The next run of
the same program is warm: its code comes from the cache and its program from the collected
store (written in the background too, not only by the exit).
2. busy: a thread of the program keeps calling functions that were never compiled (each call a
lazy compile) while main exits. The process ends within a bounded time with code 0 (no wait
for that thread, its compiles or the background compilers), and the next run is warm.
Each run owns a process group; it is stopped by group and checked empty."""
import atexit
import os
from pathlib import Path
import re
import signal
import subprocess as sp
import sys
import tempfile
import time
binary = str(Path(sys.argv[1]).resolve())
std = str(Path(__file__).resolve().parents[2] / 'std')
owned = []
def stop(p):
if p.poll() is None:
os.killpg(p.pid, signal.SIGTERM)
try:
p.wait(timeout=10)
except sp.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL)
p.wait()
try:
os.killpg(p.pid, signal.SIGKILL)
except ProcessLookupError:
pass
for _ in range(100):
rows = sp.check_output(['ps', '-eo', 'pid,pgid,stat'], text=True)
if not [r for r in rows.splitlines()[1:] if r.split()[1] == str(p.pid) and not r.split()[2].startswith('Z')]:
return
time.sleep(.05)
raise AssertionError('process group left: %d' % p.pid)
def cleanup():
for p in reversed(owned):
stop(p)
atexit.register(cleanup)
for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
signal.signal(sig, lambda s, f: sys.exit(128 + s))
# `step::<N>`: one more never-compiled instance per call (the busy thread's lazy compiles)
N = 4000
calls = '\n'.join(f' acc = acc.wrapping_add(step::<{n}>(acc));' for n in range(N))
source = '''
#[inline(never)]
fn step<const K: u64>(x: u64) -> u64 { x.rotate_left((K % 63) as u32 + 1) ^ K.wrapping_mul(0x9e3779b97f4a7c15) }
#[inline(never)]
fn work(seed: u64) -> u64 {
let mut acc = seed;
for i in 0..1000u64 { acc = acc.wrapping_mul(6364136223846793005).wrapping_add(i); }
acc
}
fn busy() -> u64 {
let mut acc = 1u64;
CALLS
acc
}
fn main() {
let mode = std::env::args().nth(1).unwrap_or_default();
println!("ready {}", work(3));
match mode.as_str() {
"idle" => loop { std::thread::sleep(std::time::Duration::from_millis(100)); },
"busy" => {
std::thread::spawn(|| loop { std::hint::black_box(busy()); });
std::thread::sleep(std::time::Duration::from_millis(1500));
println!("main exits");
std::process::exit(0);
}
_ => println!("quick {}", work(5)),
}
}
'''.replace('CALLS', calls)
with tempfile.TemporaryDirectory(prefix='relax-exit-persist-') as tmp:
root = Path(tmp)
(root / 'src').mkdir()
(root / 'Cargo.toml').write_text('[package]\nname="exit-persist"\nversion="0.0.0"\nedition="2021"\n[workspace]\n')
(root / 'src/main.rs').write_text(source)
def launch(tag, mode, cache):
env = dict(os.environ, CARGO_INCREMENTAL='0', CARGO_NET_OFFLINE='true', MAKEPAD_RUN_STD=std, MAKEPAD_RUN_CACHE='on',
MAKEPAD_RUN_EXIT_TRACE='1', MAKEPAD_RUN_CACHE_TRACE=str(root / (tag + '.cache.tsv')),
MAKEPAD_RUN_CACHE_DIR=str(root / cache), MAKEPAD_RUN_TARGET_DIR=str(root / ('target-' + cache)))
log = (root / (tag + '.log')).open('w')
p = sp.Popen([binary, 'run', '--verbose', 'exit-persist', '--', mode], cwd=root, env=env, stdout=log, stderr=sp.STDOUT,
start_new_session=True)
owned.append(p)
return p
def text(tag):
return (root / (tag + '.log')).read_text(errors='replace')
def warm(tag):
p = launch(tag, 'quick', tag.split('-')[0])
try:
p.wait(timeout=300)
finally:
stop(p)
t = text(tag)
assert p.returncode == 0 and 'quick' in t, t[-3000:]
m = re.search(r'makepad-run cache: (\d+) hits, (\d+) misses', t)
assert m, t[-3000:]
hits, misses = int(m.group(1)), int(m.group(2))
# warm: the code came from the cache -- linked one by one (hits) or with the startup image
# (prelinked: nothing left to compile)
compiled = re.search(r'compiled (\d+) fns', t)
assert (hits > 0 and hits > misses) or (compiled and int(compiled.group(1)) <= 5), (tag, hits, misses, compiled and compiled.group(0), t[-3000:])
# a live session (the default: edits patch in place) uses no collected program store
return hits, misses, 'makepad-run collected: restored' in t or 'makepad live: watching' in t
# 1. killed after the background write: the next run is warm
p = launch('kill-cold', 'idle', 'kill')
t0 = time.monotonic()
while 'mid-run write' not in text('kill-cold'):
assert p.poll() is None, text('kill-cold')[-3000:]
assert time.monotonic() - t0 < 300, 'no mid-run write: ' + text('kill-cold')[-3000:]
time.sleep(.1)
time.sleep(.5) # the write's own thread finishes its files (append is atomic per store)
os.killpg(p.pid, signal.SIGKILL)
p.wait()
stop(p)
kill = warm('kill-warm')
# the collected program too: written in the background, not only at the exit
assert kill[2], 'the run after a kill did not restore the collected program: ' + text('kill-warm')[-3000:]
print('exit_persist: killed run -> warm run: %d hits, %d misses, collected program restored (or a live session: none used)' % kill[:2])
# 2. main exits while a thread compiles: bounded, code 0, and the next run is warm
p = launch('busy-cold', 'busy', 'busy')
t0 = time.monotonic()
while 'main exits' not in text('busy-cold'):
assert p.poll() is None, text('busy-cold')[-3000:]
assert time.monotonic() - t0 < 300, text('busy-cold')[-3000:]
time.sleep(.01)
t1 = time.monotonic()
try:
p.wait(timeout=20)
except sp.TimeoutExpired:
raise AssertionError('the process did not exit within 20 s of main exiting: ' + text('busy-cold')[-3000:])
gone = (time.monotonic() - t1) * 1e3
stop(p)
assert p.returncode == 0, (p.returncode, text('busy-cold')[-3000:])
busy = warm('busy-warm')
print('exit_persist: busy exit -> gone in %.0f ms, code 0; warm run: %d hits, %d misses' % ((gone,) + busy[:2]))
print('exit_persist: ok')