Skip to content

Commit

Permalink
sea: add support for V8 bytecode-only caching
Browse files Browse the repository at this point in the history
Refs: nodejs/single-executable#73
Signed-off-by: Darshan Sen <raisinten@gmail.com>
  • Loading branch information
RaisinTen committed May 26, 2023
1 parent 38b82b0 commit 947b8c9
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 42 deletions.
8 changes: 7 additions & 1 deletion lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ let hasLoadedAnyUserCJSModule = false;

const {
codes: {
ERR_INTERNAL_ASSERTION,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_MODULE_SPECIFIER,
ERR_REQUIRE_ESM,
Expand Down Expand Up @@ -1145,7 +1146,7 @@ Module.prototype.require = function(id) {
let resolvedArgv;
let hasPausedEntry = false;
let Script;
function wrapSafe(filename, content, cjsModuleInstance) {
function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
if (patched) {
const wrapper = Module.wrap(content);
if (Script === undefined) {
Expand Down Expand Up @@ -1180,13 +1181,18 @@ function wrapSafe(filename, content, cjsModuleInstance) {
'__dirname',
], {
filename,
cachedData: codeCache,
importModuleDynamically(specifier, _, importAssertions) {
const cascadedLoader = getCascadedLoader();
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
},
});

if (codeCache && result.cachedDataRejected !== false) {
throw new ERR_INTERNAL_ASSERTION('Code cache data rejected.');
}

// Cache the source map for the module if present.
if (result.sourceMapURL) {
maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL);
Expand Down
5 changes: 3 additions & 2 deletions lib/internal/util/embedding.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict';
const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors');
const { BuiltinModule: { normalizeRequirableId } } = require('internal/bootstrap/realm');
const { Module, wrapSafe } = require('internal/modules/cjs/loader');
const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors');
const { getCodeCache } = internalBinding('sea');

// This is roughly the same as:
//
Expand All @@ -15,7 +16,7 @@ const { Module, wrapSafe } = require('internal/modules/cjs/loader');

function embedderRunCjs(contents) {
const filename = process.execPath;
const compiledWrapper = wrapSafe(filename, contents);
const compiledWrapper = wrapSafe(filename, contents, undefined, getCodeCache());

const customModule = new Module(filename, null);
customModule.filename = filename;
Expand Down
21 changes: 1 addition & 20 deletions src/json_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,15 @@
#include "util-inl.h"

namespace node {
using v8::ArrayBuffer;
using v8::Context;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
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()
: allocator_(ArrayBuffer::Allocator::NewDefaultAllocator()),
isolate_(NewIsolate(allocator_.get())),
handle_scope_(isolate_.get()),
: handle_scope_(isolate_.get()),
context_(isolate_.get(), Context::New(isolate_.get())),
context_scope_(context_.Get(isolate_.get())) {}

Expand Down
4 changes: 1 addition & 3 deletions src/json_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ class JSONParser {
private:
// We might want a lighter-weight JSON parser for this use case. But for now
// using V8 is good enough.
static void FreeIsolate(v8::Isolate* isolate);
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_;
DeleteFnPtr<v8::Isolate, FreeIsolate> isolate_;
RAIIIsolate isolate_;
v8::HandleScope handle_scope_;
v8::Global<v8::Context> context_;
v8::Context::Scope context_scope_;
Expand Down
24 changes: 24 additions & 0 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,30 @@ Maybe<bool> StoreCodeCacheResult(
return Just(true);
}

// TODO(RaisinTen): Reuse in ContextifyContext::CompileFunction().
MaybeLocal<Function> CompileFunction(Isolate* isolate,
Local<Context> context,
Local<String> filename,
Local<String> content,
std::vector<Local<String>> parameters) {
ScriptOrigin script_origin(isolate, filename, 0, 0, true);
ScriptCompiler::Source script_source(content, script_origin);

Local<Function> fn;
if (!ScriptCompiler::CompileFunction(context,
&script_source,
parameters.size(),
parameters.data(),
0,
nullptr,
ScriptCompiler::kEagerCompile)
.ToLocal(&fn)) {
return {};
}

return fn;
}

bool ContextifyScript::InstanceOf(Environment* env,
const Local<Value>& value) {
return !value.IsEmpty() &&
Expand Down
7 changes: 7 additions & 0 deletions src/node_contextify.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ v8::Maybe<bool> StoreCodeCacheResult(
bool produce_cached_data,
std::unique_ptr<v8::ScriptCompiler::CachedData> new_cached_data);

v8::MaybeLocal<v8::Function> CompileFunction(
v8::Isolate* isolate,
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 node

Expand Down
97 changes: 95 additions & 2 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "json_parser.h"
#include "node_contextify.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_union_bytes.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 Node.js project that is present only once in the entire binary. It is
Expand All @@ -26,9 +30,14 @@

using node::ExitCode;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::ScriptCompiler;
using v8::String;
using v8::Value;

namespace node {
Expand Down Expand Up @@ -78,6 +87,11 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
sea.code.data(),
sea.code.size());
written_total += WriteStringView(sea.code, StringLogMode::kAddressAndContent);

Debug("Write SEA resource code cache %p, size=%zu\n",
sea.code_cache.data(),
sea.code_cache.size());
written_total += WriteStringView(sea.code_cache, StringLogMode::kAddressOnly);
return written_total;
}

Expand Down Expand Up @@ -105,7 +119,12 @@ SeaResource SeaDeserializer::Read() {

std::string_view code = ReadStringView(StringLogMode::kAddressAndContent);
Debug("Read SEA resource code %p, size=%zu\n", code.data(), code.size());
return {flags, code};

std::string_view code_cache = ReadStringView(StringLogMode::kAddressOnly);
Debug("Read SEA resource code cache %p, size=%zu\n",
code_cache.data(),
code_cache.size());
return {flags, code, code_cache};
}

std::string_view FindSingleExecutableBlob() {
Expand Down Expand Up @@ -161,6 +180,27 @@ void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning));
}

void GetCodeCache(const FunctionCallbackInfo<Value>& args) {
if (!IsSingleExecutable()) {
return;
}

Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
HandleScope scope(isolate);

SeaResource sea_resource = FindSingleExecutableResource();

Local<Object> buf =
Buffer::Copy(
env,
reinterpret_cast<const char*>(sea_resource.code_cache.data()),
sea_resource.code_cache.length())
.ToLocalChecked();

args.GetReturnValue().Set(buf);
}

std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
// Repeats argv[0] at position 1 on argv as a replacement for the missing
// entry point file path.
Expand Down Expand Up @@ -238,6 +278,49 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
return result;
}

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()).ToLocal(&filename)) {
return {};
}

Local<String> content;
if (!String::NewFromUtf8(isolate, main_script.data()).ToLocal(&content)) {
return {};
}

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"),
};

Local<Function> fn;
if (!contextify::CompileFunction(
isolate, context, filename, content, std::move(parameters))
.ToLocal(&fn)) {
return {};
}

std::unique_ptr<ScriptCompiler::CachedData> cache{
ScriptCompiler::CreateCodeCacheForFunction(fn)};
std::string code_cache(cache->data, cache->data + cache->length);
return code_cache;
}

ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) {
std::string main_script;
// TODO(joyeecheung): unify the file utils.
Expand All @@ -248,7 +331,15 @@ ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) {
return ExitCode::kGenericUserError;
}

SeaResource sea{config.flags, main_script};
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::string code_cache = optional_code_cache.value();

SeaResource sea{config.flags, main_script, code_cache};

SeaSerializer serializer;
serializer.Write(sea);
Expand Down Expand Up @@ -288,10 +379,12 @@ void Initialize(Local<Object> target,
target,
"isExperimentalSeaWarningNeeded",
IsExperimentalSeaWarningNeeded);
SetMethod(context, target, "getCodeCache", GetCodeCache);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsExperimentalSeaWarningNeeded);
registry->Register(GetCodeCache);
}

} // namespace sea
Expand Down
1 change: 1 addition & 0 deletions src/node_sea.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum class SeaFlags : uint32_t {
struct SeaResource {
SeaFlags flags = SeaFlags::kDefault;
std::string_view code;
std::string_view code_cache;

static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
};
Expand Down
12 changes: 2 additions & 10 deletions src/node_snapshotable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ using v8::MaybeLocal;
using v8::Object;
using v8::ObjectTemplate;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::SnapshotCreator;
using v8::StartupData;
using v8::String;
Expand Down Expand Up @@ -1207,23 +1206,16 @@ void CompileSerializeMain(const FunctionCallbackInfo<Value>& args) {
Local<String> source = args[1].As<String>();
Isolate* isolate = args.GetIsolate();
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
// internal version of them.
std::vector<Local<String>> parameters = {
FIXED_ONE_BYTE_STRING(isolate, "require"),
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
};
ScriptCompiler::Source script_source(source, origin);
Local<Function> fn;
if (ScriptCompiler::CompileFunction(context,
&script_source,
parameters.size(),
parameters.data(),
0,
nullptr,
ScriptCompiler::kEagerCompile)
if (contextify::CompileFunction(
isolate, context, filename, source, std::move(parameters))
.ToLocal(&fn)) {
args.GetReturnValue().Set(fn);
}
Expand Down
18 changes: 18 additions & 0 deletions src/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "node_errors.h"
#include "node_internals.h"
#include "node_util.h"
#include "node_v8_platform-inl.h"
#include "string_bytes.h"
#include "uv.h"

Expand Down Expand Up @@ -55,6 +56,7 @@ static std::atomic_int seq = {0}; // Sequence number for diagnostic filenames.

namespace node {

using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::FunctionTemplate;
Expand Down Expand Up @@ -616,4 +618,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
Loading

0 comments on commit 947b8c9

Please sign in to comment.