Skip to content

Commit

Permalink
src: register external references for source code
Browse files Browse the repository at this point in the history
Currently we use external strings for internalized builtin source code.
However when a snapshot is taken, any external string whose resource
is not registered is flattened into a SeqString (see ref). The
result is that module source code stored in the snapshot does not
use external strings after deserialization. This patch registers an
external string resource for each internalized builtin's source. The
savings are substantial: ~1.9 MB of heap memory per isolate, or ~44%
of an otherwise empty isolate's heap usage:

```console
$ node --expose-gc -p 'gc(),process.memoryUsage().heapUsed'
4190968
$ ./node --expose-gc -p 'gc(),process.memoryUsage().heapUsed'
2327536
```

The savings can be even higher for user snapshots which may include
more internal modules.

The existing UnionBytes implementation was ill-suited, because it only
created an external string resource when ToStringChecked was called,
but we need to register the external string resources before the
isolate even exists. We change UnionBytes to no longer own the data,
and shift ownership of the data to a new external resource class
called StaticExternalByteResource.  StaticExternalByteResource are
either statically allocated (for internalized builtin code) or owned
by the static `externalized_builtin_sources` map, so they will only be
destructed when static resources are destructed. We change JS2C to emit
statements to register a string resource for each internalized builtin.

Refs: https://github.com/v8/v8/blob/d2c8fbe9ccd1a6ce5591bb7dd319c3c00d6bf489/src/snapshot/serializer.cc#L633
PR-URL: #47055
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
  • Loading branch information
