src: add helpers for creating cppgc-managed wrappers

This patch adds helpers for wrapper classes based on cppgc (Oilpan)
in `src/cppgc_helpers.h`, including `node::CppgcMixin` and
`ASSIGN_OR_RETURN_UNWRAP_CPPGC`, which are designed to have
similar interface to BaseObject helpers to help migration.
They are documented in the `CppgcMixin` section in `src/README.md`

To disambiguate, the global `node::Unwrap<>` has now been moved
as `node::BaseObject::Unwrap<>`, and `node::Cppgc::Unwrap<>`
implements a similar unwrapping mechanism for cppgc-managed
wrappers.

PR-URL: https://github.com/nodejs/node/pull/52295
Refs: https://github.com/nodejs/node/issues/40786
Refs: https://docs.google.com/document/d/1ny2Qz_EsUnXGKJRGxoA-FXIE2xpLgaMAN6jD7eAkqFQ/edit
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
Joyee Cheung 2023-12-02 18:15:24 +01:00 committed by Node.js GitHub Bot
parent 4568df4c6d
commit 849db10fb3
11 changed files with 466 additions and 21 deletions

View File

@ -202,6 +202,7 @@
'src/compile_cache.h',
'src/connect_wrap.h',
'src/connection_wrap.h',
'src/cppgc_helpers.h',
'src/dataqueue/queue.h',
'src/debug_utils.h',
'src/debug_utils-inl.h',

View File

@ -975,6 +975,228 @@ overview over libuv handles managed by Node.js.
<a id="callback-scopes"></a>
### `CppgcMixin`
V8 comes with a trace-based C++ garbage collection library called
[Oilpan][], whose API is in headers under`deps/v8/include/cppgc`.
In this document we refer to it as `cppgc` since that's the namespace
of the library.
C++ objects managed using `cppgc` are allocated in the V8 heap
and traced by V8's garbage collector. The `cppgc` library provides
APIs for embedders to create references between cppgc-managed objects
and other objects in the V8 heap (such as JavaScript objects or other
objects in the V8 C++ API that can be passed around with V8 handles)
in a way that's understood by V8's garbage collector.
This helps avoiding accidental memory leaks and use-after-frees coming
from incorrect cross-heap reference tracking, especially when there are
cyclic references. This is what powers the
[unified heap design in Chromium][] to avoid cross-heap memory issues,
and it's being rolled out in Node.js to reap similar benefits.
For general guidance on how to use `cppgc`, see the
[Oilpan documentation in Chromium][]. In Node.js there is a helper
mixin `node::CppgcMixin` from `cppgc_helpers.h` to help implementing
`cppgc`-managed wrapper objects with a [`BaseObject`][]-like interface.
`cppgc`-manged objects in Node.js internals should extend this mixin,
while non-`cppgc`-managed objects typically extend `BaseObject` - the
latter are being migrated to be `cppgc`-managed wherever it's beneficial
and practical. Typically `cppgc`-managed objects are more efficient to
keep track of (which lowers initialization cost) and work better
with V8's GC scheduling.
A `cppgc`-managed native wrapper should look something like this:
```cpp
#include "cppgc_helpers.h"
// CPPGC_MIXIN is a helper macro for inheriting from cppgc::GarbageCollected,
// cppgc::NameProvider and public CppgcMixin. Per cppgc rules, it must be
// placed at the left-most position in the class hierarchy.
class MyWrap final : CPPGC_MIXIN(ContextifyScript) {
public:
SET_CPPGC_NAME(MyWrap) // Sets the heap snapshot name to "Node / MyWrap"
// The constructor can only be called by `cppgc::MakeGarbageCollected()`.
MyWrap(Environment* env, v8::Local<v8::Object> object);
// Helper for constructing MyWrap via `cppgc::MakeGarbageCollected()`.
// Can be invoked by other C++ code outside of this class if necessary.
// In that case the raw pointer returned may need to be managed by
// cppgc::Persistent<> or cppgc::Member<> with corresponding tracing code.
static MyWrap* New(Environment* env, v8::Local<v8::Object> object);
// Binding method to help constructing MyWrap in JavaScript.
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
void Trace(cppgc::Visitor* visitor) const final;
}
```
`cppgc::GarbageCollected` types are expected to implement a
`void Trace(cppgc::Visitor* visitor) const` method. When they are the
final class in the hierarchy, this method must be marked `final`. For
classes extending `node::CppgcMixn`, this should typically dispatch a
call to `CppgcMixin::Trace()` first, then trace any additional owned data
it has. See `deps/v8/include/cppgc/garbage-collected.h` see what types of
data can be traced.
```cpp
void MyWrap::Trace(cppgc::Visitor* visitor) const {
CppgcMixin::Trace(visitor);
visitor->Trace(...); // Trace any additional data MyWrap has
}
```
#### Constructing and wrapping `cppgc`-managed objects
C++ objects subclassing `node::CppgcMixin` have a counterpart JavaScript object.
The two references each other internally - this cycle is well-understood by V8's
garbage collector and can be managed properly.
Similar to `BaseObject`s, `cppgc`-managed wrappers objects must be created from
object templates with at least `node::CppgcMixin::kInternalFieldCount` internal
fields. To unify handling of the wrappers, the internal fields of
`node::CppgcMixin` wrappers would have the same layout as `BaseObject`.
```cpp
// To create the v8::FunctionTemplate that can be used to instantiate a
// v8::Function for that serves as the JavaScript constructor of MyWrap:
Local<FunctionTemplate> ctor_template = NewFunctionTemplate(isolate, MyWrap::New);
ctor_template->InstanceTemplate()->SetInternalFieldCount(
ContextifyScript::kInternalFieldCount);
```
`cppgc::GarbageCollected` objects should not be allocated with usual C++
primitives (e.g. using `new` or `std::make_unique` is forbidden). Instead
they must be allocated using `cppgc::MakeGarbageCollected` - this would
allocate them in the V8 heap and allow V8's garbage collector to trace them.
It's recommended to use a `New` method to wrap the `cppgc::MakeGarbageCollected`
call so that external C++ code does not need to know about its memory management
scheme to construct it.
```cpp
MyWrap* MyWrap::New(Environment* env, v8::Local<v8::Object> object) {
// Per cppgc rules, the constructor of MyWrap cannot be invoked directly.
// It's recommended to implement a New() static method that prepares
// and forwards the necessary arguments to cppgc::MakeGarbageCollected()
// and just return the raw pointer around - do not use any C++ smart
// pointer with this, as this is not managed by the native memory
// allocator but by V8.
return cppgc::MakeGarbageCollected<MyWrap>(
env->isolate()->GetCppHeap()->GetAllocationHandle(), env, object);
}
// Binding method to be invoked by JavaScript.
void MyWrap::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
CHECK(args.IsConstructCall());
// Get more arguments from JavaScript land if necessary.
New(env, args.This());
}
```
In the constructor of `node::CppgcMixin` types, use
`node::CppgcMixin::Wrap()` to finish the wrapping so that
V8 can trace the C++ object from the JavaScript object.
```cpp
MyWrap::MyWrap(Environment* env, v8::Local<v8::Object> object) {
// This cannot invoke the mixin constructor and has to invoke via a static
// method from it, per cppgc rules.
CppgcMixin::Wrap(this, env, object);
}
```
#### Unwrapping `cppgc`-managed wrapper objects
When given a `v8::Local<v8::Object>` that is known to be the JavaScript
wrapper object for `MyWrap`, uses the `node::CppgcMixin::Unwrap()` to
get the C++ object from it:
```cpp
v8::Local<v8::Object> object = ...; // Obtain the JavaScript from somewhere.
MyWrap* wrap = CppgcMixin::Unwrap<MyWrap>(object);
```
Similar to `ASSIGN_OR_RETURN_UNWRAP`, there is a `ASSIGN_OR_RETURN_UNWRAP_CPPGC`
that can be used in binding methods to return early if the JavaScript object does
not wrap the desired type. And similar to `BaseObject`, `node::CppgcMixin`
provides `env()` and `object()` methods to quickly access the associated
`node::Environment` and its JavaScript wrapper object.
```cpp
ASSIGN_OR_RETURN_UNWRAP_CPPGC(&wrap, object);
CHECK_EQ(wrap->object(), object);
```
#### Creating C++ to JavaScript references in cppgc-managed objects
Unlike `BaseObject` which typically uses a `v8::Global` (either weak or strong)
to reference an object from the V8 heap, cppgc-managed objects are expected to
use `v8::TracedReference` (which supports any `v8::Data`). For example if the
`MyWrap` object owns a `v8::UnboundScript`, in the class body the reference
should be declared as
```cpp
class MyWrap : ... {
v8::TracedReference<v8::UnboundScript> script;
}
```
V8's garbage collector traces the references from `MyWrap` through the
`MyWrap::Trace()` method, which should call `cppgc::Visitor::Trace` on the
`v8::TracedReference`.
```cpp
void MyWrap::Trace(cppgc::Visitor* visitor) const {
CppgcMixin::Trace(visitor);
visitor->Trace(script); // v8::TracedReference is supported by cppgc::Visitor
}
```
As long as a `MyWrap` object is alive, the `v8::UnboundScript` in its
`v8::TracedReference` will be kept alive. When the `MyWrap` object is no longer
reachable from the V8 heap, and there are no other references to the
`v8::UnboundScript` it owns, the `v8::UnboundScript` will be garbage collected
along with its owning `MyWrap`. The reference will also be automatically
captured in the heap snapshots.
#### Creating JavaScript to C++ references for cppgc-managed objects
To create a reference from another JavaScript object to a C++ wrapper
extending `node::CppgcMixin`, just create a JavaScript to JavaScript
reference using the JavaScript side of the wrapper, which can be accessed
using `node::CppgcMixin::object()`.
```cpp
MyWrap* wrap = ....; // Obtain a reference to the cppgc-managed object.
Local<Object> referrer = ...; // This is the referrer object.
// To reference the C++ wrap from the JavaScript referrer, simply creates
// a usual JavaScript property reference - the key can be a symbol or a
// number too if necessary, or it can be a private symbol property added
// using SetPrivate(). wrap->object() can also be passed to the JavaScript
// land, which can be referenced by any JavaScript objects in an invisible
// manner using a WeakMap or being inside a closure.
referrer->Set(
context, FIXED_ONE_BYTE_STRING(isolate, "ref"), wrap->object()
).ToLocalChecked();
```
Typically, a newly created cppgc-managed wrapper object should be held alive
by the JavaScript land (for example, by being returned by a method and
staying alive in a closure). Long-lived cppgc objects can also
be held alive from C++ using persistent handles (see
`deps/v8/include/cppgc/persistent.h`) or as members of other living
cppgc-managed objects (see `deps/v8/include/cppgc/member.h`) if necessary.
Its destructor will be called when no other objects from the V8 heap reference
it, this can happen at any time after the garbage collector notices that
it's no longer reachable and before the V8 isolate is torn down.
See the [Oilpan documentation in Chromium][] for more details.
### Callback scopes
The public `CallbackScope` and the internally used `InternalCallbackScope`
@ -1082,6 +1304,8 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
[ECMAScript realm]: https://tc39.es/ecma262/#sec-code-realms
[JavaScript value handles]: #js-handles
[N-API]: https://nodejs.org/api/n-api.html
[Oilpan]: https://v8.dev/blog/oilpan-library
[Oilpan documentation in Chromium]: https://chromium.googlesource.com/v8/v8/+/main/include/cppgc/README.md
[`BaseObject`]: #baseobject
[`Context`]: #context
[`Environment`]: #environment
@ -1117,3 +1341,4 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
[libuv handles]: #libuv-handles-and-requests
[libuv requests]: #libuv-handles-and-requests
[reference documentation for the libuv API]: http://docs.libuv.org/en/v1.x/
[unified heap design in Chromium]: https://docs.google.com/document/d/1Hs60Zx1WPJ_LUjGvgzt1OQ5Cthu-fG-zif-vquUH_8c/edit

