From 849db10fb32ea3121ac7392c4647952a954d8aae Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 2 Dec 2023 18:15:24 +0100 Subject: [PATCH] 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 Reviewed-By: James M Snell Reviewed-By: Chengzhong Wu Reviewed-By: Stephen Belanger --- node.gyp | 1 + src/README.md | 225 ++++++++++++++++++++++++++++++++++++++ src/base_object.h | 11 +- src/cppgc_helpers.h | 137 +++++++++++++++++++++++ src/crypto/crypto_keys.cc | 3 +- src/crypto/crypto_util.cc | 3 +- src/node_file-inl.h | 2 +- src/node_messaging.cc | 7 +- src/stream_wrap.cc | 2 +- src/udp_wrap.cc | 2 +- test/common/heap.js | 94 ++++++++++++++-- 11 files changed, 466 insertions(+), 21 deletions(-) create mode 100644 src/cppgc_helpers.h diff --git a/node.gyp b/node.gyp index c69670c49b6..fb8c02c86dc 100644 --- a/node.gyp +++ b/node.gyp @@ -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', diff --git a/src/README.md b/src/README.md index 01d6d5fd4bc..33ec44f0d32 100644 --- a/src/README.md +++ b/src/README.md @@ -975,6 +975,228 @@ overview over libuv handles managed by Node.js. +### `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 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 object); + // Binding method to help constructing MyWrap in JavaScript. + static void New(const v8::FunctionCallbackInfo& 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 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 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( + env->isolate()->GetCppHeap()->GetAllocationHandle(), env, object); +} + +// Binding method to be invoked by JavaScript. +void MyWrap::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + Local 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 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` 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 object = ...; // Obtain the JavaScript from somewhere. +MyWrap* wrap = CppgcMixin::Unwrap(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 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 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& 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& 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 diff --git a/src/base_object.h b/src/base_object.h index 546d968e5ca..9a93e41b918 100644 --- a/src/base_object.h +++ b/src/base_object.h @@ -84,6 +84,11 @@ class BaseObject : public MemoryRetainer { static inline BaseObject* FromJSObject(v8::Local object); template static inline T* FromJSObject(v8::Local object); + // Global alias for FromJSObject() to avoid churn. + template + static inline T* Unwrap(v8::Local obj) { + return BaseObject::FromJSObject(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 -inline T* Unwrap(v8::Local obj) { - return BaseObject::FromJSObject(obj); -} - #define ASSIGN_OR_RETURN_UNWRAP(ptr, obj, ...) \ do { \ *ptr = static_cast::type>( \ diff --git a/src/cppgc_helpers.h b/src/cppgc_helpers.h new file mode 100644 index 00000000000..177ba95a023 --- /dev/null +++ b/src/cppgc_helpers.h @@ -0,0 +1,137 @@ +#ifndef SRC_CPPGC_HELPERS_H_ +#define SRC_CPPGC_HELPERS_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include // 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 + static void Wrap(T* ptr, Environment* env, v8::Local obj) { + CHECK_GE(obj->InternalFieldCount(), T::kInternalFieldCount); + ptr->env_ = env; + v8::Isolate* isolate = env->isolate(); + ptr->traced_reference_ = v8::TracedReference(isolate, obj); + v8::Object::Wrap(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 object() const { + return traced_reference_.Get(env_->isolate()); + } + + Environment* env() const { return env_; } + + template + static T* Unwrap(v8::Local 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(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 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::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, public cppgc::NameProvider, public CppgcMixin + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_CPPGC_HELPERS_H_ diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index f49bb1254cb..17a52d85279 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -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(args[*offset].As()); + KeyObjectHandle* key = + BaseObject::Unwrap(args[*offset].As()); CHECK_NOT_NULL(key); CHECK_NE(key->Data()->GetKeyType(), kKeyTypeSecret); (*offset) += 4; diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 4660be3cea1..e789cb7822b 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -419,7 +419,8 @@ ByteSource ByteSource::NullTerminatedCopy(Environment* env, ByteSource ByteSource::FromSymmetricKeyObjectHandle(Local handle) { CHECK(handle->IsObject()); - KeyObjectHandle* key = Unwrap(handle.As()); + KeyObjectHandle* key = + BaseObject::Unwrap(handle.As()); CHECK_NOT_NULL(key); return Foreign(key->Data()->GetSymmetricKey(), key->Data()->GetSymmetricKeySize()); diff --git a/src/node_file-inl.h b/src/node_file-inl.h index 36c2f8067c6..29608b8c96b 100644 --- a/src/node_file-inl.h +++ b/src/node_file-inl.h @@ -288,7 +288,7 @@ FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo& args, bool use_bigint) { v8::Local value = args[index]; if (value->IsObject()) { - return Unwrap(value.As()); + return BaseObject::Unwrap(value.As()); } Realm* realm = Realm::GetCurrent(args); diff --git a/src/node_messaging.cc b/src/node_messaging.cc index 1243002a94c..19633f78672 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -91,7 +91,7 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { CHECK_LT(id, host_objects_.size()); Local object = host_objects_[id]->object(isolate); if (env_->js_transferable_constructor_template()->HasInstance(object)) { - return Unwrap(object)->target(); + return BaseObject::Unwrap(object)->target(); } else { return object; } @@ -318,7 +318,7 @@ class SerializerDelegate : public ValueSerializer::Delegate { Maybe WriteHostObject(Isolate* isolate, Local object) override { if (BaseObject::IsBaseObject(env_->isolate_data(), object)) { return WriteHostObject( - BaseObjectPtr { Unwrap(object) }); + BaseObjectPtr{BaseObject::Unwrap(object)}); } if (JSTransferable::IsJSTransferable(env_, context_, object)) { @@ -532,7 +532,8 @@ Maybe Message::Serialize(Environment* env, } BaseObjectPtr host_object; if (BaseObject::IsBaseObject(env->isolate_data(), entry)) { - host_object = BaseObjectPtr{Unwrap(entry)}; + host_object = + BaseObjectPtr{BaseObject::Unwrap(entry)}; } else { if (!JSTransferable::IsJSTransferable(env, context, entry)) { ThrowDataCloneException(context, env->clone_untransferable_str()); diff --git a/src/stream_wrap.cc b/src/stream_wrap.cc index b7f3943bcd5..d7fa2bb48b5 100644 --- a/src/stream_wrap.cc +++ b/src/stream_wrap.cc @@ -240,7 +240,7 @@ static MaybeLocal AcceptHandle(Environment* env, if (!WrapType::Instantiate(env, parent, WrapType::SOCKET).ToLocal(&wrap_obj)) return Local(); - HandleWrap* wrap = Unwrap(wrap_obj); + HandleWrap* wrap = BaseObject::Unwrap(wrap_obj); CHECK_NOT_NULL(wrap); uv_stream_t* stream = reinterpret_cast(wrap->GetHandle()); CHECK_NOT_NULL(stream); diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index 531de7fe2c4..e31e5e50e40 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -56,7 +56,7 @@ using v8::Value; namespace { template void SetLibuvInt32(const FunctionCallbackInfo& args) { - UDPWrap* wrap = Unwrap(args.This()); + UDPWrap* wrap = BaseObject::Unwrap(args.This()); if (wrap == nullptr) { args.GetReturnValue().Set(UV_EBADF); return; diff --git a/test/common/heap.js b/test/common/heap.js index 8eb36a8bfca..bec3b3208c0 100644 --- a/test/common/heap.js +++ b/test/common/heap.js @@ -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: '' } 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, };