Skip to content

Commit f6a87ee

Browse files
joyeecheungrichardlau
authored andcommitted
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. We can't compile the code eagerly during snapshot generation and include them into the V8 snapshot itself just now because we need to start the inspector before context deserialization for coverage collection to work. So leave that as a TODO. With this patch the binary size increases by about 666KB (~0.6% increase) in return the worker startup can be 18-19% faster. PR-URL: #51672 Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
1 parent a052b8e commit f6a87ee

File tree

5 files changed

+75
-15
lines changed

5 files changed

+75
-15
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

+22-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,18 @@ 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 compiling primordials,
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 (other less-essential built-ins are compiled lazily to
193+
// avoid bloating the binary size). At runtime any additional compilation is
194+
// done lazily.
195+
bool should_eager_compile_ = false;
196+
std::unordered_set<std::string> to_eager_compile_;
197+
182198
struct BuiltinCodeCache {
183199
RwLock mutex;
184200
BuiltinCodeCacheMap map;
@@ -188,6 +204,7 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
188204

189205
friend class ::PerProcessTest;
190206
};
207+
191208
} // namespace builtins
192209

193210
} // namespace node

src/node_snapshotable.cc

+7-2
Original file line numberDiff line numberDiff line change
@@ -1040,10 +1040,12 @@ ExitCode BuildCodeCacheFromSnapshot(SnapshotData* out,
10401040
Context::Scope context_scope(context);
10411041
builtins::BuiltinLoader builtin_loader;
10421042
// Regenerate all the code cache.
1043-
if (!builtin_loader.CompileAllBuiltins(context)) {
1043+
if (!builtin_loader.CompileAllBuiltinsAndCopyCodeCache(
1044+
context,
1045+
out->env_info.principal_realm.builtins,
1046+
&(out->code_cache))) {
10441047
return ExitCode::kGenericUserError;
10451048
}
1046-
builtin_loader.CopyCodeCache(&(out->code_cache));
10471049
if (per_process::enabled_debug_list.enabled(DebugCategory::MKSNAPSHOT)) {
10481050
for (const auto& item : out->code_cache) {
10491051
std::string size_str = FormatSize(item.data.length);
@@ -1069,6 +1071,9 @@ ExitCode SnapshotBuilder::Generate(
10691071
}
10701072

10711073
if (!WithoutCodeCache(snapshot_config)) {
1074+
per_process::Debug(
1075+
DebugCategory::CODE_CACHE,
1076+
"---\nGenerate code cache to complement snapshot\n---\n");
10721077
// Deserialize the snapshot to recompile code cache. We need to do this in
10731078
// the second pass because V8 requires the code cache to be compiled with a
10741079
// finalized read-only space.

0 commit comments

Comments
 (0)