src: make BuiltinLoader threadsafe and non-global

As discussed in https://github.com/nodejs/node/pull/45888, using a
global `BuiltinLoader` instance is probably undesirable in a world
in which embedders are able to create Node.js Environments with
different sources and therefore mutually incompatible code
caching properties.

This PR makes it so that `BuiltinLoader` is no longer a global
singleton and instead only shared between `Environment`s that
have a direct relation to each other, and addresses a few
thread safety issues along with that.

PR-URL: https://github.com/nodejs/node/pull/45942
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
Anna Henningsen 2023-01-18 23:03:00 +01:00 committed by GitHub
parent 5d560978ff
commit d896f5befd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 347 additions and 173 deletions

View File

@ -473,7 +473,7 @@ MaybeLocal<Value> LoadEnvironment(
return LoadEnvironment(
env, [&](const StartExecutionCallbackInfo& info) -> MaybeLocal<Value> {
std::string name = "embedder_main_" + std::to_string(env->thread_id());
builtins::BuiltinLoader::Add(name.c_str(), main_script_source_utf8);
env->builtin_loader()->Add(name.c_str(), main_script_source_utf8);
Realm* realm = env->principal_realm();
return realm->ExecuteBootstrapper(name.c_str());
@ -714,10 +714,17 @@ Maybe<bool> InitializePrimordials(Local<Context> context) {
"internal/per_context/messageport",
nullptr};
// We do not have access to a per-Environment BuiltinLoader instance
// at this point, because this code runs before an Environment exists
// in the first place. However, creating BuiltinLoader instances is
// relatively cheap and all the scripts that we may want to run at
// startup are always present in it.
thread_local builtins::BuiltinLoader builtin_loader;
for (const char** module = context_files; *module != nullptr; module++) {
Local<Value> arguments[] = {exports, primordials};
if (builtins::BuiltinLoader::CompileAndCall(
context, *module, arraysize(arguments), arguments, nullptr)
if (builtin_loader
.CompileAndCall(
context, *module, arraysize(arguments), arguments, nullptr)
.IsEmpty()) {
// Execution failed during context creation.
return Nothing<bool>();

View File

@ -436,6 +436,10 @@ inline std::vector<double>* Environment::destroy_async_id_list() {
return &destroy_async_id_list_;
}
inline builtins::BuiltinLoader* Environment::builtin_loader() {
return &builtin_loader_;
}
inline double Environment::new_async_id() {
async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter] += 1;
return async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter];

View File

@ -675,6 +675,15 @@ Environment::Environment(IsolateData* isolate_data,
thread_id_(thread_id.id == static_cast<uint64_t>(-1)
? AllocateEnvironmentThreadId().id
: thread_id.id) {
#ifdef NODE_V8_SHARED_RO_HEAP
if (!is_main_thread()) {
CHECK_NOT_NULL(isolate_data->worker_context());
// TODO(addaleax): Adjust for the embedder API snapshot support changes
builtin_loader()->CopySourceAndCodeCacheReferenceFrom(
isolate_data->worker_context()->env()->builtin_loader());
}
#endif
// We'll be creating new objects so make sure we've entered the context.
HandleScope handle_scope(isolate);

View File

@ -721,6 +721,8 @@ class Environment : public MemoryRetainer {
// List of id's that have been destroyed and need the destroy() cb called.
inline std::vector<double>* destroy_async_id_list();
builtins::BuiltinLoader* builtin_loader();
std::unordered_multimap<int, loader::ModuleWrap*> hash_to_module_map;
std::unordered_map<uint32_t, loader::ModuleWrap*> id_to_module_map;
std::unordered_map<uint32_t, contextify::ContextifyScript*>
@ -1144,6 +1146,8 @@ class Environment : public MemoryRetainer {
std::unique_ptr<Realm> principal_realm_ = nullptr;
builtins::BuiltinLoader builtin_loader_;
// Used by allocate_managed_buffer() and release_managed_buffer() to keep
// track of the BackingStore for a given pointer.
std::unordered_map<char*, std::unique_ptr<v8::BackingStore>>

View File

@ -126,8 +126,6 @@
namespace node {
using builtins::BuiltinLoader;
using v8::EscapableHandleScope;
using v8::Isolate;
using v8::Local;
@ -1183,9 +1181,6 @@ ExitCode LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr,
}
}
if ((*snapshot_data_ptr) != nullptr) {
BuiltinLoader::RefreshCodeCache((*snapshot_data_ptr)->code_cache);
}
NodeMainInstance main_instance(*snapshot_data_ptr,
uv_default_loop(),
per_process::v8_platform.Platform(),

View File

@ -631,13 +631,13 @@ void GetInternalBinding(const FunctionCallbackInfo<Value>& args) {
CHECK(exports->SetPrototype(context, Null(isolate)).FromJust());
DefineConstants(isolate, exports);
} else if (!strcmp(*module_v, "natives")) {
exports = builtins::BuiltinLoader::GetSourceObject(context);
exports = realm->env()->builtin_loader()->GetSourceObject(context);
// Legacy feature: process.binding('natives').config contains stringified
// config.gypi
CHECK(exports
->Set(context,
realm->isolate_data()->config_string(),
builtins::BuiltinLoader::GetConfigString(isolate))
realm->env()->builtin_loader()->GetConfigString(isolate))
.FromJust());
} else {
return THROW_ERR_INVALID_MODULE(isolate, "No such binding: %s", *module_v);

View File

@ -3,6 +3,7 @@
#include "env-inl.h"
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_threadsafe_cow-inl.h"
#include "simdutf.h"
#include "util-inl.h"
@ -32,9 +33,8 @@ using v8::String;
using v8::Undefined;
using v8::Value;
BuiltinLoader BuiltinLoader::instance_;
BuiltinLoader::BuiltinLoader() : config_(GetConfig()), has_code_cache_(false) {
BuiltinLoader::BuiltinLoader()
: config_(GetConfig()), code_cache_(std::make_shared<BuiltinCodeCache>()) {
LoadJavaScriptSource();
#ifdef NODE_SHARED_BUILTIN_CJS_MODULE_LEXER_LEXER_PATH
AddExternalizedBuiltin(
@ -54,25 +54,21 @@ BuiltinLoader::BuiltinLoader() : config_(GetConfig()), has_code_cache_(false) {
#endif // NODE_SHARED_BUILTIN_UNDICI_UNDICI_PATH
}
BuiltinLoader* BuiltinLoader::GetInstance() {
return &instance_;
}
bool BuiltinLoader::Exists(const char* id) {
auto& source = GetInstance()->source_;
return source.find(id) != source.end();
auto source = source_.read();
return source->find(id) != source->end();
}
bool BuiltinLoader::Add(const char* id, const UnionBytes& source) {
auto result = GetInstance()->source_.emplace(id, source);
auto result = source_.write()->emplace(id, source);
return result.second;
}
Local<Object> BuiltinLoader::GetSourceObject(Local<Context> context) {
Isolate* isolate = context->GetIsolate();
Local<Object> out = Object::New(isolate);
auto& source = GetInstance()->source_;
for (auto const& x : source) {
auto source = source_.read();
for (auto const& x : *source) {
Local<String> key = OneByteString(isolate, x.first.c_str(), x.first.size());
out->Set(context, key, x.second.ToStringChecked(isolate)).FromJust();
}
@ -80,23 +76,21 @@ Local<Object> BuiltinLoader::GetSourceObject(Local<Context> context) {
}
Local<String> BuiltinLoader::GetConfigString(Isolate* isolate) {
return GetInstance()->config_.ToStringChecked(isolate);
return config_.ToStringChecked(isolate);
}
std::vector<std::string> BuiltinLoader::GetBuiltinIds() {
std::vector<std::string> BuiltinLoader::GetBuiltinIds() const {
std::vector<std::string> ids;
ids.reserve(source_.size());
for (auto const& x : source_) {
auto source = source_.read();
ids.reserve(source->size());
for (auto const& x : *source) {
ids.emplace_back(x.first);
}
return ids;
}
void BuiltinLoader::InitializeBuiltinCategories() {
if (builtin_categories_.is_initialized) {
DCHECK(!builtin_categories_.can_be_required.empty());
return;
}
BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
BuiltinCategories builtin_categories;
std::vector<std::string> prefixes = {
#if !HAVE_OPENSSL
@ -110,10 +104,10 @@ void BuiltinLoader::InitializeBuiltinCategories() {
"internal/main/"
};
builtin_categories_.can_be_required.emplace(
builtin_categories.can_be_required.emplace(
"internal/deps/cjs-module-lexer/lexer");
builtin_categories_.cannot_be_required = std::set<std::string> {
builtin_categories.cannot_be_required = std::set<std::string> {
#if !HAVE_INSPECTOR
"inspector", "inspector/promises", "internal/util/inspector",
#endif // !HAVE_INSPECTOR
@ -136,55 +130,35 @@ void BuiltinLoader::InitializeBuiltinCategories() {
"internal/v8_prof_processor",
};
for (auto const& x : source_) {
auto source = source_.read();
for (auto const& x : *source) {
const std::string& id = x.first;
for (auto const& prefix : prefixes) {
if (prefix.length() > id.length()) {
continue;
}
if (id.find(prefix) == 0 &&
builtin_categories_.can_be_required.count(id) == 0) {
builtin_categories_.cannot_be_required.emplace(id);
builtin_categories.can_be_required.count(id) == 0) {
builtin_categories.cannot_be_required.emplace(id);
}
}
}
for (auto const& x : source_) {
for (auto const& x : *source) {
const std::string& id = x.first;
if (0 == builtin_categories_.cannot_be_required.count(id)) {
builtin_categories_.can_be_required.emplace(id);
if (0 == builtin_categories.cannot_be_required.count(id)) {
builtin_categories.can_be_required.emplace(id);
}
}
builtin_categories_.is_initialized = true;
return builtin_categories;
}
const std::set<std::string>& BuiltinLoader::GetCannotBeRequired() {
InitializeBuiltinCategories();
return builtin_categories_.cannot_be_required;
}
const std::set<std::string>& BuiltinLoader::GetCanBeRequired() {
InitializeBuiltinCategories();
return builtin_categories_.can_be_required;
}
bool BuiltinLoader::CanBeRequired(const char* id) {
return GetCanBeRequired().count(id) == 1;
}
bool BuiltinLoader::CannotBeRequired(const char* id) {
return GetCannotBeRequired().count(id) == 1;
}
BuiltinCodeCacheMap* BuiltinLoader::code_cache() {
return &code_cache_;
}
ScriptCompiler::CachedData* BuiltinLoader::GetCodeCache(const char* id) const {
Mutex::ScopedLock lock(code_cache_mutex_);
const auto it = code_cache_.find(id);
if (it == code_cache_.end()) {
const ScriptCompiler::CachedData* BuiltinLoader::GetCodeCache(
const char* id) const {
RwLock::ScopedReadLock lock(code_cache_->mutex);
const auto it = code_cache_->map.find(id);
if (it == code_cache_->map.end()) {
// The module has not been compiled before.
return nullptr;
}
@ -209,12 +183,13 @@ static std::string OnDiskFileName(const char* id) {
#endif // NODE_BUILTIN_MODULES_PATH
MaybeLocal<String> BuiltinLoader::LoadBuiltinSource(Isolate* isolate,
const char* id) {
const char* id) const {
auto source = source_.read();
#ifdef NODE_BUILTIN_MODULES_PATH
if (strncmp(id, "embedder_main_", strlen("embedder_main_")) == 0) {
#endif // NODE_BUILTIN_MODULES_PATH
const auto source_it = source_.find(id);
if (UNLIKELY(source_it == source_.end())) {
const auto source_it = source->find(id);
if (UNLIKELY(source_it == source->end())) {
fprintf(stderr, "Cannot find native builtin: \"%s\".\n", id);
ABORT();
}
@ -239,14 +214,31 @@ MaybeLocal<String> BuiltinLoader::LoadBuiltinSource(Isolate* isolate,
#endif // NODE_BUILTIN_MODULES_PATH
}
namespace {
static Mutex externalized_builtins_mutex;
std::unordered_map<std::string, std::string> externalized_builtin_sources;
} // namespace
void BuiltinLoader::AddExternalizedBuiltin(const char* id,
const char* filename) {
std::string source;
int r = ReadFileSync(&source, filename);
if (r != 0) {
fprintf(
stderr, "Cannot load externalized builtin: \"%s:%s\".\n", id, filename);
ABORT();
{
Mutex::ScopedLock lock(externalized_builtins_mutex);
auto it = externalized_builtin_sources.find(id);
if (it != externalized_builtin_sources.end()) {
source = it->second;
}
{
int r = ReadFileSync(&source, filename);
if (r != 0) {
fprintf(stderr,
"Cannot load externalized builtin: \"%s:%s\".\n",
id,
filename);
ABORT();
}
externalized_builtin_sources[id] = source;
}
}
Add(id, source);
@ -291,12 +283,12 @@ MaybeLocal<Function> BuiltinLoader::LookupAndCompileInternal(
// `CompileFunction()` call below, because this function may recurse if
// there is a syntax error during bootstrap (because the fatal exception
// handler is invoked, which may load built-in modules).
Mutex::ScopedLock lock(code_cache_mutex_);
auto cache_it = code_cache_.find(id);
if (cache_it != code_cache_.end()) {
RwLock::ScopedLock lock(code_cache_->mutex);
auto cache_it = code_cache_->map.find(id);
if (cache_it != code_cache_->map.end()) {
// Transfer ownership to ScriptCompiler::Source later.
cached_data = cache_it->second.release();
code_cache_.erase(cache_it);
code_cache_->map.erase(cache_it);
}
}
@ -357,15 +349,8 @@ MaybeLocal<Function> BuiltinLoader::LookupAndCompileInternal(
CHECK_NOT_NULL(new_cached_data);
{
Mutex::ScopedLock lock(code_cache_mutex_);
const auto it = code_cache_.find(id);
// TODO(joyeecheung): it's safer for each thread to have its own
// copy of the code cache map.
if (it == code_cache_.end()) {
code_cache_.emplace(id, std::move(new_cached_data));
} else {
it->second.reset(new_cached_data.release());
}
RwLock::ScopedLock lock(code_cache_->mutex);
code_cache_->map[id] = std::move(new_cached_data);
}
return scope.Escape(fun);
@ -428,9 +413,10 @@ MaybeLocal<Function> BuiltinLoader::LookupAndCompile(Local<Context> context,
};
}
MaybeLocal<Function> maybe = GetInstance()->LookupAndCompileInternal(
context, id, &parameters, &result);
MaybeLocal<Function> maybe =
LookupAndCompileInternal(context, id, &parameters, &result);
if (optional_realm != nullptr) {
DCHECK_EQ(this, optional_realm->env()->builtin_loader());
RecordResult(id, result, optional_realm);
}
return maybe;
@ -506,8 +492,7 @@ MaybeLocal<Value> BuiltinLoader::CompileAndCall(Local<Context> context,
}
bool BuiltinLoader::CompileAllBuiltins(Local<Context> context) {
BuiltinLoader* loader = GetInstance();
std::vector<std::string> ids = loader->GetBuiltinIds();
std::vector<std::string> ids = GetBuiltinIds();
bool all_succeeded = true;
std::string v8_tools_prefix = "internal/deps/v8/tools/";
for (const auto& id : ids) {
@ -515,7 +500,7 @@ bool BuiltinLoader::CompileAllBuiltins(Local<Context> context) {
continue;
}
v8::TryCatch bootstrapCatch(context->GetIsolate());
USE(loader->LookupAndCompile(context, id.c_str(), nullptr));
USE(LookupAndCompile(context, id.c_str(), nullptr));
if (bootstrapCatch.HasCaught()) {
per_process::Debug(DebugCategory::CODE_CACHE,
"Failed to compile code cache for %s\n",
@ -527,11 +512,9 @@ bool BuiltinLoader::CompileAllBuiltins(Local<Context> context) {
return all_succeeded;
}
void BuiltinLoader::CopyCodeCache(std::vector<CodeCacheInfo>* out) {
BuiltinLoader* loader = GetInstance();
Mutex::ScopedLock lock(loader->code_cache_mutex());
auto in = loader->code_cache();
for (auto const& item : *in) {
void BuiltinLoader::CopyCodeCache(std::vector<CodeCacheInfo>* out) const {
RwLock::ScopedReadLock lock(code_cache_->mutex);
for (auto const& item : code_cache_->map) {
out->push_back(
{item.first,
{item.second->data, item.second->data + item.second->length}});
@ -539,24 +522,16 @@ void BuiltinLoader::CopyCodeCache(std::vector<CodeCacheInfo>* out) {
}
void BuiltinLoader::RefreshCodeCache(const std::vector<CodeCacheInfo>& in) {
BuiltinLoader* loader = GetInstance();
Mutex::ScopedLock lock(loader->code_cache_mutex());
auto out = loader->code_cache();
RwLock::ScopedLock lock(code_cache_->mutex);
for (auto const& item : in) {
size_t length = item.data.size();
uint8_t* buffer = new uint8_t[length];
memcpy(buffer, item.data.data(), length);
auto new_cache = std::make_unique<v8::ScriptCompiler::CachedData>(
buffer, length, v8::ScriptCompiler::CachedData::BufferOwned);
auto cache_it = out->find(item.id);
if (cache_it != out->end()) {
// Release the old cache and replace it with the new copy.
cache_it->second.reset(new_cache.release());
} else {
out->emplace(item.id, new_cache.release());
}
code_cache_->map[item.id] = std::move(new_cache);
}
loader->has_code_cache_ = true;
code_cache_->has_code_cache = true;
}
void BuiltinLoader::GetBuiltinCategories(
@ -566,20 +541,19 @@ void BuiltinLoader::GetBuiltinCategories(
Local<Context> context = env->context();
Local<Object> result = Object::New(isolate);
// Copy from the per-process categories
std::set<std::string> cannot_be_required =
GetInstance()->GetCannotBeRequired();
std::set<std::string> can_be_required = GetInstance()->GetCanBeRequired();
BuiltinCategories builtin_categories =
env->builtin_loader()->GetBuiltinCategories();
if (!env->owns_process_state()) {
can_be_required.erase("trace_events");
cannot_be_required.insert("trace_events");
builtin_categories.can_be_required.erase("trace_events");
builtin_categories.cannot_be_required.insert("trace_events");
}
Local<Value> cannot_be_required_js;
Local<Value> can_be_required_js;
if (!ToV8Value(context, cannot_be_required).ToLocal(&cannot_be_required_js))
if (!ToV8Value(context, builtin_categories.cannot_be_required)
.ToLocal(&cannot_be_required_js))
return;
if (result
->Set(context,
@ -587,7 +561,9 @@ void BuiltinLoader::GetBuiltinCategories(
cannot_be_required_js)
.IsNothing())
return;
if (!ToV8Value(context, can_be_required).ToLocal(&can_be_required_js)) return;
if (!ToV8Value(context, builtin_categories.can_be_required)
.ToLocal(&can_be_required_js))
return;
if (result
->Set(context,
OneByteString(isolate, "canBeRequired"),
@ -648,16 +624,19 @@ void BuiltinLoader::GetCacheUsage(const FunctionCallbackInfo<Value>& args) {
void BuiltinLoader::BuiltinIdsGetter(Local<Name> property,
const PropertyCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
Environment* env = Environment::GetCurrent(info);
Isolate* isolate = env->isolate();
std::vector<std::string> ids = GetInstance()->GetBuiltinIds();
std::vector<std::string> ids = env->builtin_loader()->GetBuiltinIds();
info.GetReturnValue().Set(
ToV8Value(isolate->GetCurrentContext(), ids).ToLocalChecked());
}
void BuiltinLoader::ConfigStringGetter(
Local<Name> property, const PropertyCallbackInfo<Value>& info) {
info.GetReturnValue().Set(GetConfigString(info.GetIsolate()));
Environment* env = Environment::GetCurrent(info);
info.GetReturnValue().Set(
env->builtin_loader()->GetConfigString(info.GetIsolate()));
}
void BuiltinLoader::RecordResult(const char* id,
@ -675,8 +654,8 @@ void BuiltinLoader::CompileFunction(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsString());
node::Utf8Value id_v(realm->isolate(), args[0].As<String>());
const char* id = *id_v;
MaybeLocal<Function> maybe =
GetInstance()->LookupAndCompile(realm->context(), id, realm);
MaybeLocal<Function> maybe = realm->env()->builtin_loader()->LookupAndCompile(
realm->context(), id, realm);
Local<Function> fn;
if (maybe.ToLocal(&fn)) {
args.GetReturnValue().Set(fn);
@ -684,8 +663,16 @@ void BuiltinLoader::CompileFunction(const FunctionCallbackInfo<Value>& args) {
}
void BuiltinLoader::HasCachedBuiltins(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(
v8::Boolean::New(args.GetIsolate(), GetInstance()->has_code_cache_));
auto instance = Environment::GetCurrent(args)->builtin_loader();
RwLock::ScopedReadLock lock(instance->code_cache_->mutex);
args.GetReturnValue().Set(v8::Boolean::New(
args.GetIsolate(), instance->code_cache_->has_code_cache));
}
void BuiltinLoader::CopySourceAndCodeCacheReferenceFrom(
const BuiltinLoader* other) {
code_cache_ = other->code_cache_;
source_ = other->source_;
}
void BuiltinLoader::CreatePerIsolateProperties(IsolateData* isolate_data,

View File

@ -6,10 +6,12 @@
#include <list>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "node_mutex.h"
#include "node_threadsafe_cow.h"
#include "node_union_bytes.h"
#include "v8.h"
@ -37,6 +39,7 @@ struct CodeCacheInfo {
// bootstrap scripts, whose source are bundled into the binary as static data.
class NODE_EXTERN_PRIVATE BuiltinLoader {
public:
BuiltinLoader();
BuiltinLoader(const BuiltinLoader&) = delete;
BuiltinLoader& operator=(const BuiltinLoader&) = delete;
@ -50,62 +53,56 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
// The parameters used to compile the scripts are detected based on
// the pattern of the id.
static v8::MaybeLocal<v8::Function> LookupAndCompile(
v8::Local<v8::Context> context, const char* id, Realm* optional_realm);
v8::MaybeLocal<v8::Function> LookupAndCompile(v8::Local<v8::Context> context,
const char* id,
Realm* optional_realm);
static v8::MaybeLocal<v8::Value> CompileAndCall(
v8::Local<v8::Context> context,
const char* id,
int argc,
v8::Local<v8::Value> argv[],
Realm* optional_realm);
v8::MaybeLocal<v8::Value> CompileAndCall(v8::Local<v8::Context> context,
const char* id,
int argc,
v8::Local<v8::Value> argv[],
Realm* optional_realm);
static v8::MaybeLocal<v8::Value> CompileAndCall(
v8::Local<v8::Context> context, const char* id, Realm* realm);
v8::MaybeLocal<v8::Value> CompileAndCall(v8::Local<v8::Context> context,
const char* id,
Realm* realm);
static v8::Local<v8::Object> GetSourceObject(v8::Local<v8::Context> context);
v8::Local<v8::Object> GetSourceObject(v8::Local<v8::Context> context);
// Returns config.gypi as a JSON string
static v8::Local<v8::String> GetConfigString(v8::Isolate* isolate);
static bool Exists(const char* id);
static bool Add(const char* id, const UnionBytes& source);
static bool Add(const char* id, std::string_view utf8source);
v8::Local<v8::String> GetConfigString(v8::Isolate* isolate);
bool Exists(const char* id);
bool Add(const char* id, const UnionBytes& source);
bool Add(const char* id, std::string_view utf8source);
static bool CompileAllBuiltins(v8::Local<v8::Context> context);
static void RefreshCodeCache(const std::vector<CodeCacheInfo>& in);
static void CopyCodeCache(std::vector<CodeCacheInfo>* out);
bool CompileAllBuiltins(v8::Local<v8::Context> context);
void RefreshCodeCache(const std::vector<CodeCacheInfo>& in);
void CopyCodeCache(std::vector<CodeCacheInfo>* out) const;
void CopySourceAndCodeCacheReferenceFrom(const BuiltinLoader* other);
private:
// Only allow access from friends.
friend class CodeCacheBuilder;
BuiltinLoader();
static BuiltinLoader* GetInstance();
// Generated by tools/js2c.py as node_javascript.cc
void LoadJavaScriptSource(); // Loads data into source_
UnionBytes GetConfig(); // Return data for config.gypi
std::vector<std::string> GetBuiltinIds();
std::vector<std::string> GetBuiltinIds() const;
struct BuiltinCategories {
bool is_initialized = false;
std::set<std::string> can_be_required;
std::set<std::string> cannot_be_required;
};
void InitializeBuiltinCategories();
const std::set<std::string>& GetCannotBeRequired();
const std::set<std::string>& GetCanBeRequired();
// This method builds `BuiltinCategories` from scratch every time,
// and is therefore somewhat expensive, but also currently only being
// used for testing, so that should not be an issue.
BuiltinCategories GetBuiltinCategories() const;
bool CanBeRequired(const char* id);
bool CannotBeRequired(const char* id);
BuiltinCodeCacheMap* code_cache();
const Mutex& code_cache_mutex() const { return code_cache_mutex_; }
v8::ScriptCompiler::CachedData* GetCodeCache(const char* id) const;
const v8::ScriptCompiler::CachedData* GetCodeCache(const char* id) const;
enum class Result { kWithCache, kWithoutCache };
v8::MaybeLocal<v8::String> LoadBuiltinSource(v8::Isolate* isolate,
const char* id);
const char* id) const;
// If an exception is encountered (e.g. source code contains
// syntax error), the returned value is empty.
v8::MaybeLocal<v8::Function> LookupAndCompileInternal(
@ -134,18 +131,18 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
static void HasCachedBuiltins(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void AddExternalizedBuiltin(const char* id, const char* filename);
void AddExternalizedBuiltin(const char* id, const char* filename);
static BuiltinLoader instance_;
BuiltinCategories builtin_categories_;
BuiltinSourceMap source_;
BuiltinCodeCacheMap code_cache_;
UnionBytes config_;
ThreadsafeCopyOnWrite<BuiltinSourceMap> source_;
// Used to synchronize access to the code cache map
Mutex code_cache_mutex_;
const UnionBytes config_;
bool has_code_cache_;
struct BuiltinCodeCache {
RwLock mutex;
BuiltinCodeCacheMap map;
bool has_code_cache = false;
};
std::shared_ptr<BuiltinCodeCache> code_cache_;
friend class ::PerProcessTest;
};

View File

@ -159,6 +159,11 @@ NodeMainInstance::CreateMainEnvironment(ExitCode* exit_code) {
&(snapshot_data_->env_info),
EnvironmentFlags::kDefaultFlags,
{}));
#ifdef NODE_V8_SHARED_RO_HEAP
// TODO(addaleax): Do this as part of creating the Environment
// once we store the SnapshotData* itself on IsolateData.
env->builtin_loader()->RefreshCodeCache(snapshot_data_->code_cache);
#endif
context = Context::FromSnapshot(isolate_,
SnapshotData::kNodeMainContextIndex,
{DeserializeNodeInternalFields, env.get()})

View File

@ -8,7 +8,6 @@
namespace node {
using builtins::BuiltinLoader;
using v8::Context;
using v8::EscapableHandleScope;
using v8::Function;
@ -174,7 +173,8 @@ void Realm::DeserializeProperties(const RealmSerializeInfo* info) {
MaybeLocal<Value> Realm::ExecuteBootstrapper(const char* id) {
EscapableHandleScope scope(isolate());
Local<Context> ctx = context();
MaybeLocal<Value> result = BuiltinLoader::CompileAndCall(ctx, id, this);
MaybeLocal<Value> result =
env()->builtin_loader()->CompileAndCall(ctx, id, this);
// If there was an error during bootstrap, it must be unrecoverable
// (e.g. max call stack exceeded). Clear the stack so that the

View File

@ -1205,10 +1205,10 @@ ExitCode SnapshotBuilder::Generate(SnapshotData* out,
#ifdef NODE_USE_NODE_CODE_CACHE
// Regenerate all the code cache.
if (!builtins::BuiltinLoader::CompileAllBuiltins(main_context)) {
if (!env->builtin_loader()->CompileAllBuiltins(main_context)) {
return ExitCode::kGenericUserError;
}
builtins::BuiltinLoader::CopyCodeCache(&(out->code_cache));
env->builtin_loader()->CopyCodeCache(&(out->code_cache));
for (const auto& item : out->code_cache) {
std::string size_str = FormatSize(item.data.size());
per_process::Debug(DebugCategory::MKSNAPSHOT,

View File

@ -0,0 +1,54 @@
#ifndef SRC_NODE_THREADSAFE_COW_INL_H_
#define SRC_NODE_THREADSAFE_COW_INL_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
namespace node {
template <typename T>
T* CopyOnWrite<T>::write() {
if (!data_.unique()) {
data_ = std::make_shared<T>(*data_);
}
return data_.get();
}
template <typename T>
ThreadsafeCopyOnWrite<T>::Read::Read(const ThreadsafeCopyOnWrite<T>* cow)
: cow_(cow), lock_(cow->impl_->mutex) {}
template <typename T>
const T& ThreadsafeCopyOnWrite<T>::Read::operator*() const {
return cow_->impl_->data;
}
template <typename T>
const T* ThreadsafeCopyOnWrite<T>::Read::operator->() const {
return &cow_->impl_->data;
}
template <typename T>
ThreadsafeCopyOnWrite<T>::Write::Write(ThreadsafeCopyOnWrite<T>* cow)
: cow_(cow), impl_(cow->impl_.write()), lock_(impl_->mutex) {}
template <typename T>
T& ThreadsafeCopyOnWrite<T>::Write::operator*() {
return impl_->data;
}
template <typename T>
T* ThreadsafeCopyOnWrite<T>::Write::operator->() {
return &impl_->data;
}
template <typename T>
ThreadsafeCopyOnWrite<T>::Impl::Impl(const Impl& other) {
RwLock::ScopedReadLock lock(other.mutex);
data = other.data;
}
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_NODE_THREADSAFE_COW_INL_H_

105
src/node_threadsafe_cow.h Normal file
View File

@ -0,0 +1,105 @@
#ifndef SRC_NODE_THREADSAFE_COW_H_
#define SRC_NODE_THREADSAFE_COW_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "util.h"
#include "uv.h"
#include <memory> // std::shared_ptr<T>
#include <utility> // std::forward<T>
namespace node {
// Copy-on-write utility. Not threadsafe, i.e. there is no synchronization
// of the copy operation with other operations.
template <typename T>
class CopyOnWrite final {
public:
template <typename... Args>
explicit CopyOnWrite(Args&&... args)
: data_(std::make_shared<T>(std::forward<Args>(args)...)) {}
CopyOnWrite(const CopyOnWrite<T>& other) = default;
CopyOnWrite& operator=(const CopyOnWrite<T>& other) = default;
CopyOnWrite(CopyOnWrite<T>&& other) = default;
CopyOnWrite& operator=(CopyOnWrite<T>&& other) = default;
const T* read() const { return data_.get(); }
T* write();
const T& operator*() const { return *read(); }
const T* operator->() const { return read(); }
private:
std::shared_ptr<T> data_;
};
// Threadsafe copy-on-write utility. Consumers need to use the Read and
// Write helpers to access the target data structure.
template <typename T>
class ThreadsafeCopyOnWrite final {
private:
// Define this early since some of the public members depend on it
// and some compilers need it to be defined first in that case.
struct Impl {
explicit Impl(const T& data) : data(data) {}
explicit Impl(T&& data) : data(std::move(data)) {}
Impl(const Impl& other);
Impl& operator=(const Impl& other) = delete;
Impl(Impl&& other) = delete;
Impl& operator=(Impl&& other) = delete;
RwLock mutex;
T data;
};
public:
template <typename... Args>
ThreadsafeCopyOnWrite(Args&&... args)
: impl_(T(std::forward<Args>(args)...)) {}
ThreadsafeCopyOnWrite(const ThreadsafeCopyOnWrite<T>& other) = default;
ThreadsafeCopyOnWrite& operator=(const ThreadsafeCopyOnWrite<T>& other) =
default;
ThreadsafeCopyOnWrite(ThreadsafeCopyOnWrite<T>&& other) = default;
ThreadsafeCopyOnWrite& operator=(ThreadsafeCopyOnWrite<T>&& other) = default;
class Read {
public:
explicit Read(const ThreadsafeCopyOnWrite<T>* cow);
const T& operator*() const;
const T* operator->() const;
private:
const ThreadsafeCopyOnWrite<T>* cow_;
RwLock::ScopedReadLock lock_;
};
class Write {
public:
explicit Write(ThreadsafeCopyOnWrite<T>* cow);
T& operator*();
T* operator->();
private:
ThreadsafeCopyOnWrite<T>* cow_;
typename ThreadsafeCopyOnWrite<T>::Impl* impl_;
RwLock::ScopedLock lock_;
};
Read read() const { return Read(this); }
Write write() { return Write(this); }
private:
CopyOnWrite<Impl> impl_;
};
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_NODE_THREADSAFE_COW_H_

View File

@ -1,4 +1,5 @@
#include "node_builtins.h"
#include "node_threadsafe_cow-inl.h"
#include "gtest/gtest.h"
#include "node_test_fixture.h"
@ -11,7 +12,7 @@ using node::builtins::BuiltinSourceMap;
class PerProcessTest : public ::testing::Test {
protected:
static const BuiltinSourceMap get_sources_for_test() {
return BuiltinLoader::instance_.source_;
return *BuiltinLoader().source_.read();
}
};

View File

@ -57,8 +57,14 @@ namespace builtins {{
{0}
namespace {{
const ThreadsafeCopyOnWrite<BuiltinSourceMap> global_source_map {{
BuiltinSourceMap{{ {1} }}
}};
}}
void BuiltinLoader::LoadJavaScriptSource() {{
{1}
source_ = global_source_map;
}}
UnionBytes BuiltinLoader::GetConfig() {{
@ -82,7 +88,7 @@ static const uint16_t {0}[] = {{
}};
"""
INITIALIZER = 'source_.emplace("{0}", UnionBytes{{{1}, {2}}});'
INITIALIZER = '{{"{0}", UnionBytes{{{1}, {2}}} }},'
CONFIG_GYPI_ID = 'config_raw'