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
  • Loading branch information
kvakil committed Mar 17, 2023
1 parent c3537ae commit db186d2
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 116 deletions.
46 changes: 27 additions & 19 deletions src/node_builtins.cc
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 @@ -703,6 +709,8 @@ void BuiltinLoader::RegisterExternalReferences(
registry->Register(GetCacheUsage);
registry->Register(CompileFunction);
registry->Register(HasCachedBuiltins);

RegisterExternalReferencesForInternalizedBuiltinCode(registry);
}

} // namespace builtins
Expand Down
5 changes: 5 additions & 0 deletions src/node_builtins.h
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
3 changes: 2 additions & 1 deletion src/node_external_reference.h
Expand Up @@ -47,7 +47,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
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
Expand Up @@ -551,65 +551,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

0 comments on commit db186d2

Please sign in to comment.