Skip to content

src: let embedders supply a builtin code cache without a snapshot - #65352

Open
codebytere wants to merge 1 commit into
nodejs:mainfrom
codebytere:embedder/builtin-code-cache-seed
Open

src: let embedders supply a builtin code cache without a snapshot#65352
codebytere wants to merge 1 commit into
nodejs:mainfrom
codebytere:embedder/builtin-code-cache-seed

Conversation

@codebytere

@codebytere codebytere commented Aug 17, 2026

Copy link
Copy Markdown
Member

An embedder that creates its Environments without Node's snapshot (its own isolate, no EmbedderSnapshotData) compiles every builtin the bootstrap touches from source in each such process, and then serializes a fresh code cache for each of them that only a later worker thread ever reads. This adds a way to hand CreateEnvironment() a cache built ahead of time, plus a flag to skip the runtime serialization; node itself is unchanged.

embedtest -- 0, process start to exit, n=30 interleaved ms
this branch, nothing supplied 55.4
+ EmbedderBuiltinCodeCache passed to CreateEnvironment() 43.8 (−21 %)
+ kNoHarvestBuiltinCodeCache 51.3 (−7 %)
+ both 42.4 (−23 %)
  • node::EmbedderBuiltinCodeCache holds entries pairing a builtin id with a v8::ScriptCompiler::CachedData. EmbedderBuiltinCodeCache::Generate(context) compiles every builtin in a context made with node::NewContext() and returns them for a build step to embed; an instance passed to CreateEnvironment() (new trailing parameter, forwarded by CommonEnvironmentSetup::Create()) seeds that Environment's loader, and one instance can serve any number of Environments, which share its buffers. CreateEnvironment() runs CachedData::CompatibilityCheck() on the entries first and returns nullptr for a cache made with another V8 version, flag set or read-only snapshot. A snapshot's entries still merge with it, so RefreshCodeCache() merges with insert_or_assign instead of asserting a single call.
  • ProcessInitializationFlags::kNoHarvestBuiltinCodeCache stops LookupAndCompile() from serializing a cache for builtins compiled without one. The default stays as it is because worker threads start from that harvested cache.

The per-context scripts NewContext() runs (internal/per_context/*) are outside an Environment and keep compiling from source.

embedtest gets --builtin-code-cache-create <file>, --builtin-code-cache <file> and --no-harvest-builtin-code-cache so the test drives this through an embedder binary: it generates a cache in one process, checks that another Environment's bootstrap compiles from it, and that a worker started with and without the flag does and doesn't find a harvested cache.

Tests: the new embedding test and a cctest for the RefreshCodeCache merge; embedding, cctest and the default suite pass. A cache with a corrupted header makes CreateEnvironment() return nullptr (EmbedderBuiltinCodeCache rejected: 5 under NODE_DEBUG_NATIVE=CODE_CACHE).


Disclosure: the code, tests, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/startup

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. labels Aug 17, 2026
Comment thread src/node_builtins.cc Outdated
Comment thread src/node_builtins.cc Outdated
@codebytere
codebytere force-pushed the embedder/builtin-code-cache-seed branch from 1b99e86 to 136d2ad Compare August 17, 2026 13:25
@codebytere codebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 17, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 17, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.74576% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.07%. Comparing base (7b6b21a) to head (189fbb1).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
src/node_builtins.cc 88.88% 1 Missing and 4 partials ⚠️
src/api/environment.cc 63.63% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65352      +/-   ##
==========================================
+ Coverage   90.05%   90.07%   +0.01%     
==========================================
  Files         751      751              
  Lines      254420   254829     +409     
  Branches    47975    48101     +126     
==========================================
+ Hits       229121   229540     +419     
+ Misses      16483    16481       -2     
+ Partials     8816     8808       -8     
Files with missing lines Coverage Δ
src/node.cc 76.74% <100.00%> (+0.09%) ⬆️
src/node.h 91.66% <ø> (-0.79%) ⬇️
src/node_builtins.h 100.00% <ø> (ø)
src/node_internals.h 80.35% <ø> (ø)
src/api/environment.cc 78.01% <63.63%> (-0.41%) ⬇️
src/node_builtins.cc 77.19% <88.88%> (+1.05%) ⬆️

... and 34 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment thread test/embedding/test-embedding-builtin-code-cache.js Outdated
@codebytere
codebytere force-pushed the embedder/builtin-code-cache-seed branch from 136d2ad to d33c3df Compare August 20, 2026 20:34
@legendecas legendecas added semver-minor PRs that contain new features and should be released in the next minor version. request-ci Add this label to start a Jenkins CI on a PR. labels Aug 20, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 20, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment thread src/node_builtins.cc Outdated
@codebytere
codebytere force-pushed the embedder/builtin-code-cache-seed branch from d33c3df to e512f86 Compare August 21, 2026 06:06
@codebytere codebytere added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 21, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 22, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment thread src/node.h Outdated
// run by NewContext(), start with these entries. Entries a snapshot provides
// still apply. Call before creating contexts/Environments; may be called
// again to replace the set for later ones.
NODE_EXTERN void SetBuiltinCodeCache(

@joyeecheung joyeecheung Aug 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am a bit hesitant of making this a process-wide method, this potentially makes it difficult for us to reorganize the hierarchy in the future. Can we make the list per-Environment on the API level? We can probably make the wrappers thin enough so that it's possible to share underlying cache across different Environments.

Also I think on the API level, it would be better to reuse/nest the v8::ScriptCompiler::CachedData struct to pass things around instead of adding an ad-hoc structure.

Another thing to safe guard: code cache must be generated from the same isolate as the snapshot data (or lack thereof) or otherwise it would crash/corrupt the memory due to readonly space mismatches. We should probably call v8::ScriptCompiler::CachedData::CompatibilityCheck somewhere to ensure that they matches or surface the error otherwise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@joyeecheung reworked along those lines in 189fbb1: it's now a node::EmbedderBuiltinCodeCache whose entries are {id, std::unique_ptr<v8::ScriptCompiler::CachedData>}, passed per Environment as a trailing CreateEnvironment() parameter (so CommonEnvironmentSetup::Create() forwards it) with one instance shareable across Environments, and CreateEnvironment() runs CachedData::CompatibilityCheck() over the entries before using them and returns nullptr on a mismatch. the process-wide setter is gone; the one thing that loses is the internal/per_context/* scripts NewContext() compiles outside any Environment, which i've left compiling from source rather than keep a global for them. does the CreateEnvironment() parameter work for you, or would you rather it hang off IsolateData next to the snapshot's cache?

Environments created from the built-in snapshot get the builtins' code
cache from that snapshot. An embedder that bootstraps an Environment
from scratch (its own isolate and context, no EmbedderSnapshotData) has
no way to provide one: every builtin the bootstrap touches is compiled
from source in every such process, and each of them then serializes a
fresh cache (SaveCodeCache) that only a later worker thread would ever
consume.

Add node::EmbedderBuiltinCodeCache for that case. Its entries pair a
builtin id with a v8::ScriptCompiler::CachedData; Generate(context)
compiles every builtin in a context of the right kind of isolate and
returns them for a build step to embed, and an instance passed to
CreateEnvironment() (new trailing parameter, forwarded by
CommonEnvironmentSetup::Create()) seeds that Environment's loader.
CreateEnvironment() runs CachedData::CompatibilityCheck() on the
entries first and returns nullptr for a cache made with another V8
version, flag set or read-only snapshot. One instance can be passed to
any number of Environments; entries from a snapshot still merge with it
(RefreshCodeCache() now merges instead of assuming a single call).

ProcessInitializationFlags::kNoHarvestBuiltinCodeCache stops
serializing caches for builtins compiled without one, for embedders
that supply their own or never create workers. The default is
unchanged because worker threads copy the harvested cache.

embedtest gains --builtin-code-cache-create, --builtin-code-cache and
--no-harvest-builtin-code-cache, and a test that generates a cache in
one process, checks that another Environment's bootstrap compiles with
it, and that a worker does or does not find a harvested cache depending
on the flag.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@codebytere
codebytere force-pushed the embedder/builtin-code-cache-seed branch from e512f86 to 189fbb1 Compare August 26, 2026 14:16
@codebytere
codebytere requested a review from joyeecheung August 26, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. semver-minor PRs that contain new features and should be released in the next minor version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants