Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,10 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
cppgc::InitializeProcess(allocator);
}

if (flags & ProcessInitializationFlags::kNoHarvestBuiltinCodeCache) {
builtins::BuiltinLoader::SetHarvestCodeCache(false);
}

if (!(flags & ProcessInitializationFlags::kNoInitializeV8)) {
V8::Initialize();

Expand Down
35 changes: 35 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ enum Flags : uint32_t {
kNoInitializeCppgc = 1 << 13,
// Initialize the process for predictable snapshot generation.
kGeneratePredictableSnapshot = 1 << 14,
// Do not serialize a code cache for builtins that had to be compiled without
// one. By default such caches are kept so that worker threads created later
// start faster; an embedder that supplies its own cache (SetBuiltinCodeCache)
// or never creates workers only pays for the serialization.
kNoHarvestBuiltinCodeCache = 1 << 15,

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

// A V8 code cache for one of Node.js's built-in JavaScript modules.
struct BuiltinCodeCacheEntry {
std::string id; // e.g. "internal/bootstrap/node"
const uint8_t* data; // must stay valid for the rest of the process
size_t length;
};
struct OwnedBuiltinCodeCacheEntry {
std::string id;
std::vector<uint8_t> data;
};

// Contexts and Environments created from Node.js's built-in snapshot get the
// builtins' code cache from that snapshot. An embedder that bootstraps them
// from scratch (its own isolate/context, no EmbedderSnapshotData) can supply a
// cache built ahead of time with GenerateBuiltinCodeCache() against the same
// kind of isolate (same V8 version, flags and read-only snapshot): every
// Environment created afterwards, and the loader for the per-context scripts
// 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(
const std::vector<BuiltinCodeCacheEntry>& entries);

// Compiles every built-in module in `context` (which must have been created
// with node::NewContext() in the kind of isolate the cache is for) and returns
// their code caches, e.g. for a build step that embeds them and passes them to
// SetBuiltinCodeCache() at runtime. Returns an empty vector on failure.
NODE_EXTERN std::vector<OwnedBuiltinCodeCacheEntry> GenerateBuiltinCodeCache(
v8::Local<v8::Context> context);

// TODO(addaleax): Maybe move per-Environment options parsing here.
// Returns nullptr when the Environment cannot be created e.g. there are
// pending JavaScript exceptions.
Expand Down
77 changes: 71 additions & 6 deletions src/node_builtins.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
#include "v8-value.h"

namespace node {
namespace builtins {

using loader::HostDefinedOptions;
using v8::Boolean;
Expand Down Expand Up @@ -44,9 +43,41 @@ using v8::TryCatch;
using v8::Undefined;
using v8::Value;

namespace builtins {

namespace {
struct ProcessCodeCache {
Mutex mutex;
std::vector<CodeCacheInfo> entries;
bool harvest = true;
};
ProcessCodeCache& GetProcessCodeCache() {
static ProcessCodeCache process_code_cache;
return process_code_cache;
}
} // namespace

void BuiltinLoader::SetProcessCodeCache(std::vector<CodeCacheInfo> entries) {
ProcessCodeCache& pcc = GetProcessCodeCache();
Mutex::ScopedLock lock(pcc.mutex);
pcc.entries = std::move(entries);
}

void BuiltinLoader::SetHarvestCodeCache(bool on) {
ProcessCodeCache& pcc = GetProcessCodeCache();
Mutex::ScopedLock lock(pcc.mutex);
pcc.harvest = on;
}

BuiltinLoader::BuiltinLoader()
: config_(GetConfig()), code_cache_(std::make_shared<BuiltinCodeCache>()) {
LoadJavaScriptSource();
{
ProcessCodeCache& pcc = GetProcessCodeCache();
Mutex::ScopedLock lock(pcc.mutex);
harvest_code_cache_ = pcc.harvest;
if (!pcc.entries.empty()) RefreshCodeCache(pcc.entries);
}
#ifdef NODE_SHARED_BUILTIN_UNDICI_UNDICI_PATH
AddExternalizedBuiltin("internal/deps/undici/undici",
STRINGIFY(NODE_SHARED_BUILTIN_UNDICI_UNDICI_PATH));
Expand Down Expand Up @@ -422,6 +453,7 @@ MaybeLocal<Data> BuiltinLoader::LookupAndCompile(
}

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

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

} // namespace builtins

void SetBuiltinCodeCache(const std::vector<BuiltinCodeCacheEntry>& entries) {
std::vector<builtins::CodeCacheInfo> infos;
infos.reserve(entries.size());
for (const BuiltinCodeCacheEntry& e : entries) {
auto cached_data = std::make_shared<ScriptCompiler::CachedData>(
e.data,
static_cast<int>(e.length),
ScriptCompiler::CachedData::BufferNotOwned);
infos.push_back({e.id, builtins::BuiltinCodeCacheData(cached_data)});
}
builtins::BuiltinLoader::SetProcessCodeCache(std::move(infos));
}

std::vector<OwnedBuiltinCodeCacheEntry> GenerateBuiltinCodeCache(
Local<Context> context) {
std::vector<OwnedBuiltinCodeCacheEntry> out;
builtins::BuiltinLoader loader;
loader.SetEagerCompile();
std::vector<builtins::CodeCacheInfo> infos;
if (!loader.CompileAllBuiltinsAndCopyCodeCache(context, {}, &infos)) {
return out;
}
out.reserve(infos.size());
for (const builtins::CodeCacheInfo& info : infos) {
out.push_back({info.id,
std::vector<uint8_t>(info.data.data,
info.data.data + info.data.length)});
}
return out;
}

} // namespace node

NODE_BINDING_PER_ISOLATE_INIT(
Expand Down
14 changes: 14 additions & 0 deletions src/node_builtins.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,21 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
v8::Local<v8::Context> context,
const std::vector<std::string>& lazy_builtins,
std::vector<CodeCacheInfo>* out);
// Adds the given code cache entries, replacing existing entries with the
// same id. Can be called more than once (e.g. with the snapshot's code cache
// and then with caches an embedder built for further builtin ids).
void RefreshCodeCache(const std::vector<CodeCacheInfo>& in);

// Process-wide entries every BuiltinLoader created afterwards starts with
// (each Environment's and the per-context script loader): lets embedders
// whose contexts are not deserialized from a snapshot still compile the
// builtins with a cache. See node::SetBuiltinCodeCache().
static void SetProcessCodeCache(std::vector<CodeCacheInfo> entries);
// Whether builtins compiled without a cache serialize one for later
// consumers (worker threads copy it). See
// ProcessInitializationFlags::kNoHarvestBuiltinCodeCache.
static void SetHarvestCodeCache(bool on);

void CopySourceAndCodeCacheReferenceFrom(const BuiltinLoader* other);

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

struct BuiltinCodeCache {
Expand Down
59 changes: 59 additions & 0 deletions test/cctest/test_per_process.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,45 @@
#include "gtest/gtest.h"
#include "node_test_fixture.h"

#include <algorithm>
#include <memory>
#include <string>
#include <vector>

using node::builtins::BuiltinCodeCacheData;
using node::builtins::BuiltinLoader;
using node::builtins::BuiltinSourceMap;
using node::builtins::CodeCacheInfo;

class PerProcessTest : public ::testing::Test {
protected:
static const BuiltinSourceMap get_sources_for_test() {
return *BuiltinLoader().source_.read();
}

// id -> first byte of the cached data, after feeding `batches` in order.
static std::vector<std::pair<std::string, uint8_t>> RefreshCodeCacheWith(
const std::vector<std::vector<CodeCacheInfo>>& batches) {
BuiltinLoader loader;
for (const auto& batch : batches) loader.RefreshCodeCache(batch);
std::vector<std::pair<std::string, uint8_t>> out;
node::RwLock::ScopedReadLock lock(loader.code_cache_->mutex);
EXPECT_TRUE(loader.code_cache_->has_code_cache);
for (const auto& [id, data] : loader.code_cache_->map) {
out.emplace_back(id, data.data[0]);
}
std::sort(out.begin(), out.end());
return out;
}
};

CodeCacheInfo MakeCodeCacheInfo(const std::string& id, uint8_t marker) {
auto* bytes = new uint8_t[4]{marker, marker, marker, marker};
auto cached_data = std::make_shared<v8::ScriptCompiler::CachedData>(
bytes, 4, v8::ScriptCompiler::CachedData::BufferOwned);
return CodeCacheInfo{id, BuiltinCodeCacheData(std::move(cached_data))};
}

namespace {

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

// RefreshCodeCache() merges: it can be fed the snapshot's code cache and then
// an embedder's, and the entry supplied last wins for a shared id.
TEST_F(PerProcessTest, RefreshCodeCacheMerges) {
const auto merged = PerProcessTest::RefreshCodeCacheWith({
{MakeCodeCacheInfo("internal/a", 1), MakeCodeCacheInfo("internal/b", 1)},
{MakeCodeCacheInfo("internal/b", 2), MakeCodeCacheInfo("embedder/c", 2)},
});
const std::vector<std::pair<std::string, uint8_t>> expected = {
{"embedder/c", 2}, {"internal/a", 1}, {"internal/b", 2}};
EXPECT_EQ(merged, expected);

// A single call still behaves as before.
const auto single = PerProcessTest::RefreshCodeCacheWith(
{{MakeCodeCacheInfo("internal/a", 7)}});
ASSERT_EQ(single.size(), 1u);
EXPECT_EQ(single[0].second, 7);
}

// SetProcessCodeCache() seeds every BuiltinLoader created afterwards, and a
// later RefreshCodeCache() (e.g. from a snapshot) merges on top of the seed.
TEST_F(PerProcessTest, ProcessCodeCacheSeedsNewLoaders) {
BuiltinLoader::SetProcessCodeCache(
{MakeCodeCacheInfo("internal/a", 3), MakeCodeCacheInfo("embedder/x", 3)});
const auto seeded = PerProcessTest::RefreshCodeCacheWith(
{{MakeCodeCacheInfo("internal/a", 4)}});
const std::vector<std::pair<std::string, uint8_t>> expected = {
{"embedder/x", 3}, {"internal/a", 4}};
EXPECT_EQ(seeded, expected);
BuiltinLoader::SetProcessCodeCache({});
EXPECT_TRUE(PerProcessTest::RefreshCodeCacheWith({{}}).empty());
}

} // end namespace
Loading
Loading