Skip to content

Commit e262e31

Browse files
committed
src: compile code eagerly in snapshot builder
By default V8 only compiles the top-level function and skips code generation for inner functions - that would only be done when those inner functions are invoked. Since builtins are compiled as wrapped functions, most functions that look visually top-level are not actually included in the built-in code cache. For most of the builtins this is not too bad because usually only a subset of all builtin functions are needed by a particular application and including all their code in the binary would incur an unnecessary size overhead. But there is also a subset of more commonly used builtins and it would be better to include the inner functions in the built-in code cache because they are more universally used by most applications. This patch changes the compilation strategy to eager compilation (including inner functions) for the following scripts: 1. Primordials (internal/per_context/*), in all situations. 2. Bootstrap scripts (internal/bootstrap/*) and main scripts (internal/main/*), when being compiled for built-in code cache. 3. Any scripts loaded during built-in snapshot generation. With this patch the binary size increases by about 1.2MB (~1% increase) in return the startup of --version command of bundled CLIs and the core startup of processes are 2-4% faster and worker startup can be 10-12% faster.
1 parent fc80168 commit e262e31

File tree

5 files changed

+81
-18
lines changed

5 files changed

+81
-18
lines changed

src/api/environment.cc

+3
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,9 @@ Maybe<bool> InitializePrimordials(Local<Context> context) {
802802
// relatively cheap and all the scripts that we may want to run at
803803
// startup are always present in it.
804804
thread_local builtins::BuiltinLoader builtin_loader;
805+
// Primordials can always be just eagerly compiled.
806+
builtin_loader.SetEagerCompile();
807+
805808
for (const char** module = context_files; *module != nullptr; module++) {
806809
Local<Value> arguments[] = {exports, primordials};
807810
if (builtin_loader

src/env.cc

+9
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,15 @@ Environment::Environment(IsolateData* isolate_data,
827827
}
828828
}
829829

830+
// We are supposed to call builtin_loader_.SetEagerCompile() in
831+
// snapshot mode here because it's beneficial to compile built-ins
832+
// loaded in the snapshot eagerly and include the code of inner functions
833+
// that are likely to be used by user since they are part of the core
834+
// startup. But this requires us to start the coverage collections
835+
// before Environment/Context creation which is not currently possible.
836+
// TODO(joyeecheung): refactor V8ProfilerConnection classes to parse
837+
// JSON without v8 and lift this restriction.
838+
830839
// We'll be creating new objects so make sure we've entered the context.
831840
HandleScope handle_scope(isolate);
832841

src/node_builtins.cc

+34-8
Original file line numberDiff line numberDiff line change
@@ -286,15 +286,24 @@ MaybeLocal<Function> BuiltinLoader::LookupAndCompileInternal(
286286
ScriptCompiler::CompileOptions options =
287287
has_cache ? ScriptCompiler::kConsumeCodeCache
288288
: ScriptCompiler::kNoCompileOptions;
289+
if (should_eager_compile_) {
290+
options = ScriptCompiler::kEagerCompile;
291+
} else if (!to_eager_compile_.empty()) {
292+
if (to_eager_compile_.find(id) != to_eager_compile_.end()) {
293+
options = ScriptCompiler::kEagerCompile;
294+
}
295+
}
289296
ScriptCompiler::Source script_source(
290297
source,
291298
origin,
292299
has_cache ? cached_data.AsCachedData().release() : nullptr);
293300

294-
per_process::Debug(DebugCategory::CODE_CACHE,
295-
"Compiling %s %s code cache\n",
296-
id,
297-
has_cache ? "with" : "without");
301+
per_process::Debug(
302+
DebugCategory::CODE_CACHE,
303+
"Compiling %s %s code cache %s\n",
304+
id,
305+
has_cache ? "with" : "without",
306+
options == ScriptCompiler::kEagerCompile ? "eagerly" : "lazily");
298307

299308
MaybeLocal<Function> maybe_fun =
300309
ScriptCompiler::CompileFunction(context,
@@ -481,14 +490,33 @@ MaybeLocal<Value> BuiltinLoader::CompileAndCall(Local<Context> context,
481490
return fn->Call(context, undefined, argc, argv);
482491
}
483492

484-
bool BuiltinLoader::CompileAllBuiltins(Local<Context> context) {
493+
bool BuiltinLoader::CompileAllBuiltinsAndCopyCodeCache(
494+
Local<Context> context,
495+
const std::vector<std::string>& eager_builtins,
496+
std::vector<CodeCacheInfo>* out) {
485497
std::vector<std::string_view> ids = GetBuiltinIds();
486498
bool all_succeeded = true;
487499
std::string v8_tools_prefix = "internal/deps/v8/tools/";
500+
std::string primordial_prefix = "internal/per_context/";
501+
std::string bootstrap_prefix = "internal/bootstrap/";
502+
std::string main_prefix = "internal/main/";
503+
to_eager_compile_ = std::unordered_set<std::string>(eager_builtins.begin(),
504+
eager_builtins.end());
505+
488506
for (const auto& id : ids) {
489507
if (id.compare(0, v8_tools_prefix.size(), v8_tools_prefix) == 0) {
508+
// No need to generate code cache for v8 scripts.
490509
continue;
491510
}
511+
512+
// Eagerly compile primordials/boostrap/main scripts during code cache
513+
// generation.
514+
if (id.compare(0, primordial_prefix.size(), primordial_prefix) == 0 ||
515+
id.compare(0, bootstrap_prefix.size(), bootstrap_prefix) == 0 ||
516+
id.compare(0, main_prefix.size(), main_prefix) == 0) {
517+
to_eager_compile_.emplace(id);
518+
}
519+
492520
v8::TryCatch bootstrapCatch(context->GetIsolate());
493521
auto fn = LookupAndCompile(context, id.data(), nullptr);
494522
if (bootstrapCatch.HasCaught()) {
@@ -503,14 +531,12 @@ bool BuiltinLoader::CompileAllBuiltins(Local<Context> context) {
503531
SaveCodeCache(id.data(), fn.ToLocalChecked());
504532
}
505533
}
506-
return all_succeeded;
507-
}
508534

509-
void BuiltinLoader::CopyCodeCache(std::vector<CodeCacheInfo>* out) const {
510535
RwLock::ScopedReadLock lock(code_cache_->mutex);
511536
for (auto const& item : code_cache_->map) {
512537
out->push_back({item.first, item.second});
513538
}
539+
return all_succeeded;
514540
}
515541

516542
void BuiltinLoader::RefreshCodeCache(const std::vector<CodeCacheInfo>& in) {

src/node_builtins.h

+21-5
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
#include <map>
88
#include <memory>
99
#include <optional>
10-
#include <set>
1110
#include <string>
11+
#include <unordered_set>
1212
#include <vector>
1313
#include "node_external_reference.h"
1414
#include "node_mutex.h"
@@ -112,12 +112,18 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
112112
bool Exists(const char* id);
113113
bool Add(const char* id, const UnionBytes& source);
114114

115-
bool CompileAllBuiltins(v8::Local<v8::Context> context);
115+
bool CompileAllBuiltinsAndCopyCodeCache(
116+
v8::Local<v8::Context> context,
117+
const std::vector<std::string>& lazy_builtins,
118+
std::vector<CodeCacheInfo>* out);
116119
void RefreshCodeCache(const std::vector<CodeCacheInfo>& in);
117-
void CopyCodeCache(std::vector<CodeCacheInfo>* out) const;
118120

119121
void CopySourceAndCodeCacheReferenceFrom(const BuiltinLoader* other);
120122

123+
std::vector<std::string_view> GetBuiltinIds() const;
124+
125+
void SetEagerCompile() { should_eager_compile_ = true; }
126+
121127
private:
122128
// Only allow access from friends.
123129
friend class CodeCacheBuilder;
@@ -126,8 +132,6 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
126132
void LoadJavaScriptSource(); // Loads data into source_
127133
UnionBytes GetConfig(); // Return data for config.gypi
128134

129-
std::vector<std::string_view> GetBuiltinIds() const;
130-
131135
struct BuiltinCategories {
132136
std::set<std::string> can_be_required;
133137
std::set<std::string> cannot_be_required;
@@ -179,6 +183,17 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
179183

180184
const UnionBytes config_;
181185

186+
// If any bulitins should be eagerly compiled i.e. with inner functions
187+
// compiled too, either use should_eager_compile_ to compile all builtins
188+
// eagerly, or use to_eager_compile_ to compile specific builtins eagerly.
189+
// Currently we set should_eager_compile_ to true when building the snapshot,
190+
// and use to_eager_compile_ to compile code cache that complements the
191+
// snapshot, where builtins already loaded in the snapshot and a few extras
192+
// are compiled eagerly. At runtime any additional compilation is done
193+
// lazily (except primordials in no-snapshot mode).
194+
bool should_eager_compile_ = false;
195+
std::unordered_set<std::string> to_eager_compile_;
196+
182197
struct BuiltinCodeCache {
183198
RwLock mutex;
184199
BuiltinCodeCacheMap map;
@@ -188,6 +203,7 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
188203

189204
friend class ::PerProcessTest;
190205
};
206+
191207
} // namespace builtins
192208

193209
} // namespace node

src/node_snapshotable.cc

+14-5
Original file line numberDiff line numberDiff line change
@@ -1069,10 +1069,12 @@ ExitCode BuildCodeCacheFromSnapshot(SnapshotData* out,
10691069
Context::Scope context_scope(context);
10701070
builtins::BuiltinLoader builtin_loader;
10711071
// Regenerate all the code cache.
1072-
if (!builtin_loader.CompileAllBuiltins(context)) {
1072+
if (!builtin_loader.CompileAllBuiltinsAndCopyCodeCache(
1073+
context,
1074+
out->env_info.principal_realm.builtins,
1075+
&(out->code_cache))) {
10731076
return ExitCode::kGenericUserError;
10741077
}
1075-
builtin_loader.CopyCodeCache(&(out->code_cache));
10761078
if (per_process::enabled_debug_list.enabled(DebugCategory::MKSNAPSHOT)) {
10771079
for (const auto& item : out->code_cache) {
10781080
std::string size_str = FormatSize(item.data.length);
@@ -1098,6 +1100,9 @@ ExitCode SnapshotBuilder::Generate(
10981100
}
10991101

11001102
if (!WithoutCodeCache(snapshot_config)) {
1103+
per_process::Debug(
1104+
DebugCategory::CODE_CACHE,
1105+
"---\nGenerate code cache to complement snapshot\n---\n");
11011106
// Deserialize the snapshot to recompile code cache. We need to do this in
11021107
// the second pass because V8 requires the code cache to be compiled with a
11031108
// finalized read-only space.
@@ -1176,10 +1181,14 @@ ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out,
11761181
CHECK_EQ(index, SnapshotData::kNodeMainContextIndex);
11771182
}
11781183

1179-
// Must be out of HandleScope
1184+
// Must be out of HandleScope.
1185+
// Clear the code in the snapshot because we can't compile them eagerly
1186+
// during snapshot generation yet. We'll rely on the accompanying eagerly
1187+
// compiled code cache instead.
1188+
// TODO(joyeecheung): use FunctionCodeHandling::kKeep when we can start
1189+
// profilers before Environment/Context creation.
11801190
SnapshotCreator::FunctionCodeHandling handling =
1181-
WithoutCodeCache(*config) ? SnapshotCreator::FunctionCodeHandling::kClear
1182-
: SnapshotCreator::FunctionCodeHandling::kKeep;
1191+
SnapshotCreator::FunctionCodeHandling::kClear;
11831192
out->v8_snapshot_blob_data = creator->CreateBlob(handling);
11841193

11851194
// We must be able to rehash the blob when we restore it or otherwise

0 commit comments

Comments
 (0)