js-on-sx: harness cache — precompute HARNESS_STUB SX once per run
Root cause: every sx_server worker session used js-eval on the 3.6KB HARNESS_STUB, paying ~15s for tokenize+parse+transpile even though every session does the same thing. Over a full scoreboard with periodic worker restarts that's minutes of wasted work. Fix: transpile once per Python process. Spin up a throwaway sx_server, run (inspect (js-transpile (js-parse (js-tokenize HARNESS_STUB)))), write the resulting SX source to lib/js/.harness-cache/stub.<fingerprint>.sx and a stable-name symlink-ish copy stub.sx. Every worker session then does a single (load .harness-cache/stub.sx) instead of re-running js-eval. Fingerprint: sha256(HARNESS_STUB + lexer.sx + parser.sx + transpile.sx). Transpiler edits invalidate the cache automatically. Runs back-to-back reuse the cache — only the first run after a transpiler change pays the ~15s precompute. Transpile had to gain a $-to-_js_dollar_ name-mangler: the SX tokenizer rejects $ in identifiers, which broke round-tripping via inspect. JS $DONOTEVALUATE → SX _js_dollar_DONOTEVALUATE. Internal JS-on-SX names are unaffected (none contain $). Measured: 300-test wide (Math+Number+String @ 100/cat, --per-test-timeout 5): 593.7s → 288.0s, 2.06x speedup. Scoreboard 114→115/300 (38.3%, noise band). Math 40%, Number 44%, String 30% — same shape as prior. Baselines: 520/522 unit, 148/148 slice — unchanged.
This commit is contained in:
@@ -55,6 +55,10 @@ HARNESS_DIR = UPSTREAM / "harness"
|
||||
DEFAULT_PER_TEST_TIMEOUT_S = 5.0
|
||||
DEFAULT_BATCH_TIMEOUT_S = 120
|
||||
|
||||
# Cache dir for precomputed SX source of harness JS (one file per Python run).
|
||||
# Written once in main(), read via (load ...) by every worker session.
|
||||
HARNESS_CACHE_DIR = REPO / "lib" / "js" / ".harness-cache"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Harness stub — replaces assert.js + sta.js with something our parser handles.
|
||||
@@ -588,6 +592,175 @@ def load_test(path: Path):
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Harness cache — transpile HARNESS_STUB once, write SX to disk.
|
||||
# Every worker then loads the cached .sx (a few ms) instead of re-running
|
||||
# js-tokenize + js-parse + js-transpile (15+ s).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Remembered across the Python process. None until we've run the precompute.
|
||||
_HARNESS_CACHE_PATH: "Path | None" = None
|
||||
# Per-filename include cache: maps 'compareArray.js' -> Path of cached .sx.
|
||||
_EXTRA_HARNESS_CACHE: dict = {}
|
||||
|
||||
|
||||
def _harness_cache_rel_path() -> "str | None":
|
||||
if _HARNESS_CACHE_PATH is None:
|
||||
return None
|
||||
try:
|
||||
return _HARNESS_CACHE_PATH.relative_to(REPO).as_posix()
|
||||
except ValueError:
|
||||
return str(_HARNESS_CACHE_PATH)
|
||||
|
||||
|
||||
def _precompute_sx(js_source: str, timeout_s: float = 120.0) -> str:
|
||||
"""Run one throwaway sx_server to turn a chunk of JS into the SX text that
|
||||
js-eval would have evaluated. Returns the raw SX source (no outer quotes).
|
||||
"""
|
||||
proc = subprocess.Popen(
|
||||
[str(SX_SERVER)],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
cwd=str(REPO),
|
||||
bufsize=0,
|
||||
)
|
||||
fd = proc.stdout.fileno()
|
||||
os.set_blocking(fd, False)
|
||||
|
||||
buf = [b""]
|
||||
|
||||
def readline(timeout: float):
|
||||
deadline = time.monotonic() + timeout
|
||||
while True:
|
||||
nl = buf[0].find(b"\n")
|
||||
if nl >= 0:
|
||||
line = buf[0][: nl + 1]
|
||||
buf[0] = buf[0][nl + 1 :]
|
||||
return line
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError("precompute readline timeout")
|
||||
rlist, _, _ = select.select([fd], [], [], remaining)
|
||||
if not rlist:
|
||||
raise TimeoutError("precompute readline timeout")
|
||||
try:
|
||||
chunk = os.read(fd, 65536)
|
||||
except (BlockingIOError, InterruptedError):
|
||||
continue
|
||||
if not chunk:
|
||||
return None
|
||||
buf[0] += chunk
|
||||
|
||||
def run(epoch: int, cmd: str, to: float = 60.0):
|
||||
proc.stdin.write(f"(epoch {epoch})\n{cmd}\n".encode("utf-8"))
|
||||
proc.stdin.flush()
|
||||
deadline = time.monotonic() + to
|
||||
while time.monotonic() < deadline:
|
||||
line = readline(deadline - time.monotonic())
|
||||
if line is None:
|
||||
raise RuntimeError("precompute: sx_server closed stdout")
|
||||
m = RX_OK_INLINE.match(line.decode("utf-8", "replace"))
|
||||
if m and int(m.group(1)) == epoch:
|
||||
return "ok", m.group(2)
|
||||
m = RX_OK_LEN.match(line.decode("utf-8", "replace"))
|
||||
if m and int(m.group(1)) == epoch:
|
||||
val = readline(deadline - time.monotonic())
|
||||
return "ok", (val or b"").decode("utf-8", "replace").rstrip("\n")
|
||||
m = RX_ERR.match(line.decode("utf-8", "replace"))
|
||||
if m and int(m.group(1)) == epoch:
|
||||
return "error", m.group(2)
|
||||
raise TimeoutError(f"precompute epoch {epoch}")
|
||||
|
||||
try:
|
||||
# Wait for ready
|
||||
deadline = time.monotonic() + 15.0
|
||||
while time.monotonic() < deadline:
|
||||
line = readline(deadline - time.monotonic())
|
||||
if line is None:
|
||||
raise RuntimeError("precompute: sx_server closed before ready")
|
||||
if b"(ready)" in line:
|
||||
break
|
||||
# Load JS kernel
|
||||
run(1, '(load "lib/r7rs.sx")')
|
||||
run(2, '(load "lib/js/lexer.sx")')
|
||||
run(3, '(load "lib/js/parser.sx")')
|
||||
run(4, '(load "lib/js/transpile.sx")')
|
||||
# Transpile to SX source via inspect
|
||||
inner = js_source.replace("\\", "\\\\").replace('"', '\\"')
|
||||
inner = inner.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
|
||||
outer = inner.replace("\\", "\\\\").replace('"', '\\"')
|
||||
cmd = f'(eval "(inspect (js-transpile (js-parse (js-tokenize \\"{outer}\\"))))")'
|
||||
kind, payload = run(5, cmd, timeout_s)
|
||||
if kind != "ok":
|
||||
raise RuntimeError(f"precompute error: {payload[:200]}")
|
||||
# payload is an SX string-literal — peel one layer of quoting.
|
||||
import json as _json
|
||||
if payload.startswith('"') and payload.endswith('"'):
|
||||
return _json.loads(payload)
|
||||
return payload
|
||||
finally:
|
||||
try:
|
||||
proc.stdin.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=3)
|
||||
except Exception:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _harness_fingerprint() -> str:
|
||||
import hashlib
|
||||
# Include the exact runtime/transpile source hash so a change to the
|
||||
# transpiler invalidates the cache automatically.
|
||||
h = hashlib.sha256()
|
||||
h.update(HARNESS_STUB.encode("utf-8"))
|
||||
for p in ("lib/js/lexer.sx", "lib/js/parser.sx", "lib/js/transpile.sx"):
|
||||
try:
|
||||
h.update((REPO / p).read_bytes())
|
||||
except Exception:
|
||||
pass
|
||||
return h.hexdigest()[:16]
|
||||
|
||||
|
||||
def precompute_harness_cache() -> Path:
|
||||
"""Populate _HARNESS_CACHE_PATH by transpiling HARNESS_STUB once and
|
||||
writing it to disk. Every worker session then does (load <path>) instead.
|
||||
|
||||
Reuses a prior cache file from a previous `python3 test262-runner.py`
|
||||
run when the fingerprint (harness text + transpiler source hash) still
|
||||
matches — that covers the common case of re-running scoreboards back-to-back
|
||||
without touching transpile.sx.
|
||||
"""
|
||||
global _HARNESS_CACHE_PATH
|
||||
HARNESS_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
fp = _harness_fingerprint()
|
||||
dst = HARNESS_CACHE_DIR / f"stub.{fp}.sx"
|
||||
stable = HARNESS_CACHE_DIR / "stub.sx"
|
||||
if dst.exists() and dst.stat().st_size > 0:
|
||||
# Expose both the canonical and fingerprinted names — sessions load
|
||||
# the canonical one.
|
||||
stable.write_bytes(dst.read_bytes())
|
||||
_HARNESS_CACHE_PATH = stable
|
||||
print(f"harness cache: reused {dst.name} ({dst.stat().st_size} bytes)",
|
||||
file=sys.stderr)
|
||||
return stable
|
||||
t0 = time.monotonic()
|
||||
sx = _precompute_sx(HARNESS_STUB)
|
||||
dst.write_text(sx, encoding="utf-8")
|
||||
stable.write_text(sx, encoding="utf-8")
|
||||
_HARNESS_CACHE_PATH = stable
|
||||
dt = time.monotonic() - t0
|
||||
print(f"harness cache: {len(HARNESS_STUB)} JS chars → {len(sx)} SX chars "
|
||||
f"at {stable.relative_to(REPO)} (fp={fp}, {dt:.2f}s)", file=sys.stderr)
|
||||
return stable
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Long-lived server session
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -625,13 +798,18 @@ class ServerSession:
|
||||
self._run_and_collect(3, '(load "lib/js/parser.sx")', timeout=60.0)
|
||||
self._run_and_collect(4, '(load "lib/js/transpile.sx")', timeout=60.0)
|
||||
self._run_and_collect(5, '(load "lib/js/runtime.sx")', timeout=60.0)
|
||||
# Preload the stub harness as one big js-eval
|
||||
stub_escaped = sx_escape_for_nested_eval(HARNESS_STUB)
|
||||
self._run_and_collect(
|
||||
6,
|
||||
f'(eval "(js-eval \\"{stub_escaped}\\")")',
|
||||
timeout=60.0,
|
||||
)
|
||||
# Preload the stub harness — use precomputed SX cache when available
|
||||
# (huge win: ~15s js-eval HARNESS_STUB → ~0s load precomputed .sx).
|
||||
cache_rel = _harness_cache_rel_path()
|
||||
if cache_rel is not None:
|
||||
self._run_and_collect(6, f'(load "{cache_rel}")', timeout=60.0)
|
||||
else:
|
||||
stub_escaped = sx_escape_for_nested_eval(HARNESS_STUB)
|
||||
self._run_and_collect(
|
||||
6,
|
||||
f'(eval "(js-eval \\"{stub_escaped}\\")")',
|
||||
timeout=60.0,
|
||||
)
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.proc is not None:
|
||||
@@ -964,6 +1142,14 @@ def main(argv):
|
||||
all_paths = all_paths[: args.limit]
|
||||
print(f"Discovered {len(all_paths)} test files.", file=sys.stderr)
|
||||
|
||||
# Precompute harness cache once per run. Workers (forked) inherit module
|
||||
# globals, so the cache path is visible to every session.start() call.
|
||||
try:
|
||||
precompute_harness_cache()
|
||||
except Exception as e:
|
||||
print(f"harness cache precompute failed ({e}); falling back to js-eval per session",
|
||||
file=sys.stderr)
|
||||
|
||||
tests = []
|
||||
results = []
|
||||
per_cat_count = defaultdict(int)
|
||||
|
||||
Reference in New Issue
Block a user