View File

@ -84,6 +84,11 @@ class BaseObject : public MemoryRetainer {
static inline BaseObject* FromJSObject(v8::Local<v8::Value> object);
template <typename T>
static inline T* FromJSObject(v8::Local<v8::Value> object);
// Global alias for FromJSObject() to avoid churn.
template <typename T>
static inline T* Unwrap(v8::Local<v8::Value> obj) {
return BaseObject::FromJSObject<T>(obj);
}
// Make the `v8::Global` a weak reference and, `delete` this object once
// the JS object has been garbage collected and there are no (strong)
@ -234,12 +239,6 @@ class BaseObject : public MemoryRetainer {
PointerData* pointer_data_ = nullptr;
};
// Global alias for FromJSObject() to avoid churn.
template <typename T>
inline T* Unwrap(v8::Local<v8::Value> obj) {
return BaseObject::FromJSObject<T>(obj);
}
#define ASSIGN_OR_RETURN_UNWRAP(ptr, obj, ...) \
do { \
*ptr = static_cast<typename std::remove_reference<decltype(*ptr)>::type>( \

137
src/cppgc_helpers.h Normal file
View File

@ -0,0 +1,137 @@
#ifndef SRC_CPPGC_HELPERS_H_
#define SRC_CPPGC_HELPERS_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include <type_traits> // std::remove_reference
#include "cppgc/garbage-collected.h"
#include "cppgc/name-provider.h"
#include "env.h"
#include "memory_tracker.h"
#include "v8-cppgc.h"
#include "v8-sandbox.h"
#include "v8.h"
namespace node {
/**
* This is a helper mixin with a BaseObject-like interface to help
* implementing wrapper objects managed by V8's cppgc (Oilpan) library.
* cppgc-manged objects in Node.js internals should extend this mixin,
* while non-cppgc-managed objects typically extend BaseObject - the
* latter are being migrated to be cppgc-managed wherever it's beneficial
* and practical. Typically cppgc-managed objects are more efficient to
* keep track of (which lowers initialization cost) and work better
* with V8's GC scheduling.
*
* A cppgc-managed native wrapper should look something like this, note
* that per cppgc rules, CPPGC_MIXIN(Klass) must be at the left-most
* position in the hierarchy (which ensures cppgc::GarbageCollected
* is at the left-most position).
*
* class Klass final : CPPGC_MIXIN(Klass) {
* public:
* SET_CPPGC_NAME(Klass) // Sets the heap snapshot name to "Node / Klass"
* void Trace(cppgc::Visitor* visitor) const final {
* CppgcMixin::Trace(visitor);
* visitor->Trace(...); // Trace any additional owned traceable data
* }
* }
*/
class CppgcMixin : public cppgc::GarbageCollectedMixin {
public:
// To help various callbacks access wrapper objects with different memory
// management, cppgc-managed objects share the same layout as BaseObjects.
enum InternalFields { kEmbedderType = 0, kSlot, kInternalFieldCount };
// The initialization cannot be done in the mixin constructor but has to be
// invoked from the child class constructor, per cppgc::GarbageCollectedMixin
// rules.
template <typename T>
static void Wrap(T* ptr, Environment* env, v8::Local<v8::Object> obj) {
CHECK_GE(obj->InternalFieldCount(), T::kInternalFieldCount);
ptr->env_ = env;
v8::Isolate* isolate = env->isolate();
ptr->traced_reference_ = v8::TracedReference<v8::Object>(isolate, obj);
v8::Object::Wrap<v8::CppHeapPointerTag::kDefaultTag>(isolate, obj, ptr);
// Keep the layout consistent with BaseObjects.
obj->SetAlignedPointerInInternalField(
kEmbedderType, env->isolate_data()->embedder_id_for_cppgc());
obj->SetAlignedPointerInInternalField(kSlot, ptr);
}
v8::Local<v8::Object> object() const {
return traced_reference_.Get(env_->isolate());
}
Environment* env() const { return env_; }
template <typename T>
static T* Unwrap(v8::Local<v8::Object> obj) {
// We are not using v8::Object::Unwrap currently because that requires
// access to isolate which the ASSIGN_OR_RETURN_UNWRAP macro that we'll shim
// with ASSIGN_OR_RETURN_UNWRAP_GC doesn't take, and we also want a
// signature consistent with BaseObject::Unwrap() to avoid churn. Since
// cppgc-managed objects share the same layout as BaseObjects, just unwrap
// from the pointer in the internal field, which should be valid as long as
// the object is still alive.
if (obj->InternalFieldCount() != T::kInternalFieldCount) {
return nullptr;
}
T* ptr = static_cast<T*>(obj->GetAlignedPointerFromInternalField(T::kSlot));
return ptr;
}
// Subclasses are expected to invoke CppgcMixin::Trace() in their own Trace()
// methods.
void Trace(cppgc::Visitor* visitor) const override {
visitor->Trace(traced_reference_);
}
private:
Environment* env_;
v8::TracedReference<v8::Object> traced_reference_;
};
// If the class doesn't have additional owned traceable data, use this macro to
// save the implementation of a custom Trace() method.
#define DEFAULT_CPPGC_TRACE() \
void Trace(cppgc::Visitor* visitor) const final { \
CppgcMixin::Trace(visitor); \
}
// This macro sets the node name in the heap snapshot with a "Node /" prefix.
// Classes that use this macro must extend cppgc::NameProvider.
#define SET_CPPGC_NAME(Klass) \
inline const char* GetHumanReadableName() const final { \
return "Node / " #Klass; \
}
/**
* Similar to ASSIGN_OR_RETURN_UNWRAP() but works on cppgc-managed types
* inheriting CppgcMixin.
*/
#define ASSIGN_OR_RETURN_UNWRAP_CPPGC(ptr, obj, ...) \
do { \
*ptr = CppgcMixin::Unwrap< \
typename std::remove_reference<decltype(**ptr)>::type>(obj); \
if (*ptr == nullptr) return __VA_ARGS__; \
} while (0)
} // namespace node
/**
* Helper macro the manage the cppgc-based wrapper hierarchy. This must
* be used at the left-most postion - right after `:` in the class inheritance,
* like this:
* class Klass : CPPGC_MIXIN(Klass) ... {}
*
* This needs to disable linters because it will be at odds with
* clang-format.
*/
#define CPPGC_MIXIN(Klass) \
public /* NOLINT(whitespace/indent) */ \
cppgc::GarbageCollected<Klass>, public cppgc::NameProvider, public CppgcMixin
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_CPPGC_HELPERS_H_

View File

@ -805,7 +805,8 @@ ManagedEVPPKey ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(
env, std::move(pkey), ret, "Failed to read asymmetric key");
} else {
CHECK(args[*offset]->IsObject());
KeyObjectHandle* key = Unwrap<KeyObjectHandle>(args[*offset].As<Object>());
KeyObjectHandle* key =
BaseObject::Unwrap<KeyObjectHandle>(args[*offset].As<Object>());
CHECK_NOT_NULL(key);
CHECK_NE(key->Data()->GetKeyType(), kKeyTypeSecret);
(*offset) += 4;

View File

@ -419,7 +419,8 @@ ByteSource ByteSource::NullTerminatedCopy(Environment* env,
ByteSource ByteSource::FromSymmetricKeyObjectHandle(Local<Value> handle) {
CHECK(handle->IsObject());
KeyObjectHandle* key = Unwrap<KeyObjectHandle>(handle.As<Object>());
KeyObjectHandle* key =
BaseObject::Unwrap<KeyObjectHandle>(handle.As<Object>());
CHECK_NOT_NULL(key);
return Foreign(key->Data()->GetSymmetricKey(),
key->Data()->GetSymmetricKeySize());

View File

@ -288,7 +288,7 @@ FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo<v8::Value>& args,
bool use_bigint) {
v8::Local<v8::Value> value = args[index];
if (value->IsObject()) {
return Unwrap<FSReqBase>(value.As<v8::Object>());
return BaseObject::Unwrap<FSReqBase>(value.As<v8::Object>());
}
Realm* realm = Realm::GetCurrent(args);

View File

@ -91,7 +91,7 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
CHECK_LT(id, host_objects_.size());
Local<Object> object = host_objects_[id]->object(isolate);
if (env_->js_transferable_constructor_template()->HasInstance(object)) {
return Unwrap<JSTransferable>(object)->target();
return BaseObject::Unwrap<JSTransferable>(object)->target();
} else {
return object;
}
@ -318,7 +318,7 @@ class SerializerDelegate : public ValueSerializer::Delegate {
Maybe<bool> WriteHostObject(Isolate* isolate, Local<Object> object) override {
if (BaseObject::IsBaseObject(env_->isolate_data(), object)) {
return WriteHostObject(
BaseObjectPtr<BaseObject> { Unwrap<BaseObject>(object) });
BaseObjectPtr<BaseObject>{BaseObject::Unwrap<BaseObject>(object)});
}
if (JSTransferable::IsJSTransferable(env_, context_, object)) {
@ -532,7 +532,8 @@ Maybe<bool> Message::Serialize(Environment* env,
}
BaseObjectPtr<BaseObject> host_object;
if (BaseObject::IsBaseObject(env->isolate_data(), entry)) {
host_object = BaseObjectPtr<BaseObject>{Unwrap<BaseObject>(entry)};
host_object =
BaseObjectPtr<BaseObject>{BaseObject::Unwrap<BaseObject>(entry)};
} else {
if (!JSTransferable::IsJSTransferable(env, context, entry)) {
ThrowDataCloneException(context, env->clone_untransferable_str());

View File

@ -240,7 +240,7 @@ static MaybeLocal<Object> AcceptHandle(Environment* env,
if (!WrapType::Instantiate(env, parent, WrapType::SOCKET).ToLocal(&wrap_obj))
return Local<Object>();
HandleWrap* wrap = Unwrap<HandleWrap>(wrap_obj);
HandleWrap* wrap = BaseObject::Unwrap<HandleWrap>(wrap_obj);
CHECK_NOT_NULL(wrap);
uv_stream_t* stream = reinterpret_cast<uv_stream_t*>(wrap->GetHandle());
CHECK_NOT_NULL(stream);

View File

@ -56,7 +56,7 @@ using v8::Value;
namespace {
template <int (*fn)(uv_udp_t*, int)>
void SetLibuvInt32(const FunctionCallbackInfo<Value>& args) {
UDPWrap* wrap = Unwrap<UDPWrap>(args.This());
UDPWrap* wrap = BaseObject::Unwrap<UDPWrap>(args.This());
if (wrap == nullptr) {
args.GetReturnValue().Set(UV_EBADF);
return;

View File

@ -2,15 +2,21 @@
const assert = require('assert');
const util = require('util');
let internalBinding;
try {
internalBinding = require('internal/test/binding').internalBinding;
} catch (e) {
console.log('using `test/common/heap.js` requires `--expose-internals`');
throw e;
let _buildEmbedderGraph;
function buildEmbedderGraph() {
if (_buildEmbedderGraph) { return _buildEmbedderGraph(); }
let internalBinding;
try {
internalBinding = require('internal/test/binding').internalBinding;
} catch (e) {
console.error('The test must be run with `--expose-internals`');
throw e;
}
({ buildEmbedderGraph: _buildEmbedderGraph } = internalBinding('heap_utils'));
return _buildEmbedderGraph();
}
const { buildEmbedderGraph } = internalBinding('heap_utils');
const { getHeapSnapshot } = require('v8');
function createJSHeapSnapshot(stream = getHeapSnapshot()) {
@ -211,6 +217,79 @@ function validateSnapshotNodes(...args) {
return recordState().validateSnapshotNodes(...args);
}
/**
* A alternative heap snapshot validator that can be used to verify cppgc-managed nodes.
* Modified from
* https://chromium.googlesource.com/v8/v8/+/b00e995fb212737802810384ba2b868d0d92f7e5/test/unittests/heap/cppgc-js/unified-heap-snapshot-unittest.cc#134
* @param {string} rootName Name of the root node. Typically a class name used to filter all native nodes with
* this name. For cppgc-managed objects, this is typically the name configured by
* SET_CPPGC_NAME() prefixed with an additional "Node /" prefix e.g.
* "Node / ContextifyScript"
* @param {[{
* node_name?: string,
* edge_name?: string,
* node_type?: string,
* edge_type?: string,
* }]} retainingPath The retaining path specification to search from the root nodes.
* @returns {[object]} All the leaf nodes matching the retaining path specification. If none can be found,
* logs the nodes found in the last matching step of the path (if any), and throws an
* assertion error.
*/
function findByRetainingPath(rootName, retainingPath) {
const nodes = createJSHeapSnapshot();
let haystack = nodes.filter((n) => n.name === rootName && n.type !== 'string');
for (let i = 0; i < retainingPath.length; ++i) {
const expected = retainingPath[i];
const newHaystack = [];
for (const parent of haystack) {
for (let j = 0; j < parent.outgoingEdges.length; j++) {
const edge = parent.outgoingEdges[j];
// The strings are represented as { type: 'string', name: '<string content>' } in the snapshot.
// Ignore them or we'll poke into strings that are just referenced as names of real nodes,
// unless the caller is specifically looking for string nodes via `node_type`.
let match = (edge.to.type !== 'string');
if (expected.node_type) {
match = (edge.to.type === expected.node_type);
}
if (expected.node_name && edge.to.name !== expected.node_name) {
match = false;
}
if (expected.edge_name && edge.name !== expected.edge_name) {
match = false;
}
if (expected.edge_type && edge.type !== expected.type) {
match = false;
}
if (match) {
newHaystack.push(edge.to);
}
}
}
if (newHaystack.length === 0) {
const format = (val) => util.inspect(val, { breakLength: 128, depth: 3 });
console.error('#');
console.error('# Retaining path to search for:');
for (let j = 0; j < retainingPath.length; ++j) {
console.error(`# - '${format(retainingPath[j])}'${i === j ? '\t<--- not found' : ''}`);
}
console.error('#\n');
console.error('# Nodes found in the last step include:');
for (let j = 0; j < haystack.length; ++j) {
console.error(`# - '${format(haystack[j])}`);
}
assert.fail(`Could not find target edge ${format(expected)} in the heap snapshot.`);
}
haystack = newHaystack;
}
return haystack;
}
function getHeapSnapshotOptionTests() {
const fixtures = require('../common/fixtures');
const cases = [
@ -245,5 +324,6 @@ function getHeapSnapshotOptionTests() {
module.exports = {
recordState,
validateSnapshotNodes,
findByRetainingPath,
getHeapSnapshotOptionTests,
};