mirror of https://github.com/nodejs/node.git
sea: add support for V8 bytecode-only caching
Refs: https://github.com/nodejs/single-executable/issues/73 Signed-off-by: Darshan Sen <raisinten@gmail.com> PR-URL: https://github.com/nodejs/node/pull/48191 Fixes: https://github.com/nodejs/single-executable/issues/73 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
parent
d246536924
commit
6cd678965f
|
@ -10,6 +10,9 @@ changes:
|
||||||
- version: REPLACEME
|
- version: REPLACEME
|
||||||
pr-url: https://github.com/nodejs/node/pull/46824
|
pr-url: https://github.com/nodejs/node/pull/46824
|
||||||
description: Added support for "useSnapshot".
|
description: Added support for "useSnapshot".
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/48191
|
||||||
|
description: Added support for "useCodeCache".
|
||||||
-->
|
-->
|
||||||
|
|
||||||
> Stability: 1 - Experimental: This feature is being designed and will change.
|
> Stability: 1 - Experimental: This feature is being designed and will change.
|
||||||
|
@ -174,7 +177,8 @@ The configuration currently reads the following top-level fields:
|
||||||
"main": "/path/to/bundled/script.js",
|
"main": "/path/to/bundled/script.js",
|
||||||
"output": "/path/to/write/the/generated/blob.blob",
|
"output": "/path/to/write/the/generated/blob.blob",
|
||||||
"disableExperimentalSEAWarning": true, // Default: false
|
"disableExperimentalSEAWarning": true, // Default: false
|
||||||
"useSnapshot": false // Default: false
|
"useSnapshot": false, // Default: false
|
||||||
|
"useCodeCache": true // Default: false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -213,6 +217,18 @@ and the main script can use the [`v8.startupSnapshot` API][] to adapt to
|
||||||
these constraints. See
|
these constraints. See
|
||||||
[documentation about startup snapshot support in Node.js][].
|
[documentation about startup snapshot support in Node.js][].
|
||||||
|
|
||||||
|
### V8 code cache support
|
||||||
|
|
||||||
|
When `useCodeCache` is set to `true` in the configuration, during the generation
|
||||||
|
of the single executable preparation blob, Node.js will compile the `main`
|
||||||
|
script to generate the V8 code cache. The generated code cache would be part of
|
||||||
|
the preparation blob and get injected into the final executable. When the single
|
||||||
|
executable application is launched, instead of compiling the `main` script from
|
||||||
|
scratch, Node.js would use the code cache to speed up the compilation, then
|
||||||
|
execute the script, which would improve the startup performance.
|
||||||
|
|
||||||
|
**Note:** `import()` does not work when `useCodeCache` is `true`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
### `require(id)` in the injected module is not file based
|
### `require(id)` in the injected module is not file based
|
||||||
|
|
|
@ -1123,7 +1123,7 @@ Module.prototype.require = function(id) {
|
||||||
let resolvedArgv;
|
let resolvedArgv;
|
||||||
let hasPausedEntry = false;
|
let hasPausedEntry = false;
|
||||||
let Script;
|
let Script;
|
||||||
function wrapSafe(filename, content, cjsModuleInstance) {
|
function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
|
||||||
if (patched) {
|
if (patched) {
|
||||||
const wrapper = Module.wrap(content);
|
const wrapper = Module.wrap(content);
|
||||||
if (Script === undefined) {
|
if (Script === undefined) {
|
||||||
|
@ -1158,6 +1158,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
|
||||||
'__dirname',
|
'__dirname',
|
||||||
], {
|
], {
|
||||||
filename,
|
filename,
|
||||||
|
cachedData: codeCache,
|
||||||
importModuleDynamically(specifier, _, importAssertions) {
|
importModuleDynamically(specifier, _, importAssertions) {
|
||||||
const cascadedLoader = getCascadedLoader();
|
const cascadedLoader = getCascadedLoader();
|
||||||
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
|
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
|
||||||
|
@ -1165,6 +1166,13 @@ function wrapSafe(filename, content, cjsModuleInstance) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The code cache is used for SEAs only.
|
||||||
|
if (codeCache &&
|
||||||
|
result.cachedDataRejected !== false &&
|
||||||
|
internalBinding('sea').isSea()) {
|
||||||
|
process.emitWarning('Code cache data rejected.');
|
||||||
|
}
|
||||||
|
|
||||||
// Cache the source map for the module if present.
|
// Cache the source map for the module if present.
|
||||||
if (result.sourceMapURL) {
|
if (result.sourceMapURL) {
|
||||||
maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL);
|
maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors');
|
|
||||||
const { BuiltinModule: { normalizeRequirableId } } = require('internal/bootstrap/realm');
|
const { BuiltinModule: { normalizeRequirableId } } = require('internal/bootstrap/realm');
|
||||||
const { Module, wrapSafe } = require('internal/modules/cjs/loader');
|
const { Module, wrapSafe } = require('internal/modules/cjs/loader');
|
||||||
|
const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors');
|
||||||
|
const { getCodeCache, getCodePath, isSea } = internalBinding('sea');
|
||||||
|
|
||||||
// This is roughly the same as:
|
// This is roughly the same as:
|
||||||
//
|
//
|
||||||
|
@ -15,7 +16,11 @@ const { Module, wrapSafe } = require('internal/modules/cjs/loader');
|
||||||
|
|
||||||
function embedderRunCjs(contents) {
|
function embedderRunCjs(contents) {
|
||||||
const filename = process.execPath;
|
const filename = process.execPath;
|
||||||
const compiledWrapper = wrapSafe(filename, contents);
|
const compiledWrapper = wrapSafe(
|
||||||
|
isSea() ? getCodePath() : filename,
|
||||||
|
contents,
|
||||||
|
undefined,
|
||||||
|
getCodeCache());
|
||||||
|
|
||||||
const customModule = new Module(filename, null);
|
const customModule = new Module(filename, null);
|
||||||
customModule.filename = filename;
|
customModule.filename = filename;
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
#include "util-inl.h"
|
#include "util-inl.h"
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
using v8::ArrayBuffer;
|
|
||||||
using v8::Context;
|
using v8::Context;
|
||||||
using v8::Isolate;
|
using v8::Isolate;
|
||||||
using v8::Local;
|
using v8::Local;
|
||||||
|
@ -12,26 +11,8 @@ using v8::Object;
|
||||||
using v8::String;
|
using v8::String;
|
||||||
using v8::Value;
|
using v8::Value;
|
||||||
|
|
||||||
static Isolate* NewIsolate(v8::ArrayBuffer::Allocator* allocator) {
|
|
||||||
Isolate* isolate = Isolate::Allocate();
|
|
||||||
CHECK_NOT_NULL(isolate);
|
|
||||||
per_process::v8_platform.Platform()->RegisterIsolate(isolate,
|
|
||||||
uv_default_loop());
|
|
||||||
Isolate::CreateParams params;
|
|
||||||
params.array_buffer_allocator = allocator;
|
|
||||||
Isolate::Initialize(isolate, params);
|
|
||||||
return isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
void JSONParser::FreeIsolate(Isolate* isolate) {
|
|
||||||
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
|
|
||||||
isolate->Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONParser::JSONParser()
|
JSONParser::JSONParser()
|
||||||
: allocator_(ArrayBuffer::Allocator::NewDefaultAllocator()),
|
: handle_scope_(isolate_.get()),
|
||||||
isolate_(NewIsolate(allocator_.get())),
|
|
||||||
handle_scope_(isolate_.get()),
|
|
||||||
context_(isolate_.get(), Context::New(isolate_.get())),
|
context_(isolate_.get(), Context::New(isolate_.get())),
|
||||||
context_scope_(context_.Get(isolate_.get())) {}
|
context_scope_(context_.Get(isolate_.get())) {}
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,7 @@ class JSONParser {
|
||||||
private:
|
private:
|
||||||
// We might want a lighter-weight JSON parser for this use case. But for now
|
// We might want a lighter-weight JSON parser for this use case. But for now
|
||||||
// using V8 is good enough.
|
// using V8 is good enough.
|
||||||
static void FreeIsolate(v8::Isolate* isolate);
|
RAIIIsolate isolate_;
|
||||||
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_;
|
|
||||||
DeleteFnPtr<v8::Isolate, FreeIsolate> isolate_;
|
|
||||||
v8::HandleScope handle_scope_;
|
v8::HandleScope handle_scope_;
|
||||||
v8::Global<v8::Context> context_;
|
v8::Global<v8::Context> context_;
|
||||||
v8::Context::Scope context_scope_;
|
v8::Context::Scope context_scope_;
|
||||||
|
|
|
@ -935,6 +935,22 @@ Maybe<bool> StoreCodeCacheResult(
|
||||||
return Just(true);
|
return Just(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(RaisinTen): Reuse in ContextifyContext::CompileFunction().
|
||||||
|
MaybeLocal<Function> CompileFunction(Local<Context> context,
|
||||||
|
Local<String> filename,
|
||||||
|
Local<String> content,
|
||||||
|
std::vector<Local<String>>* parameters) {
|
||||||
|
ScriptOrigin script_origin(context->GetIsolate(), filename, 0, 0, true);
|
||||||
|
ScriptCompiler::Source script_source(content, script_origin);
|
||||||
|
|
||||||
|
return ScriptCompiler::CompileFunction(context,
|
||||||
|
&script_source,
|
||||||
|
parameters->size(),
|
||||||
|
parameters->data(),
|
||||||
|
0,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
bool ContextifyScript::InstanceOf(Environment* env,
|
bool ContextifyScript::InstanceOf(Environment* env,
|
||||||
const Local<Value>& value) {
|
const Local<Value>& value) {
|
||||||
return !value.IsEmpty() &&
|
return !value.IsEmpty() &&
|
||||||
|
|
|
@ -210,6 +210,12 @@ v8::Maybe<bool> StoreCodeCacheResult(
|
||||||
bool produce_cached_data,
|
bool produce_cached_data,
|
||||||
std::unique_ptr<v8::ScriptCompiler::CachedData> new_cached_data);
|
std::unique_ptr<v8::ScriptCompiler::CachedData> new_cached_data);
|
||||||
|
|
||||||
|
v8::MaybeLocal<v8::Function> CompileFunction(
|
||||||
|
v8::Local<v8::Context> context,
|
||||||
|
v8::Local<v8::String> filename,
|
||||||
|
v8::Local<v8::String> content,
|
||||||
|
std::vector<v8::Local<v8::String>>* parameters);
|
||||||
|
|
||||||
} // namespace contextify
|
} // namespace contextify
|
||||||
} // namespace node
|
} // namespace node
|
||||||
|
|
||||||
|
|
187
src/node_sea.cc
187
src/node_sea.cc
|
@ -4,11 +4,14 @@
|
||||||
#include "debug_utils-inl.h"
|
#include "debug_utils-inl.h"
|
||||||
#include "env-inl.h"
|
#include "env-inl.h"
|
||||||
#include "json_parser.h"
|
#include "json_parser.h"
|
||||||
|
#include "node_contextify.h"
|
||||||
|
#include "node_errors.h"
|
||||||
#include "node_external_reference.h"
|
#include "node_external_reference.h"
|
||||||
#include "node_internals.h"
|
#include "node_internals.h"
|
||||||
#include "node_snapshot_builder.h"
|
#include "node_snapshot_builder.h"
|
||||||
#include "node_union_bytes.h"
|
#include "node_union_bytes.h"
|
||||||
#include "node_v8_platform-inl.h"
|
#include "node_v8_platform-inl.h"
|
||||||
|
#include "util-inl.h"
|
||||||
|
|
||||||
// The POSTJECT_SENTINEL_FUSE macro is a string of random characters selected by
|
// The POSTJECT_SENTINEL_FUSE macro is a string of random characters selected by
|
||||||
// the Node.js project that is present only once in the entire binary. It is
|
// the Node.js project that is present only once in the entire binary. It is
|
||||||
|
@ -27,10 +30,19 @@
|
||||||
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
||||||
|
|
||||||
using node::ExitCode;
|
using node::ExitCode;
|
||||||
|
using v8::ArrayBuffer;
|
||||||
|
using v8::BackingStore;
|
||||||
using v8::Context;
|
using v8::Context;
|
||||||
|
using v8::DataView;
|
||||||
|
using v8::Function;
|
||||||
using v8::FunctionCallbackInfo;
|
using v8::FunctionCallbackInfo;
|
||||||
|
using v8::HandleScope;
|
||||||
|
using v8::Isolate;
|
||||||
using v8::Local;
|
using v8::Local;
|
||||||
|
using v8::NewStringType;
|
||||||
using v8::Object;
|
using v8::Object;
|
||||||
|
using v8::ScriptCompiler;
|
||||||
|
using v8::String;
|
||||||
using v8::Value;
|
using v8::Value;
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
|
@ -76,6 +88,12 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
|
||||||
written_total += WriteArithmetic<uint32_t>(flags);
|
written_total += WriteArithmetic<uint32_t>(flags);
|
||||||
DCHECK_EQ(written_total, SeaResource::kHeaderSize);
|
DCHECK_EQ(written_total, SeaResource::kHeaderSize);
|
||||||
|
|
||||||
|
Debug("Write SEA code path %p, size=%zu\n",
|
||||||
|
sea.code_path.data(),
|
||||||
|
sea.code_path.size());
|
||||||
|
written_total +=
|
||||||
|
WriteStringView(sea.code_path, StringLogMode::kAddressAndContent);
|
||||||
|
|
||||||
Debug("Write SEA resource %s %p, size=%zu\n",
|
Debug("Write SEA resource %s %p, size=%zu\n",
|
||||||
sea.use_snapshot() ? "snapshot" : "code",
|
sea.use_snapshot() ? "snapshot" : "code",
|
||||||
sea.main_code_or_snapshot.data(),
|
sea.main_code_or_snapshot.data(),
|
||||||
|
@ -84,6 +102,14 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
|
||||||
WriteStringView(sea.main_code_or_snapshot,
|
WriteStringView(sea.main_code_or_snapshot,
|
||||||
sea.use_snapshot() ? StringLogMode::kAddressOnly
|
sea.use_snapshot() ? StringLogMode::kAddressOnly
|
||||||
: StringLogMode::kAddressAndContent);
|
: StringLogMode::kAddressAndContent);
|
||||||
|
|
||||||
|
if (sea.code_cache.has_value()) {
|
||||||
|
Debug("Write SEA resource code cache %p, size=%zu\n",
|
||||||
|
sea.code_cache->data(),
|
||||||
|
sea.code_cache->size());
|
||||||
|
written_total +=
|
||||||
|
WriteStringView(sea.code_cache.value(), StringLogMode::kAddressOnly);
|
||||||
|
}
|
||||||
return written_total;
|
return written_total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +135,11 @@ SeaResource SeaDeserializer::Read() {
|
||||||
Debug("Read SEA flags %x\n", static_cast<uint32_t>(flags));
|
Debug("Read SEA flags %x\n", static_cast<uint32_t>(flags));
|
||||||
CHECK_EQ(read_total, SeaResource::kHeaderSize);
|
CHECK_EQ(read_total, SeaResource::kHeaderSize);
|
||||||
|
|
||||||
|
std::string_view code_path =
|
||||||
|
ReadStringView(StringLogMode::kAddressAndContent);
|
||||||
|
Debug(
|
||||||
|
"Read SEA code path %p, size=%zu\n", code_path.data(), code_path.size());
|
||||||
|
|
||||||
bool use_snapshot = static_cast<bool>(flags & SeaFlags::kUseSnapshot);
|
bool use_snapshot = static_cast<bool>(flags & SeaFlags::kUseSnapshot);
|
||||||
std::string_view code =
|
std::string_view code =
|
||||||
ReadStringView(use_snapshot ? StringLogMode::kAddressOnly
|
ReadStringView(use_snapshot ? StringLogMode::kAddressOnly
|
||||||
|
@ -118,7 +149,15 @@ SeaResource SeaDeserializer::Read() {
|
||||||
use_snapshot ? "snapshot" : "code",
|
use_snapshot ? "snapshot" : "code",
|
||||||
code.data(),
|
code.data(),
|
||||||
code.size());
|
code.size());
|
||||||
return {flags, code};
|
|
||||||
|
std::string_view code_cache;
|
||||||
|
if (static_cast<bool>(flags & SeaFlags::kUseCodeCache)) {
|
||||||
|
code_cache = ReadStringView(StringLogMode::kAddressOnly);
|
||||||
|
Debug("Read SEA resource code cache %p, size=%zu\n",
|
||||||
|
code_cache.data(),
|
||||||
|
code_cache.size());
|
||||||
|
}
|
||||||
|
return {flags, code_path, code, code_cache};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string_view FindSingleExecutableBlob() {
|
std::string_view FindSingleExecutableBlob() {
|
||||||
|
@ -167,6 +206,10 @@ bool IsSingleExecutable() {
|
||||||
return postject_has_resource();
|
return postject_has_resource();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IsSea(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
args.GetReturnValue().Set(IsSingleExecutable());
|
||||||
|
}
|
||||||
|
|
||||||
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
|
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
|
||||||
bool is_building_sea =
|
bool is_building_sea =
|
||||||
!per_process::cli_options->experimental_sea_config.empty();
|
!per_process::cli_options->experimental_sea_config.empty();
|
||||||
|
@ -185,6 +228,54 @@ void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
|
||||||
sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning));
|
sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GetCodeCache(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
if (!IsSingleExecutable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Isolate* isolate = args.GetIsolate();
|
||||||
|
|
||||||
|
SeaResource sea_resource = FindSingleExecutableResource();
|
||||||
|
|
||||||
|
if (!static_cast<bool>(sea_resource.flags & SeaFlags::kUseCodeCache)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<BackingStore> backing_store = ArrayBuffer::NewBackingStore(
|
||||||
|
const_cast<void*>(
|
||||||
|
static_cast<const void*>(sea_resource.code_cache->data())),
|
||||||
|
sea_resource.code_cache->length(),
|
||||||
|
[](void* /* data */, size_t /* length */, void* /* deleter_data */) {
|
||||||
|
// The code cache data blob is not freed here because it is a static
|
||||||
|
// blob which is not allocated by the BackingStore allocator.
|
||||||
|
},
|
||||||
|
nullptr);
|
||||||
|
Local<ArrayBuffer> array_buffer = ArrayBuffer::New(isolate, backing_store);
|
||||||
|
Local<DataView> data_view =
|
||||||
|
DataView::New(array_buffer, 0, array_buffer->ByteLength());
|
||||||
|
|
||||||
|
args.GetReturnValue().Set(data_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetCodePath(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
DCHECK(IsSingleExecutable());
|
||||||
|
|
||||||
|
Isolate* isolate = args.GetIsolate();
|
||||||
|
|
||||||
|
SeaResource sea_resource = FindSingleExecutableResource();
|
||||||
|
|
||||||
|
Local<String> code_path;
|
||||||
|
if (!String::NewFromUtf8(isolate,
|
||||||
|
sea_resource.code_path.data(),
|
||||||
|
NewStringType::kNormal,
|
||||||
|
sea_resource.code_path.length())
|
||||||
|
.ToLocal(&code_path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.GetReturnValue().Set(code_path);
|
||||||
|
}
|
||||||
|
|
||||||
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
|
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
|
||||||
// Repeats argv[0] at position 1 on argv as a replacement for the missing
|
// Repeats argv[0] at position 1 on argv as a replacement for the missing
|
||||||
// entry point file path.
|
// entry point file path.
|
||||||
|
@ -269,6 +360,17 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
|
||||||
result.flags |= SeaFlags::kUseSnapshot;
|
result.flags |= SeaFlags::kUseSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<bool> use_code_cache =
|
||||||
|
parser.GetTopLevelBoolField("useCodeCache");
|
||||||
|
if (!use_code_cache.has_value()) {
|
||||||
|
FPrintF(
|
||||||
|
stderr, "\"useCodeCache\" field of %s is not a Boolean\n", config_path);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (use_code_cache.value()) {
|
||||||
|
result.flags |= SeaFlags::kUseCodeCache;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,6 +409,59 @@ ExitCode GenerateSnapshotForSEA(const SeaConfig& config,
|
||||||
return ExitCode::kNoFailure;
|
return ExitCode::kNoFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> GenerateCodeCache(std::string_view main_path,
|
||||||
|
std::string_view main_script) {
|
||||||
|
RAIIIsolate raii_isolate;
|
||||||
|
Isolate* isolate = raii_isolate.get();
|
||||||
|
|
||||||
|
HandleScope handle_scope(isolate);
|
||||||
|
Local<Context> context = Context::New(isolate);
|
||||||
|
Context::Scope context_scope(context);
|
||||||
|
|
||||||
|
errors::PrinterTryCatch bootstrapCatch(
|
||||||
|
isolate, errors::PrinterTryCatch::kPrintSourceLine);
|
||||||
|
|
||||||
|
Local<String> filename;
|
||||||
|
if (!String::NewFromUtf8(isolate,
|
||||||
|
main_path.data(),
|
||||||
|
NewStringType::kNormal,
|
||||||
|
main_path.length())
|
||||||
|
.ToLocal(&filename)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Local<String> content;
|
||||||
|
if (!String::NewFromUtf8(isolate,
|
||||||
|
main_script.data(),
|
||||||
|
NewStringType::kNormal,
|
||||||
|
main_script.length())
|
||||||
|
.ToLocal(&content)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Local<String>> parameters = {
|
||||||
|
FIXED_ONE_BYTE_STRING(isolate, "exports"),
|
||||||
|
FIXED_ONE_BYTE_STRING(isolate, "require"),
|
||||||
|
FIXED_ONE_BYTE_STRING(isolate, "module"),
|
||||||
|
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
|
||||||
|
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(RaisinTen): Using the V8 code cache prevents us from using `import()`
|
||||||
|
// in the SEA code. Support it.
|
||||||
|
// Refs: https://github.com/nodejs/node/pull/48191#discussion_r1213271430
|
||||||
|
Local<Function> fn;
|
||||||
|
if (!contextify::CompileFunction(context, filename, content, ¶meters)
|
||||||
|
.ToLocal(&fn)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<ScriptCompiler::CachedData> cache{
|
||||||
|
ScriptCompiler::CreateCodeCacheForFunction(fn)};
|
||||||
|
std::string code_cache(cache->data, cache->data + cache->length);
|
||||||
|
return code_cache;
|
||||||
|
}
|
||||||
|
|
||||||
ExitCode GenerateSingleExecutableBlob(
|
ExitCode GenerateSingleExecutableBlob(
|
||||||
const SeaConfig& config,
|
const SeaConfig& config,
|
||||||
const std::vector<std::string>& args,
|
const std::vector<std::string>& args,
|
||||||
|
@ -331,11 +486,33 @@ ExitCode GenerateSingleExecutableBlob(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> optional_code_cache =
|
||||||
|
GenerateCodeCache(config.main_path, main_script);
|
||||||
|
if (!optional_code_cache.has_value()) {
|
||||||
|
FPrintF(stderr, "Cannot generate V8 code cache\n");
|
||||||
|
return ExitCode::kGenericUserError;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string_view> optional_sv_code_cache;
|
||||||
|
std::string code_cache;
|
||||||
|
if (static_cast<bool>(config.flags & SeaFlags::kUseCodeCache)) {
|
||||||
|
std::optional<std::string> optional_code_cache =
|
||||||
|
GenerateCodeCache(config.main_path, main_script);
|
||||||
|
if (!optional_code_cache.has_value()) {
|
||||||
|
FPrintF(stderr, "Cannot generate V8 code cache\n");
|
||||||
|
return ExitCode::kGenericUserError;
|
||||||
|
}
|
||||||
|
code_cache = optional_code_cache.value();
|
||||||
|
optional_sv_code_cache = code_cache;
|
||||||
|
}
|
||||||
|
|
||||||
SeaResource sea{
|
SeaResource sea{
|
||||||
config.flags,
|
config.flags,
|
||||||
|
config.main_path,
|
||||||
builds_snapshot_from_main
|
builds_snapshot_from_main
|
||||||
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
|
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
|
||||||
: std::string_view{main_script.data(), main_script.size()}};
|
: std::string_view{main_script.data(), main_script.size()},
|
||||||
|
optional_sv_code_cache};
|
||||||
|
|
||||||
SeaSerializer serializer;
|
SeaSerializer serializer;
|
||||||
serializer.Write(sea);
|
serializer.Write(sea);
|
||||||
|
@ -374,14 +551,20 @@ void Initialize(Local<Object> target,
|
||||||
Local<Value> unused,
|
Local<Value> unused,
|
||||||
Local<Context> context,
|
Local<Context> context,
|
||||||
void* priv) {
|
void* priv) {
|
||||||
|
SetMethod(context, target, "isSea", IsSea);
|
||||||
SetMethod(context,
|
SetMethod(context,
|
||||||
target,
|
target,
|
||||||
"isExperimentalSeaWarningNeeded",
|
"isExperimentalSeaWarningNeeded",
|
||||||
IsExperimentalSeaWarningNeeded);
|
IsExperimentalSeaWarningNeeded);
|
||||||
|
SetMethod(context, target, "getCodePath", GetCodePath);
|
||||||
|
SetMethod(context, target, "getCodeCache", GetCodeCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||||
|
registry->Register(IsSea);
|
||||||
registry->Register(IsExperimentalSeaWarningNeeded);
|
registry->Register(IsExperimentalSeaWarningNeeded);
|
||||||
|
registry->Register(GetCodePath);
|
||||||
|
registry->Register(GetCodeCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sea
|
} // namespace sea
|
||||||
|
|
|
@ -6,10 +6,12 @@
|
||||||
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
||||||
|
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "node_exit_code.h"
|
#include "node_exit_code.h"
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
|
@ -24,11 +26,14 @@ enum class SeaFlags : uint32_t {
|
||||||
kDefault = 0,
|
kDefault = 0,
|
||||||
kDisableExperimentalSeaWarning = 1 << 0,
|
kDisableExperimentalSeaWarning = 1 << 0,
|
||||||
kUseSnapshot = 1 << 1,
|
kUseSnapshot = 1 << 1,
|
||||||
|
kUseCodeCache = 1 << 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SeaResource {
|
struct SeaResource {
|
||||||
SeaFlags flags = SeaFlags::kDefault;
|
SeaFlags flags = SeaFlags::kDefault;
|
||||||
|
std::string_view code_path;
|
||||||
std::string_view main_code_or_snapshot;
|
std::string_view main_code_or_snapshot;
|
||||||
|
std::optional<std::string_view> code_cache;
|
||||||
|
|
||||||
bool use_snapshot() const;
|
bool use_snapshot() const;
|
||||||
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
|
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
|
||||||
|
|
|
@ -42,7 +42,6 @@ using v8::MaybeLocal;
|
||||||
using v8::Object;
|
using v8::Object;
|
||||||
using v8::ObjectTemplate;
|
using v8::ObjectTemplate;
|
||||||
using v8::ScriptCompiler;
|
using v8::ScriptCompiler;
|
||||||
using v8::ScriptOrigin;
|
|
||||||
using v8::SnapshotCreator;
|
using v8::SnapshotCreator;
|
||||||
using v8::StartupData;
|
using v8::StartupData;
|
||||||
using v8::String;
|
using v8::String;
|
||||||
|
@ -1261,7 +1260,6 @@ void CompileSerializeMain(const FunctionCallbackInfo<Value>& args) {
|
||||||
Local<String> source = args[1].As<String>();
|
Local<String> source = args[1].As<String>();
|
||||||
Isolate* isolate = args.GetIsolate();
|
Isolate* isolate = args.GetIsolate();
|
||||||
Local<Context> context = isolate->GetCurrentContext();
|
Local<Context> context = isolate->GetCurrentContext();
|
||||||
ScriptOrigin origin(isolate, filename, 0, 0, true);
|
|
||||||
// TODO(joyeecheung): do we need all of these? Maybe we would want a less
|
// TODO(joyeecheung): do we need all of these? Maybe we would want a less
|
||||||
// internal version of them.
|
// internal version of them.
|
||||||
std::vector<Local<String>> parameters = {
|
std::vector<Local<String>> parameters = {
|
||||||
|
@ -1269,15 +1267,8 @@ void CompileSerializeMain(const FunctionCallbackInfo<Value>& args) {
|
||||||
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
|
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
|
||||||
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
|
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
|
||||||
};
|
};
|
||||||
ScriptCompiler::Source script_source(source, origin);
|
|
||||||
Local<Function> fn;
|
Local<Function> fn;
|
||||||
if (ScriptCompiler::CompileFunction(context,
|
if (contextify::CompileFunction(context, filename, source, ¶meters)
|
||||||
&script_source,
|
|
||||||
parameters.size(),
|
|
||||||
parameters.data(),
|
|
||||||
0,
|
|
||||||
nullptr,
|
|
||||||
ScriptCompiler::kNoCompileOptions)
|
|
||||||
.ToLocal(&fn)) {
|
.ToLocal(&fn)) {
|
||||||
args.GetReturnValue().Set(fn);
|
args.GetReturnValue().Set(fn);
|
||||||
}
|
}
|
||||||
|
|
18
src/util.cc
18
src/util.cc
|
@ -28,6 +28,7 @@
|
||||||
#include "node_errors.h"
|
#include "node_errors.h"
|
||||||
#include "node_internals.h"
|
#include "node_internals.h"
|
||||||
#include "node_util.h"
|
#include "node_util.h"
|
||||||
|
#include "node_v8_platform-inl.h"
|
||||||
#include "string_bytes.h"
|
#include "string_bytes.h"
|
||||||
#include "uv.h"
|
#include "uv.h"
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ static std::atomic_int seq = {0}; // Sequence number for diagnostic filenames.
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
|
|
||||||
|
using v8::ArrayBuffer;
|
||||||
using v8::ArrayBufferView;
|
using v8::ArrayBufferView;
|
||||||
using v8::Context;
|
using v8::Context;
|
||||||
using v8::FunctionTemplate;
|
using v8::FunctionTemplate;
|
||||||
|
@ -629,4 +631,20 @@ Local<String> UnionBytes::ToStringChecked(Isolate* isolate) const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RAIIIsolate::RAIIIsolate()
|
||||||
|
: allocator_{ArrayBuffer::Allocator::NewDefaultAllocator()} {
|
||||||
|
isolate_ = Isolate::Allocate();
|
||||||
|
CHECK_NOT_NULL(isolate_);
|
||||||
|
per_process::v8_platform.Platform()->RegisterIsolate(isolate_,
|
||||||
|
uv_default_loop());
|
||||||
|
Isolate::CreateParams params;
|
||||||
|
params.array_buffer_allocator = allocator_.get();
|
||||||
|
Isolate::Initialize(isolate_, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
RAIIIsolate::~RAIIIsolate() {
|
||||||
|
per_process::v8_platform.Platform()->UnregisterIsolate(isolate_);
|
||||||
|
isolate_->Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace node
|
} // namespace node
|
||||||
|
|
13
src/util.h
13
src/util.h
|
@ -958,6 +958,19 @@ void SetConstructorFunction(v8::Isolate* isolate,
|
||||||
SetConstructorFunctionFlag flag =
|
SetConstructorFunctionFlag flag =
|
||||||
SetConstructorFunctionFlag::SET_CLASS_NAME);
|
SetConstructorFunctionFlag::SET_CLASS_NAME);
|
||||||
|
|
||||||
|
// Simple RAII class to spin up a v8::Isolate instance.
|
||||||
|
class RAIIIsolate {
|
||||||
|
public:
|
||||||
|
RAIIIsolate();
|
||||||
|
~RAIIIsolate();
|
||||||
|
|
||||||
|
v8::Isolate* get() const { return isolate_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_;
|
||||||
|
v8::Isolate* isolate_;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace node
|
} // namespace node
|
||||||
|
|
||||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
|
|
|
@ -4,8 +4,8 @@ throw new Error('Should include grayed stack trace')
|
||||||
|
|
||||||
Error: Should include grayed stack trace
|
Error: Should include grayed stack trace
|
||||||
at Object.<anonymous> [90m(/[39mtest*force_colors.js:1:7[90m)[39m
|
at Object.<anonymous> [90m(/[39mtest*force_colors.js:1:7[90m)[39m
|
||||||
[90m at Module._compile (node:internal*modules*cjs*loader:1233:14)[39m
|
[90m at Module._compile (node:internal*modules*cjs*loader:1241:14)[39m
|
||||||
[90m at Module._extensions..js (node:internal*modules*cjs*loader:1287:10)[39m
|
[90m at Module._extensions..js (node:internal*modules*cjs*loader:1295:10)[39m
|
||||||
[90m at Module.load (node:internal*modules*cjs*loader:1091:32)[39m
|
[90m at Module.load (node:internal*modules*cjs*loader:1091:32)[39m
|
||||||
[90m at Module._load (node:internal*modules*cjs*loader:938:12)[39m
|
[90m at Module._load (node:internal*modules*cjs*loader:938:12)[39m
|
||||||
[90m at Function.executeUserEntryPoint [as runMain] (node:internal*modules*run_main:83:12)[39m
|
[90m at Function.executeUserEntryPoint [as runMain] (node:internal*modules*run_main:83:12)[39m
|
||||||
|
|
|
@ -5,12 +5,15 @@ const createdRequire = createRequire(__filename);
|
||||||
// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI.
|
// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI.
|
||||||
const { expectWarning, mustNotCall } = createdRequire(process.env.COMMON_DIRECTORY);
|
const { expectWarning, mustNotCall } = createdRequire(process.env.COMMON_DIRECTORY);
|
||||||
|
|
||||||
|
// This additionally makes sure that no unexpected warnings are emitted.
|
||||||
if (createdRequire('./sea-config.json').disableExperimentalSEAWarning) {
|
if (createdRequire('./sea-config.json').disableExperimentalSEAWarning) {
|
||||||
process.on('warning', mustNotCall());
|
process.on('warning', mustNotCall());
|
||||||
} else {
|
} else {
|
||||||
expectWarning('ExperimentalWarning',
|
expectWarning('ExperimentalWarning',
|
||||||
'Single executable application is an experimental feature and ' +
|
'Single executable application is an experimental feature and ' +
|
||||||
'might change at any time');
|
'might change at any time');
|
||||||
|
// Any unexpected warning would throw this error:
|
||||||
|
// https://github.com/nodejs/node/blob/c301404105a7256b79a0b8c4522ce47af96dfa17/test/common/index.js#L697-L700.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be possible to require core modules that optionally require the
|
// Should be possible to require core modules that optionally require the
|
||||||
|
@ -18,6 +21,9 @@ if (createdRequire('./sea-config.json').disableExperimentalSEAWarning) {
|
||||||
const { deepStrictEqual, strictEqual, throws } = require('assert');
|
const { deepStrictEqual, strictEqual, throws } = require('assert');
|
||||||
const { dirname } = require('node:path');
|
const { dirname } = require('node:path');
|
||||||
|
|
||||||
|
// Checks that the source filename is used in the error stack trace.
|
||||||
|
strictEqual(new Error('lol').stack.split('\n')[1], ' at sea.js:25:13');
|
||||||
|
|
||||||
// Should be possible to require a core module that requires using the "node:"
|
// Should be possible to require a core module that requires using the "node:"
|
||||||
// scheme.
|
// scheme.
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('../common');
|
||||||
|
|
||||||
|
const {
|
||||||
|
injectAndCodeSign,
|
||||||
|
skipIfSingleExecutableIsNotSupported,
|
||||||
|
} = require('../common/sea');
|
||||||
|
|
||||||
|
skipIfSingleExecutableIsNotSupported();
|
||||||
|
|
||||||
|
// This tests the creation of a single executable application which uses the
|
||||||
|
// V8 code cache.
|
||||||
|
|
||||||
|
const fixtures = require('../common/fixtures');
|
||||||
|
const tmpdir = require('../common/tmpdir');
|
||||||
|
const { copyFileSync, writeFileSync, existsSync } = require('fs');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
const { join } = require('path');
|
||||||
|
const { strictEqual } = require('assert');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const inputFile = fixtures.path('sea.js');
|
||||||
|
const requirableFile = join(tmpdir.path, 'requirable.js');
|
||||||
|
const configFile = join(tmpdir.path, 'sea-config.json');
|
||||||
|
const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob');
|
||||||
|
const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea');
|
||||||
|
|
||||||
|
tmpdir.refresh();
|
||||||
|
|
||||||
|
writeFileSync(requirableFile, `
|
||||||
|
module.exports = {
|
||||||
|
hello: 'world',
|
||||||
|
};
|
||||||
|
`);
|
||||||
|
|
||||||
|
writeFileSync(configFile, `
|
||||||
|
{
|
||||||
|
"main": "sea.js",
|
||||||
|
"output": "sea-prep.blob",
|
||||||
|
"useCodeCache": true
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Copy input to working directory
|
||||||
|
copyFileSync(inputFile, join(tmpdir.path, 'sea.js'));
|
||||||
|
execFileSync(process.execPath, ['--experimental-sea-config', 'sea-config.json'], {
|
||||||
|
cwd: tmpdir.path
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(existsSync(seaPrepBlob));
|
||||||
|
|
||||||
|
copyFileSync(process.execPath, outputFile);
|
||||||
|
injectAndCodeSign(outputFile, seaPrepBlob);
|
||||||
|
|
||||||
|
const singleExecutableApplicationOutput = execFileSync(
|
||||||
|
outputFile,
|
||||||
|
[ '-a', '--b=c', 'd' ],
|
||||||
|
{ env: { COMMON_DIRECTORY: join(__dirname, '..', 'common') } });
|
||||||
|
strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n');
|
Loading…
Reference in New Issue