Repository navigation
compile: mid-size record literals (~10k–24k nodes) take minutes and GBs on the ordinary lowering path (records-3200: 342 s / 1.9 GB) — the cliff just below #10161's compact cutoff #10173
Description
Activity
- addedbugConfirmed defect or regressionConfirmed defect or regressionperformanceRuntime, compile-time, build-size, or memory performanceRuntime, compile-time, build-size, or memory performance
on Sep 13, 2026 Handoff notes (everything needed to start cold)
Reproduction — it already exists in-tree
benchmarks/large_json_literals/(added by #10161's PR) generates exactly the shape this issue measures, and its README documents the protocol:python3 benchmarks/large_json_literals/generate.py /tmp/literal-probes cd /tmp/literal-probes && printf '{}\n' > perry.json # don't inherit defines from a parent dir export PERRY_RUNTIME_DIR=/path/to/pinned/compiler-and-archives PERRY_BIN=$PERRY_RUNTIME_DIR/perry # the cliff case from the table above (~22k nodes): /usr/bin/time -v timeout 600 env PERRY_CODEGEN_PROGRESS=all "$PERRY_BIN" compile records-3200.ts \ --no-link --no-auto-optimize --output records-3200.o --cache-dir cache-records-3200 # controls: records-400 (fast), records-4800 (475 s), records-6400 (compact path, 0.94 s)
hot.ts/records.tsin the same directory are the runtime arms: compare the printed checksum plus mediantable_ms/recs_msover five alternating runs. That is the "no runtime regression" check in the acceptance list — the record arm must keep literal-shape property-read speed, which is why #10161 deliberately did not route mid-size records throughJSON.parse.Code map
crates/perry-hir/src/lower/lower_expr/json_literal.rs— the compact path and its cutoffs (flat primitive arrays 1,024 nodes / 64 KiB; records and nested arrays 24,576 nodes / 1 MiB). Tests injson_literal/tests.rs.crates/perry-hir/src/lower/lower_expr.rs— the hook that routes a literal into that path.crates/perry/tests/issue_10151_large_json_define.rs— timed define/semantics/cache regressions.docs/src/cli/flags.md— the documented knobs.- The ordinary path is what runs below those cutoffs: the synthetic
AnonShape_<hash>_constructorplus the module's__init_body. Both appear by name in perry's own compile notices, which makes per-module attribution easy (below).
Measurement recipes
- Instructions after IR optimization (the numbers in the table): perry prints them itself.
grep "instructions after IR optimization" <compile log>gives<function> has <n> instructions …per offending function. - Fast-emit fallback — an unexplored lead. The same notices end with: "shrinking this function is what lifts the whole unit back. Override with
PERRY_LL_FAST_EMIT_MAX_INSTRS=<n>(raise) or=0(disable)." So one oversized function drops its whole codegen unit off the fast emission path. Worth measuring whether the minutes are spent there rather than in RS4GC. - Per-module
.text: build withPERRY_DEBUG_SYMBOLS=1, then size the module's symbols by its mangled prefix (nm --size-sort <binary> | grep <module_prefix>); the prefix is the path-derived name that appears in the notices. - IR dumps:
PERRY_SAVE_LL=<dir>writes per-unit IR (it takes a directory, not1).
OpenCode modules that compile through this path today
Measure these standalone with
--no-linkinstead of building the whole app (a full OpenCode compile is ~2 h and ~25 GB):
html-entities/lib/named_references.js(twoAnonShape_…_constructorat 223,112 and 151,112 pre-RS4GC, expanding to 5.2 M and 2.4 M),mime/types/other.ts(522,756 after IR optimization;mime/types/standard.ts144,416),entities/lib/esm/generated/encode_html.js(94,108 → 4,568,704),@octokit/plugin-rest-endpoint-methods generated/endpoints.js(328,913 / 364,146),@agentclientprotocol/sdk schema.zod.gen.js(307,564),@babel/helpers lib/helpers-generated.js(271,252). A v1.18.30 checkout withnode_modulessits at/root/claude-opencode/opencodeon the build host (read-only for lanes).Why this matters beyond compile time
In today's OpenCode build perry reports 38 oversized modules that must run exclusively to cap peak memory. They serialize the tail of every compile: at 3 module jobs the run averages ~52 modules/min overall, and these are the ones that cannot overlap. They are also a large share of the 1,048 MiB of
.textin the binary.Host rules
Build and measure on the Linux host, never on a laptop: per-lane
CARGO_TARGET_DIR, own--cache-dir, and checkpgrep -f "perry compile src/index[.]ts"is empty before anything large. A memory watchdog now runs there (/root/claude-opencode/perry-mem-guard.sh) and kills any perry compile above 36 GB RSS — two OOM kills took the box (and its sshd) down today. Do not start a full OpenCode compile without coordinating; the standalone--no-linkmodule measurements above are enough for this work.Ready-for-review PR: #10273
Includes phase attribution, the fast-emission control, three standalone OpenCode module size/instruction comparisons, five alternating runtime runs per arm, and regression coverage.
records-3200: 95.49 s → 0.63 s; peak RSS 1,440 → 238 MiB;.text6,297,159 → 6,017 bytes. Required Linux tests/checks pass; hosted CI is running.
Follow-up to #10151 / PR #10161 (landed in train #10172). Parent: #10107 (OpenCode); relevant to the code-size campaign (
secret-tests/PROMPT_CODE_SIZE_PER_FUNCTION.md).Problem
Ordinary lowering of a mid-size record literal still hits a compile-time cliff below the compact-path cutoff that #10161 set (24,576 nodes / 1 MiB for records). Measured by the merge-queue re-audit of #10161 on macOS and by the lane on Linux, with the audit's
{ id, name, tags, w }record-array shape:So any literal table between roughly 10k and 24k nodes costs minutes and gigabytes to compile. OpenCode's graph carries several such tables that compile through this path today:
html-entities/lib/named_references.js(twoAnonShape_…_constructorfunctions at 223,112 and 151,112 pre-RS4GC instructions, RS4GC-expanded to 5.2 M and 2.4 M),mime/types/other.ts(522,756 instructions after IR optimization),entities/lib/esm/generated/encode_html.jsinit_body(94,108 → 4,568,704),@octokit/plugin-rest-endpoint-methods generated/endpoints.jsinit_body(328,913 / 364,146),@agentclientprotocol/sdk schema.zod.gen.jsinit_body(307,564). These are also a large share of the 1,048 MiB of.textin the OpenCode binary.What to find out
AnonShapeconstructor, LLVM on a single huge straight-line function, RS4GC relocation explosion, or the shadow-frame retry).JSON.parse) without the minutes-long compile and the multi-MB functions.Acceptance
.textper module before/after) and no runtime regression on the benchmark suite.