diff --git a/src/env.cc b/src/env.cc index 7ec8c50b1aba..06c6359dbf08 100644 --- a/src/env.cc +++ b/src/env.cc @@ -906,6 +906,9 @@ Environment::Environment(IsolateData* isolate_data, isolate_data->snapshot_data()->code_cache); } } + if (is_main_thread() && !isolate_data->builtin_code_cache().empty()) { + builtin_loader()->RefreshCodeCache(isolate_data->builtin_code_cache()); + } // Compile builtins eagerly when building the snapshot so that inner functions // of essential builtins that are loaded in the snapshot can have faster first diff --git a/src/env.h b/src/env.h index 826ed33f697e..af5f0214506d 100644 --- a/src/env.h +++ b/src/env.h @@ -168,6 +168,13 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { inline uv_loop_t* event_loop() const; inline MultiIsolatePlatform* platform() const; inline const SnapshotData* snapshot_data() const; + // See node::SetBuiltinCodeCache(). + const std::vector& builtin_code_cache() const { + return builtin_code_cache_; + } + void set_builtin_code_cache(std::vector entries) { + builtin_code_cache_ = std::move(entries); + } inline std::shared_ptr options(); inline NodeArrayBufferAllocator* node_allocator() const; @@ -257,6 +264,7 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { MultiIsolatePlatform* platform_; const SnapshotData* snapshot_data_; + std::vector builtin_code_cache_; std::optional snapshot_config_; std::shared_ptr options_; diff --git a/src/node.cc b/src/node.cc index b4e8cdaf2aaa..774867be04a0 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1289,6 +1289,10 @@ InitializeOncePerProcessInternal(const std::vector& args, cppgc::InitializeProcess(allocator); } + if (flags & ProcessInitializationFlags::kNoHarvestBuiltinCodeCache) { + builtins::BuiltinLoader::SetHarvestCodeCache(false); + } + if (!(flags & ProcessInitializationFlags::kNoInitializeV8)) { V8::Initialize(); diff --git a/src/node.h b/src/node.h index e827a46e14dd..8e7d1e6a2516 100644 --- a/src/node.h +++ b/src/node.h @@ -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 @@ -686,6 +691,43 @@ struct InspectorParentHandle { virtual ~InspectorParentHandle() = default; }; +// Code cache for the built-in JavaScript of Environments that are bootstrapped +// rather than deserialized from a snapshot; see SetBuiltinCodeCache(). +class NODE_EXTERN EmbedderBuiltinCodeCache { + public: + struct Entry { + std::string id; // e.g. "internal/bootstrap/node" + std::unique_ptr data; + }; + explicit EmbedderBuiltinCodeCache(std::vector 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 Generate(v8::Local context); + + 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_; + friend NODE_EXTERN v8::ScriptCompiler::CachedData::CompatibilityCheckResult + SetBuiltinCodeCache(IsolateData*, const EmbedderBuiltinCodeCache*); +}; + +// Environments created from `isolate_data` afterwards start with `cache`'s +// entries (they share its buffers; `cache` itself may be freed after the call); +// nullptr clears it. Returns the result of `cache->CompatibilityCheck()` and +// leaves `isolate_data` unchanged unless that is kSuccess. +NODE_EXTERN v8::ScriptCompiler::CachedData::CompatibilityCheckResult +SetBuiltinCodeCache(IsolateData* isolate_data, + const EmbedderBuiltinCodeCache* cache); + // TODO(addaleax): Maybe move per-Environment options parsing here. // Returns nullptr when the Environment cannot be created e.g. there are // pending JavaScript exceptions. diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 43a9a388c2ba..2cefc8d74e1a 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -1,4 +1,6 @@ #include "node_builtins.h" +#include +#include #include "debug_utils-inl.h" #include "env-inl.h" #include "module_wrap.h" @@ -12,7 +14,6 @@ #include "v8-value.h" namespace node { -namespace builtins { using loader::HostDefinedOptions; using v8::Boolean; @@ -44,6 +45,16 @@ using v8::TryCatch; using v8::Undefined; using v8::Value; +namespace builtins { + +namespace { +std::atomic harvest_code_cache{true}; +} // namespace + +void BuiltinLoader::SetHarvestCodeCache(bool on) { + harvest_code_cache = on; +} + BuiltinLoader::BuiltinLoader() : config_(GetConfig()), code_cache_(std::make_shared()) { LoadJavaScriptSource(); @@ -422,6 +433,7 @@ MaybeLocal 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 @@ -593,12 +605,13 @@ bool BuiltinLoader::CompileAllBuiltinsAndCopyCodeCache( void BuiltinLoader::RefreshCodeCache(const std::vector& 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; } @@ -918,6 +931,76 @@ void BuiltinLoader::RegisterExternalReferences( } } // namespace builtins + +struct EmbedderBuiltinCodeCache::Impl { + std::vector entries; +}; + +EmbedderBuiltinCodeCache::EmbedderBuiltinCodeCache(std::vector entries) + : impl_(std::make_unique()) { + impl_->entries.reserve(entries.size()); + for (Entry& e : entries) { + impl_->entries.push_back( + {std::move(e.id), + builtins::BuiltinCodeCacheData( + std::shared_ptr(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(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::Generate( + Local context) { + std::vector out; + builtins::BuiltinLoader loader; + loader.SetEagerCompile(); + std::vector 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( + copy, + static_cast(info.data.length), + ScriptCompiler::CachedData::BufferOwned)}); + } + return out; +} + +ScriptCompiler::CachedData::CompatibilityCheckResult SetBuiltinCodeCache( + IsolateData* isolate_data, const EmbedderBuiltinCodeCache* cache) { + if (cache == nullptr) { + isolate_data->set_builtin_code_cache({}); + return ScriptCompiler::CachedData::kSuccess; + } + auto check = cache->CompatibilityCheck(isolate_data->isolate()); + if (check == ScriptCompiler::CachedData::kSuccess) { + isolate_data->set_builtin_code_cache(cache->impl_->entries); + } else { + per_process::Debug(DebugCategory::CODE_CACHE, + "EmbedderBuiltinCodeCache rejected: %d\n", + static_cast(check)); + } + return check; +} + } // namespace node NODE_BINDING_PER_ISOLATE_INIT( diff --git a/src/node_builtins.h b/src/node_builtins.h index b51b85ff6f23..89fe7f5c1aa7 100644 --- a/src/node_builtins.h +++ b/src/node_builtins.h @@ -125,8 +125,16 @@ class NODE_EXTERN_PRIVATE BuiltinLoader { v8::Local context, const std::vector& lazy_builtins, std::vector* 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& 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< diff --git a/test/cctest/test_environment.cc b/test/cctest/test_environment.cc index 109e224f788d..36fbc0e79d46 100644 --- a/test/cctest/test_environment.cc +++ b/test/cctest/test_environment.cc @@ -1,6 +1,7 @@ #include "libplatform/libplatform.h" #include "node_buffer.h" #include "node_internals.h" +#include "node_realm-inl.h" #include "node_url.h" #include "util.h" @@ -869,6 +870,41 @@ TEST_F(EnvironmentTest, RequestInterruptAtExit) { context->Exit(); } +TEST_F(EnvironmentTest, EmbedderBuiltinCodeCache) { + v8::HandleScope handle_scope(isolate_); + v8::Local context = node::NewContext(isolate_); + v8::Context::Scope context_scope(context); + + std::vector entries = + node::EmbedderBuiltinCodeCache::Generate(context); + ASSERT_GT(entries.size(), 100u); + { + node::EmbedderBuiltinCodeCache cache(std::move(entries)); + EXPECT_EQ(node::SetBuiltinCodeCache(isolate_data_, &cache), + v8::ScriptCompiler::CachedData::kSuccess); + } + std::unique_ptr env( + node::CreateEnvironment(isolate_data_, context, {}, {}), + node::FreeEnvironment); + node::Realm* realm = env->principal_realm(); + EXPECT_EQ(realm->builtins_with_cache.count("internal/bootstrap/node"), 1u); + for (const std::string& id : realm->builtins_without_cache) { + EXPECT_EQ(id.rfind("internal/per_context/", 0), 0u) << id; + } + + uint8_t* bytes = new uint8_t[64](); + std::vector bad; + bad.push_back({"internal/bootstrap/node", + std::make_unique( + bytes, 64, v8::ScriptCompiler::CachedData::BufferOwned)}); + node::EmbedderBuiltinCodeCache bad_cache(std::move(bad)); + EXPECT_NE(node::SetBuiltinCodeCache(isolate_data_, &bad_cache), + v8::ScriptCompiler::CachedData::kSuccess); + EXPECT_FALSE(isolate_data_->builtin_code_cache().empty()); + node::SetBuiltinCodeCache(isolate_data_, nullptr); + EXPECT_TRUE(isolate_data_->builtin_code_cache().empty()); +} + TEST_F(EnvironmentTest, EmbedderPreload) { v8::HandleScope handle_scope(isolate_); v8::Local context = node::NewContext(isolate_); diff --git a/test/cctest/test_per_process.cc b/test/cctest/test_per_process.cc index 7a6f53d56222..937e9cab54dd 100644 --- a/test/cctest/test_per_process.cc +++ b/test/cctest/test_per_process.cc @@ -4,18 +4,45 @@ #include "gtest/gtest.h" #include "node_test_fixture.h" +#include +#include #include +#include +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> RefreshCodeCacheWith( + const std::vector>& batches) { + BuiltinLoader loader; + for (const auto& batch : batches) loader.RefreshCodeCache(batch); + std::vector> 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( + bytes, 4, v8::ScriptCompiler::CachedData::BufferOwned); + return CodeCacheInfo{id, BuiltinCodeCacheData(std::move(cached_data))}; +} + namespace { TEST_F(PerProcessTest, EmbeddedSources) { @@ -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> 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 diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index 045c01211cf2..f3483d46af48 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -79,19 +79,24 @@ NODE_MAIN(int argc, node::argv_type raw_argv[]) { node::FixupMain(argc, raw_argv, &argv); std::vector args(argv, argv + argc); + uint32_t flags = + node::ProcessInitializationFlags::kNoInitializeV8 | + node::ProcessInitializationFlags::kNoInitializeNodeV8Platform | + // This is used to test NODE_REPL_EXTERNAL_MODULE is disabled with + // kDisableNodeOptionsEnv. If other tests need NODE_OPTIONS + // support in the future, split this configuration out as a + // command line option. + node::ProcessInitializationFlags::kDisableNodeOptionsEnv | + node::ProcessInitializationFlags::kNoInitializeCppgc; + auto it = + std::find(args.begin(), args.end(), "--no-harvest-builtin-code-cache"); + if (it != args.end()) { + args.erase(it); + flags |= node::ProcessInitializationFlags::kNoHarvestBuiltinCodeCache; + } std::shared_ptr result = node::InitializeOncePerProcess( - args, - { - node::ProcessInitializationFlags::kNoInitializeV8, - node::ProcessInitializationFlags::kNoInitializeNodeV8Platform, - // This is used to test NODE_REPL_EXTERNAL_MODULE is disabled with - // kDisableNodeOptionsEnv. If other tests need NODE_OPTIONS - // support in the future, split this configuration out as a - // command line option. - node::ProcessInitializationFlags::kDisableNodeOptionsEnv, - node::ProcessInitializationFlags::kNoInitializeCppgc, - }); + args, static_cast(flags)); for (const std::string& error : result->errors()) fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str()); diff --git a/test/embedding/test-embedding-builtin-code-cache-harvest.js b/test/embedding/test-embedding-builtin-code-cache-harvest.js new file mode 100644 index 000000000000..19cdd499c6ea --- /dev/null +++ b/test/embedding/test-embedding-builtin-code-cache-harvest.js @@ -0,0 +1,25 @@ +'use strict'; +// By default a builtin compiled without a code cache serializes one that worker +// threads then start from; with kNoHarvestBuiltinCodeCache it does not. +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +tmpdir.refresh(); +const embedtest = common.resolveBuiltBinary('embedtest'); +const workerScript = 'new (require("worker_threads").Worker)("", { eval: true })'; + +function workerCompileLog(args) { + let log; + spawnSyncAndAssert( + embedtest, ['--', ...args, workerScript], + { cwd: tmpdir.path, env: { ...process.env, NODE_DEBUG_NATIVE: 'CODE_CACHE' } }, + { stderr(output) { log = output; return true; } }); + const worker = log.slice(log.lastIndexOf('Compiling internal/bootstrap/realm')); + assert.notStrictEqual(worker, log); + return worker; +} +assert.match(workerCompileLog([]), /Code cache of internal\/bootstrap\/node \(\w+\) is accepted/); +assert.match(workerCompileLog(['--no-harvest-builtin-code-cache']), + /Compiling internal\/bootstrap\/node without code cache/);