Skip to content

SourceProvider: hand the provider the live UnlinkedCodeBlock CodeCache generated - #493

Merged
Jarred-Sumner merged 1 commit into
mainfrom
claude/compile-cache-unlinked-code-block-hook
Aug 23, 2026
Merged

SourceProvider: hand the provider the live UnlinkedCodeBlock CodeCache generated#493
Jarred-Sumner merged 1 commit into
mainfrom
claude/compile-cache-unlinked-code-block-hook

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Adds one virtual under USE(BUN_JSC_ADDITIONS):

virtual void didGenerateUnlinkedCodeBlock(VM&, const SourceCodeKey&, UnlinkedCodeBlock*) const { }

CodeCache::getUnlinkedGlobalCodeBlock calls it right after cacheBytecode(), with the block it just generated and the key it was generated under.

Why

Bun's node compile cache (module.enableCompileCache()) currently persists a missed module by re-parsing it on a second VM at exit and eagerly compiling every function. The live tree on the main VM already has the top-level block plus the code blocks of every function that ran, and encodeCodeBlock(vm, key, block) writes exactly that. The only thing missing was a way for the provider to get hold of the block: cacheBytecode() passes a generator that encodes immediately, before any function has been compiled — for a CommonJS module that is just the wrapper stub.

With this hook Bun keeps a Weak to the block and encodes it once, before its own GC drops unlinked code or at exit. No rooting, no second VM, no incremental addFunctionUpdate layout, no leafExecutables() use — so this composes with #490.

Bun side: oven-sh/bun#40174.

No behavior change for anything not built with BUN_JSC_ADDITIONS; the default implementation is a no-op.

…e generated

Bun's node compile cache wants to encode a module's bytecode once at exit,
after the functions that ran were compiled, instead of re-parsing it on a
second VM. cacheBytecode() only gives the provider a generator that encodes
immediately, so add didGenerateUnlinkedCodeBlock(VM&, const SourceCodeKey&,
UnlinkedCodeBlock*) under USE(BUN_JSC_ADDITIONS): the provider can root the
block and call encodeCodeBlock() with the same key later.
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 52 minutes

Limit details: You’ve used the included review currently available. Your 63 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

You’re in a promotional period — use the checkbox below to run this review for free:

  • Run review for free

On-demand reviews are free for the next 28 days. After that, they cost $0.25 per reviewed file.

How can I continue?

Run this review now using the option above, or comment @coderabbitai review --use-credits.

You can also wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 50fe4859-eca8-46fd-bd9c-4abab9b5110c

📥 Commits

Reviewing files that changed from the base of the PR and between aea1f01 and 0cd6187.

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

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

@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.

LGTM — adds a Bun-guarded no-op virtual hook and calls it alongside cacheBytecode().

What was reviewed:

  • New virtual follows the same pattern as cacheBytecode/commitCachedBytecode/memoryCost directly above it; default no-op, so existing providers are unaffected.
  • Call site in getUnlinkedGlobalCodeBlock is inside the if (unlinkedCodeBlock && Options::useCodeCache()) block, so the pointer is non-null and key is live.
  • Forward declarations of VM/SourceCodeKey/UnlinkedCodeBlock are sufficient since parameters are by reference/pointer.
Extended reasoning...

Overview

Two-file, ~7-line addition. SourceProvider.h gains three forward declarations and one new virtual, didGenerateUnlinkedCodeBlock(VM&, const SourceCodeKey&, UnlinkedCodeBlock*), with an empty default body, gated behind #if USE(BUN_JSC_ADDITIONS). CodeCache.cpp invokes it in getUnlinkedGlobalCodeBlock immediately after the existing cacheBytecode() call, under the same guard. The purpose (per the description) is to let Bun's provider grab and root the freshly-generated UnlinkedCodeBlock for later encoding, avoiding a second-VM re-parse in the compile cache path.

Security risks

None. This is a notification hook with a no-op default; no new data flows into parsing, allocation, or codegen. It hands out a pointer to an object the provider could already reach indirectly via the encode-generator lambda. No untrusted input, no auth/crypto/permissions surface.

Level of scrutiny

Low. The change is tiny, mechanical, and pattern-matched against the adjacent virtuals (cacheBytecode, updateCache, commitCachedBytecode, and the Bun-only memoryCost right above it — same JS_EXPORT_PRIVATE virtual ... const { } shape). Both additions are behind USE(BUN_JSC_ADDITIONS), so upstream/non-Bun builds compile identically. The unconditional forward declarations are harmless.

Other factors

The call is placed where unlinkedCodeBlock is already checked non-null and key is a live stack local, so there's no lifetime or null-deref concern at the call site. Any interesting behavior (rooting via Strong<UnlinkedCodeBlock>, deferred encoding) lives in the Bun-side override, not in this PR — here it's purely the extension point. No outstanding reviewer comments; the only timeline entry is a CodeRabbit rate-limit notice.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
0cd61870 autobuild-preview-pr-493-0cd61870 2026-08-23 07:04:18 UTC

Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Aug 23, 2026
…-parsing every module on a second VM

A cold run with module.enableCompileCache() used to spend its exit
re-parsing every missed module on a dedicated "BunCompileCache" thread with
its own JSC VM and eagerly compiling every function in it.

The SourceProvider now gets the live top-level UnlinkedCodeBlock from JSC
right after CodeCache generates it (oven-sh/WebKit#493) and keeps a Weak to
it on the VM's client data. The block is encoded once with encodeCodeBlock():
either right before Bun's GC drops unlinked function code
(JSC__VM__runGC → deleteAllUnlinkedCodeBlocks), or at persist time (exit,
module.flushCompileCache(), a --watch reload), whichever comes first. That
writes the top-level block plus the code blocks of every function that ran
up to that point; functions that never ran stay lazy stubs. Nothing is
rooted: a block the GC already collected is simply not cached this run.
The file format is unchanged: one CachedBytecode, decoded by the same path
as before.

Workers encode what they compiled in their own on_exit and hand the bytes
to the entry; the main thread's exit writes them. Entries carry an id so
bytecode for a module that was rewritten and re-required in the same
process is dropped in favor of the new version's.

--watch reloads now go through the JS thread when the cache is enabled,
like they already did with --watch-kill-signal listeners, so the modules
get encoded before execve; the grace timer still forces the reload if the
JS thread is stuck, persisting only what was already encoded.

Pins WebKit to the PR preview build until #493 lands.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Aug 23, 2026
…-parsing every module on a second VM

A cold run with module.enableCompileCache() used to spend its exit
re-parsing every missed module on a dedicated "BunCompileCache" thread with
its own JSC VM and eagerly compiling every function in it.

The SourceProvider now gets the live top-level UnlinkedCodeBlock from JSC
right after CodeCache generates it (oven-sh/WebKit#493) and keeps a Weak to
it on the VM's client data. The block is encoded once with encodeCodeBlock():
either right before Bun's GC drops unlinked function code
(JSC__VM__runGC → deleteAllUnlinkedCodeBlocks), or at persist time (exit,
module.flushCompileCache(), a --watch reload), whichever comes first. That
writes the top-level block plus the code blocks of every function that ran
up to that point; functions that never ran stay lazy stubs. Nothing is
rooted: a block the GC already collected is simply not cached this run.
The file format is unchanged: one CachedBytecode, decoded by the same path
as before.

Workers encode what they compiled in their own on_exit and hand the bytes
to the entry; the main thread's exit writes them. Entries carry an id so
bytecode for a module that was rewritten and re-required in the same
process is dropped in favor of the new version's.

--watch reloads now go through the JS thread when the cache is enabled,
like they already did with --watch-kill-signal listeners, so the modules
get encoded before execve; the grace timer still forces the reload if the
JS thread is stuck, persisting only what was already encoded.

Pins WebKit to the PR preview build until #493 lands.
@Jarred-Sumner
Jarred-Sumner merged commit 46a19b0 into main Aug 23, 2026
47 checks passed
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