Skip to content

perry compile OOMs at ~34 GB RSS on Effect (effect@3.21.2) — codegen phase #309

Description

@proggeramlug

What happened

perry compile of a 4-line program importing Effect (a popular TypeScript framework, ~12k weekly downloads of the effect package) consumes 34 GB of RSS and 249 GB of peak memory footprint before being SIGKILL'd by the OS after ~7 minutes of wall-clock time, during the codegen phase.

Module collection and parse succeed cleanly (152 native modules collected). The compiler reaches Generating code... and then memory grows unboundedly.

What you expected

perry compile to either (a) finish, (b) finish in proportional memory to the source size (Effect is ~150 modules and ~30k LOC for Effect.ts alone — substantial but not pathological), or (c) emit a clear "compiler limit exceeded" error at a sensible memory bound rather than letting the OS kill the process.

Minimal reproduction

mkdir effect-oom && cd effect-oom
cat > package.json <<'JSON'
{
  "name": "effect-oom",
  "version": "0.0.0",
  "type": "module",
  "perry": { "compilePackages": ["effect"] }
}
JSON
bun add effect    # installs effect@3.21.2 + 3 transitive deps
// test_direct.ts
import * as Effect from "effect/Effect";

const program = Effect.succeed(42);
const result = Effect.runSync(program);
console.log("result:", result);

Command:

/usr/bin/time -l perry compile test_direct.ts -o /tmp/out

Diagnostic output

Collecting modules...
  Compile package: effect
Found 152 module(s): 152 native, 0 JavaScript
Generating code...
[ ~7 min later, OS SIGKILLs the process ]
exit=137 (SIGKILL)

/usr/bin/time -l summary on the run that crashed:

      430.45 real       384.12 user        35.67 sys
  36482269184  maximum resident set size       (~34.0 GB)
     23261581  page reclaims
        21674  page faults
            0  swaps
       442    voluntary context switches
    186928    involuntary context switches
6795075182564  instructions retired
1253129492975  cycles elapsed
267389742376  peak memory footprint            (~249 GB virtual)
time: command terminated abnormally

The compiler emits 173 Warning: unknown identifier '...' lines during HIR lowering — most of these are arguments (148×, used heavily by Effect's dual() data-first/data-last helper) and Intl (~10×, used in DateTime / Schedule modules). These are pre-existing categorical gaps (arguments is documented in docs/src/language/limitations.md); I do not believe they cause the OOM but they may be amplifying it via the per-warning lowering fallback path.

Environment

  • Perry version: 0.5.399
  • Host OS: macOS 26.4 (arm64, Apple Silicon)
  • Target: native (default — no --target flag)
  • Installed via: from source (cargo build --release against current main)
  • Effect version: 3.21.2 (latest as of filing)

Notes for triage

  • Module collection + parse phases finish promptly. Memory growth begins after Generating code.... This points the finger at HIR transforms, codegen, or a pass that runs between them. Reasonable starting suspects from CHANGELOG.md history:
    • widen_mutable_captures in crates/perry-hir/src/lower.rs — v0.5.336 noted three back-to-back walks; quadratic factor possible on a deeply-nested HIR like Effect's Channel/Stream/Fiber generic chains.
    • compute_max_local_id walks — class-scan was added for Class method body cannot capture enclosing-function locals #212 (v0.5.323) and Gap: await using (Symbol.asyncDispose / ES2024 explicit resource management) #154 (v0.5.388); similar walk over Effect's many class methods could compound.
    • The async_to_generator transform (v0.5.371) — Effect uses generators heavily for Effect.gen; this pass walks every fn body.
    • The inliner (crates/perry-transform/src/inline.rs) — Effect's dual() helper produces functions that are tiny and called from everywhere; inlining cost may be quadratic in call-site count.
  • Suggested investigation: build Perry with debug symbols (RUSTFLAGS='-g' cargo build --release), run the repro under samply or Instruments → Allocations, look for the call site holding the most live bytes when RSS is at ~10-20 GB. If a single transform pass owns most of the heap, that's the wedge.
  • Workaround for users today: none. import { Effect } from "effect" (the bare form) instead "compiles" but silently produces result: 0 because Perry's collector doesn't traverse export * as Effect from "./Effect.js" — filed separately as #ADJACENT.

Why this matters

Effect is one of the largest and most-watched TypeScript-only frameworks. A successful "perry compile your Effect app to a native binary" story is a strong lever for adoption — but at 34 GB RSS the bar isn't just "Effect works", it's "Perry works on any large TypeScript codebase." This OOM almost certainly reproduces on other large libraries too (Angular, NestJS, RxJS-heavy apps); Effect is just the largest pure-TS surface I've tested.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions