Skip to content

Commit e512f86

Browse files
committed
src: let embedders supply a builtin code cache without a snapshot
Contexts and Environments created from the built-in snapshot get the builtins' code cache from that snapshot. An embedder that bootstraps them from scratch (its own isolate and context, no EmbedderSnapshotData) has no way to provide one: every builtin the bootstrap touches, and the per-context scripts NewContext() runs, are 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 a small public API for that case: - node::GenerateBuiltinCodeCache(context) compiles every builtin in a context of the right kind of isolate and returns the caches, for a build step that embeds them. - node::SetBuiltinCodeCache(entries) installs process-wide entries that every BuiltinLoader created afterwards starts with, i.e. each Environment's and the loader for the per-context scripts. Entries a snapshot provides still merge on top (RefreshCodeCache() now merges with insert_or_assign 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 the bootstrap and per-context scripts of another compile with it, and that a worker does or does not find a harvested cache depending on the flag. On x64 Linux embedtest's start-to-exit goes from ~64 to ~44 ms with a supplied cache; not harvesting alone saves ~5 ms on a snapshot-less start and is a no-op with the snapshot. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
1 parent 30bff4a commit e512f86

9 files changed

Lines changed: 332 additions & 18 deletions

File tree

‎src/api/environment.cc‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,11 @@ Maybe<void> InitializePrimordials(Local<Context> context,
10051005
// relatively cheap and all the scripts that we may want to run at
10061006
// startup are always present in it.
10071007
thread_local builtins::BuiltinLoader builtin_loader;
1008+
thread_local bool seeded = false;
1009+
if (!seeded) {
1010+
builtin_loader.SeedFromProcessCodeCache();
1011+
seeded = true;
1012+
}
10081013
// Primordials can always be just eagerly compiled.
10091014
builtin_loader.SetEagerCompile();
10101015

‎src/env.cc‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,10 @@ Environment::Environment(IsolateData* isolate_data,
871871
CHECK_NOT_NULL(isolate_data->worker_context());
872872
builtin_loader()->CopySourceAndCodeCacheReferenceFrom(
873873
isolate_data->worker_context()->env()->builtin_loader());
874-
} else if (isolate_data->snapshot_data() != nullptr) {
874+
} else {
875+
builtin_loader()->SeedFromProcessCodeCache();
876+
}
877+
if (is_main_thread() && isolate_data->snapshot_data() != nullptr) {
875878
// ... otherwise, if a snapshot was provided, use its code cache.
876879
size_t cache_size = isolate_data->snapshot_data()->code_cache.size();
877880
per_process::Debug(DebugCategory::CODE_CACHE,

‎src/node.cc‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,10 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
12861286
cppgc::InitializeProcess(allocator);
12871287
}
12881288

1289+
if (flags & ProcessInitializationFlags::kNoHarvestBuiltinCodeCache) {
1290+
builtins::BuiltinLoader::SetHarvestCodeCache(false);
1291+
}
1292+
12891293
if (!(flags & ProcessInitializationFlags::kNoInitializeV8)) {
12901294
V8::Initialize();
12911295

‎src/node.h‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ enum Flags : uint32_t {
233233
kNoInitializeCppgc = 1 << 13,
234234
// Initialize the process for predictable snapshot generation.
235235
kGeneratePredictableSnapshot = 1 << 14,
236+
// Do not serialize a code cache for builtins that had to be compiled without
237+
// one. By default such caches are kept so that worker threads created later
238+
// start faster; an embedder that supplies its own cache (SetBuiltinCodeCache)
239+
// or never creates workers only pays for the serialization.
240+
kNoHarvestBuiltinCodeCache = 1 << 15,
236241

237242
// Emulate the behavior of InitializeNodeWithArgs() when passing
238243
// a flags argument to the InitializeOncePerProcess() replacement
@@ -678,6 +683,36 @@ struct InspectorParentHandle {
678683
virtual ~InspectorParentHandle() = default;
679684
};
680685

686+
// A V8 code cache for one of Node.js's built-in JavaScript modules.
687+
struct BuiltinCodeCacheEntry {
688+
std::string id; // e.g. "internal/bootstrap/node"
689+
const uint8_t* data; // must stay valid for the rest of the process
690+
size_t length;
691+
};
692+
struct OwnedBuiltinCodeCacheEntry {
693+
std::string id;
694+
std::vector<uint8_t> data;
695+
};
696+
697+
// Contexts and Environments created from Node.js's built-in snapshot get the
698+
// builtins' code cache from that snapshot. An embedder that bootstraps them
699+
// from scratch (its own isolate/context, no EmbedderSnapshotData) can supply a
700+
// cache built ahead of time with GenerateBuiltinCodeCache() against the same
701+
// kind of isolate (same V8 version, flags and read-only snapshot): every
702+
// Environment created afterwards, and the loader for the per-context scripts
703+
// run by NewContext(), start with these entries. Entries a snapshot provides
704+
// still apply. Call before creating contexts/Environments; may be called
705+
// again to replace the set for later ones.
706+
NODE_EXTERN void SetBuiltinCodeCache(
707+
const std::vector<BuiltinCodeCacheEntry>& entries);
708+
709+
// Compiles every built-in module in `context` (which must have been created
710+
// with node::NewContext() in the kind of isolate the cache is for) and returns
711+
// their code caches, e.g. for a build step that embeds them and passes them to
712+
// SetBuiltinCodeCache() at runtime. Returns an empty vector on failure.
713+
NODE_EXTERN std::vector<OwnedBuiltinCodeCacheEntry> GenerateBuiltinCodeCache(
714+
v8::Local<v8::Context> context);
715+
681716
// TODO(addaleax): Maybe move per-Environment options parsing here.
682717
// Returns nullptr when the Environment cannot be created e.g. there are
683718
// pending JavaScript exceptions.

‎src/node_builtins.cc‎

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
#include "v8-value.h"
1313

1414
namespace node {
15-
namespace builtins {
1615

1716
using loader::HostDefinedOptions;
1817
using v8::Boolean;
@@ -44,6 +43,39 @@ using v8::TryCatch;
4443
using v8::Undefined;
4544
using v8::Value;
4645

46+
namespace builtins {
47+
48+
namespace {
49+
struct ProcessCodeCache {
50+
Mutex mutex;
51+
std::vector<CodeCacheInfo> entries;
52+
bool harvest = true;
53+
};
54+
ProcessCodeCache& GetProcessCodeCache() {
55+
static ProcessCodeCache process_code_cache;
56+
return process_code_cache;
57+
}
58+
} // namespace
59+
60+
void BuiltinLoader::SetProcessCodeCache(std::vector<CodeCacheInfo> entries) {
61+
ProcessCodeCache& pcc = GetProcessCodeCache();
62+
Mutex::ScopedLock lock(pcc.mutex);
63+
pcc.entries = std::move(entries);
64+
}
65+
66+
void BuiltinLoader::SetHarvestCodeCache(bool on) {
67+
ProcessCodeCache& pcc = GetProcessCodeCache();
68+
Mutex::ScopedLock lock(pcc.mutex);
69+
pcc.harvest = on;
70+
}
71+
72+
void BuiltinLoader::SeedFromProcessCodeCache() {
73+
ProcessCodeCache& pcc = GetProcessCodeCache();
74+
Mutex::ScopedLock lock(pcc.mutex);
75+
harvest_code_cache_ = pcc.harvest;
76+
if (!pcc.entries.empty()) RefreshCodeCache(pcc.entries);
77+
}
78+
4779
BuiltinLoader::BuiltinLoader()
4880
: config_(GetConfig()), code_cache_(std::make_shared<BuiltinCodeCache>()) {
4981
LoadJavaScriptSource();
@@ -422,6 +454,7 @@ MaybeLocal<Data> BuiltinLoader::LookupAndCompile(
422454
}
423455

424456
if (result == Result::kWithoutCache && optional_realm != nullptr &&
457+
harvest_code_cache_ &&
425458
!optional_realm->env()->isolate_data()->is_building_snapshot()) {
426459
// We failed to accept this cache, maybe because it was rejected, maybe
427460
// because it wasn't present. Either way, we'll attempt to replace this
@@ -593,12 +626,13 @@ bool BuiltinLoader::CompileAllBuiltinsAndCopyCodeCache(
593626

594627
void BuiltinLoader::RefreshCodeCache(const std::vector<CodeCacheInfo>& in) {
595628
RwLock::ScopedLock lock(code_cache_->mutex);
596-
code_cache_->map.reserve(in.size());
597-
DCHECK(code_cache_->map.empty());
629+
// May be called more than once, e.g. first with the code cache carried by
630+
// the snapshot and then by an embedder with caches it built for additional
631+
// (or the same) builtin ids against this isolate: merge, and let the entry
632+
// supplied last win for an id present in both.
633+
code_cache_->map.reserve(code_cache_->map.size() + in.size());
598634
for (auto const& [id, data] : in) {
599-
auto result = code_cache_->map.emplace(id, data);
600-
USE(result.second);
601-
DCHECK(result.second);
635+
code_cache_->map.insert_or_assign(id, data);
602636
}
603637
code_cache_->has_code_cache = true;
604638
}
@@ -918,6 +952,38 @@ void BuiltinLoader::RegisterExternalReferences(
918952
}
919953

920954
} // namespace builtins
955+
956+
void SetBuiltinCodeCache(const std::vector<BuiltinCodeCacheEntry>& entries) {
957+
std::vector<builtins::CodeCacheInfo> infos;
958+
infos.reserve(entries.size());
959+
for (const BuiltinCodeCacheEntry& e : entries) {
960+
auto cached_data = std::make_shared<ScriptCompiler::CachedData>(
961+
e.data,
962+
static_cast<int>(e.length),
963+
ScriptCompiler::CachedData::BufferNotOwned);
964+
infos.push_back({e.id, builtins::BuiltinCodeCacheData(cached_data)});
965+
}
966+
builtins::BuiltinLoader::SetProcessCodeCache(std::move(infos));
967+
}
968+
969+
std::vector<OwnedBuiltinCodeCacheEntry> GenerateBuiltinCodeCache(
970+
Local<Context> context) {
971+
std::vector<OwnedBuiltinCodeCacheEntry> out;
972+
builtins::BuiltinLoader loader;
973+
loader.SetEagerCompile();
974+
std::vector<builtins::CodeCacheInfo> infos;
975+
if (!loader.CompileAllBuiltinsAndCopyCodeCache(context, {}, &infos)) {
976+
return out;
977+
}
978+
out.reserve(infos.size());
979+
for (const builtins::CodeCacheInfo& info : infos) {
980+
out.push_back({info.id,
981+
std::vector<uint8_t>(info.data.data,
982+
info.data.data + info.data.length)});
983+
}
984+
return out;
985+
}
986+
921987
} // namespace node
922988

923989
NODE_BINDING_PER_ISOLATE_INIT(

‎src/node_builtins.h‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,24 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
125125
v8::Local<v8::Context> context,
126126
const std::vector<std::string>& lazy_builtins,
127127
std::vector<CodeCacheInfo>* out);
128+
// Adds the given code cache entries, replacing existing entries with the
129+
// same id. Can be called more than once (e.g. with the snapshot's code cache
130+
// and then with caches an embedder built for further builtin ids).
128131
void RefreshCodeCache(const std::vector<CodeCacheInfo>& in);
129132

133+
// Process-wide entries every BuiltinLoader created afterwards starts with
134+
// (each Environment's and the per-context script loader): lets embedders
135+
// whose contexts are not deserialized from a snapshot still compile the
136+
// builtins with a cache. See node::SetBuiltinCodeCache().
137+
static void SetProcessCodeCache(std::vector<CodeCacheInfo> entries);
138+
// Whether builtins compiled without a cache serialize one for later
139+
// consumers (worker threads copy it). See
140+
// ProcessInitializationFlags::kNoHarvestBuiltinCodeCache.
141+
static void SetHarvestCodeCache(bool on);
142+
// For loaders that do not inherit another loader's cache: start from the
143+
// process-wide entries and harvest setting. Workers inherit their parent's.
144+
void SeedFromProcessCodeCache();
145+
130146
void CopySourceAndCodeCacheReferenceFrom(const BuiltinLoader* other);
131147

132148
[[nodiscard]] std::ranges::keys_view<
@@ -217,6 +233,7 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
217233
// avoid bloating the binary size). At runtime any additional compilation is
218234
// done lazily.
219235
bool should_eager_compile_ = false;
236+
bool harvest_code_cache_ = true;
220237
std::unordered_set<std::string> to_eager_compile_;
221238

222239
struct BuiltinCodeCache {

‎test/cctest/test_per_process.cc‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,47 @@
44
#include "gtest/gtest.h"
55
#include "node_test_fixture.h"
66

7+
#include <algorithm>
8+
#include <memory>
79
#include <string>
10+
#include <vector>
811

12+
using node::builtins::BuiltinCodeCacheData;
913
using node::builtins::BuiltinLoader;
1014
using node::builtins::BuiltinSourceMap;
15+
using node::builtins::CodeCacheInfo;
1116

1217
class PerProcessTest : public ::testing::Test {
1318
protected:
1419
static const BuiltinSourceMap get_sources_for_test() {
1520
return *BuiltinLoader().source_.read();
1621
}
22+
23+
// id -> first byte of the cached data, after feeding `batches` in order.
24+
static std::vector<std::pair<std::string, uint8_t>> RefreshCodeCacheWith(
25+
const std::vector<std::vector<CodeCacheInfo>>& batches,
26+
bool seed_from_process = false) {
27+
BuiltinLoader loader;
28+
if (seed_from_process) loader.SeedFromProcessCodeCache();
29+
for (const auto& batch : batches) loader.RefreshCodeCache(batch);
30+
std::vector<std::pair<std::string, uint8_t>> out;
31+
node::RwLock::ScopedReadLock lock(loader.code_cache_->mutex);
32+
EXPECT_TRUE(loader.code_cache_->has_code_cache);
33+
for (const auto& [id, data] : loader.code_cache_->map) {
34+
out.emplace_back(id, data.data[0]);
35+
}
36+
std::sort(out.begin(), out.end());
37+
return out;
38+
}
1739
};
1840

41+
CodeCacheInfo MakeCodeCacheInfo(const std::string& id, uint8_t marker) {
42+
auto* bytes = new uint8_t[4]{marker, marker, marker, marker};
43+
auto cached_data = std::make_shared<v8::ScriptCompiler::CachedData>(
44+
bytes, 4, v8::ScriptCompiler::CachedData::BufferOwned);
45+
return CodeCacheInfo{id, BuiltinCodeCacheData(std::move(cached_data))};
46+
}
47+
1948
namespace {
2049

2150
TEST_F(PerProcessTest, EmbeddedSources) {
@@ -29,4 +58,37 @@ TEST_F(PerProcessTest, EmbeddedSources) {
2958
})) << "BuiltinLoader::source_ should have some 16bit items";
3059
}
3160

61+
// RefreshCodeCache() merges: it can be fed the snapshot's code cache and then
62+
// an embedder's, and the entry supplied last wins for a shared id.
63+
TEST_F(PerProcessTest, RefreshCodeCacheMerges) {
64+
const auto merged = PerProcessTest::RefreshCodeCacheWith({
65+
{MakeCodeCacheInfo("internal/a", 1), MakeCodeCacheInfo("internal/b", 1)},
66+
{MakeCodeCacheInfo("internal/b", 2), MakeCodeCacheInfo("embedder/c", 2)},
67+
});
68+
const std::vector<std::pair<std::string, uint8_t>> expected = {
69+
{"embedder/c", 2}, {"internal/a", 1}, {"internal/b", 2}};
70+
EXPECT_EQ(merged, expected);
71+
72+
// A single call still behaves as before.
73+
const auto single = PerProcessTest::RefreshCodeCacheWith(
74+
{{MakeCodeCacheInfo("internal/a", 7)}});
75+
ASSERT_EQ(single.size(), 1u);
76+
EXPECT_EQ(single[0].second, 7);
77+
}
78+
79+
// SeedFromProcessCodeCache() starts a loader from SetProcessCodeCache()'s
80+
// entries, and a later RefreshCodeCache() (e.g. from a snapshot) merges on top.
81+
TEST_F(PerProcessTest, ProcessCodeCacheSeedsLoaders) {
82+
BuiltinLoader::SetProcessCodeCache(
83+
{MakeCodeCacheInfo("internal/a", 3), MakeCodeCacheInfo("embedder/x", 3)});
84+
EXPECT_EQ(PerProcessTest::RefreshCodeCacheWith({{}}).size(), 0u);
85+
const auto seeded = PerProcessTest::RefreshCodeCacheWith(
86+
{{MakeCodeCacheInfo("internal/a", 4)}}, true);
87+
const std::vector<std::pair<std::string, uint8_t>> expected = {
88+
{"embedder/x", 3}, {"internal/a", 4}};
89+
EXPECT_EQ(seeded, expected);
90+
BuiltinLoader::SetProcessCodeCache({});
91+
EXPECT_TRUE(PerProcessTest::RefreshCodeCacheWith({{}}, true).empty());
92+
}
93+
3294
} // end namespace

0 commit comments

Comments
 (0)