Skip to content

share one slow path for creating iso subspaces - #39503

Closed
alii wants to merge 3 commits into
mainfrom
ali/size-subspace-slow-path
Closed

alii wants to merge 3 commits into
mainfrom
ali/size-subspace-slow-path

Conversation

@alii

@alii alii commented Aug 18, 2026 •

Copy link
Copy Markdown
Member

Problem

  • WebCore::subspaceForImpl in src/jsc/bindings/BunClientData.h is ALWAYS_INLINE, so its IsoSubspace creation path (take the heap lock, pick the HeapCellType, construct the IsoSubspace, register it for output constraints, construct the per-VM GCClient::IsoSubspace, store both) is inlined into every subspaceFor<T> in the binary.
  • That path is about 640 bytes of code and runs once per (VM, type). In the linux-x64 binary 249 Bun-side functions carry a copy of it; the copies differ only in two member offsets, the cell type, sizeof(T) and two constants.

Fix

  • The creation path is now one NEVER_INLINE function, subspaceForImplSlow in BunClientData.cpp, taking those per-type values as arguments. The slot to fill is passed as a pointer to member of DOMClientIsoSubspaces / DOMIsoSubspaces, replacing the four getter and setter lambdas each caller used to pass.

  • subspaceForImpl keeps only the per-allocation fast path inline: load the client data, load the slot, return it if set, otherwise call the slow path. The static_assert on destructibility and the visitOutputConstraints check still happen per type at compile time, in the inline part.

  • Same behavior as before: same subspace per type, same lock, same two slot writes. The IsoSubspace name stays "T", which is what ISO_SUBSPACE_INIT(heap, cellType, T) stringized when T was a template parameter.

  • Every caller is updated (129 files: the hand-written bindings plus the generate-classes.ts and generate-jssink.ts templates); a grep for the old lambda form finds nothing left in src/. The implementing-jsc-classes-cpp skill snippet that showed the old call form is updated too.

  • Stripped binary size, this PR vs its base commit (main build of dc59d3e), from CI's binary-size step:

    target delta
    bun-darwin-aarch64 -177.6 KB
    bun-darwin-x64 -224.1 KB
    bun-linux-aarch64 -128.0 KB
    bun-linux-x64 -160.0 KB
    bun-linux-aarch64-musl -128.0 KB
    bun-linux-x64-musl -160.0 KB
    bun-linux-aarch64-android -64.0 KB
    bun-linux-x64-android -96.0 KB
    bun-freebsd-x64 -112.0 KB
    bun-freebsd-aarch64 -96.0 KB
    bun-windows-x64 -240.0 KB
    bun-windows-aarch64 -77.0 KB
  • Test: test/internal/source-lints/iso-subspace-creation.test.ts greps src/**/*.{h,cpp} and src/codegen/**/*.ts for anything constructing an IsoSubspace (ISO_SUBSPACE_INIT, makeUnique<IsoSubspace>, new IsoSubspace) outside BunClientData.cpp. It fails on the base commit (the four inlined constructions in BunClientData.h) and passes here, and keeps a future class from growing its own copy of the creation path. source-lints.yml now also triggers on the C++ and codegen files it reads.

  • Verified on a debug build: workers, node:vm, Headers, streams, mock functions, bun:sqlite, node:sqlite, node:crypto (hmac, ecdh, x509), FormData, URLSearchParams, MessageChannel, AbortSignal and web-globals tests pass. The generated ZigGeneratedClasses.h uses the new form.

Background

  • JSC allocates every cell type that has C++ fields out of its own IsoSubspace (a size-segregated heap region per type, so a freed slot can only be reused by the same type). JSC::IsoSubspace is the per-heap object; GCClient::IsoSubspace is the per-VM allocator handle onto it. Bun creates both lazily the first time a type is allocated.
  • DOMIsoSubspaces and DOMClientIsoSubspaces are structs with one std::unique_ptr slot per type (the heap-side and VM-side objects respectively). subspaceFor<T> is the static hook JSC calls on every allocation of T to find the right subspace, which is why its inline body matters and its creation path does not.
  • A pointer to data member (&DOMIsoSubspaces::m_subspaceForFoo) is a compile-time offset; obj.*slot reads that field. Passing it lets one out-of-line function fill any slot without a per-type lambda.

[review] gate passed · iteration 1 · 132 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/iso-subspace-creation.test.ts
bun test v1.4.0 (8326d1bd3)

