perf: closes #753 — Eager/Deferred init split for dynamic import()#768
Merged
Conversation
1accf75 to
34e2c80
Compare
…mic 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.
dd0e81f to
603e507
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
import()).<prefix>__initfires lazily from eachExpr::DynamicImportdispatch site.<prefix>__initbecomes an idempotent guard wrapper around<prefix>__init_body, with transitive dep init so a Deferred → re-export → Deferred chain still populates its namespace correctly.Closes #753.
Benchmark
bench_heavy.tsbuilds a 1M-entry int array at top level;bench_main.tsdynamically imports it only whenprocess.argv[2] === 'use'.hyperfine -N -w 5 -r 30:use(deferred loaded)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
usebranch is statistically indistinguishable (heavy init still runs, just lazily with one extra call indirection).Test plan
node --experimental-strip-types:literal,ternary,template,reexport,tla,cycle,init_time.test_gap_dynamic_import_deferred.ts— marker module's top-levelconsole.logfires only on the dispatch branch (argv[2]=1), suppressed on the no-arg path. Byte-equal to Node on both branches.cargo test --release --workspacegreen (excluding cross-host UI crates).dynamic_import_cycle_a.tsnow compiles standalone (entry no-op__initstub satisfies cycle_b's dispatch-site reference)..o.Version bump +
CHANGELOG.mdentry will land in a follow-up commit on this branch once CI is green.