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
17 changes: 16 additions & 1 deletion src/api/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -425,12 +425,23 @@ Environment* CreateEnvironment(
EnvironmentFlags::Flags flags,
ThreadId thread_id,
std::unique_ptr<InspectorParentHandle> inspector_parent_handle,
std::string_view thread_name) {
std::string_view thread_name,
const EmbedderBuiltinCodeCache* builtin_code_cache) {
Isolate* isolate = isolate_data->isolate();

Isolate::Scope isolate_scope(isolate);
HandleScope handle_scope(isolate);

if (builtin_code_cache != nullptr) {
auto check = builtin_code_cache->CompatibilityCheck(isolate);
if (check != v8::ScriptCompiler::CachedData::kSuccess) {
per_process::Debug(DebugCategory::CODE_CACHE,
"EmbedderBuiltinCodeCache rejected: %d\n",
static_cast<int>(check));
return nullptr;
}
}

const bool use_snapshot = context.IsEmpty();
const EnvSerializeInfo* env_snapshot_info = nullptr;
if (use_snapshot) {
Expand All @@ -449,6 +460,10 @@ Environment* CreateEnvironment(
thread_id,
thread_name);
CHECK_NOT_NULL(env);
if (builtin_code_cache != nullptr) {
env->builtin_loader()->RefreshCodeCache(
GetBuiltinCodeCacheEntries(*builtin_code_cache->impl_));
}

if (use_snapshot) {
context = Context::FromSnapshot(isolate,
Expand Down
4 changes: 4 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,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
48 changes: 47 additions & 1 deletion src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,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 an EmbedderBuiltinCodeCache 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 @@ -686,6 +691,46 @@ struct InspectorParentHandle {
virtual ~InspectorParentHandle() = default;
};

// Code cache for the built-in JavaScript of Environments that are bootstrapped
// rather than deserialized from a snapshot; pass to CreateEnvironment(). One
// instance can serve many Environments, which share its buffers.
class NODE_EXTERN EmbedderBuiltinCodeCache {
public:
struct Entry {
std::string id; // e.g. "internal/bootstrap/node"
std::unique_ptr<v8::ScriptCompiler::CachedData> data;
};
explicit EmbedderBuiltinCodeCache(std::vector<Entry> entries);
~EmbedderBuiltinCodeCache();

// Compiles every built-in module in `context`, which must come from
// NewContext(), and returns their code caches; empty on failure.
static std::vector<Entry> Generate(v8::Local<v8::Context> context);

// Whether the entries can be used in `isolate`; CreateEnvironment() returns
// nullptr for a cache that does not pass.
v8::ScriptCompiler::CachedData::CompatibilityCheckResult CompatibilityCheck(
v8::Isolate* isolate) const;

EmbedderBuiltinCodeCache(const EmbedderBuiltinCodeCache&) = delete;
EmbedderBuiltinCodeCache& operator=(const EmbedderBuiltinCodeCache&) = delete;

struct Impl;

private:
std::unique_ptr<Impl> impl_;
friend NODE_EXTERN Environment* CreateEnvironment(
IsolateData*,
v8::Local<v8::Context>,
const std::vector<std::string>&,
const std::vector<std::string>&,
EnvironmentFlags::Flags,
ThreadId,
std::unique_ptr<InspectorParentHandle>,
std::string_view,
const EmbedderBuiltinCodeCache*);
};

// 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 All @@ -699,7 +744,8 @@ NODE_EXTERN Environment* CreateEnvironment(
EnvironmentFlags::Flags flags = EnvironmentFlags::kDefaultFlags,
ThreadId thread_id = {} /* allocates a thread id automatically */,
std::unique_ptr<InspectorParentHandle> inspector_parent_handle = {},
std::string_view thread_name = {});
std::string_view thread_name = {},
const EmbedderBuiltinCodeCache* builtin_code_cache = nullptr);

// Returns a handle that can be passed to `LoadEnvironment()`, making the
// child Environment accessible to the inspector as if it were a Node.js Worker.
Expand Down
83 changes: 77 additions & 6 deletions src/node_builtins.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include "node_builtins.h"
#include <atomic>
#include <cstring>
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "module_wrap.h"
Expand All @@ -12,7 +14,6 @@
#include "v8-value.h"

namespace node {
namespace builtins {

using loader::HostDefinedOptions;
using v8::Boolean;
Expand Down Expand Up @@ -44,6 +45,16 @@ using v8::TryCatch;
using v8::Undefined;
using v8::Value;

namespace builtins {

namespace {
std::atomic<bool> harvest_code_cache{true};
} // namespace

void BuiltinLoader::SetHarvestCodeCache(bool on) {
harvest_code_cache = on;
}

BuiltinLoader::BuiltinLoader()
: config_(GetConfig()), code_cache_(std::make_shared<BuiltinCodeCache>()) {
LoadJavaScriptSource();
Expand Down Expand Up @@ -422,6 +433,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 +605,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 +931,64 @@ void BuiltinLoader::RegisterExternalReferences(
}

} // namespace builtins

struct EmbedderBuiltinCodeCache::Impl {
std::vector<builtins::CodeCacheInfo> entries;
};

EmbedderBuiltinCodeCache::EmbedderBuiltinCodeCache(std::vector<Entry> entries)
: impl_(std::make_unique<Impl>()) {
impl_->entries.reserve(entries.size());
for (Entry& e : entries) {
impl_->entries.push_back(
{std::move(e.id),
builtins::BuiltinCodeCacheData(
std::shared_ptr<ScriptCompiler::CachedData>(std::move(e.data)))});
}
}

EmbedderBuiltinCodeCache::~EmbedderBuiltinCodeCache() = default;

ScriptCompiler::CachedData::CompatibilityCheckResult
EmbedderBuiltinCodeCache::CompatibilityCheck(Isolate* isolate) const {
for (const builtins::CodeCacheInfo& info : impl_->entries) {
ScriptCompiler::CachedData probe(
info.data.data,
static_cast<int>(info.data.length),
ScriptCompiler::CachedData::BufferNotOwned);
auto result = probe.CompatibilityCheck(isolate);
if (result != ScriptCompiler::CachedData::kSuccess) return result;
}
return ScriptCompiler::CachedData::kSuccess;
}

std::vector<EmbedderBuiltinCodeCache::Entry> EmbedderBuiltinCodeCache::Generate(
Local<Context> context) {
std::vector<Entry> 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) {
uint8_t* copy = new uint8_t[info.data.length];
memcpy(copy, info.data.data, info.data.length);
out.push_back({info.id,
std::make_unique<ScriptCompiler::CachedData>(
copy,
static_cast<int>(info.data.length),
ScriptCompiler::CachedData::BufferOwned)});
}
return out;
}

const std::vector<builtins::CodeCacheInfo>& GetBuiltinCodeCacheEntries(
const EmbedderBuiltinCodeCache::Impl& impl) {
return impl.entries;
}

} // namespace node

NODE_BINDING_PER_ISOLATE_INIT(
Expand Down
8 changes: 8 additions & 0 deletions src/node_builtins.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,16 @@ 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);

// 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
6 changes: 6 additions & 0 deletions src/node_internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ struct sockaddr;

namespace node {

namespace builtins {
struct CodeCacheInfo;
}
const std::vector<builtins::CodeCacheInfo>& GetBuiltinCodeCacheEntries(
const EmbedderBuiltinCodeCache::Impl& impl);

namespace builtins {
class BuiltinLoader;
}
Expand Down
45 changes: 45 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,22 @@ 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);
}

} // end namespace
Loading
Loading