Skip to content

Commit 0dee86f

Browse files
joyeecheungrichardlau
authored andcommittedMar 25, 2024
src: support configurable snapshot
- Add support for --build-snapshot-config which allows passing snapshot configurations via a JSON configuration file. - Add support for node::SnapshotConfig in the embedder API The initial configurable options are: - "builder" (SnapshotConfig::builder_script_path): path to the builder script. - "withoutCodeCache" (SnapshotFlags::kWithoutCodeCache): disable code cache generation. PR-URL: #50453 Refs: #42566 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Stephen Belanger <[email protected]>
1 parent 54a29ee commit 0dee86f

17 files changed

+454
-103
lines changed
 

‎src/api/embed_helpers.cc

+8-9
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ CommonEnvironmentSetup::CommonEnvironmentSetup(
9292
std::vector<std::string>* errors,
9393
const EmbedderSnapshotData* snapshot_data,
9494
uint32_t flags,
95-
std::function<Environment*(const CommonEnvironmentSetup*)> make_env)
95+
std::function<Environment*(const CommonEnvironmentSetup*)> make_env,
96+
const SnapshotConfig* snapshot_config)
9697
: impl_(new Impl()) {
9798
CHECK_NOT_NULL(platform);
9899
CHECK_NOT_NULL(errors);
@@ -142,8 +143,7 @@ CommonEnvironmentSetup::CommonEnvironmentSetup(
142143

143144
impl_->isolate_data.reset(CreateIsolateData(
144145
isolate, loop, platform, impl_->allocator.get(), snapshot_data));
145-
impl_->isolate_data->set_is_building_snapshot(
146-
impl_->snapshot_creator.has_value());
146+
impl_->isolate_data->set_snapshot_config(snapshot_config);
147147

148148
if (snapshot_data) {
149149
impl_->env.reset(make_env(this));
@@ -176,7 +176,8 @@ CommonEnvironmentSetup::CreateForSnapshotting(
176176
MultiIsolatePlatform* platform,
177177
std::vector<std::string>* errors,
178178
const std::vector<std::string>& args,
179-
const std::vector<std::string>& exec_args) {
179+
const std::vector<std::string>& exec_args,
180+
const SnapshotConfig& snapshot_config) {
180181
// It's not guaranteed that a context that goes through
181182
// v8_inspector::V8Inspector::contextCreated() is runtime-independent,
182183
// so do not start the inspector on the main context when building
@@ -196,7 +197,8 @@ CommonEnvironmentSetup::CreateForSnapshotting(
196197
args,
197198
exec_args,
198199
static_cast<EnvironmentFlags::Flags>(env_flags));
199-
}));
200+
},
201+
&snapshot_config));
200202
if (!errors->empty()) ret.reset();
201203
return ret;
202204
}
@@ -240,10 +242,7 @@ EmbedderSnapshotData::Pointer CommonEnvironmentSetup::CreateSnapshot() {
240242
EmbedderSnapshotData::Pointer result{
241243
new EmbedderSnapshotData(snapshot_data, true)};
242244

243-
auto exit_code = SnapshotBuilder::CreateSnapshot(
244-
snapshot_data,
245-
this,
246-
static_cast<uint8_t>(SnapshotMetadata::Type::kFullyCustomized));
245+
auto exit_code = SnapshotBuilder::CreateSnapshot(snapshot_data, this);
247246
if (exit_code != ExitCode::kNoFailure) return {};
248247

249248
return result;

‎src/env.cc

+15-2
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ std::ostream& operator<<(std::ostream& output,
289289
return output;
290290
}
291291

292+
std::ostream& operator<<(std::ostream& output, const SnapshotFlags& flags) {
293+
output << "static_cast<SnapshotFlags>(" << static_cast<uint32_t>(flags)
294+
<< ")";
295+
return output;
296+
}
297+
292298
std::ostream& operator<<(std::ostream& output, const SnapshotMetadata& i) {
293299
output << "{\n"
294300
<< " "
@@ -300,6 +306,7 @@ std::ostream& operator<<(std::ostream& output, const SnapshotMetadata& i) {
300306
<< " \"" << i.node_arch << "\", // node_arch\n"
301307
<< " \"" << i.node_platform << "\", // node_platform\n"
302308
<< " " << i.v8_cache_version_tag << ", // v8_cache_version_tag\n"
309+
<< " " << i.flags << ", // flags\n"
303310
<< "}";
304311
return output;
305312
}
@@ -810,8 +817,14 @@ Environment::Environment(IsolateData* isolate_data,
810817
isolate_data->worker_context()->env()->builtin_loader());
811818
} else if (isolate_data->snapshot_data() != nullptr) {
812819
// ... otherwise, if a snapshot was provided, use its code cache.
813-
builtin_loader()->RefreshCodeCache(
814-
isolate_data->snapshot_data()->code_cache);
820+
size_t cache_size = isolate_data->snapshot_data()->code_cache.size();
821+
per_process::Debug(DebugCategory::CODE_CACHE,
822+
"snapshot contains %zu code cache\n",
823+
cache_size);
824+
if (cache_size > 0) {
825+
builtin_loader()->RefreshCodeCache(
826+
isolate_data->snapshot_data()->code_cache);
827+
}
815828
}
816829

817830
// We'll be creating new objects so make sure we've entered the context.

‎src/env.h

+13-3
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,15 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
147147
void MemoryInfo(MemoryTracker* tracker) const override;
148148
IsolateDataSerializeInfo Serialize(v8::SnapshotCreator* creator);
149149

150-
bool is_building_snapshot() const { return is_building_snapshot_; }
151-
void set_is_building_snapshot(bool value) { is_building_snapshot_ = value; }
150+
bool is_building_snapshot() const { return snapshot_config_.has_value(); }
151+
const SnapshotConfig* snapshot_config() const {
152+
return snapshot_config_.has_value() ? &(snapshot_config_.value()) : nullptr;
153+
}
154+
void set_snapshot_config(const SnapshotConfig* config) {
155+
if (config != nullptr) {
156+
snapshot_config_ = *config; // Copy the config.
157+
}
158+
}
152159

153160
uint16_t* embedder_id_for_cppgc() const;
154161
uint16_t* embedder_id_for_non_cppgc() const;
@@ -237,11 +244,13 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
237244
uv_loop_t* const event_loop_;
238245
NodeArrayBufferAllocator* const node_allocator_;
239246
MultiIsolatePlatform* platform_;
247+
240248
const SnapshotData* snapshot_data_;
249+
std::optional<SnapshotConfig> snapshot_config_;
250+
241251
std::unique_ptr<v8::CppHeap> cpp_heap_;
242252
std::shared_ptr<PerIsolateOptions> options_;
243253
worker::Worker* worker_context_ = nullptr;
244-
bool is_building_snapshot_ = false;
245254
PerIsolateWrapperData* wrapper_data_;
246255

247256
static Mutex isolate_data_mutex_;
@@ -526,6 +535,7 @@ struct SnapshotMetadata {
526535
std::string node_platform;
527536
// Result of v8::ScriptCompiler::CachedDataVersionTag().
528537
uint32_t v8_cache_version_tag;
538+
SnapshotFlags flags;
529539
};
530540

531541
struct SnapshotData {

‎src/node.cc

+41-10
Original file line numberDiff line numberDiff line change
@@ -1209,10 +1209,39 @@ ExitCode GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr,
12091209
// nullptr indicates there's no snapshot data.
12101210
DCHECK_NULL(*snapshot_data_ptr);
12111211

1212+
SnapshotConfig snapshot_config;
1213+
const std::string& config_path =
1214+
per_process::cli_options->per_isolate->build_snapshot_config;
1215+
// For snapshot config read from JSON, we fix up process.argv[1] using the
1216+
// "builder" field.
1217+
std::vector<std::string> args_maybe_patched;
1218+
args_maybe_patched.reserve(result->args().size() + 1);
1219+
if (!config_path.empty()) {
1220+
std::optional<SnapshotConfig> optional_config =
1221+
ReadSnapshotConfig(config_path.c_str());
1222+
if (!optional_config.has_value()) {
1223+
return ExitCode::kGenericUserError;
1224+
}
1225+
snapshot_config = std::move(optional_config.value());
1226+
DCHECK(snapshot_config.builder_script_path.has_value());
1227+
args_maybe_patched.emplace_back(result->args()[0]);
1228+
args_maybe_patched.emplace_back(
1229+
snapshot_config.builder_script_path.value());
1230+
if (result->args().size() > 1) {
1231+
args_maybe_patched.insert(args_maybe_patched.end(),
1232+
result->args().begin() + 1,
1233+
result->args().end());
1234+
}
1235+
} else {
1236+
snapshot_config.builder_script_path = result->args()[1];
1237+
args_maybe_patched = result->args();
1238+
}
1239+
DCHECK(snapshot_config.builder_script_path.has_value());
1240+
const std::string& builder_script =
1241+
snapshot_config.builder_script_path.value();
12121242
// node:embedded_snapshot_main indicates that we are using the
12131243
// embedded snapshot and we are not supposed to clean it up.
1214-
const std::string& main_script = result->args()[1];
1215-
if (main_script == "node:embedded_snapshot_main") {
1244+
if (builder_script == "node:embedded_snapshot_main") {
12161245
*snapshot_data_ptr = SnapshotBuilder::GetEmbeddedSnapshotData();
12171246
if (*snapshot_data_ptr == nullptr) {
12181247
// The Node.js binary is built without embedded snapshot
@@ -1224,24 +1253,25 @@ ExitCode GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr,
12241253
return exit_code;
12251254
}
12261255
} else {
1227-
// Otherwise, load and run the specified main script.
1256+
// Otherwise, load and run the specified builder script.
12281257
std::unique_ptr<SnapshotData> generated_data =
12291258
std::make_unique<SnapshotData>();
1230-
std::string main_script_content;
1231-
int r = ReadFileSync(&main_script_content, main_script.c_str());
1259+
std::string builder_script_content;
1260+
int r = ReadFileSync(&builder_script_content, builder_script.c_str());
12321261
if (r != 0) {
12331262
FPrintF(stderr,
1234-
"Cannot read main script %s for building snapshot. %s: %s",
1235-
main_script,
1263+
"Cannot read builder script %s for building snapshot. %s: %s",
1264+
builder_script,
12361265
uv_err_name(r),
12371266
uv_strerror(r));
12381267
return ExitCode::kGenericUserError;
12391268
}
12401269

12411270
exit_code = node::SnapshotBuilder::Generate(generated_data.get(),
1242-
result->args(),
1271+
args_maybe_patched,
12431272
result->exec_args(),
1244-
main_script_content);
1273+
builder_script_content,
1274+
snapshot_config);
12451275
if (exit_code == ExitCode::kNoFailure) {
12461276
*snapshot_data_ptr = generated_data.release();
12471277
} else {
@@ -1371,7 +1401,8 @@ static ExitCode StartInternal(int argc, char** argv) {
13711401

13721402
// --build-snapshot indicates that we are in snapshot building mode.
13731403
if (per_process::cli_options->per_isolate->build_snapshot) {
1374-
if (result->args().size() < 2) {
1404+
if (per_process::cli_options->per_isolate->build_snapshot_config.empty() &&
1405+
result->args().size() < 2) {
13751406
fprintf(stderr,
13761407
"--build-snapshot must be used with an entry point script.\n"
13771408
"Usage: node --build-snapshot /path/to/entry.js\n");

‎src/node.h

+32-2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080

8181
#include <functional>
8282
#include <memory>
83+
#include <optional>
8384
#include <ostream>
8485

8586
// We cannot use __POSIX__ in this header because that's only defined when
@@ -659,6 +660,33 @@ enum Flags : uint64_t {
659660
};
660661
} // namespace EnvironmentFlags
661662

663+
enum class SnapshotFlags : uint32_t {
664+
kDefault = 0,
665+
// Whether code cache should be generated as part of the snapshot.
666+
// Code cache reduces the time spent on compiling functions included
667+
// in the snapshot at the expense of a bigger snapshot size and
668+
// potentially breaking portability of the snapshot.
669+
kWithoutCodeCache = 1 << 0,
670+
};
671+
672+
struct SnapshotConfig {
673+
SnapshotFlags flags = SnapshotFlags::kDefault;
674+
675+
// When builder_script_path is std::nullopt, the snapshot is generated as a
676+
// built-in snapshot instead of a custom one, and it's expected that the
677+
// built-in snapshot only contains states that reproduce in every run of the
678+
// application. The event loop won't be run when generating a built-in
679+
// snapshot, so asynchronous operations should be avoided.
680+
//
681+
// When builder_script_path is an std::string, it should match args[1]
682+
// passed to CreateForSnapshotting(). The embedder is also expected to use
683+
// LoadEnvironment() to run a script matching this path. In that case the
684+
// snapshot is generated as a custom snapshot and the event loop is run, so
685+
// the snapshot builder can execute asynchronous operations as long as they
686+
// are run to completion when the snapshot is taken.
687+
std::optional<std::string> builder_script_path;
688+
};
689+
662690
struct InspectorParentHandle {
663691
virtual ~InspectorParentHandle() = default;
664692
};
@@ -870,7 +898,8 @@ class NODE_EXTERN CommonEnvironmentSetup {
870898
MultiIsolatePlatform* platform,
871899
std::vector<std::string>* errors,
872900
const std::vector<std::string>& args = {},
873-
const std::vector<std::string>& exec_args = {});
901+
const std::vector<std::string>& exec_args = {},
902+
const SnapshotConfig& snapshot_config = {});
874903
EmbedderSnapshotData::Pointer CreateSnapshot();
875904

876905
struct uv_loop_s* event_loop() const;
@@ -905,7 +934,8 @@ class NODE_EXTERN CommonEnvironmentSetup {
905934
std::vector<std::string>*,
906935
const EmbedderSnapshotData*,
907936
uint32_t flags,
908-
std::function<Environment*(const CommonEnvironmentSetup*)>);
937+
std::function<Environment*(const CommonEnvironmentSetup*)>,
938+
const SnapshotConfig* config = nullptr);
909939
};
910940

911941
// Implementation for CommonEnvironmentSetup::Create

‎src/node_internals.h

+1
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ std::string Basename(const std::string& str, const std::string& extension);
417417

418418
node_module napi_module_to_node_module(const napi_module* mod);
419419

420+
std::ostream& operator<<(std::ostream& output, const SnapshotFlags& flags);
420421
std::ostream& operator<<(std::ostream& output,
421422
const std::vector<SnapshotIndex>& v);
422423
std::ostream& operator<<(std::ostream& output,

‎src/node_main_instance.cc

-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ NodeMainInstance::NodeMainInstance(const SnapshotData* snapshot_data,
5656
platform,
5757
array_buffer_allocator_.get(),
5858
snapshot_data->AsEmbedderWrapper().get()));
59-
isolate_data_->set_is_building_snapshot(
60-
per_process::cli_options->per_isolate->build_snapshot);
6159

6260
isolate_data_->max_young_gen_size =
6361
isolate_params_->constraints.max_young_generation_size_in_bytes();

‎src/node_options.cc

+6
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,12 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
849849
"Generate a snapshot blob when the process exits.",
850850
&PerIsolateOptions::build_snapshot,
851851
kDisallowedInEnvvar);
852+
AddOption("--build-snapshot-config",
853+
"Generate a snapshot blob when the process exits using a"
854+
"JSON configuration in the specified path.",
855+
&PerIsolateOptions::build_snapshot_config,
856+
kDisallowedInEnvvar);
857+
Implies("--build-snapshot-config", "--build-snapshot");
852858

853859
Insert(eop, &PerIsolateOptions::get_per_env_options);
854860
}

‎src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ class PerIsolateOptions : public Options {
235235
bool experimental_shadow_realm = false;
236236
std::string report_signal = "SIGUSR2";
237237
bool build_snapshot = false;
238+
std::string build_snapshot_config;
238239
inline EnvironmentOptions* get_per_env_options();
239240
void CheckOptions(std::vector<std::string>* errors,
240241
std::vector<std::string>* argv) override;

‎src/node_sea.cc

+11-4
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,18 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
377377
ExitCode GenerateSnapshotForSEA(const SeaConfig& config,
378378
const std::vector<std::string>& args,
379379
const std::vector<std::string>& exec_args,
380-
const std::string& main_script,
380+
const std::string& builder_script_content,
381+
const SnapshotConfig& snapshot_config,
381382
std::vector<char>* snapshot_blob) {
382383
SnapshotData snapshot;
383384
// TODO(joyeecheung): make the arguments configurable through the JSON
384385
// config or a programmatic API.
385386
std::vector<std::string> patched_args = {args[0], config.main_path};
386-
ExitCode exit_code = SnapshotBuilder::Generate(
387-
&snapshot, patched_args, exec_args, main_script);
387+
ExitCode exit_code = SnapshotBuilder::Generate(&snapshot,
388+
patched_args,
389+
exec_args,
390+
builder_script_content,
391+
snapshot_config);
388392
if (exit_code != ExitCode::kNoFailure) {
389393
return exit_code;
390394
}
@@ -481,8 +485,11 @@ ExitCode GenerateSingleExecutableBlob(
481485
bool builds_snapshot_from_main =
482486
static_cast<bool>(config.flags & SeaFlags::kUseSnapshot);
483487
if (builds_snapshot_from_main) {
488+
// TODO(joyeecheung): allow passing snapshot configuration in SEA configs.
489+
SnapshotConfig snapshot_config;
490+
snapshot_config.builder_script_path = main_script;
484491
ExitCode exit_code = GenerateSnapshotForSEA(
485-
config, args, exec_args, main_script, &snapshot_blob);
492+
config, args, exec_args, main_script, snapshot_config, &snapshot_blob);
486493
if (exit_code != ExitCode::kNoFailure) {
487494
return exit_code;
488495
}

‎src/node_snapshot_builder.h

+17-14
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,25 @@ namespace node {
1616
class ExternalReferenceRegistry;
1717
struct SnapshotData;
1818

19+
std::optional<SnapshotConfig> ReadSnapshotConfig(const char* path);
20+
1921
class NODE_EXTERN_PRIVATE SnapshotBuilder {
2022
public:
21-
static ExitCode GenerateAsSource(
22-
const char* out_path,
23+
static ExitCode GenerateAsSource(const char* out_path,
24+
const std::vector<std::string>& args,
25+
const std::vector<std::string>& exec_args,
26+
const SnapshotConfig& config,
27+
bool use_array_literals = false);
28+
29+
// Generate the snapshot into out. builder_script_content should match
30+
// config.builder_script_path. This is passed separately
31+
// in case the script is already read for other purposes.
32+
static ExitCode Generate(
33+
SnapshotData* out,
2334
const std::vector<std::string>& args,
2435
const std::vector<std::string>& exec_args,
25-
std::optional<std::string_view> main_script_path = std::nullopt,
26-
bool use_array_literals = false);
27-
28-
// Generate the snapshot into out.
29-
static ExitCode Generate(SnapshotData* out,
30-
const std::vector<std::string>& args,
31-
const std::vector<std::string>& exec_args,
32-
std::optional<std::string_view> main_script);
36+
std::optional<std::string_view> builder_script_content,
37+
const SnapshotConfig& config);
3338

3439
// If nullptr is returned, the binary is not built with embedded
3540
// snapshot.
@@ -39,10 +44,8 @@ class NODE_EXTERN_PRIVATE SnapshotBuilder {
3944

4045
static const std::vector<intptr_t>& CollectExternalReferences();
4146

42-
static ExitCode CreateSnapshot(
43-
SnapshotData* out,
44-
CommonEnvironmentSetup* setup,
45-
/*SnapshotMetadata::Type*/ uint8_t snapshot_type);
47+
static ExitCode CreateSnapshot(SnapshotData* out,
48+
CommonEnvironmentSetup* setup);
4649

4750
private:
4851
static std::unique_ptr<ExternalReferenceRegistry> registry_;

‎src/node_snapshotable.cc

+127-44
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "debug_utils-inl.h"
1111
#include "encoding_binding.h"
1212
#include "env-inl.h"
13+
#include "json_parser.h"
1314
#include "node_blob.h"
1415
#include "node_builtins.h"
1516
#include "node_contextify.h"
@@ -541,6 +542,7 @@ SnapshotMetadata SnapshotDeserializer::Read() {
541542
result.node_arch = ReadString();
542543
result.node_platform = ReadString();
543544
result.v8_cache_version_tag = ReadArithmetic<uint32_t>();
545+
result.flags = static_cast<SnapshotFlags>(ReadArithmetic<uint32_t>());
544546

545547
if (is_debug) {
546548
std::string str = ToStr(result);
@@ -570,6 +572,9 @@ size_t SnapshotSerializer::Write(const SnapshotMetadata& data) {
570572
Debug("Write V8 cached data version tag %" PRIx32 "\n",
571573
data.v8_cache_version_tag);
572574
written_total += WriteArithmetic<uint32_t>(data.v8_cache_version_tag);
575+
Debug("Write snapshot flags %" PRIx32 "\n",
576+
static_cast<uint32_t>(data.flags));
577+
written_total += WriteArithmetic<uint32_t>(static_cast<uint32_t>(data.flags));
573578
return written_total;
574579
}
575580

@@ -690,19 +695,21 @@ bool SnapshotData::Check() const {
690695
return false;
691696
}
692697

693-
uint32_t current_cache_version = v8::ScriptCompiler::CachedDataVersionTag();
694-
if (metadata.v8_cache_version_tag != current_cache_version &&
695-
metadata.type == SnapshotMetadata::Type::kFullyCustomized) {
696-
// For now we only do this check for the customized snapshots - we know
697-
// that the flags we use in the default snapshot are limited and safe
698-
// enough so we can relax the constraints for it.
699-
fprintf(stderr,
700-
"Failed to load the startup snapshot because it was built with "
701-
"a different version of V8 or with different V8 configurations.\n"
702-
"Expected tag %" PRIx32 ", read %" PRIx32 "\n",
703-
current_cache_version,
704-
metadata.v8_cache_version_tag);
705-
return false;
698+
if (metadata.type == SnapshotMetadata::Type::kFullyCustomized &&
699+
!WithoutCodeCache(metadata.flags)) {
700+
uint32_t current_cache_version = v8::ScriptCompiler::CachedDataVersionTag();
701+
if (metadata.v8_cache_version_tag != current_cache_version) {
702+
// For now we only do this check for the customized snapshots - we know
703+
// that the flags we use in the default snapshot are limited and safe
704+
// enough so we can relax the constraints for it.
705+
fprintf(stderr,
706+
"Failed to load the startup snapshot because it was built with "
707+
"a different version of V8 or with different V8 configurations.\n"
708+
"Expected tag %" PRIx32 ", read %" PRIx32 "\n",
709+
current_cache_version,
710+
metadata.v8_cache_version_tag);
711+
return false;
712+
}
706713
}
707714

708715
// TODO(joyeecheung): check incompatible Node.js flags.
@@ -912,23 +919,91 @@ void SnapshotBuilder::InitializeIsolateParams(const SnapshotData* data,
912919
const_cast<v8::StartupData*>(&(data->v8_snapshot_blob_data));
913920
}
914921

922+
SnapshotFlags operator|(SnapshotFlags x, SnapshotFlags y) {
923+
return static_cast<SnapshotFlags>(static_cast<uint32_t>(x) |
924+
static_cast<uint32_t>(y));
925+
}
926+
927+
SnapshotFlags operator&(SnapshotFlags x, SnapshotFlags y) {
928+
return static_cast<SnapshotFlags>(static_cast<uint32_t>(x) &
929+
static_cast<uint32_t>(y));
930+
}
931+
932+
SnapshotFlags operator|=(/* NOLINT (runtime/references) */ SnapshotFlags& x,
933+
SnapshotFlags y) {
934+
return x = x | y;
935+
}
936+
937+
bool WithoutCodeCache(const SnapshotFlags& flags) {
938+
return static_cast<bool>(flags & SnapshotFlags::kWithoutCodeCache);
939+
}
940+
941+
bool WithoutCodeCache(const SnapshotConfig& config) {
942+
return WithoutCodeCache(config.flags);
943+
}
944+
945+
std::optional<SnapshotConfig> ReadSnapshotConfig(const char* config_path) {
946+
std::string config_content;
947+
int r = ReadFileSync(&config_content, config_path);
948+
if (r != 0) {
949+
FPrintF(stderr,
950+
"Cannot read snapshot configuration from %s: %s\n",
951+
config_path,
952+
uv_strerror(r));
953+
return std::nullopt;
954+
}
955+
956+
JSONParser parser;
957+
if (!parser.Parse(config_content)) {
958+
FPrintF(stderr, "Cannot parse JSON from %s\n", config_path);
959+
return std::nullopt;
960+
}
961+
962+
SnapshotConfig result;
963+
result.builder_script_path = parser.GetTopLevelStringField("builder");
964+
if (!result.builder_script_path.has_value()) {
965+
FPrintF(stderr,
966+
"\"builder\" field of %s is not a non-empty string\n",
967+
config_path);
968+
return std::nullopt;
969+
}
970+
971+
std::optional<bool> WithoutCodeCache =
972+
parser.GetTopLevelBoolField("withoutCodeCache");
973+
if (!WithoutCodeCache.has_value()) {
974+
FPrintF(stderr,
975+
"\"withoutCodeCache\" field of %s is not a boolean\n",
976+
config_path);
977+
return std::nullopt;
978+
}
979+
if (WithoutCodeCache.value()) {
980+
result.flags |= SnapshotFlags::kWithoutCodeCache;
981+
}
982+
983+
return result;
984+
}
985+
915986
ExitCode BuildSnapshotWithoutCodeCache(
916987
SnapshotData* out,
917988
const std::vector<std::string>& args,
918989
const std::vector<std::string>& exec_args,
919-
std::optional<std::string_view> main_script) {
990+
std::optional<std::string_view> builder_script_content,
991+
const SnapshotConfig& config) {
992+
DCHECK(builder_script_content.has_value() ==
993+
config.builder_script_path.has_value());
920994
// The default snapshot is meant to be runtime-independent and has more
921995
// restrictions. We do not enable the inspector and do not run the event
922996
// loop when building the default snapshot to avoid inconsistencies, but
923997
// we do for the fully customized one, and they are expected to fixup the
924998
// inconsistencies using v8.startupSnapshot callbacks.
925999
SnapshotMetadata::Type snapshot_type =
926-
main_script.has_value() ? SnapshotMetadata::Type::kFullyCustomized
927-
: SnapshotMetadata::Type::kDefault;
1000+
builder_script_content.has_value()
1001+
? SnapshotMetadata::Type::kFullyCustomized
1002+
: SnapshotMetadata::Type::kDefault;
9281003

9291004
std::vector<std::string> errors;
9301005
auto setup = CommonEnvironmentSetup::CreateForSnapshotting(
931-
per_process::v8_platform.Platform(), &errors, args, exec_args);
1006+
per_process::v8_platform.Platform(), &errors, args, exec_args, config);
9321007
if (!setup) {
9331008
for (const std::string& err : errors)
9341009
fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str());
@@ -954,7 +1029,7 @@ ExitCode BuildSnapshotWithoutCodeCache(
9541029
#if HAVE_INSPECTOR
9551030
env->InitializeInspector({});
9561031
#endif
957-
if (LoadEnvironment(env, main_script.value()).IsEmpty()) {
1032+
if (LoadEnvironment(env, builder_script_content.value()).IsEmpty()) {
9581033
return ExitCode::kGenericUserError;
9591034
}
9601035

@@ -969,8 +1044,7 @@ ExitCode BuildSnapshotWithoutCodeCache(
9691044
}
9701045
}
9711046

972-
return SnapshotBuilder::CreateSnapshot(
973-
out, setup.get(), static_cast<uint8_t>(snapshot_type));
1047+
return SnapshotBuilder::CreateSnapshot(out, setup.get());
9741048
}
9751049

9761050
ExitCode BuildCodeCacheFromSnapshot(SnapshotData* out,
@@ -1014,28 +1088,32 @@ ExitCode SnapshotBuilder::Generate(
10141088
SnapshotData* out,
10151089
const std::vector<std::string>& args,
10161090
const std::vector<std::string>& exec_args,
1017-
std::optional<std::string_view> main_script) {
1018-
ExitCode code =
1019-
BuildSnapshotWithoutCodeCache(out, args, exec_args, main_script);
1091+
std::optional<std::string_view> builder_script_content,
1092+
const SnapshotConfig& snapshot_config) {
1093+
ExitCode code = BuildSnapshotWithoutCodeCache(
1094+
out, args, exec_args, builder_script_content, snapshot_config);
10201095
if (code != ExitCode::kNoFailure) {
10211096
return code;
10221097
}
10231098

1024-
#ifdef NODE_USE_NODE_CODE_CACHE
1025-
// Deserialize the snapshot to recompile code cache. We need to do this in the
1026-
// second pass because V8 requires the code cache to be compiled with a
1027-
// finalized read-only space.
1028-
return BuildCodeCacheFromSnapshot(out, args, exec_args);
1029-
#else
1099+
if (!WithoutCodeCache(snapshot_config)) {
1100+
// Deserialize the snapshot to recompile code cache. We need to do this in
1101+
// the second pass because V8 requires the code cache to be compiled with a
1102+
// finalized read-only space.
1103+
return BuildCodeCacheFromSnapshot(out, args, exec_args);
1104+
}
1105+
10301106
return ExitCode::kNoFailure;
1031-
#endif
10321107
}
10331108

10341109
ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out,
1035-
CommonEnvironmentSetup* setup,
1036-
uint8_t snapshot_type_u8) {
1110+
CommonEnvironmentSetup* setup) {
1111+
const SnapshotConfig* config = setup->isolate_data()->snapshot_config();
1112+
DCHECK_NOT_NULL(config);
10371113
SnapshotMetadata::Type snapshot_type =
1038-
static_cast<SnapshotMetadata::Type>(snapshot_type_u8);
1114+
config->builder_script_path.has_value()
1115+
? SnapshotMetadata::Type::kFullyCustomized
1116+
: SnapshotMetadata::Type::kDefault;
10391117
Isolate* isolate = setup->isolate();
10401118
Environment* env = setup->env();
10411119
SnapshotCreator* creator = setup->snapshot_creator();
@@ -1098,8 +1176,10 @@ ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out,
10981176
}
10991177

11001178
// Must be out of HandleScope
1101-
out->v8_snapshot_blob_data =
1102-
creator->CreateBlob(SnapshotCreator::FunctionCodeHandling::kKeep);
1179+
SnapshotCreator::FunctionCodeHandling handling =
1180+
WithoutCodeCache(*config) ? SnapshotCreator::FunctionCodeHandling::kClear
1181+
: SnapshotCreator::FunctionCodeHandling::kKeep;
1182+
out->v8_snapshot_blob_data = creator->CreateBlob(handling);
11031183

11041184
// We must be able to rehash the blob when we restore it or otherwise
11051185
// the hash seed would be fixed by V8, introducing a vulnerability.
@@ -1111,7 +1191,8 @@ ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out,
11111191
per_process::metadata.versions.node,
11121192
per_process::metadata.arch,
11131193
per_process::metadata.platform,
1114-
v8::ScriptCompiler::CachedDataVersionTag()};
1194+
v8::ScriptCompiler::CachedDataVersionTag(),
1195+
config->flags};
11151196

11161197
// We cannot resurrect the handles from the snapshot, so make sure that
11171198
// no handles are left open in the environment after the blob is created
@@ -1132,21 +1213,22 @@ ExitCode SnapshotBuilder::GenerateAsSource(
11321213
const char* out_path,
11331214
const std::vector<std::string>& args,
11341215
const std::vector<std::string>& exec_args,
1135-
std::optional<std::string_view> main_script_path,
1216+
const SnapshotConfig& config,
11361217
bool use_array_literals) {
1137-
std::string main_script_content;
1138-
std::optional<std::string_view> main_script_optional;
1139-
if (main_script_path.has_value()) {
1140-
int r = ReadFileSync(&main_script_content, main_script_path.value().data());
1218+
std::string builder_script_content;
1219+
std::optional<std::string_view> builder_script_optional;
1220+
if (config.builder_script_path.has_value()) {
1221+
std::string_view builder_script_path = config.builder_script_path.value();
1222+
int r = ReadFileSync(&builder_script_content, builder_script_path.data());
11411223
if (r != 0) {
11421224
FPrintF(stderr,
11431225
"Cannot read main script %s for building snapshot. %s: %s",
1144-
main_script_path.value(),
1226+
builder_script_path,
11451227
uv_err_name(r),
11461228
uv_strerror(r));
11471229
return ExitCode::kGenericUserError;
11481230
}
1149-
main_script_optional = main_script_content;
1231+
builder_script_optional = builder_script_content;
11501232
}
11511233

11521234
std::ofstream out(out_path, std::ios::out | std::ios::binary);
@@ -1156,7 +1238,8 @@ ExitCode SnapshotBuilder::GenerateAsSource(
11561238
}
11571239

11581240
SnapshotData data;
1159-
ExitCode exit_code = Generate(&data, args, exec_args, main_script_optional);
1241+
ExitCode exit_code =
1242+
Generate(&data, args, exec_args, builder_script_optional, config);
11601243
if (exit_code != ExitCode::kNoFailure) {
11611244
return exit_code;
11621245
}

‎src/node_snapshotable.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ struct PropInfo {
2525

2626
typedef size_t SnapshotIndex;
2727

28+
bool WithoutCodeCache(const SnapshotFlags& flags);
29+
bool WithoutCodeCache(const SnapshotConfig& config);
30+
2831
// When serializing an embedder object, we'll serialize the native states
2932
// into a chunk that can be mapped into a subclass of InternalFieldInfoBase,
3033
// and pass it into the V8 callback as the payload of StartupData.
@@ -154,7 +157,6 @@ class BindingData : public SnapshotableObject {
154157
AliasedUint8Array is_building_snapshot_buffer_;
155158
InternalFieldInfo* internal_field_info_ = nullptr;
156159
};
157-
158160
} // namespace mksnapshot
159161

160162
} // namespace node

‎test/embedding/embedtest.cc

+26-8
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ int RunNodeInstance(MultiIsolatePlatform* platform,
6868
// --embedder-snapshot-blob blob-path
6969
// --embedder-snapshot-create
7070
// [--embedder-snapshot-as-file]
71+
// [--without-code-cache]
7172
// Running snapshot:
7273
// embedtest --embedder-snapshot-blob blob-path
7374
// [--embedder-snapshot-as-file]
@@ -80,13 +81,21 @@ int RunNodeInstance(MultiIsolatePlatform* platform,
8081
std::vector<std::string> filtered_args;
8182
bool is_building_snapshot = false;
8283
bool snapshot_as_file = false;
84+
std::optional<node::SnapshotConfig> snapshot_config;
8385
std::string snapshot_blob_path;
8486
for (size_t i = 0; i < args.size(); ++i) {
8587
const std::string& arg = args[i];
8688
if (arg == "--embedder-snapshot-create") {
8789
is_building_snapshot = true;
8890
} else if (arg == "--embedder-snapshot-as-file") {
8991
snapshot_as_file = true;
92+
} else if (arg == "--without-code-cache") {
93+
if (!snapshot_config.has_value()) {
94+
snapshot_config = node::SnapshotConfig{};
95+
}
96+
snapshot_config.value().flags = static_cast<node::SnapshotFlags>(
97+
static_cast<uint32_t>(snapshot_config.value().flags) |
98+
static_cast<uint32_t>(node::SnapshotFlags::kWithoutCodeCache));
9099
} else if (arg == "--embedder-snapshot-blob") {
91100
assert(i + 1 < args.size());
92101
snapshot_blob_path = args[i + 1];
@@ -130,14 +139,23 @@ int RunNodeInstance(MultiIsolatePlatform* platform,
130139
}
131140

132141
std::vector<std::string> errors;
133-
std::unique_ptr<CommonEnvironmentSetup> setup =
134-
snapshot
135-
? CommonEnvironmentSetup::CreateFromSnapshot(
136-
platform, &errors, snapshot.get(), filtered_args, exec_args)
137-
: is_building_snapshot ? CommonEnvironmentSetup::CreateForSnapshotting(
138-
platform, &errors, filtered_args, exec_args)
139-
: CommonEnvironmentSetup::Create(
140-
platform, &errors, filtered_args, exec_args);
142+
std::unique_ptr<CommonEnvironmentSetup> setup;
143+
144+
if (snapshot) {
145+
setup = CommonEnvironmentSetup::CreateFromSnapshot(
146+
platform, &errors, snapshot.get(), filtered_args, exec_args);
147+
} else if (is_building_snapshot) {
148+
if (snapshot_config.has_value()) {
149+
setup = CommonEnvironmentSetup::CreateForSnapshotting(
150+
platform, &errors, filtered_args, exec_args, snapshot_config.value());
151+
} else {
152+
setup = CommonEnvironmentSetup::CreateForSnapshotting(
153+
platform, &errors, filtered_args, exec_args);
154+
}
155+
} else {
156+
setup = CommonEnvironmentSetup::Create(
157+
platform, &errors, filtered_args, exec_args);
158+
}
141159
if (!setup) {
142160
for (const std::string& err : errors)
143161
fprintf(stderr, "%s: %s\n", binary_path.c_str(), err.c_str());

‎test/embedding/test-embedding.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ function getReadFileCodeForPath(path) {
7777
}
7878

7979
// Basic snapshot support
80-
for (const extraSnapshotArgs of [[], ['--embedder-snapshot-as-file']]) {
80+
for (const extraSnapshotArgs of [
81+
[], ['--embedder-snapshot-as-file'], ['--without-code-cache'],
82+
]) {
8183
// readSync + eval since snapshots don't support userland require() (yet)
8284
const snapshotFixture = fixtures.path('snapshot', 'echo-args.js');
8385
const blobPath = tmpdir.resolve('embedder-snapshot.blob');

‎test/parallel/test-snapshot-config.js

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
'use strict';
2+
3+
// This tests --build-snapshot-config.
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const {
8+
spawnSyncAndExitWithoutError,
9+
spawnSyncAndExit,
10+
} = require('../common/child_process');
11+
const tmpdir = require('../common/tmpdir');
12+
const fixtures = require('../common/fixtures');
13+
const fs = require('fs');
14+
15+
const blobPath = tmpdir.resolve('snapshot.blob');
16+
const builderScript = fixtures.path('snapshot', 'mutate-fs.js');
17+
const checkFile = fixtures.path('snapshot', 'check-mutate-fs.js');
18+
const configPath = tmpdir.resolve('snapshot.json');
19+
tmpdir.refresh();
20+
{
21+
// Relative path.
22+
spawnSyncAndExit(process.execPath, [
23+
'--snapshot-blob',
24+
blobPath,
25+
'--build-snapshot-config',
26+
'snapshot.json',
27+
], {
28+
cwd: tmpdir.path
29+
}, {
30+
signal: null,
31+
status: 1,
32+
trim: true,
33+
stderr: /Cannot read snapshot configuration from snapshot\.json/
34+
});
35+
36+
// Absolute path.
37+
spawnSyncAndExit(process.execPath, [
38+
'--snapshot-blob',
39+
blobPath,
40+
'--build-snapshot-config',
41+
configPath,
42+
], {
43+
cwd: tmpdir.path
44+
}, {
45+
signal: null,
46+
status: 1,
47+
trim: true,
48+
stderr: /Cannot read snapshot configuration from .+snapshot\.json/
49+
});
50+
}
51+
52+
function writeConfig(config) {
53+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
54+
}
55+
56+
{
57+
tmpdir.refresh();
58+
// Config without "builder" field should be rejected.
59+
writeConfig({});
60+
spawnSyncAndExit(process.execPath, [
61+
'--snapshot-blob',
62+
blobPath,
63+
'--build-snapshot-config',
64+
configPath,
65+
], {
66+
cwd: tmpdir.path
67+
}, {
68+
signal: null,
69+
status: 1,
70+
trim: true,
71+
stderr: /"builder" field of .+snapshot\.json is not a non-empty string/
72+
});
73+
}
74+
75+
let sizeWithCache;
76+
{
77+
tmpdir.refresh();
78+
// Create a working snapshot.
79+
writeConfig({ builder: builderScript });
80+
spawnSyncAndExitWithoutError(process.execPath, [
81+
'--snapshot-blob',
82+
blobPath,
83+
'--build-snapshot-config',
84+
configPath,
85+
], {
86+
cwd: tmpdir.path
87+
}, {});
88+
const stats = fs.statSync(blobPath);
89+
assert(stats.isFile());
90+
sizeWithCache = stats.size;
91+
92+
// Check the snapshot.
93+
spawnSyncAndExitWithoutError(process.execPath, [
94+
'--snapshot-blob',
95+
blobPath,
96+
checkFile,
97+
], {
98+
cwd: tmpdir.path
99+
});
100+
}
101+
102+
let sizeWithoutCache;
103+
{
104+
tmpdir.refresh();
105+
// Create a working snapshot.
106+
writeConfig({ builder: builderScript, withoutCodeCache: true });
107+
spawnSyncAndExitWithoutError(process.execPath, [
108+
'--snapshot-blob',
109+
blobPath,
110+
'--build-snapshot-config',
111+
configPath,
112+
], {
113+
env: {
114+
...process.env,
115+
NODE_DEBUG_NATIVE: 'CODE_CACHE'
116+
},
117+
cwd: tmpdir.path
118+
}, {});
119+
const stats = fs.statSync(blobPath);
120+
assert(stats.isFile());
121+
sizeWithoutCache = stats.size;
122+
assert(sizeWithoutCache < sizeWithCache,
123+
`sizeWithoutCache = ${sizeWithoutCache} >= sizeWithCache ${sizeWithCache}`);
124+
// Check the snapshot.
125+
spawnSyncAndExitWithoutError(process.execPath, [
126+
'--snapshot-blob',
127+
blobPath,
128+
checkFile,
129+
], {
130+
cwd: tmpdir.path,
131+
env: {
132+
...process.env,
133+
NODE_DEBUG_NATIVE: 'CODE_CACHE'
134+
},
135+
}, {
136+
stderr: /snapshot contains 0 code cache/
137+
});
138+
}

‎tools/snapshot/node_mksnapshot.cc

+12-3
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ int BuildSnapshot(int argc, char* argv[]) {
7070
CHECK_EQ(result->exit_code(), 0);
7171

7272
std::string out_path;
73-
std::optional<std::string_view> main_script_path = std::nullopt;
73+
std::optional<std::string_view> builder_script_path = std::nullopt;
7474
if (node::per_process::cli_options->per_isolate->build_snapshot) {
75-
main_script_path = result->args()[1];
75+
builder_script_path = result->args()[1];
7676
out_path = result->args()[2];
7777
} else {
7878
out_path = result->args()[1];
@@ -84,11 +84,20 @@ int BuildSnapshot(int argc, char* argv[]) {
8484
bool use_array_literals = false;
8585
#endif
8686

87+
node::SnapshotConfig snapshot_config;
88+
snapshot_config.builder_script_path = builder_script_path;
89+
90+
#ifdef NODE_USE_NODE_CODE_CACHE
91+
snapshot_config.flags = node::SnapshotFlags::kDefault;
92+
#else
93+
snapshot_config.flags = node::SnapshotFlags::kWithoutCodeCache;
94+
#endif
95+
8796
node::ExitCode exit_code =
8897
node::SnapshotBuilder::GenerateAsSource(out_path.c_str(),
8998
result->args(),
9099
result->exec_args(),
91-
main_script_path,
100+
snapshot_config,
92101
use_array_literals);
93102

94103
node::TearDownOncePerProcess();

0 commit comments

Comments
 (0)
Please sign in to comment.