test/internal/source-lints/iso-subspace-creation.test.ts:
60 |   // tree, and the one permitted file must still be where the creation lives.
61 |   expect(scanned).toBeGreaterThan(1000);
62 |   expect(linesInCreationSite).toBeGreaterThan(0);
63 | 
64 |   violations.sort();
65 |   expect(violations).toEqual([]);
                          ^
error: expect(received).toEqual(expected)

- []
+ [
+   "src/jsc/bindings/BunClientData.h:278",
+   "src/jsc/bindings/BunClientData.h:281",
+   "src/jsc/bindings/BunClientData.h:283",
+   "src/jsc/bindings/BunClientData.h:298",
+ ]

- Expected  - 1
+ Received  + 6

      at <anonymous> (/workspace/bun/test/internal/source-lints/iso-subspace-creation.test.ts:65:22)
(fail) IsoSubspaces are only constructed in BunClientData.cpp [746.40ms]

 0 pass
 1 fail
 3 expect() calls
Ran 1 test across 1 file. [3.02s]
error: script "bd" exited with code 1
__F:1:S:0

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (8326d1bd3)

test/internal/source-lints/iso-subspace-creation.test.ts:
60 |   // tree, and the one permitted file must still be where the creation lives.
61 |   expect(scanned).toBeGreaterThan(1000);
62 |   expect(linesInCreationSite).toBeGreaterThan(0);
63 | 
64 |   violations.sort();
65 |   expect(violations).toEqual([]);
                          ^
error: expect(received).toEqual(expected)

- []
+ [
+   "src/jsc/bindings/BunClientData.h:278",
+   "src/jsc/bindings/BunClientData.h:281",
+   "src/jsc/bindings/BunClientData.h:283",
+   "src/jsc/bindings/BunClientData.h:298",
+ ]

- Expected  - 1
+ Received  + 6

      at <anonymous> (/workspace/bun/test/internal/source-lints/iso-subspace-creation.test.ts:65:22)
(fail) IsoSubspaces are only constructed in BunClientData.cpp [43.44ms]

 0 pass
 1 fail
 3 expect() calls
Ran 1 test across 1 file. [193.00ms]
__F:1:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/iso-subspace-creation.test.ts
bun test v1.4.0 (8326d1bd3)

test/internal/source-lints/iso-subspace-creation.test.ts:
(pass) IsoSubspaces are only constructed in BunClientData.cpp [755.83ms]

 1 pass
 0 fail
 3 expect() calls
Ran 1 test across 1 file. [3.10s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     4045096fa5
  features     baseline

22 deps, 120 codegen, 1144 objects in 926ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1206] install /workspace/bun
bun install v1.4.0-canary.1 (8326d1bd3)

Checked 26 installs across 63 packages (no changes) [4.00ms]
[2/1206] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (8326d1bd3)

Checked 1 install across 2 packages (no changes) [1.00ms]
[3/1206] gen ErrorCode+*.h
[4/1206] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (8326d1bd3)

Checked 111 installs across 104 packages (no changes) [10.00ms]
[5/1206] gen bindgenv2
[6/1206] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[7/1206] fetch zlib
[zlib] up to date
[8/1206] fetch tinycc
[tinycc] up to date
[9/1205] gen .bind.ts → GeneratedBindings.cpp
[10/1205] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[11/1205] gen ProcessBindingConstants.lut.h
Gen
... (truncated)
diff hotspot
.../skills/implementing-jsc-classes-cpp/SKILL.md   |  5 +-
 .github/workflows/source-lints.yml                 |  8 ++-
 src/codegen/generate-classes.ts                    | 10 +---
 src/codegen/generate-jssink.ts                     | 15 +----
 src/jsc/bindings/AsyncContextFrame.h               |  5 +-
 src/jsc/bindings/BunClientData.cpp                 | 19 +++++++
 src/jsc/bindings/BunClientData.h                   | 66 +++++++++-------------
 src/jsc/bindings/BunDebugger.cpp                   |  5 +-
 src/jsc/bindings/BunPlugin.cpp                     |  5 +-
 src/jsc/bindings/BunProcess.h                      |  5 +-
 src/jsc/bindings/CallSite.h                        |  5 +-
 src/jsc/bindings/ErrorCode.h                       |  5 +-
 src/jsc/bindings/ImportMetaObject.h                |  5 +-
 src/jsc/bindings/InternalModuleRegistry.h          |  5 +-
 src/jsc/bindings/JSBakeResponse.cpp                |  5 +-
 src/jsc/bindings/JSBufferList.cpp                  |  5 +-
 src/jsc/bindings/JSBunRequest.cpp                  |  5 +-
 src/jsc/bindings/JSBundlerPlugin.cpp               |  5 +-
 src/jsc/bindings/JSCommonJSExtensions.h            |  5 +-
 src/jsc/bindings/JSCommonJSModule.h                |  5 +-
 src/jsc/bindings/JSFFIFunction.h                   |  5 +-
 src/jsc/bindings/JSMockFunction.cpp                | 15 +----
 src/jsc/bindings/JSNextTickQueue.cpp               |  5 +-
 src/jsc/bindings/JSNodePerformanceHooksHistogram.h |  5 +-
 src/jsc/bindings/JSSocketHandlers.cpp              |  5 +-
 src/jsc/bindings/JSStringDecoder.cpp               |  5 +-
 src/jsc/bindings/JSWrappingFunction.h              |  5 +-
 src/jsc/bindings/JSX509Certificate.h               |  5 +-
 src/jsc/bindings/ModuleLoader.h                    |  5 +-
 src/jsc/bindings/NativePromiseContext.h            |  5 +-
 src/jsc/bindings/NodeVM.cpp                        | 10 +---
 src/jsc/bindings/NodeVMScript.h                    |  5 +-
 src/jsc/bindings/NodeVMSourc
... (truncated)

gate history · 1 passed · 1 rejected · iteration 1

evidence per changed file
file                                                  reads  edits  tests
.claude/skills/implementing-jsc-classes-cpp/SKILL.md      1      1      0
.github/workflows/source-lints.yml                        1      1      0
src/codegen/generate-classes.ts                           0      0      0
src/codegen/generate-jssink.ts                            0      0      0
src/jsc/bindings/AsyncContextFrame.h                      0      0      0
src/jsc/bindings/BunClientData.cpp                        0      0      0
src/jsc/bindings/BunClientData.h                          1      0      0
src/jsc/bindings/BunDebugger.cpp                          0      0      0
src/jsc/bindings/BunPlugin.cpp                            0      0      0
src/jsc/bindings/BunProcess.h                             0      0      0
src/jsc/bindings/CallSite.h                               0      0      0
src/jsc/bindings/ErrorCode.h                              0      0      0
src/jsc/bindings/ImportMetaObject.h                       0      0      0
src/jsc/bindings/InternalModuleRegistry.h                 0      0      0
src/jsc/bindings/JSBakeResponse.cpp                       0      0      0
src/jsc/bindings/JSBufferList.cpp                         0      0      0
(+ 116 more files)

@alii

alii commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@robobun

robobun commented Aug 18, 2026 •

Copy link
Copy Markdown
Collaborator

Superseded by #39770, which landed on main with the same subspaceForImpl change (one out-of-line subspaceForImplSlow) and is the source of the conflicts here. The two leftovers that still apply, the SKILL.md snippet and the source lint, are in #39833. Closing this one.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3aca76aa-642c-4b47-979f-c6ef430481a6

📥 Commits

Reviewing files that changed from the base of the PR and between 194d0d0 and d8806a4.

📒 Files selected for processing (1)
  • .claude/skills/implementing-jsc-classes-cpp/SKILL.md

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


Walkthrough

Changes

The subspace initialization API now accepts direct client and server IsoSubspace member pointers. Generated code and binding callers use the new API. Shared subspace creation uses a synchronized slow path.

IsoSubspace initialization

