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:
2026-04-24 11:20:55 +00:00
parent f14a257533
commit 4a277941b6
5 changed files with 298 additions and 81 deletions

View File

@@ -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)