Skip to content

[JSC] JSModuleLoader::clearAll(): pin prelinked edges in one pass - #627

Open
dylan-conway wants to merge 1 commit into
mainfrom
dylan/module-loader-clear-all-linear
Open

[JSC] JSModuleLoader::clearAll(): pin prelinked edges in one pass#627
dylan-conway wants to merge 1 commit into
mainfrom
dylan/module-loader-clear-all-linear

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Sep 11, 2026

Copy link
Copy Markdown
Member

JSModuleLoader::clearAll() drops a loader's whole registry. With a prelinked module graph it first has to move the graph edges that the loader's table was holding into the records' own [[LoadedModules]], so records that are still referenced keep resolving their imports.

It did that through forgetPrelinkedRecordsWithKey(nullptr), which called pinPrelinkedEdges(i) for every registered slot; each of those calls walks the request lists of all the other registered records looking for edges into i. That is O(records × edges) for a single clearAll().

This makes the clear-everything case one pass: pin each registered record's edges to all of its registered targets while the table is still complete, then clear the slots — O(edges). The end state is the same as before: every edge between two registered records is in the importer's [[LoadedModules]], and none are added for slots that had already been removed. removeEntry(key) (records of one key) is unchanged, and pinPrelinkedEdgesOf(importer, onlyTarget) is the former local lambda; the clearing loop itself is untouched apart from skipping the per-slot pin when everything is being cleared.

Testing

Built Bun against this branch. A --compile --bytecode (with splitting) program that loads the same modules into several loaders, clears them while functions from their records are still referenced, and then keeps calling those functions (live let bindings, import * as ns, export * from re-exports, dynamic import()) prints the same output before and after this change, and a profile of repeated load/clearAll() cycles over a ~600-module graph no longer shows pinPrelinkedEdges (previously the top self-time symbol on that thread).

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this and didn't find any bugs — the new bulk-forget path produces the same set of pinned edges as the old per-slot loop, just without the O(n²) importer scan. Leaving final sign-off to a human since this is JSC runtime module-resolution bookkeeping and /Source/JavaScriptCore is CODEOWNERS-gated.

What was reviewed:

  • Verified the hoisted pinPrelinkedEdges(importer, onlyTarget) is byte-for-byte the old lambda body, with this-> access replacing the [&] capture.
  • Traced the null-key fast path: pinning runs before any slot.clear() / m_prelinkedRecordRemoved.quickSet(), so prelinkedRecordForResolution(target) sees the same populated table the old first iteration did; every edge (a→b) with both slots populated is pinned exactly once, matching the old order-dependent walk.
  • Confirmed the non-null-key branch is unchanged aside from dropping the now-dead !keyOrNullForAll disjunct.
Extended reasoning...

Overview

The PR touches two files in Source/JavaScriptCore/runtime/: JSModuleLoader.cpp and JSModuleLoader.h. It hoists a local lambda into a public overload pinPrelinkedEdges(AbstractModuleRecord*, uint32_t) and adds a dedicated fast path in forgetPrelinkedRecordsWithKey(nullptr) (the clearAll() case) that pins every importer's outgoing edges in one pass while the table is still fully populated, then clears and marks each slot. The single-key removeEntry path and forgetPrelinkedRecord(uint32_t) are semantically unchanged. All of this lives inside the USE(BUN_JSC_ADDITIONS) block.

Security risks

None identified. This is internal bookkeeping over an already-trusted in-memory module graph; there is no parsing, no untrusted input, no auth/crypto/permissions surface. The only correctness-adjacent risk would be dropping a graph edge (leaving a module unable to resolve an import after clearAll()), and I traced the old vs. new edge sets to be identical: previously each edge was pinned either when its importer left (outgoing pass) or when its target left first (incoming pass); now every populated importer pins all outgoing edges before anything is cleared, which yields exactly the same set. Slots already marked in m_prelinkedRecordRemoved before the call still return null from prelinkedRecordForResolution in both versions.

Level of scrutiny

Moderate. The diff is small (~40 lines) and mechanical, but it sits in JSC's module loader where a missed edge would surface as a hard-to-reproduce "binding resolves to undefined after registry clear" bug. /Source/JavaScriptCore is listed in .github/CODEOWNERS (@ WebKit/jsc-reviewers), and while that file is an upstream artifact in Bun's fork and explicitly says contributors don't "own" components, the guideline to defer on CODEOWNERS-covered paths applies. A reviewer familiar with the prelinked-graph invariants should confirm the equivalence argument.

Other factors

The PR description reports manual end-to-end testing (a --compile --bytecode program exercising live bindings, namespace imports, re-exports, and dynamic import across clearAll()) with identical output before/after, plus a profile showing the hot symbol gone. There's no automated test added, which is typical for this fork's performance changes. The header addition is a plain non-exported method declaration alongside the existing overload — no ABI or JS_EXPORT_PRIVATE concerns.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 71200d51-43ef-45b7-bf77-e393e40787d9

📥 Commits

Reviewing files that changed from the base of the PR and between cf1b36e and 5fe89a3.

📒 Files selected for processing (2)
  • Source/JavaScriptCore/runtime/JSModuleLoader.cpp
  • Source/JavaScriptCore/runtime/JSModuleLoader.h

Included review availability: 3 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 4 reviews per hour.


Walkthrough

JSModuleLoader adds importer-scoped prelinked edge pinning. Module-index pinning delegates to the new overload. Clearing all prelinked records pins importer edges before removing slots.

Changes

Prelinked edge pinning

Layer / File(s) Summary
Importer-scoped pinning and delegation
Source/JavaScriptCore/runtime/JSModuleLoader.h, Source/JavaScriptCore/runtime/JSModuleLoader.cpp
Adds the importer-and-target overload. The module-index overload uses it for the leaving record and affected importers.
Prelinked record clearing
Source/JavaScriptCore/runtime/JSModuleLoader.cpp
Pins every record’s edges before clearing slots. Key-filtered removal retains matching-key cleanup.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 5fe89

The updated clear-all path preserves prelinked edges before registry removal without introducing an identified merge risk.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the performance issue, implementation change, preserved behavior, and testing. It does not follow the repository template because it omits the Bugzilla bug title and link, rev… Add the associated Bugzilla URL and bug title, include the required “Reviewed by NOBODY (OOPS!).” line or actual review status, and list the changed paths and relevant functions.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: pinning prelinked edges in one pass during JSModuleLoader::clearAll().
Full details: Description check

Explanation

The description explains the performance issue, implementation change, preserved behavior, and testing. It does not follow the repository template because it omits the Bugzilla bug title and link, review status, and changed-file or function list.

  • Fix all pre-merge checks with AI

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
31c9967d autobuild-preview-pr-627-31c9967d 2026-09-11 23:56:30 UTC
5fe89a38 autobuild-preview-pr-627-5fe89a38 2026-09-11 20:56:32 UTC

forgetPrelinkedRecordsWithKey(nullptr) called pinPrelinkedEdges(i) for every
registered slot, and each call walked the request lists of all the other
registered records looking for edges into i: O(records x edges) for a registry
that is being dropped as a whole.

When every slot leaves, first pin each record's edges to all of its registered
targets, while the table is still complete, then clear the slots as before:
O(edges). The records end up with the same [[LoadedModules]] entries (every
edge between two registered records, none into slots already removed).
Removing the records of a single key is unchanged.

pinPrelinkedEdgesOf(importer, onlyTarget) is the former local lambda.
@dylan-conway
dylan-conway force-pushed the dylan/module-loader-clear-all-linear branch from 5fe89a3 to 31c9967 Compare September 11, 2026 23:27

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant