Skip to content

[JSC] Share one ScriptFetchParameters per type instead of allocating one per module request - #561

Merged
Jarred-Sumner merged 1 commit into
mainfrom
claude/fetch-parameters-singletons
Sep 8, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
claude/fetch-parameters-singletons

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

ScriptFetchParameters for the built-in types (None / JavaScript / WebAssembly / JSON / Text) carries nothing but a one-byte Type, yet every import statement's ModuleRequest, every loadModule() / requestImportModule(), and (in Bun) every typed export entry did adoptRef(*new ScriptFetchParameters(type)) and kept it for the life of the module record. A mimalloc heap profile of an idle Claude Code process (~800 modules) shows ~2.1 MB of these immortal 32-byte objects.

What

  • ScriptFetchParameters::create(Type) returns a process-wide immortal instance per built-in type (lazily created once); createUnique(Type) keeps the allocate-per-call behaviour.
  • Callers that attach per-request state keep their own object: WebCore's ModuleFetchParameters subclass constructs directly (unchanged), create(const String& hostDefinedImportType) is unchanged, and the one Bun-additions site in JSModuleLoader::requestImportModule that calls setAttributes() now always does so on a createUnique() object (it previously could only have been a fresh one anyway).
  • RefCounted → ThreadSafeRefCounted: module records in every VM (workers too) ref the shared instances.

Verification

JSTests/modules/* and the import-attributes stress tests give identical results to the parent commit when run under the bare shell (102 pass; the same 8 need the harness either way). Static + dynamic import with with { type: "json" } works. In Bun/Claude Code the ScriptFetchParameters line disappears from the idle heap profile (numbers in the Bun PR).

…one per module request

ScriptFetchParameters for the built-in types (None/JavaScript/WebAssembly/JSON/Text) carries nothing but the one-byte
type, yet every import statement's ModuleRequest, every loadModule()/requestImportModule() and every export entry that
names a module allocated its own RefCounted object and kept it for the life of the module record. In a large module
graph (Claude Code: ~800 modules) that is ~2 MB of immortal 32-byte allocations.

create(Type) now returns a process-wide immortal instance per type; createUnique(Type) keeps the old behaviour for the
callers that attach per-request state (WebCore's ModuleFetchParameters subclass constructs directly; a HostDefined type
string and the Bun import-attributes map still get their own object, and the one site that called setAttributes() on a
freshly created object now always creates a unique one). The class becomes ThreadSafeRefCounted since module records
in every VM (workers included) ref the shared instances.

@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 PR and didn't find any bugs. Because it introduces process-wide shared ScriptFetchParameters instances referenced across VMs and switches the class to ThreadSafeRefCounted, a human look at the shared-state invariants would still be worthwhile.

What was reviewed:

  • create(Type) lazy init: std::call_once + LazyNeverDestroyed is thread-safe; HostDefined (index 5) falls through to createUnique and no caller passes it here anyway.
  • Only mutation path is setAttributes() at JSModuleLoader.cpp:503; the new guard ensures shared per-type instances are never mutated (HostDefined params are always freshly allocated via create(String)).
  • Other create(Type) callers (JSModuleLoader.cpp:360/385, Completion.cpp:254) only read type(), and getRegisteredMayBeNull keys on type value not pointer identity.
  • WebCore's ModuleFetchParameters subclass has its own create() and constructs directly, so it's unaffected by the base-class factory change.
Extended reasoning...

Overview

This PR converts ScriptFetchParameters::create(Type) from a per-call allocation into a lookup of one of five process-wide immortal instances (one per built-in Type), stored in a LazyNeverDestroyed<std::array<RefPtr, 5>> initialized under std::call_once. A new createUnique(Type) preserves the old allocate-per-call behaviour. The class moves from RefCounted to ThreadSafeRefCounted so module records in multiple VMs/workers can safely ref the shared instances. The one Bun-additions mutation site (setAttributes in JSModuleLoader::importModule) is updated to allocate a fresh unique instance whenever it would otherwise mutate a shared per-type instance. A new ScriptFetchParameters.cpp translation unit is added to Sources.txt.

Security risks

None identified. There is no user-controlled input flowing into the static array index (it's an internal enum), no auth/crypto/permission code, and the ThreadSafeRefCounted change strictly increases safety over RefCounted. The shared instances carry only a one-byte type and empty default members; the only mutator (setAttributes) is guarded so it never touches a shared instance.

Level of scrutiny

Moderate. The diff is small and follows established WTF idioms, but it introduces process-global shared state in the module loader — a core runtime path exercised by every import — and relies on the invariant that no current or future caller mutates a ScriptFetchParameters obtained from create(Type). The guard in importModule uses type() != HostDefined as a proxy for "this is a shared instance", which is correct today but is an indirect check worth a maintainer's eye. /Source/JavaScriptCore is also listed in .github/CODEOWNERS (inherited from upstream), which nudges toward a human sign-off.

Other factors

I traced every ScriptFetchParameters::create( caller and the sole setAttributes call site: none read pointer identity, and the registry key is (specifier, type) by value. WebCore's ModuleFetchParameters subclass shadows create with its own factory and is unaffected. The PR author reports the JSTests module suite and import-attributes stress tests pass unchanged. Nothing blocks merging from a correctness standpoint that I can see; deferring purely because shared cross-VM state in the module loader merits a second pair of eyes.

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review 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: 64cb8cb5-9ef8-4ebf-8297-032189ea6978

📥 Commits

Reviewing files that changed from the base of the PR and between d4e7e20 and 402195b.

📒 Files selected for processing (4)
  • Source/JavaScriptCore/Sources.txt
  • Source/JavaScriptCore/runtime/JSModuleLoader.cpp
  • Source/JavaScriptCore/runtime/ScriptFetchParameters.cpp
  • Source/JavaScriptCore/runtime/ScriptFetchParameters.h

Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

Script Fetch Parameter Lifecycle

Layer / File(s) Summary
Parameter ownership and factory contract
Source/JavaScriptCore/runtime/ScriptFetchParameters.h
ScriptFetchParameters now uses thread-safe reference counting. create(Type) supports shared instances, while createUnique(Type) allocates distinct instances.
Shared factory implementation
Source/JavaScriptCore/runtime/ScriptFetchParameters.cpp, Source/JavaScriptCore/Sources.txt
The new implementation lazily initializes shared instances for built-in types and uses unique allocation for unsupported types. The source manifest includes the implementation file.
Dynamic import attribute handling
Source/JavaScriptCore/runtime/JSModuleLoader.cpp
Dynamic imports preserve existing host-defined parameters and use unique parameters before applying request attributes.

Merge Risk: ⚪ Minimal · up to 40219

Built-in script fetch parameter types now reuse shared thread-safe instances while attribute-bearing imports retain unique mutable parameters. The current change has no identified merge-blocking risk.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a clear rationale, implementation summary, scope, and verification results. It does not follow the repository template because it omits the bug title and Bugzilla URL, the rev… Add the associated Bugzilla title and URL, include the required Reviewed by NOBODY (OOPS!). line or actual reviewer, and list each changed path with the affected functions or classes. Retain the current explanation and verification detail…
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: sharing one ScriptFetchParameters instance per built-in type instead of allocating one per module request.
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.
Full details: Description check

Explanation

The description provides a clear rationale, implementation summary, scope, and verification results. It does not follow the repository template because it omits the bug title and Bugzilla URL, the reviewer line, and the required changed-file and function list.

Resolution

Add the associated Bugzilla title and URL, include the required Reviewed by NOBODY (OOPS!). line or actual reviewer, and list each changed path with the affected functions or classes. Retain the current explanation and verification details after these template sections.

  • 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 4, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
402195ba autobuild-preview-pr-561-402195ba 2026-09-04 13:24:51 UTC

@Jarred-Sumner
Jarred-Sumner merged commit 10350dd into main Sep 8, 2026
48 checks passed
robobun added a commit to oven-sh/bun that referenced this pull request Sep 8, 2026
… target's return value for the caller's realm

Pins WEBKIT_VERSION at the preview build of oven-sh/WebKit#590. A
callable returned by a wrapped function whose target is not a plain
JSFunction (a callable Proxy, or a built-in such as the realm's own
Function) was wrapped for the target's realm instead of the caller's.
The caller could read the other realm's Function constructor off the
wrapper's prototype and reach that realm's global object, in both
directions.

The range from 2e2aa2290fac also contains oven-sh/WebKit#561, #566 and
#568, already merged on oven-sh/WebKit main.
This was referenced Sep 8, 2026
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