Commit 1accf75
committed
perf: closes #753 — skip eager init for modules reached only via dynamic import()
Follow-up to #100/#752. The dynamic-import resolver registered every
target as a regular Import with `is_dynamic: true` and put it in the
eager init chain at program start — functionally correct, but a heavy
locale bundle or optional feature module was paid upfront even when no
dispatch site ever fired.
Reachability classification (compile.rs): fixed-point pass starting from
the entry, propagating Eager across non-type-only static imports and
re-export sources. Everything unmarked is Deferred. Writes the result
to each Module::init_kind (HIR field from #100, no codegen consulted
it before).
Codegen (codegen.rs + expr.rs):
- Entry main filters `deferred_module_prefixes` out of the eager init
call sequence — Deferred modules only fire from dispatch sites.
- Each non-entry module gains a 3-block wrapper `<prefix>__init` (load
`@__perry_init_done_<prefix>`, icmp ne 0, cond_br to ret-or-do; do
block stores 1, calls dep wrappers transitively, then calls
`<prefix>__init_body`). Existing body code keeps every semantic;
rename to `_body` is invisible to other code paths.
- `Expr::DynamicImport` (single + multi-path arms) calls
`<target>__init` before loading `@__perry_ns_<target_prefix>`. For
Eager targets the guard short-circuits; for Deferred targets it's
the only invocation that builds the namespace.
- Entry emits a no-op `<entry_prefix>__init` stub so a non-entry
module dispatching `await import("./entry.ts")` resolves at link.
The entry's actual body still runs in main; the stub just satisfies
the dispatch's unconditional init call.
Module init deps (per-module: static-import + re-export sources) are
plumbed to the wrapper's do block so a Deferred module that reaches
another Deferred only through its own re-export chain still initializes
the source before its namespace populator runs.
Cache key hashes deferred_module_prefixes (sorted) and module_init_deps
(ordered) so a program that gains or loses a dynamic-import reachability
path invalidates the entry's cached .o.
Acceptance:
- All 7 dynamic-import gap tests from #100 (literal, ternary, template,
reexport, tla, cycle, init_time) byte-equal `node --experimental-strip-types`.
- New test_gap_dynamic_import_deferred.ts covers the Deferred-only case:
marker module's top-level console.log fires only on the dispatch
branch, never on the no-arg path.
- cargo test --workspace green (excluding cross-host UI crates).
Benchmark (heavy.ts builds a 1M-entry int array at top level,
main.ts dynamically imports it only when argv[2] === 'use'):
| | PRE-#753 | POST-#753 |
|--------------|-----------------------|-----------------------|
| no-arg | 8.4 ms ± 1.6 (min 7.4) | 4.8 ms ± 2.2 (min 3.1) |
| use | 7.6 ms ± 0.4 (min 7.1) | 8.4 ms ± 0.7 (min 7.5) |
hyperfine -N -w 5 -r 30. No-arg branch drops by 43% on mean / 58% on
min — that 4 ms is the for-loop that no longer runs at startup. The
`use` branch is statistically indistinguishable; the heavy init still
runs, just lazily, with one extra call indirection.1 parent 8a7ea99 commit 1accf75
10 files changed
Lines changed: 433 additions & 73 deletions
File tree
- crates
- perry-codegen/src
- perry/src/commands
- compile
- test-files
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
5 | 14 | | |
6 | 15 | | |
7 | 16 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
0 commit comments