Skip to content

Commit 136d2ad

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 136d2ad

7 files changed

Lines changed: 316 additions & 17 deletions

File tree

‎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: 71 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,9 +43,41 @@ 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+
4772
BuiltinLoader::BuiltinLoader()
4873
: config_(GetConfig()), code_cache_(std::make_shared<BuiltinCodeCache>()) {
4974
LoadJavaScriptSource();
75+
{
76+
ProcessCodeCache& pcc = GetProcessCodeCache();
77+
Mutex::ScopedLock lock(pcc.mutex);
78+
harvest_code_cache_ = pcc.harvest;
79+
if (!pcc.entries.empty()) RefreshCodeCache(pcc.entries);
80+
}
5081
#ifdef NODE_SHARED_BUILTIN_UNDICI_UNDICI_PATH
5182
AddExternalizedBuiltin("internal/deps/undici/undici",
5283
STRINGIFY(NODE_SHARED_BUILTIN_UNDICI_UNDICI_PATH));
@@ -422,6 +453,7 @@ MaybeLocal<Data> BuiltinLoader::LookupAndCompile(
422453
}
423454

424455
if (result == Result::kWithoutCache && optional_realm != nullptr &&
456+
harvest_code_cache_ &&
425457
!optional_realm->env()->isolate_data()->is_building_snapshot()) {
426458
// We failed to accept this cache, maybe because it was rejected, maybe
427459
// because it wasn't present. Either way, we'll attempt to replace this
@@ -593,12 +625,13 @@ bool BuiltinLoader::CompileAllBuiltinsAndCopyCodeCache(
593625

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

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

923988
NODE_BINDING_PER_ISOLATE_INIT(

‎src/node_builtins.h‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,21 @@ 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+
130143
void CopySourceAndCodeCacheReferenceFrom(const BuiltinLoader* other);
131144

132145
[[nodiscard]] std::ranges::keys_view<
@@ -217,6 +230,7 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
217230
// avoid bloating the binary size). At runtime any additional compilation is
218231
// done lazily.
219232
bool should_eager_compile_ = false;
233+
bool harvest_code_cache_ = true;
220234
std::unordered_set<std::string> to_eager_compile_;
221235

222236
struct BuiltinCodeCache {

‎test/cctest/test_per_process.cc‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,45 @@
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+
BuiltinLoader loader;
27+
for (const auto& batch : batches) loader.RefreshCodeCache(batch);
28+
std::vector<std::pair<std::string, uint8_t>> out;
29+
node::RwLock::ScopedReadLock lock(loader.code_cache_->mutex);
30+
EXPECT_TRUE(loader.code_cache_->has_code_cache);
31+
for (const auto& [id, data] : loader.code_cache_->map) {
32+
out.emplace_back(id, data.data[0]);
33+
}
34+
std::sort(out.begin(), out.end());
35+
return out;
36+
}
1737
};
1838

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

2148
TEST_F(PerProcessTest, EmbeddedSources) {
@@ -29,4 +56,36 @@ TEST_F(PerProcessTest, EmbeddedSources) {
2956
})) << "BuiltinLoader::source_ should have some 16bit items";
3057
}
3158

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

0 commit comments

Comments
 (0)