Layer / File(s) Summary
Slot-based subspace creation
src/jsc/bindings/BunClientData.*
subspaceForImpl uses member-pointer slots and delegates synchronized server/client IsoSubspace creation to subspaceForImplSlow.
Generated binding initialization
src/codegen/generate-classes.ts, src/codegen/generate-jssink.ts
Generated constructors, classes, sinks, and controllers pass direct subspace members.
Native and shim binding callers
src/jsc/bindings/*, src/jsc/bindings/node/*, src/jsc/bindings/sqlite/*, src/jsc/bindings/v8/*
Binding classes replace getter/setter lambdas with direct client and server subspace pointers.
WebCore and runtime callers
src/jsc/bindings/webcore/*, src/jsc/bindings/webcrypto/*, src/runtime/*
WebCore and runtime classes use direct subspace members with the updated helper.
Streams binding callers
src/jsc/bindings/webcore/streams/*
Stream constructors, objects, controllers, readers, writers, and operations use direct subspace members.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 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 summarizes the main change: consolidating IsoSubspace creation into one slow path.
Description check ✅ Passed The description explains the problem, fix, behavior, and verification results, although it does not use the template headings exactly.

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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Beyond the SKILL.md nit, I verified the refactor is behavior-preserving: reading getCustomHeapCellType/heap.*HeapCellType before taking heapData.lock() is safe (those members are immutable after JSHeapData construction); all slot members live on the base DOMClientIsoSubspaces/DOMIsoSubspaces so the pointer-to-member applies correctly to the Extended* instances; the "T"_s name matches what ISO_SUBSPACE_INIT(..., T) stringized before (the literal token T); and no old-form 4-lambda call sites remain in src/.

Extended reasoning...

The only finding is a stale documentation snippet. The core change — extracting the IsoSubspace-creation slow path into one shared NEVER_INLINE function keyed by pointer-to-member slots — reads as a faithful mechanical hoist. I checked the four things a reviewer would worry about on this path (lock ordering vs. the heapCellType read, pointer-to-member base/derived compatibility, the constructor argument equivalence with ISO_SUBSPACE_INIT, and completeness of the call-site sweep) and each held. Deferring rather than approving because this is 129 files on the GC allocation hot path and warrants a human sign-off on the equivalence claim.

Comment thread src/jsc/bindings/BunClientData.h

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I re-reviewed after d8806a4 and didn't find any issues — the SKILL.md nit is addressed and a grep confirms no old-form subspaceForImpl call sites remain in src/. Given this refactors the IsoSubspace creation path that every Bun-side JSC class allocates through, a maintainer look before merge is still worthwhile.

What was reviewed:

  • subspaceForImplSlow preserves the old inline path's behavior: same lock, same server-slot null check, same two slot writes, same outputConstraintSpaces append.
  • The per-type static_assert, cell-type selection, and visitOutputConstraints comparison stay compile-time in the inline template; getCustomHeapCellType now runs before the lock, which is fine (it just returns a reference to an eagerly-constructed JSHeapData member).
  • Spot-checked ~20 call sites including the UseCustomHeapCellType::Yes ones (NodeVMGlobalObject, NapiHandleScopeImpl, BakeGlobalObject, ZigGlobalObject, NativePromiseContext) — all pass the same member slots the old lambdas touched.
Extended reasoning...

Overview

This PR de-duplicates the IsoSubspace creation slow path across all ~250 subspaceFor<T> instantiations. WebCore::subspaceForImpl in BunClientData.h is refactored: instead of four templated lambdas (get/set client, get/set server), callers pass two pointer-to-member slots (&DOMClientIsoSubspaces::m_clientSubspaceForFoo, &DOMIsoSubspaces::m_subspaceForFoo). The inline template keeps only the fast path (load client slot → return if set) and the compile-time per-type work (static_assert, cell-type selection, visitOutputConstraints comparison); the once-per-(VM,type) creation path moves to a single NEVER_INLINE subspaceForImplSlow in BunClientData.cpp. 127 hand-written call sites plus the generate-classes.ts / generate-jssink.ts templates and the implementing-jsc-classes-cpp skill snippet are updated to the new form. Net binary-size win is 64–240 KB per target.

Security risks

None. No user input is involved; this is internal GC-allocator plumbing. The pointer-to-member arguments are compile-time constants at every call site.

Level of scrutiny

High. subspaceFor<T> is what JSC calls on every allocation of every Bun-defined JS class; the slow path constructs the type's IsoSubspace and registers it for output constraints. A regression here would surface as allocation failures, wrong-destructor calls, or missed GC visits across the whole runtime. That said, the actual logic change is ~25 lines in two files; the other 128 files are a mechanical signature update that would fail to compile if wrong.

I verified behavior preservation line-by-line against the old body: the lock is taken at the same point relative to the server-slot read/write; the server subspace is created only when the slot is empty and appended to outputConstraintSpaces under the same predicate; the client subspace is always freshly created and stored (matching the old setClient on every miss). The IsoSubspace name argument is now the literal "T", which is what ISO_SUBSPACE_INIT(heap, cellType, T) stringized when T was the template parameter — a diagnostic label only. sizeof(T) and T::numberOfLowerTierPreciseCells are passed explicitly instead of via the macro. Moving the getCustomHeapCellType call outside the lock is safe: it dereferences an eagerly-constructed IsoHeapCellType member of JSHeapData, not the locked subspaces() map.

Other factors

CI passed 179/179 on the first run and the PR description lists debug-build verification across workers, node:vm, streams, sqlite, crypto, and web-globals — all subsystems that exercise distinct IsoSubspace slots including several UseCustomHeapCellType::Yes cases. My earlier nit (stale SKILL.md snippet) was addressed in d8806a4; a grep confirms no old-form call sites remain in src/. I'm deferring rather than approving only because this sits directly on the GC allocation path for every class in the runtime, which per the review guidelines warrants a maintainer sign-off even for a mechanical refactor.

Guards the invariant this change introduces: the creation path exists in
one out-of-line place, and new classes reach it through subspaceForImpl.
The source-lints workflow now also triggers on the C++ and codegen files
the lint reads.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

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 found no bugs; the earlier SKILL.md nit was addressed in d8806a4. Because it rewrites WebCore::subspaceForImpl — the hook JSC calls on every allocation of every Bun-defined cell type — a maintainer look is still worthwhile before merge.

What was reviewed:

  • subspaceForImplSlow reproduces the exact ISO_SUBSPACE_INIT args (name "T"_s, vm.heap, cell type, sizeof(T), numberOfLowerTierPreciseCells), same lock, same output-constraint registration, same two slot writes.
  • Pointer-to-member slot types work: all m_clientSubspaceFor*/m_subspaceFor* members live on the DOMClientIsoSubspaces/DOMIsoSubspaces base (the Extended* subclasses are empty), so DOMClientIsoSubspaces::* applied to an ExtendedDOMClientIsoSubspaces& is well-defined.
  • getCustomHeapCellType now runs before the heap lock instead of under it — it only returns a reference to a JSHeapData member, so no ordering concern.
  • Spot-checked ~20 call sites plus both codegen templates for slot-name mismatches; the compiler enforces the rest.
Extended reasoning...

Overview

This PR refactors WebCore::subspaceForImpl in src/jsc/bindings/BunClientData.{h,cpp}: the once-per-(VM,type) IsoSubspace creation path moves out of the ALWAYS_INLINE template into a single NEVER_INLINE function subspaceForImplSlow, and the four getter/setter lambdas each caller passed are replaced by two pointer-to-data-member arguments. 127 hand-written call sites, both codegen templates (generate-classes.ts, generate-jssink.ts), and the implementing-jsc-classes-cpp skill snippet are updated to the new form. A new source-lint test asserts IsoSubspace construction stays confined to BunClientData.cpp, and source-lints.yml is widened to run on .h/.cpp/codegen/** changes. CI reports 64-240 KB smaller stripped binaries per target.

Security risks

None identified. No user input reaches this path; it is purely allocation-infrastructure plumbing. The pointer-to-member offsets are compile-time constants and the type system prevents passing a slot from the wrong struct.

Level of scrutiny

High. subspaceFor<T> is the static hook JSC calls on every allocation of every Bun-defined cell type; a wrong cell-type, wrong size, or missed output-constraint registration here is a type-confusion or UAF vector across the whole runtime. That is exactly the "most-blocked category" in REVIEW.md's native memory-safety section, and src/jsc/bindings/ is CODEOWNER-gated. The refactor itself is small and I believe behavior-preserving — the fast path is unchanged, and the slow path is a straight extraction with the per-type facts (sizeof(T), numberOfLowerTierPreciseCells, visitOutputConstraints comparison, cell-type selection) still computed in the templated inline part and passed as plain values — but a maintainer familiar with JSC's IsoSubspace invariants should confirm.

Other factors

  • The 127 call-site edits are mechanical and compiler-enforced: a mistyped member name or mismatched slot pair fails to compile. I spot-checked a range including the UseCustomHeapCellType::Yes sites (NativePromiseContext, NodeVMGlobalObject, NapiHandleScopeImpl, ZigGlobalObject, BakeGlobalObject) — those keep their custom-heap-cell-type lambda as the fourth argument, unchanged.
  • The one semantic timing shift I noticed — getCustomHeapCellType(heapData) now runs before Locker locker { heapData.lock() } rather than under it — is safe: those callbacks return server.m_heapCellTypeForX, a reference to a member constructed in JSHeapData::JSHeapData and never mutated.
  • The "T"_s subspace name is not a regression: the old ISO_SUBSPACE_INIT(heap, cellType, T) stringized the template parameter name, so the name was already the literal "T" for every type routed through this helper.
  • My earlier inline nit (stale SKILL.md snippet) was addressed in d8806a4 and the thread is resolved. First CI run on the full change was 179/179 green.

@robobun robobun closed this Aug 21, 2026
Jarred-Sumner pushed a commit that referenced this pull request Aug 21, 2026
…e construction (#39833)

Follow-up to #39770, which moved the IsoSubspace creation path out of
`WebCore::subspaceForImpl` into one out-of-line `subspaceForImplSlow`.
#39503 did the same thing and is closed as superseded. These are the two
pieces of it that still apply.

### Problem
- `.claude/skills/implementing-jsc-classes-cpp/SKILL.md:30-35` still
shows the four-lambda `subspaceForImpl` call. That signature no longer
exists, so the snippet does not compile when copied.
- Nothing stops a new class, or a codegen template, from constructing
its own `IsoSubspace` again. The compiler does not enforce that
invariant, and a template that does it pays for it once per generated
class.

### Fix
- The skill snippet now shows the `BUN_SUBSPACE_SLOTS` form that every
caller on main uses (for example
`src/jsc/bindings/AsyncContextFrame.h:39`), and says where the subspaces
are created.
- `test/internal/source-lints/iso-subspace-creation.test.ts` scans
`src/**/*.{h,cpp}` and `src/codegen/**/*.ts` for `ISO_SUBSPACE_INIT`,
`makeUnique<IsoSubspace>` or `new IsoSubspace` outside
`src/jsc/bindings/BunClientData.cpp`. It also asserts that file still
constructs one, so the lint cannot pass on an empty scan.
- `source-lints.yml` now also triggers on `src/**/*.h`, `src/**/*.cpp`
and `src/codegen/**`, the files this lint reads (the README in that
directory asks for this). `src/codegen/**` replaces the narrower
`src/codegen/class-definitions.ts` entry.
- Verified: the lint passes on main (`bun bd test` and the released
bun). It fails on the tree before #39770, listing the four constructions
in `BunClientData.h`, and it fails when a construction is added to a
header or to `generate-jssink.ts`, naming the file and line.

### Background
- Every JS class with C++ fields has a `subspaceFor<T>` hook that JSC
calls on each allocation of `T`. The creation of the subspace behind it
(lock, construct, register, fill the two slots) is about 640 bytes of
code and runs once per type. Inlined per class it cost about 160 KB of
the linux-x64 binary, which is what #39770 removed.
- `test/internal/source-lints/` holds grep-style lints over `src/`. They
run on GitHub Actions against a released bun and are excluded from the
Buildkite shards, so they only run on PRs that touch the paths listed in
the workflow.

<!-- robobun:evidence:begin -->

---

**[stamp-90s]** gate passed · iteration 1 · 3 files touched

<details><summary>passes on PR (with fix)</summary>

```console
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/internal/source-lints/iso-subspace-creation.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/internal/source-lints/iso-subspace-creation.test.ts
bun test v1.4.0 (6e906e4)

test/internal/source-lints/iso-subspace-creation.test.ts:
(pass) IsoSubspaces are only constructed in BunClientData.cpp [1035.72ms]

 1 pass
 0 fail
 3 expect() calls
Ran 1 test across 1 file. [4.46s]
Exit: 0
```

</details>

<details><summary>diff hotspot</summary>

```
.../skills/implementing-jsc-classes-cpp/SKILL.md   |  8 +--
 .github/workflows/source-lints.yml                 |  8 ++-
 .../source-lints/iso-subspace-creation.test.ts     | 67 ++++++++++++++++++++++
 3 files changed, 76 insertions(+), 7 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 1

<details><summary>evidence per changed file</summary>

```
file                                                      reads  edits  tests
.claude/skills/implementing-jsc-classes-cpp/SKILL.md          2      2      0
.github/workflows/source-lints.yml                            1      1      0
test/internal/source-lints/iso-subspace-creation.test.ts      4      8      0
```

</details>

<!-- robobun:evidence:end -->
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.

2 participants