kvakil authored and targos committed May 12, 2023
1 parent 106dc61 commit 190596c
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 117 deletions.
46 changes: 27 additions & 19 deletions src/node_builtins.cc
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,18 @@ MaybeLocal<String> BuiltinLoader::LoadBuiltinSource(Isolate* isolate,

namespace {
static Mutex externalized_builtins_mutex;
std::unordered_map<std::string, std::string> externalized_builtin_sources;
std::unordered_map<std::string, std::unique_ptr<StaticExternalTwoByteResource>>
externalized_builtin_sources;
} // namespace

void BuiltinLoader::AddExternalizedBuiltin(const char* id,
const char* filename) {
std::string source;
StaticExternalTwoByteResource* resource;
{
Mutex::ScopedLock lock(externalized_builtins_mutex);
auto it = externalized_builtin_sources.find(id);
if (it != externalized_builtin_sources.end()) {
source = it->second;
} else {
if (it == externalized_builtin_sources.end()) {
std::string source;
int r = ReadFileSync(&source, filename);
if (r != 0) {
fprintf(stderr,
Expand All @@ -233,23 +233,29 @@ void BuiltinLoader::AddExternalizedBuiltin(const char* id,
filename);
ABORT();
}
externalized_builtin_sources[id] = source;
size_t expected_u16_length =
simdutf::utf16_length_from_utf8(source.data(), source.length());
auto out = std::make_shared<std::vector<uint16_t>>(expected_u16_length);
size_t u16_length = simdutf::convert_utf8_to_utf16(
source.data(),
source.length(),
reinterpret_cast<char16_t*>(out->data()));
out->resize(u16_length);

auto result = externalized_builtin_sources.emplace(
id,
std::make_unique<StaticExternalTwoByteResource>(
out->data(), out->size(), out));
CHECK(result.second);
it = result.first;
}
// OK to get the raw pointer, since externalized_builtin_sources owns
// the resource, resources are never removed from the map, and
// externalized_builtin_sources has static lifetime.
resource = it->second.get();
}

Add(id, source);
}

bool BuiltinLoader::Add(const char* id, std::string_view utf8source) {
size_t expected_u16_length =
simdutf::utf16_length_from_utf8(utf8source.data(), utf8source.length());
auto out = std::make_shared<std::vector<uint16_t>>(expected_u16_length);
size_t u16_length =
simdutf::convert_utf8_to_utf16(utf8source.data(),
utf8source.length(),
reinterpret_cast<char16_t*>(out->data()));
out->resize(u16_length);
return Add(id, UnionBytes(out));
Add(id, UnionBytes(resource));
}

MaybeLocal<Function> BuiltinLoader::LookupAndCompileInternal(
Expand Down Expand Up @@ -719,6 +725,8 @@ void BuiltinLoader::RegisterExternalReferences(
registry->Register(CompileFunction);
registry->Register(HasCachedBuiltins);
registry->Register(SetInternalLoaders);

RegisterExternalReferencesForInternalizedBuiltinCode(registry);
}

} // namespace builtins
Expand Down
6 changes: 5 additions & 1 deletion src/node_builtins.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <set>
#include <string>
#include <vector>
#include "node_external_reference.h"
#include "node_mutex.h"
#include "node_threadsafe_cow.h"
#include "node_union_bytes.h"
Expand All @@ -30,6 +31,10 @@ using BuiltinCodeCacheMap =
std::unordered_map<std::string,
std::unique_ptr<v8::ScriptCompiler::CachedData>>;

// Generated by tools/js2c.py as node_javascript.cc
void RegisterExternalReferencesForInternalizedBuiltinCode(
ExternalReferenceRegistry* registry);

struct CodeCacheInfo {
std::string id;
std::vector<uint8_t> data;
Expand Down Expand Up @@ -72,7 +77,6 @@ class NODE_EXTERN_PRIVATE BuiltinLoader {
v8::Local<v8::String> GetConfigString(v8::Isolate* isolate);
bool Exists(const char* id);
bool Add(const char* id, const UnionBytes& source);
bool Add(const char* id, std::string_view utf8source);

bool CompileAllBuiltins(v8::Local<v8::Context> context);
void RefreshCodeCache(const std::vector<CodeCacheInfo>& in);
Expand Down
3 changes: 2 additions & 1 deletion src/node_external_reference.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ class ExternalReferenceRegistry {
V(v8::IndexedPropertyDefinerCallback) \
V(v8::IndexedPropertyDeleterCallback) \
V(v8::IndexedPropertyQueryCallback) \
V(v8::IndexedPropertyDescriptorCallback)
V(v8::IndexedPropertyDescriptorCallback) \
V(const v8::String::ExternalStringResourceBase*)

#define V(ExternalReferenceType) \
void Register(ExternalReferenceType addr) { RegisterT(addr); }
Expand Down
76 changes: 50 additions & 26 deletions src/node_union_bytes.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,74 @@

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

// A union of const uint8_t* or const uint16_t* data that can be
// turned into external v8::String when given an isolate.

#include "v8.h"

namespace node {

// An external resource intended to be used with static lifetime.
template <typename Char, typename IChar, typename Base>
class StaticExternalByteResource : public Base {
static_assert(sizeof(IChar) == sizeof(Char),
"incompatible interface and internal pointers");

public:
explicit StaticExternalByteResource(const Char* data,
size_t length,
std::shared_ptr<void> owning_ptr)
: data_(data), length_(length), owning_ptr_(owning_ptr) {}

const IChar* data() const override {
return reinterpret_cast<const IChar*>(data_);
}
size_t length() const override { return length_; }

void Dispose() override {
// We ignore Dispose calls from V8, even if we "own" a resource via
// owning_ptr_. All instantiations of this class are static or owned by a
// static map, and will be destructed when static variables are destructed.
}

StaticExternalByteResource(const StaticExternalByteResource&) = delete;
StaticExternalByteResource& operator=(const StaticExternalByteResource&) =
delete;

private:
const Char* data_;
const size_t length_;
std::shared_ptr<void> owning_ptr_;
};

using StaticExternalOneByteResource =
StaticExternalByteResource<uint8_t,
char,
v8::String::ExternalOneByteStringResource>;
using StaticExternalTwoByteResource =
StaticExternalByteResource<uint16_t,
uint16_t,
v8::String::ExternalStringResource>;

// Similar to a v8::String, but it's independent from Isolates
// and can be materialized in Isolates as external Strings
// via ToStringChecked.
class UnionBytes {
public:
UnionBytes(const uint16_t* data, size_t length)
: one_bytes_(nullptr), two_bytes_(data), length_(length) {}
UnionBytes(const uint8_t* data, size_t length)
: one_bytes_(data), two_bytes_(nullptr), length_(length) {}
template <typename T> // T = uint8_t or uint16_t
explicit UnionBytes(std::shared_ptr<std::vector</*const*/ T>> data)
: UnionBytes(data->data(), data->size()) {
owning_ptr_ = data;
}
explicit UnionBytes(StaticExternalOneByteResource* one_byte_resource)
: one_byte_resource_(one_byte_resource), two_byte_resource_(nullptr) {}
explicit UnionBytes(StaticExternalTwoByteResource* two_byte_resource)
: one_byte_resource_(nullptr), two_byte_resource_(two_byte_resource) {}

UnionBytes(const UnionBytes&) = default;
UnionBytes& operator=(const UnionBytes&) = default;
UnionBytes(UnionBytes&&) = default;
UnionBytes& operator=(UnionBytes&&) = default;

bool is_one_byte() const { return one_bytes_ != nullptr; }
const uint16_t* two_bytes_data() const {
CHECK_NOT_NULL(two_bytes_);
return two_bytes_;
}
const uint8_t* one_bytes_data() const {
CHECK_NOT_NULL(one_bytes_);
return one_bytes_;
}
bool is_one_byte() const { return one_byte_resource_ != nullptr; }

v8::Local<v8::String> ToStringChecked(v8::Isolate* isolate) const;
size_t length() const { return length_; }

private:
const uint8_t* one_bytes_;
const uint16_t* two_bytes_;
size_t length_;
std::shared_ptr<void> owning_ptr_;
StaticExternalOneByteResource* one_byte_resource_;
StaticExternalTwoByteResource* two_byte_resource_;
};

} // namespace node
Expand Down
60 changes: 4 additions & 56 deletions src/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -606,65 +606,13 @@ void SetConstructorFunction(Isolate* isolate,
that->Set(name, tmpl);
}

namespace {

class NonOwningExternalOneByteResource
: public v8::String::ExternalOneByteStringResource {
public:
explicit NonOwningExternalOneByteResource(const UnionBytes& source)
: source_(source) {}
~NonOwningExternalOneByteResource() override = default;

const char* data() const override {
return reinterpret_cast<const char*>(source_.one_bytes_data());
}
size_t length() const override { return source_.length(); }

NonOwningExternalOneByteResource(const NonOwningExternalOneByteResource&) =
delete;
NonOwningExternalOneByteResource& operator=(
const NonOwningExternalOneByteResource&) = delete;

private:
const UnionBytes source_;
};

class NonOwningExternalTwoByteResource
: public v8::String::ExternalStringResource {
public:
explicit NonOwningExternalTwoByteResource(const UnionBytes& source)
: source_(source) {}
~NonOwningExternalTwoByteResource() override = default;

const uint16_t* data() const override { return source_.two_bytes_data(); }
size_t length() const override { return source_.length(); }

NonOwningExternalTwoByteResource(const NonOwningExternalTwoByteResource&) =
delete;
NonOwningExternalTwoByteResource& operator=(
const NonOwningExternalTwoByteResource&) = delete;

private:
const UnionBytes source_;
};

} // anonymous namespace

Local<String> UnionBytes::ToStringChecked(Isolate* isolate) const {
if (UNLIKELY(length() == 0)) {
// V8 requires non-null data pointers for empty external strings,
// but we don't guarantee that. Solve this by not creating an
// external string at all in that case.
return String::Empty(isolate);
}
if (is_one_byte()) {
NonOwningExternalOneByteResource* source =
new NonOwningExternalOneByteResource(*this);
return String::NewExternalOneByte(isolate, source).ToLocalChecked();
return String::NewExternalOneByte(isolate, one_byte_resource_)
.ToLocalChecked();
} else {
NonOwningExternalTwoByteResource* source =
new NonOwningExternalTwoByteResource(*this);
return String::NewExternalTwoByte(isolate, source).ToLocalChecked();
return String::NewExternalTwoByte(isolate, two_byte_resource_)
.ToLocalChecked();
}
}

Expand Down
Loading

0 comments on commit 190596c

Please sign in to comment.