diff --git a/lib/internal/bootstrap/cache.js b/lib/internal/bootstrap/cache.js index 8658f5b5d4ccec..580e7a9cb0da7a 100644 --- a/lib/internal/bootstrap/cache.js +++ b/lib/internal/bootstrap/cache.js @@ -6,7 +6,9 @@ // cannot be tampered with even with --expose-internals. const { NativeModule } = require('internal/bootstrap/loaders'); -const { source, compileCodeCache } = internalBinding('native_module'); +const { + source, getCodeCache, compileFunction +} = internalBinding('native_module'); const { hasTracing } = process.binding('config'); const depsModule = Object.keys(source).filter( @@ -70,6 +72,7 @@ module.exports = { (key) => !cannotUseCache.includes(key) ), getSource(id) { return source[id]; }, - getCodeCache: compileCodeCache, + getCodeCache, + compileFunction, cannotUseCache }; diff --git a/src/node.cc b/src/node.cc index 583099b31f970a..2d84ccf5ae8c85 100644 --- a/src/node.cc +++ b/src/node.cc @@ -110,7 +110,6 @@ typedef int mode_t; namespace node { -using native_module::NativeModuleLoader; using options_parser::kAllowedInEnvironment; using options_parser::kDisallowedInEnvironment; using v8::Array; @@ -162,7 +161,6 @@ double prog_start_time; Mutex per_process_opts_mutex; std::shared_ptr per_process_opts { new PerProcessOptions() }; -NativeModuleLoader per_process_loader; static Mutex node_isolate_mutex; static Isolate* node_isolate; @@ -1191,7 +1189,7 @@ static MaybeLocal ExecuteBootstrapper( const char* id, std::vector>* parameters, std::vector>* arguments) { - MaybeLocal ret = per_process_loader.CompileAndCall( + MaybeLocal ret = per_process::native_module_loader.CompileAndCall( env->context(), id, parameters, arguments, env); // If there was an error during bootstrap then it was either handled by the @@ -1908,7 +1906,7 @@ Local NewContext(Isolate* isolate, std::vector> parameters = { FIXED_ONE_BYTE_STRING(isolate, "global")}; std::vector> arguments = {context->Global()}; - MaybeLocal result = per_process_loader.CompileAndCall( + MaybeLocal result = per_process::native_module_loader.CompileAndCall( context, "internal/per_context", ¶meters, &arguments, nullptr); if (result.IsEmpty()) { // Execution failed during context creation. diff --git a/src/node_binding.cc b/src/node_binding.cc index cc422a39e08aee..d24afe25b314b0 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -389,13 +389,14 @@ void GetInternalBinding(const FunctionCallbackInfo& args) { exports->SetPrototype(env->context(), Null(env->isolate())).FromJust()); DefineConstants(env->isolate(), exports); } else if (!strcmp(*module_v, "natives")) { - exports = per_process_loader.GetSourceObject(env->context()); + exports = per_process::native_module_loader.GetSourceObject(env->context()); // Legacy feature: process.binding('natives').config contains stringified // config.gypi CHECK(exports ->Set(env->context(), env->config_string(), - per_process_loader.GetConfigString(env->isolate())) + per_process::native_module_loader.GetConfigString( + env->isolate())) .FromJust()); } else { return ThrowIfNoSuchModule(env, *module_v); diff --git a/src/node_internals.h b/src/node_internals.h index 6fe1d9a80a22b1..d9a72a10a440ea 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -91,7 +91,6 @@ extern bool v8_initialized; extern Mutex per_process_opts_mutex; extern std::shared_ptr per_process_opts; -extern native_module::NativeModuleLoader per_process_loader; // Forward declaration class Environment; diff --git a/src/node_native_module.cc b/src/node_native_module.cc index 98cc128b735cca..d7fa5798d0b92e 100644 --- a/src/node_native_module.cc +++ b/src/node_native_module.cc @@ -3,6 +3,11 @@ #include "node_internals.h" namespace node { + +namespace per_process { +native_module::NativeModuleLoader native_module_loader; +} // namespace per_process + namespace native_module { using v8::Array; @@ -78,13 +83,14 @@ void NativeModuleLoader::GetCacheUsage( void NativeModuleLoader::SourceObjectGetter( Local property, const PropertyCallbackInfo& info) { Local context = info.GetIsolate()->GetCurrentContext(); - info.GetReturnValue().Set(per_process_loader.GetSourceObject(context)); + info.GetReturnValue().Set( + per_process::native_module_loader.GetSourceObject(context)); } void NativeModuleLoader::ConfigStringGetter( Local property, const PropertyCallbackInfo& info) { info.GetReturnValue().Set( - per_process_loader.GetConfigString(info.GetIsolate())); + per_process::native_module_loader.GetConfigString(info.GetIsolate())); } Local NativeModuleLoader::GetSourceObject( @@ -96,41 +102,62 @@ Local NativeModuleLoader::GetConfigString(Isolate* isolate) const { return config_.ToStringChecked(isolate); } -Local NativeModuleLoader::GetSource(Isolate* isolate, - const char* id) const { - const auto it = source_.find(id); - CHECK_NE(it, source_.end()); - return it->second.ToStringChecked(isolate); -} - NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) { LoadJavaScriptSource(); LoadCodeCache(); } -void NativeModuleLoader::CompileCodeCache( - const FunctionCallbackInfo& args) { +// This is supposed to be run only by the main thread in +// tools/generate_code_cache.js +void NativeModuleLoader::GetCodeCache(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + CHECK(env->is_main_thread()); + CHECK(args[0]->IsString()); - node::Utf8Value id(env->isolate(), args[0].As()); + node::Utf8Value id_v(isolate, args[0].As()); + const char* id = *id_v; - // TODO(joyeecheung): allow compiling cache for bootstrapper by - // switching on id - MaybeLocal result = - CompileAsModule(env, *id, CompilationResultType::kCodeCache); - if (!result.IsEmpty()) { - args.GetReturnValue().Set(result.ToLocalChecked()); + const NativeModuleLoader& loader = per_process::native_module_loader; + MaybeLocal ret = loader.GetCodeCache(isolate, id); + if (!ret.IsEmpty()) { + args.GetReturnValue().Set(ret.ToLocalChecked()); } } +// This is supposed to be run only by the main thread in +// tools/generate_code_cache.js +MaybeLocal NativeModuleLoader::GetCodeCache(Isolate* isolate, + const char* id) const { + EscapableHandleScope scope(isolate); + Mutex::ScopedLock lock(code_cache_mutex_); + + ScriptCompiler::CachedData* cached_data = nullptr; + const auto it = code_cache_.find(id); + if (it == code_cache_.end()) { + // The module has not been compiled before. + return MaybeLocal(); + } + + cached_data = it->second.get(); + + MallocedBuffer copied(cached_data->length); + memcpy(copied.data, cached_data->data, cached_data->length); + Local buf = + ArrayBuffer::New(isolate, + copied.release(), + cached_data->length, + ArrayBufferCreationMode::kInternalized); + return scope.Escape(Uint8Array::New(buf, 0, cached_data->length)); +} + void NativeModuleLoader::CompileFunction( const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK(args[0]->IsString()); node::Utf8Value id(env->isolate(), args[0].As()); - MaybeLocal result = - CompileAsModule(env, *id, CompilationResultType::kFunction); + MaybeLocal result = CompileAsModule(env, *id); if (!result.IsEmpty()) { args.GetReturnValue().Set(result.ToLocalChecked()); } @@ -145,57 +172,43 @@ MaybeLocal NativeModuleLoader::CompileAndCall( std::vector>* arguments, Environment* optional_env) { Isolate* isolate = context->GetIsolate(); - MaybeLocal compiled = per_process_loader.LookupAndCompile( - context, id, parameters, CompilationResultType::kFunction, nullptr); + MaybeLocal compiled = + per_process::native_module_loader.LookupAndCompile( + context, id, parameters, nullptr); if (compiled.IsEmpty()) { - return compiled; + return MaybeLocal(); } Local fn = compiled.ToLocalChecked().As(); return fn->Call( context, v8::Null(isolate), arguments->size(), arguments->data()); } -MaybeLocal NativeModuleLoader::CompileAsModule( - Environment* env, const char* id, CompilationResultType result) { +MaybeLocal NativeModuleLoader::CompileAsModule(Environment* env, + const char* id) { std::vector> parameters = {env->exports_string(), env->require_string(), env->module_string(), env->process_string(), env->internal_binding_string()}; - return per_process_loader.LookupAndCompile( - env->context(), id, ¶meters, result, env); -} - -// Returns nullptr if there is no code cache corresponding to the id -ScriptCompiler::CachedData* NativeModuleLoader::GetCachedData( - const char* id) const { - const auto it = per_process_loader.code_cache_.find(id); - // This could be false if the module cannot be cached somehow. - // See lib/internal/bootstrap/cache.js on the modules that cannot be cached - if (it == per_process_loader.code_cache_.end()) { - return nullptr; - } - - const uint8_t* code_cache_value = it->second.one_bytes_data(); - size_t code_cache_length = it->second.length(); - - return new ScriptCompiler::CachedData(code_cache_value, code_cache_length); + return per_process::native_module_loader.LookupAndCompile( + env->context(), id, ¶meters, env); } // Returns Local of the compiled module if return_code_cache // is false (we are only compiling the function). // Otherwise return a Local containing the cache. -MaybeLocal NativeModuleLoader::LookupAndCompile( +MaybeLocal NativeModuleLoader::LookupAndCompile( Local context, const char* id, std::vector>* parameters, - CompilationResultType result_type, Environment* optional_env) { Isolate* isolate = context->GetIsolate(); EscapableHandleScope scope(isolate); Local ret; // Used to convert to MaybeLocal before return - Local source = GetSource(isolate, id); + const auto source_it = source_.find(id); + CHECK_NE(source_it, source_.end()); + Local source = source_it->second.ToStringChecked(isolate); std::string filename_s = id + std::string(".js"); Local filename = @@ -204,31 +217,24 @@ MaybeLocal NativeModuleLoader::LookupAndCompile( Local column_offset = Integer::New(isolate, 0); ScriptOrigin origin(filename, line_offset, column_offset); - bool use_cache = false; - ScriptCompiler::CachedData* cached_data = nullptr; + Mutex::ScopedLock lock(code_cache_mutex_); - // 1. We won't even check the existence of the cache if the binary is not - // built with them. - // 2. If we are generating code cache for tools/general_code_cache.js, we - // are not going to use any cache ourselves. - if (has_code_cache_ && result_type == CompilationResultType::kFunction) { - cached_data = GetCachedData(id); - if (cached_data != nullptr) { - use_cache = true; + ScriptCompiler::CachedData* cached_data = nullptr; + { + auto cache_it = code_cache_.find(id); + if (cache_it != code_cache_.end()) { + // Transfer ownership to ScriptCompiler::Source later. + cached_data = cache_it->second.release(); + code_cache_.erase(cache_it); } } + const bool use_cache = cached_data != nullptr; + ScriptCompiler::CompileOptions options = + use_cache ? ScriptCompiler::kConsumeCodeCache + : ScriptCompiler::kEagerCompile; ScriptCompiler::Source script_source(source, origin, cached_data); - ScriptCompiler::CompileOptions options; - if (result_type == CompilationResultType::kCodeCache) { - options = ScriptCompiler::kEagerCompile; - } else if (use_cache) { - options = ScriptCompiler::kConsumeCodeCache; - } else { - options = ScriptCompiler::kNoCompileOptions; - } - MaybeLocal maybe_fun = ScriptCompiler::CompileFunctionInContext(context, &script_source, @@ -244,10 +250,14 @@ MaybeLocal NativeModuleLoader::LookupAndCompile( // In the case of early errors, v8 is already capable of // decorating the stack for us - note that we use CompileFunctionInContext // so there is no need to worry about wrappers. - return MaybeLocal(); + return MaybeLocal(); } Local fun = maybe_fun.ToLocalChecked(); + // XXX(joyeecheung): this bookkeeping is not exactly accurate because + // it only starts after the Environment is created, so the per_context.js + // will never be in any of these two sets, but the two sets are only for + // testing anyway. if (use_cache) { if (optional_env != nullptr) { // This could happen when Node is run with any v8 flag, but @@ -264,29 +274,15 @@ MaybeLocal NativeModuleLoader::LookupAndCompile( } } - if (result_type == CompilationResultType::kCodeCache) { - std::unique_ptr cached_data( - ScriptCompiler::CreateCodeCacheForFunction(fun)); - CHECK_NE(cached_data, nullptr); - size_t cached_data_length = cached_data->length; - // Since we have no special allocator to create an ArrayBuffer - // from a new'ed pointer, we will need to copy it - but this - // code path is only run by the tooling that generates the code - // cache to be bundled in the binary - // so it should be fine. - MallocedBuffer copied(cached_data->length); - memcpy(copied.data, cached_data->data, cached_data_length); - Local buf = - ArrayBuffer::New(isolate, - copied.release(), - cached_data_length, - ArrayBufferCreationMode::kInternalized); - ret = Uint8Array::New(buf, 0, cached_data_length); - } else { - ret = fun; - } + // Generate new cache for next compilation + std::unique_ptr new_cached_data( + ScriptCompiler::CreateCodeCacheForFunction(fun)); + CHECK_NE(new_cached_data, nullptr); - return scope.Escape(ret); + // The old entry should've been erased by now so we can just emplace + code_cache_.emplace(id, std::move(new_cached_data)); + + return scope.Escape(fun); } void NativeModuleLoader::Initialize(Local target, @@ -320,8 +316,7 @@ void NativeModuleLoader::Initialize(Local target, target, "getCacheUsage", NativeModuleLoader::GetCacheUsage); env->SetMethod( target, "compileFunction", NativeModuleLoader::CompileFunction); - env->SetMethod( - target, "compileCodeCache", NativeModuleLoader::CompileCodeCache); + env->SetMethod(target, "getCodeCache", NativeModuleLoader::GetCodeCache); // internalBinding('native_module') should be frozen target->SetIntegrityLevel(context, IntegrityLevel::kFrozen).FromJust(); } diff --git a/src/node_native_module.h b/src/node_native_module.h index 54c5f388f7480f..2e5821c2cfc6a1 100644 --- a/src/node_native_module.h +++ b/src/node_native_module.h @@ -7,6 +7,7 @@ #include #include #include "env.h" +#include "node_mutex.h" #include "node_union_bytes.h" #include "v8.h" @@ -14,7 +15,9 @@ namespace node { namespace native_module { using NativeModuleRecordMap = std::map; -using NativeModuleHashMap = std::map; +using NativeModuleCacheMap = + std::unordered_map>; // The native (C++) side of the NativeModule in JS land, which // handles compilation and caching of builtin modules (NativeModule) @@ -25,16 +28,12 @@ using NativeModuleHashMap = std::map; // The instances of this class are per-process. class NativeModuleLoader { public: - // kCodeCache indicates that the compilation result should be returned - // as a Uint8Array, whereas kFunction indicates that the result should - // be returned as a Function. - // TODO(joyeecheung): it's possible to always produce code cache - // on the main thread and consume them in worker threads, or just - // share the cache among all the threads, although - // we need to decide whether to do that even when workers are not used. - enum class CompilationResultType { kCodeCache, kFunction }; - NativeModuleLoader(); + // TODO(joyeecheung): maybe we should make this a singleton, instead of + // putting it in per_process. + NativeModuleLoader(const NativeModuleLoader&) = delete; + NativeModuleLoader& operator=(const NativeModuleLoader&) = delete; + static void Initialize(v8::Local target, v8::Local unused, v8::Local context, @@ -43,8 +42,6 @@ class NativeModuleLoader { // Returns config.gypi as a JSON string v8::Local GetConfigString(v8::Isolate* isolate) const; - v8::Local GetSource(v8::Isolate* isolate, const char* id) const; - // Run a script with JS source bundled inside the binary as if it's wrapped // in a function called with a null receiver and arguments specified in C++. // The returned value is empty if an exception is encountered. @@ -68,8 +65,10 @@ class NativeModuleLoader { static void ConfigStringGetter( v8::Local property, const v8::PropertyCallbackInfo& info); - // Compile code cache for a specific native module - static void CompileCodeCache(const v8::FunctionCallbackInfo& args); + // Get code cache for a specific native module + static void GetCodeCache(const v8::FunctionCallbackInfo& args); + v8::MaybeLocal GetCodeCache(v8::Isolate* isolate, + const char* id) const; // Compile a specific native module as a function static void CompileFunction(const v8::FunctionCallbackInfo& args); @@ -82,30 +81,34 @@ class NativeModuleLoader { // in node_code_cache_stub.cc void LoadCodeCache(); // Loads data into code_cache_ - v8::ScriptCompiler::CachedData* GetCachedData(const char* id) const; - // Compile a script as a NativeModule that can be loaded via // NativeModule.p.require in JS land. - static v8::MaybeLocal CompileAsModule( - Environment* env, const char* id, CompilationResultType result_type); + static v8::MaybeLocal CompileAsModule(Environment* env, + const char* id); // For bootstrappers optional_env may be a nullptr. // If an exception is encountered (e.g. source code contains // syntax error), the returned value is empty. - v8::MaybeLocal LookupAndCompile( + v8::MaybeLocal LookupAndCompile( v8::Local context, const char* id, std::vector>* parameters, - CompilationResultType result_type, Environment* optional_env); - bool has_code_cache_ = false; NativeModuleRecordMap source_; - NativeModuleRecordMap code_cache_; + NativeModuleCacheMap code_cache_; UnionBytes config_; + + // Used to synchronize access to the code cache map + Mutex code_cache_mutex_; }; } // namespace native_module + +namespace per_process { +extern native_module::NativeModuleLoader native_module_loader; +} // namespace per_process + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/test/code-cache/test-code-cache.js b/test/code-cache/test-code-cache.js index 669e1367d60d53..367e72168a6b67 100644 --- a/test/code-cache/test-code-cache.js +++ b/test/code-cache/test-code-cache.js @@ -43,11 +43,17 @@ const loadedModules = process.moduleLoadList // are all compiled without cache and we are doing the bookkeeping right. if (process.config.variables.node_code_cache_path === undefined) { console.log('The binary is not configured with code cache'); - assert.deepStrictEqual(compiledWithCache, new Set()); - - for (const key of loadedModules) { - assert(compiledWithoutCache.has(key), - `"${key}" should've been compiled without code cache`); + if (isMainThread) { + assert.deepStrictEqual(compiledWithCache, new Set()); + for (const key of loadedModules) { + assert(compiledWithoutCache.has(key), + `"${key}" should've been compiled without code cache`); + } + } else { + // TODO(joyeecheung): create a list of modules whose cache can be shared + // from the main thread to the worker thread and check that their + // cache are hit + assert.notDeepStrictEqual(compiledWithCache, new Set()); } } else { console.log('The binary is configured with code cache'); diff --git a/tools/generate_code_cache.js b/tools/generate_code_cache.js index 3e21743f2c71b6..a434f640c29157 100644 --- a/tools/generate_code_cache.js +++ b/tools/generate_code_cache.js @@ -9,6 +9,7 @@ const { getCodeCache, + compileFunction, cachableBuiltins } = require('internal/bootstrap/cache'); @@ -57,10 +58,13 @@ function getInitalizer(key, cache) { const defName = `${key.replace(/\//g, '_').replace(/-/g, '_')}_raw`; const definition = `static const uint8_t ${defName}[] = {\n` + `${cache.join(',')}\n};`; + const dataDef = 'std::make_unique(' + + `${defName}, static_cast(arraysize(${defName})), ` + + 'policy)'; const initializer = 'code_cache_.emplace(\n' + ` "${key}",\n` + - ` UnionBytes(${defName}, arraysize(${defName}))\n` + + ` ${dataDef}\n` + ');'; return { definition, initializer @@ -82,6 +86,7 @@ function lexical(a, b) { } for (const key of cachableBuiltins.sort(lexical)) { + compileFunction(key); // compile it const cachedData = getCodeCache(key); if (!isUint8Array(cachedData)) { console.error(`Failed to generate code cache for '${key}'`); @@ -110,7 +115,7 @@ namespace native_module { ${cacheDefinitions.join('\n\n')} void NativeModuleLoader::LoadCodeCache() { - has_code_cache_ = true; + auto policy = v8::ScriptCompiler::CachedData::BufferPolicy::BufferNotOwned; ${cacheInitializers.join('\n ')} }