Skip to content

Commit ee461fe

Browse files
joyeecheungaddaleax
authored andcommitted
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: #24950 Reviewed-By: Anna Henningsen <[email protected]>
1 parent fd913fe commit ee461fe

File tree

8 files changed

+138
-128
lines changed

8 files changed

+138
-128
lines changed

lib/internal/bootstrap/cache.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
// cannot be tampered with even with --expose-internals.
77

88
const { NativeModule } = require('internal/bootstrap/loaders');
9-
const { source, compileCodeCache } = internalBinding('native_module');
9+
const {
10+
source, getCodeCache, compileFunction
11+
} = internalBinding('native_module');
1012
const { hasTracing } = process.binding('config');
1113

1214
const depsModule = Object.keys(source).filter(
@@ -70,6 +72,7 @@ module.exports = {
7072
(key) => !cannotUseCache.includes(key)
7173
),
7274
getSource(id) { return source[id]; },
73-
getCodeCache: compileCodeCache,
75+
getCodeCache,
76+
compileFunction,
7477
cannotUseCache
7578
};

src/node.cc

+2-4
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ typedef int mode_t;
110110

111111
namespace node {
112112

113-
using native_module::NativeModuleLoader;
114113
using options_parser::kAllowedInEnvironment;
115114
using options_parser::kDisallowedInEnvironment;
116115
using v8::Array;
@@ -162,7 +161,6 @@ double prog_start_time;
162161
Mutex per_process_opts_mutex;
163162
std::shared_ptr<PerProcessOptions> per_process_opts {
164163
new PerProcessOptions() };
165-
NativeModuleLoader per_process_loader;
166164
static Mutex node_isolate_mutex;
167165
static Isolate* node_isolate;
168166

@@ -1191,7 +1189,7 @@ static MaybeLocal<Value> ExecuteBootstrapper(
11911189
const char* id,
11921190
std::vector<Local<String>>* parameters,
11931191
std::vector<Local<Value>>* arguments) {
1194-
MaybeLocal<Value> ret = per_process_loader.CompileAndCall(
1192+
MaybeLocal<Value> ret = per_process::native_module_loader.CompileAndCall(
11951193
env->context(), id, parameters, arguments, env);
11961194

11971195
// If there was an error during bootstrap then it was either handled by the
@@ -1908,7 +1906,7 @@ Local<Context> NewContext(Isolate* isolate,
19081906
std::vector<Local<String>> parameters = {
19091907
FIXED_ONE_BYTE_STRING(isolate, "global")};
19101908
std::vector<Local<Value>> arguments = {context->Global()};
1911-
MaybeLocal<Value> result = per_process_loader.CompileAndCall(
1909+
MaybeLocal<Value> result = per_process::native_module_loader.CompileAndCall(
19121910
context, "internal/per_context", &parameters, &arguments, nullptr);
19131911
if (result.IsEmpty()) {
19141912
// Execution failed during context creation.

src/node_binding.cc

+3-2
Original file line numberDiff line numberDiff line change
@@ -389,13 +389,14 @@ void GetInternalBinding(const FunctionCallbackInfo<Value>& args) {
389389
exports->SetPrototype(env->context(), Null(env->isolate())).FromJust());
390390
DefineConstants(env->isolate(), exports);
391391
} else if (!strcmp(*module_v, "natives")) {
392-
exports = per_process_loader.GetSourceObject(env->context());
392+
exports = per_process::native_module_loader.GetSourceObject(env->context());
393393
// Legacy feature: process.binding('natives').config contains stringified
394394
// config.gypi
395395
CHECK(exports
396396
->Set(env->context(),
397397
env->config_string(),
398-
per_process_loader.GetConfigString(env->isolate()))
398+
per_process::native_module_loader.GetConfigString(
399+
env->isolate()))
399400
.FromJust());
400401
} else {
401402
return ThrowIfNoSuchModule(env, *module_v);

src/node_internals.h

-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ extern bool v8_initialized;
9191

9292
extern Mutex per_process_opts_mutex;
9393
extern std::shared_ptr<PerProcessOptions> per_process_opts;
94-
extern native_module::NativeModuleLoader per_process_loader;
9594

9695
// Forward declaration
9796
class Environment;

src/node_native_module.cc

+85-90
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
#include "node_internals.h"
44

55
namespace node {
6+
7+
namespace per_process {
8+
native_module::NativeModuleLoader native_module_loader;
9+
} // namespace per_process
10+
611
namespace native_module {
712

813
using v8::Array;
@@ -78,13 +83,14 @@ void NativeModuleLoader::GetCacheUsage(
7883
void NativeModuleLoader::SourceObjectGetter(
7984
Local<Name> property, const PropertyCallbackInfo<Value>& info) {
8085
Local<Context> context = info.GetIsolate()->GetCurrentContext();
81-
info.GetReturnValue().Set(per_process_loader.GetSourceObject(context));
86+
info.GetReturnValue().Set(
87+
per_process::native_module_loader.GetSourceObject(context));
8288
}
8389

8490
void NativeModuleLoader::ConfigStringGetter(
8591
Local<Name> property, const PropertyCallbackInfo<Value>& info) {
8692
info.GetReturnValue().Set(
87-
per_process_loader.GetConfigString(info.GetIsolate()));
93+
per_process::native_module_loader.GetConfigString(info.GetIsolate()));
8894
}
8995

9096
Local<Object> NativeModuleLoader::GetSourceObject(
@@ -96,41 +102,62 @@ Local<String> NativeModuleLoader::GetConfigString(Isolate* isolate) const {
96102
return config_.ToStringChecked(isolate);
97103
}
98104

99-
Local<String> NativeModuleLoader::GetSource(Isolate* isolate,
100-
const char* id) const {
101-
const auto it = source_.find(id);
102-
CHECK_NE(it, source_.end());
103-
return it->second.ToStringChecked(isolate);
104-
}
105-
106105
NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) {
107106
LoadJavaScriptSource();
108107
LoadCodeCache();
109108
}
110109

111-
void NativeModuleLoader::CompileCodeCache(
112-
const FunctionCallbackInfo<Value>& args) {
110+
// This is supposed to be run only by the main thread in
111+
// tools/generate_code_cache.js
112+
void NativeModuleLoader::GetCodeCache(const FunctionCallbackInfo<Value>& args) {
113113
Environment* env = Environment::GetCurrent(args);
114+
Isolate* isolate = env->isolate();
115+
CHECK(env->is_main_thread());
116+
114117
CHECK(args[0]->IsString());
115-
node::Utf8Value id(env->isolate(), args[0].As<String>());
118+
node::Utf8Value id_v(isolate, args[0].As<String>());
119+
const char* id = *id_v;
116120

117-
// TODO(joyeecheung): allow compiling cache for bootstrapper by
118-
// switching on id
119-
MaybeLocal<Value> result =
120-
CompileAsModule(env, *id, CompilationResultType::kCodeCache);
121-
if (!result.IsEmpty()) {
122-
args.GetReturnValue().Set(result.ToLocalChecked());
121+
const NativeModuleLoader& loader = per_process::native_module_loader;
122+
MaybeLocal<Uint8Array> ret = loader.GetCodeCache(isolate, id);
123+
if (!ret.IsEmpty()) {
124+
args.GetReturnValue().Set(ret.ToLocalChecked());
123125
}
124126
}
125127

128+
// This is supposed to be run only by the main thread in
129+
// tools/generate_code_cache.js
130+
MaybeLocal<Uint8Array> NativeModuleLoader::GetCodeCache(Isolate* isolate,
131+
const char* id) const {
132+
EscapableHandleScope scope(isolate);
133+
Mutex::ScopedLock lock(code_cache_mutex_);
134+
135+
ScriptCompiler::CachedData* cached_data = nullptr;
136+
const auto it = code_cache_.find(id);
137+
if (it == code_cache_.end()) {
138+
// The module has not been compiled before.
139+
return MaybeLocal<Uint8Array>();
140+
}
141+
142+
cached_data = it->second.get();
143+
144+
MallocedBuffer<uint8_t> copied(cached_data->length);
145+
memcpy(copied.data, cached_data->data, cached_data->length);
146+
Local<ArrayBuffer> buf =
147+
ArrayBuffer::New(isolate,
148+
copied.release(),
149+
cached_data->length,
150+
ArrayBufferCreationMode::kInternalized);
151+
return scope.Escape(Uint8Array::New(buf, 0, cached_data->length));
152+
}
153+
126154
void NativeModuleLoader::CompileFunction(
127155
const FunctionCallbackInfo<Value>& args) {
128156
Environment* env = Environment::GetCurrent(args);
129157
CHECK(args[0]->IsString());
130158
node::Utf8Value id(env->isolate(), args[0].As<String>());
131159

132-
MaybeLocal<Value> result =
133-
CompileAsModule(env, *id, CompilationResultType::kFunction);
160+
MaybeLocal<Function> result = CompileAsModule(env, *id);
134161
if (!result.IsEmpty()) {
135162
args.GetReturnValue().Set(result.ToLocalChecked());
136163
}
@@ -145,57 +172,43 @@ MaybeLocal<Value> NativeModuleLoader::CompileAndCall(
145172
std::vector<Local<Value>>* arguments,
146173
Environment* optional_env) {
147174
Isolate* isolate = context->GetIsolate();
148-
MaybeLocal<Value> compiled = per_process_loader.LookupAndCompile(
149-
context, id, parameters, CompilationResultType::kFunction, nullptr);
175+
MaybeLocal<Function> compiled =
176+
per_process::native_module_loader.LookupAndCompile(
177+
context, id, parameters, nullptr);
150178
if (compiled.IsEmpty()) {
151-
return compiled;
179+
return MaybeLocal<Value>();
152180
}
153181
Local<Function> fn = compiled.ToLocalChecked().As<Function>();
154182
return fn->Call(
155183
context, v8::Null(isolate), arguments->size(), arguments->data());
156184
}
157185

158-
MaybeLocal<Value> NativeModuleLoader::CompileAsModule(
159-
Environment* env, const char* id, CompilationResultType result) {
186+
MaybeLocal<Function> NativeModuleLoader::CompileAsModule(Environment* env,
187+
const char* id) {
160188
std::vector<Local<String>> parameters = {env->exports_string(),
161189
env->require_string(),
162190
env->module_string(),
163191
env->process_string(),
164192
env->internal_binding_string()};
165-
return per_process_loader.LookupAndCompile(
166-
env->context(), id, &parameters, result, env);
167-
}
168-
169-
// Returns nullptr if there is no code cache corresponding to the id
170-
ScriptCompiler::CachedData* NativeModuleLoader::GetCachedData(
171-
const char* id) const {
172-
const auto it = per_process_loader.code_cache_.find(id);
173-
// This could be false if the module cannot be cached somehow.
174-
// See lib/internal/bootstrap/cache.js on the modules that cannot be cached
175-
if (it == per_process_loader.code_cache_.end()) {
176-
return nullptr;
177-
}
178-
179-
const uint8_t* code_cache_value = it->second.one_bytes_data();
180-
size_t code_cache_length = it->second.length();
181-
182-
return new ScriptCompiler::CachedData(code_cache_value, code_cache_length);
193+
return per_process::native_module_loader.LookupAndCompile(
194+
env->context(), id, &parameters, env);
183195
}
184196

185197
// Returns Local<Function> of the compiled module if return_code_cache
186198
// is false (we are only compiling the function).
187199
// Otherwise return a Local<Object> containing the cache.
188-
MaybeLocal<Value> NativeModuleLoader::LookupAndCompile(
200+
MaybeLocal<Function> NativeModuleLoader::LookupAndCompile(
189201
Local<Context> context,
190202
const char* id,
191203
std::vector<Local<String>>* parameters,
192-
CompilationResultType result_type,
193204
Environment* optional_env) {
194205
Isolate* isolate = context->GetIsolate();
195206
EscapableHandleScope scope(isolate);
196207
Local<Value> ret; // Used to convert to MaybeLocal before return
197208

198-
Local<String> source = GetSource(isolate, id);
209+
const auto source_it = source_.find(id);
210+
CHECK_NE(source_it, source_.end());
211+
Local<String> source = source_it->second.ToStringChecked(isolate);
199212

200213
std::string filename_s = id + std::string(".js");
201214
Local<String> filename =
@@ -204,31 +217,24 @@ MaybeLocal<Value> NativeModuleLoader::LookupAndCompile(
204217
Local<Integer> column_offset = Integer::New(isolate, 0);
205218
ScriptOrigin origin(filename, line_offset, column_offset);
206219

207-
bool use_cache = false;
208-
ScriptCompiler::CachedData* cached_data = nullptr;
220+
Mutex::ScopedLock lock(code_cache_mutex_);
209221

210-
// 1. We won't even check the existence of the cache if the binary is not
211-
// built with them.
212-
// 2. If we are generating code cache for tools/general_code_cache.js, we
213-
// are not going to use any cache ourselves.
214-
if (has_code_cache_ && result_type == CompilationResultType::kFunction) {
215-
cached_data = GetCachedData(id);
216-
if (cached_data != nullptr) {
217-
use_cache = true;
222+
ScriptCompiler::CachedData* cached_data = nullptr;
223+
{
224+
auto cache_it = code_cache_.find(id);
225+
if (cache_it != code_cache_.end()) {
226+
// Transfer ownership to ScriptCompiler::Source later.
227+
cached_data = cache_it->second.release();
228+
code_cache_.erase(cache_it);
218229
}
219230
}
220231

232+
const bool use_cache = cached_data != nullptr;
233+
ScriptCompiler::CompileOptions options =
234+
use_cache ? ScriptCompiler::kConsumeCodeCache
235+
: ScriptCompiler::kEagerCompile;
221236
ScriptCompiler::Source script_source(source, origin, cached_data);
222237

223-
ScriptCompiler::CompileOptions options;
224-
if (result_type == CompilationResultType::kCodeCache) {
225-
options = ScriptCompiler::kEagerCompile;
226-
} else if (use_cache) {
227-
options = ScriptCompiler::kConsumeCodeCache;
228-
} else {
229-
options = ScriptCompiler::kNoCompileOptions;
230-
}
231-
232238
MaybeLocal<Function> maybe_fun =
233239
ScriptCompiler::CompileFunctionInContext(context,
234240
&script_source,
@@ -244,10 +250,14 @@ MaybeLocal<Value> NativeModuleLoader::LookupAndCompile(
244250
// In the case of early errors, v8 is already capable of
245251
// decorating the stack for us - note that we use CompileFunctionInContext
246252
// so there is no need to worry about wrappers.
247-
return MaybeLocal<Value>();
253+
return MaybeLocal<Function>();
248254
}
249255

250256
Local<Function> fun = maybe_fun.ToLocalChecked();
257+
// XXX(joyeecheung): this bookkeeping is not exactly accurate because
258+
// it only starts after the Environment is created, so the per_context.js
259+
// will never be in any of these two sets, but the two sets are only for
260+
// testing anyway.
251261
if (use_cache) {
252262
if (optional_env != nullptr) {
253263
// This could happen when Node is run with any v8 flag, but
@@ -264,29 +274,15 @@ MaybeLocal<Value> NativeModuleLoader::LookupAndCompile(
264274
}
265275
}
266276

267-
if (result_type == CompilationResultType::kCodeCache) {
268-
std::unique_ptr<ScriptCompiler::CachedData> cached_data(
269-
ScriptCompiler::CreateCodeCacheForFunction(fun));
270-
CHECK_NE(cached_data, nullptr);
271-
size_t cached_data_length = cached_data->length;
272-
// Since we have no special allocator to create an ArrayBuffer
273-
// from a new'ed pointer, we will need to copy it - but this
274-
// code path is only run by the tooling that generates the code
275-
// cache to be bundled in the binary
276-
// so it should be fine.
277-
MallocedBuffer<uint8_t> copied(cached_data->length);
278-
memcpy(copied.data, cached_data->data, cached_data_length);
279-
Local<ArrayBuffer> buf =
280-
ArrayBuffer::New(isolate,
281-
copied.release(),
282-
cached_data_length,
283-
ArrayBufferCreationMode::kInternalized);
284-
ret = Uint8Array::New(buf, 0, cached_data_length);
285-
} else {
286-
ret = fun;
287-
}
277+
// Generate new cache for next compilation
278+
std::unique_ptr<ScriptCompiler::CachedData> new_cached_data(
279+
ScriptCompiler::CreateCodeCacheForFunction(fun));
280+
CHECK_NE(new_cached_data, nullptr);
288281

289-
return scope.Escape(ret);
282+
// The old entry should've been erased by now so we can just emplace
283+
code_cache_.emplace(id, std::move(new_cached_data));
284+
285+
return scope.Escape(fun);
290286
}
291287

292288
void NativeModuleLoader::Initialize(Local<Object> target,
@@ -320,8 +316,7 @@ void NativeModuleLoader::Initialize(Local<Object> target,
320316
target, "getCacheUsage", NativeModuleLoader::GetCacheUsage);
321317
env->SetMethod(
322318
target, "compileFunction", NativeModuleLoader::CompileFunction);
323-
env->SetMethod(
324-
target, "compileCodeCache", NativeModuleLoader::CompileCodeCache);
319+
env->SetMethod(target, "getCodeCache", NativeModuleLoader::GetCodeCache);
325320
// internalBinding('native_module') should be frozen
326321
target->SetIntegrityLevel(context, IntegrityLevel::kFrozen).FromJust();
327322
}

0 commit comments

Comments
 (0)