From 0858e5d9d8db085cb83b3f1f3f94ed6b550a7bc5 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 3 Dec 2018 21:47:57 +0800 Subject: src: always compile and store code cache for native modules This patch changes the NativeModuleLoader to always try to find code cache for native modules when it compiles them, and always produce and store the code cache after compilation. The cache map is protected by a mutex and can be accessed by different threads - including the worker threads and the main thread. Hence any thread can reuse the code cache if the native module has already been compiled by another thread - in particular the cache of the bootstrappers and per_context.js will always be hit when a new thread is spun. This results in a ~6% startup overhead in the worst case (when only the main thread is launched without requiring any additional native module - it now needs to do the extra work of finding and storing caches), which balances out the recent improvements by moving the compilation to C++, but it also leads to a ~60% improvement in the best case (when a worker thread is spun and requires a lot of native modules thus hitting the cache compiled by the main thread). PR-URL: https://github.com/nodejs/node/pull/24950 Reviewed-By: Anna Henningsen --- src/node_native_module.cc | 175 ++++++++++++++++++++++------------------------ 1 file changed, 85 insertions(+), 90 deletions(-) (limited to 'src/node_native_module.cc') diff --git a/src/node_native_module.cc b/src/node_native_module.cc index 98cc128b73..d7fa5798d0 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(); } -- cgit v1.2.3