Skip to content

perf: #6559 new Function interpreter re-parses+tree-walks every call → ~40s TUI-init stall (TypeBox/pi; fastify/kimi next) #6693

Description

@proggeramlug

Wall of the pi TUI bring-up (gate 3, tracker #6564) — and a strategic one, because it's a PERFORMANCE cliff in the #6559 new Function interpreter, not a correctness bug, and it will hit every new-Function-heavy target (pi via TypeBox now; kimi via fastify's ajv + fast-json-stringify + find-my-way next).

Symptom

pi-native (bare, interactive TUI, under a PTY, fd pre-placed so the download wall # is skipped) produces ZERO output and pegs one core at 100% for ~30–40 seconds, where node pi-bundle.mjs renders the onboarding screen (~23 KB) in under a second. It is NOT an infinite loop — after the grind perry emits terminal-setup escapes and then a separate uncaught TypeError: Cannot read properties of undefined (reading 'get') (a downstream correctness wall to be filed separately once reachable in reasonable time).

Root cause (sampled, debug-symbol build)

sample of the hung process: main thread 100% busy (not blocked in a syscall). Hottest native frames:

  • swc_ecma_parser::parser::expr::…490 samples (perry's runtime parser)
  • perry_runtime::object::field_get_set::get_field_by_name… — ~700 samples
  • js_native_call_method, js_object_has_property, js_object_define_property, prototype/descriptor helpers — hundreds.

swc_ecma_parser runs at RUNTIME only via the #6559 dyn_eval path (new Function/eval). The bundle has 761 TypeBox references; TypeBox's TypeCompiler emits validators through new Function(generatedSource). So pi's TUI init compiles many TypeBox validators, and perry re-parses the generated source with SWC and tree-walks it on every call — vs node compiling/JITting once. The interpreter is correct (that's #6559) but orders of magnitude slower, and TUI init does enough of it to look like a hang.

Why it matters / decision

The #6559 interpreter was shipped as "necessary but not fast." This is the first workload where the speed is the blocker, and it's the SAME mechanism kimi's fastify stack needs. Options, roughly ascending:

  1. Cache the parsed AST / prepared interpreter form keyed by the source string, so repeated new Function(sameSource) skips re-parsing. Cheap; helps if sources repeat (measure TypeBox's call pattern — many identical vs many distinct).
  2. Cache + optimize the tree-walk (precompile the AST to a faster internal form; intern property keys — note the huge get_field_by_name/descriptor cost suggests the interpreted property accesses are also a hotspot, not just parsing).
  3. Actually compile new Function bodies to native (perry already has the full compiler; wire runtime-generated source through it and cache the produced code) — matches node's JIT, biggest lift, the real fix. Removes the interpreter from the hot path entirely.

Recommendation: measure the call pattern first (how many new Function calls, how many distinct sources, time per call), then (1)+(2) as the quick win and (3) as the durable answer — (3) also retires the interpreter's correctness tail.

Repro: HOME=<clean+fd> ./pi-native under scratchpad/pty_cap.py; debug binary pi-target/dist/pi-native-dbg; sample saved. Blocks gate 3 (usable TUI) for pi and is on kimi's critical path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime or compile-time performance

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions