From f59ec2abee82f22822b7b3231ca2056fc028a279 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Wed, 27 Mar 2019 15:46:03 -0400 Subject: [PATCH] src: implement MemoryRetainer in Environment This allows us to track the essentially-global objects in Environment in the heap snapshot. Note that this patch only tracks the fields that can be tracked correctly. There are still several types of fields that cannot be tracked: - v8::Data including v8::Private, v8::ObjectTemplate etc. - Internal types that do not implement MemoryRetainer yet - STL containers with MemoryRetainer* inside - STL containers with numeric types inside that should not have their nodes elided e.g. numeric keys in maps. The `BaseObject`s are now no longer globals. They are tracked as arguments in CleanupHookCallbacks referenced by the Environment node. This model is closer to how their lifetime is managed internally. To track the per-environment strong persistent properties, this patch divides them into those that are also `v8::Value` and those that are just `v8::Data`. The values can be tracked by the current memory tracker while the data cannot. This patch also implements the `MemoryRetainer` interface in several internal classes so that they can be tracked in the heap snapshot. PR-URL: https://github.com/nodejs/node/pull/27018 Refs: https://github.com/nodejs/node/issues/26776 Reviewed-By: Anna Henningsen Reviewed-By: James M Snell --- src/base_object.h | 3 +- src/env-inl.h | 7 +- src/env.cc | 119 +++++++++++++++++++++++-- src/env.h | 143 +++++++++++++++++++------------ src/memory_tracker-inl.h | 7 ++ src/memory_tracker.h | 13 +++ test/pummel/test-heapdump-env.js | 66 ++++++++++++++ 7 files changed, 292 insertions(+), 66 deletions(-) create mode 100644 test/pummel/test-heapdump-env.js diff --git a/src/base_object.h b/src/base_object.h index 091c3d5af01954..f1c666224f0401 100644 --- a/src/base_object.h +++ b/src/base_object.h @@ -86,7 +86,6 @@ class BaseObject : public MemoryRetainer { private: v8::Local WrappedObject() const override; - bool IsRootNode() const override; static void DeleteMe(void* data); // persistent_handle_ needs to be at a fixed offset from the start of the @@ -95,7 +94,7 @@ class BaseObject : public MemoryRetainer { // position of members in memory are predictable. For more information please // refer to `doc/guides/node-postmortem-support.md` friend int GenDebugSymbols(); - friend class Environment; + friend class CleanupHookCallback; Persistent persistent_handle_; Environment* env_; diff --git a/src/env-inl.h b/src/env-inl.h index bce36c0f69fe3e..ef054be4cb8bf8 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -980,17 +980,17 @@ void Environment::RemoveCleanupHook(void (*fn)(void*), void* arg) { cleanup_hooks_.erase(search); } -size_t Environment::CleanupHookCallback::Hash::operator()( +size_t CleanupHookCallback::Hash::operator()( const CleanupHookCallback& cb) const { return std::hash()(cb.arg_); } -bool Environment::CleanupHookCallback::Equal::operator()( +bool CleanupHookCallback::Equal::operator()( const CleanupHookCallback& a, const CleanupHookCallback& b) const { return a.fn_ == b.fn_ && a.arg_ == b.arg_; } -BaseObject* Environment::CleanupHookCallback::GetBaseObject() const { +BaseObject* CleanupHookCallback::GetBaseObject() const { if (fn_ == BaseObject::DeleteMe) return static_cast(arg_); else @@ -1054,6 +1054,7 @@ void AsyncRequest::set_stopped(bool flag) { PropertyName ## _.Reset(isolate(), value); \ } ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) + ENVIRONMENT_STRONG_PERSISTENT_VALUES(V) #undef V } // namespace node diff --git a/src/env.cc b/src/env.cc index cc8554032ef0da..1542638e77d509 100644 --- a/src/env.cc +++ b/src/env.cc @@ -112,6 +112,29 @@ IsolateData::IsolateData(Isolate* isolate, #undef V } +void IsolateData::MemoryInfo(MemoryTracker* tracker) const { +#define V(PropertyName, StringValue) \ + tracker->TrackField(#PropertyName, PropertyName(isolate())); + PER_ISOLATE_SYMBOL_PROPERTIES(V) +#undef V + +#define V(PropertyName, StringValue) \ + tracker->TrackField(#PropertyName, PropertyName(isolate())); + PER_ISOLATE_STRING_PROPERTIES(V) +#undef V + + if (node_allocator_ != nullptr) { + tracker->TrackFieldWithSize( + "node_allocator", sizeof(*node_allocator_), "NodeArrayBufferAllocator"); + } else { + tracker->TrackFieldWithSize( + "allocator", sizeof(*allocator_), "v8::ArrayBuffer::Allocator"); + } + tracker->TrackFieldWithSize( + "platform", sizeof(*platform_), "MultiIsolatePlatform"); + // TODO(joyeecheung): implement MemoryRetainer in the option classes. +} + void InitThreadLocalOnce() { CHECK_EQ(0, uv_key_create(&Environment::thread_local_env)); } @@ -707,6 +730,7 @@ void Environment::set_debug_categories(const std::string& cats, bool enabled) { } DEBUG_CATEGORY_NAMES(V) +#undef V if (comma_pos == std::string::npos) break; @@ -775,6 +799,21 @@ void Environment::CollectUVExceptionInfo(Local object, syscall, message, path, dest); } +void ImmediateInfo::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("fields", fields_); +} + +void TickInfo::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("fields", fields_); +} + +void AsyncHooks::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("providers", providers_); + tracker->TrackField("async_ids_stack", async_ids_stack_); + tracker->TrackField("fields", fields_); + tracker->TrackField("async_id_fields", async_id_fields_); +} + void AsyncHooks::grow_async_ids_stack() { async_ids_stack_.reserve(async_ids_stack_.Length() * 3); @@ -805,13 +844,83 @@ void Environment::stop_sub_worker_contexts() { } } +void MemoryTracker::TrackField(const char* edge_name, + const CleanupHookCallback& value, + const char* node_name) { + v8::HandleScope handle_scope(isolate_); + // Here, we utilize the fact that CleanupHookCallback instances + // are all unique and won't be tracked twice in one BuildEmbedderGraph + // callback. + MemoryRetainerNode* n = + PushNode("CleanupHookCallback", sizeof(value), edge_name); + // TODO(joyeecheung): at the moment only arguments of type BaseObject will be + // identified and tracked here (based on their deleters), + // but we may convert and track other known types here. + BaseObject* obj = value.GetBaseObject(); + if (obj != nullptr) { + this->TrackField("arg", obj); + } + CHECK_EQ(CurrentNode(), n); + CHECK_NE(n->size_, 0); + PopNode(); +} + void Environment::BuildEmbedderGraph(Isolate* isolate, EmbedderGraph* graph, void* data) { MemoryTracker tracker(isolate, graph); - static_cast(data)->ForEachBaseObject([&](BaseObject* obj) { - tracker.Track(obj); - }); + Environment* env = static_cast(data); + tracker.Track(env); +} + +inline size_t Environment::SelfSize() const { + size_t size = sizeof(*this); + // Remove non pointer fields that will be tracked in MemoryInfo() + // TODO(joyeecheung): refactor the MemoryTracker interface so + // this can be done for common types within the Track* calls automatically + // if a certain scope is entered. + size -= sizeof(thread_stopper_); + size -= sizeof(async_hooks_); + size -= sizeof(tick_info_); + size -= sizeof(immediate_info_); + return size; +} + +void Environment::MemoryInfo(MemoryTracker* tracker) const { + // Iteratable STLs have their own sizes subtracted from the parent + // by default. + tracker->TrackField("isolate_data", isolate_data_); + tracker->TrackField("native_modules_with_cache", native_modules_with_cache); + tracker->TrackField("native_modules_without_cache", + native_modules_without_cache); + tracker->TrackField("destroy_async_id_list", destroy_async_id_list_); + tracker->TrackField("exec_argv", exec_argv_); + tracker->TrackField("should_abort_on_uncaught_toggle", + should_abort_on_uncaught_toggle_); + tracker->TrackField("stream_base_state", stream_base_state_); + tracker->TrackField("fs_stats_field_array", fs_stats_field_array_); + tracker->TrackField("fs_stats_field_bigint_array", + fs_stats_field_bigint_array_); + tracker->TrackField("thread_stopper", thread_stopper_); + tracker->TrackField("cleanup_hooks", cleanup_hooks_); + tracker->TrackField("async_hooks", async_hooks_); + tracker->TrackField("immediate_info", immediate_info_); + tracker->TrackField("tick_info", tick_info_); + +#define V(PropertyName, TypeName) \ + tracker->TrackField(#PropertyName, PropertyName()); + ENVIRONMENT_STRONG_PERSISTENT_VALUES(V) +#undef V + + // FIXME(joyeecheung): track other fields in Environment. + // Currently MemoryTracker is unable to track these + // correctly: + // - Internal types that do not implement MemoryRetainer yet + // - STL containers with MemoryRetainer* inside + // - STL containers with numeric types inside that should not have their + // nodes elided e.g. numeric keys in maps. + // We also need to make sure that when we add a non-pointer field as its own + // node, we shift its sizeof() size out of the Environment node. } char* Environment::Reallocate(char* data, size_t old_size, size_t size) { @@ -875,8 +984,4 @@ Local BaseObject::WrappedObject() const { return object(); } -bool BaseObject::IsRootNode() const { - return !persistent_handle_.IsWeak(); -} - } // namespace node diff --git a/src/env.h b/src/env.h index 92b45ff563ec82..9d28907212ac6f 100644 --- a/src/env.h +++ b/src/env.h @@ -38,6 +38,7 @@ #include "uv.h" #include "v8.h" +#include #include #include #include @@ -330,31 +331,48 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(zero_return_string, "ZERO_RETURN") #define ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) \ - V(as_callback_data, v8::Object) \ V(as_callback_data_template, v8::FunctionTemplate) \ + V(async_wrap_ctor_template, v8::FunctionTemplate) \ + V(async_wrap_object_ctor_template, v8::FunctionTemplate) \ + V(context, v8::Context) \ + V(fd_constructor_template, v8::ObjectTemplate) \ + V(fdclose_constructor_template, v8::ObjectTemplate) \ + V(filehandlereadwrap_template, v8::ObjectTemplate) \ + V(fsreqpromise_constructor_template, v8::ObjectTemplate) \ + V(handle_wrap_ctor_template, v8::FunctionTemplate) \ + V(http2settings_constructor_template, v8::ObjectTemplate) \ + V(http2stream_constructor_template, v8::ObjectTemplate) \ + V(http2ping_constructor_template, v8::ObjectTemplate) \ + V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \ + V(message_event_object_template, v8::ObjectTemplate) \ + V(message_port_constructor_template, v8::FunctionTemplate) \ + V(pipe_constructor_template, v8::FunctionTemplate) \ + V(promise_wrap_template, v8::ObjectTemplate) \ + V(sab_lifetimepartner_constructor_template, v8::FunctionTemplate) \ + V(script_context_constructor_template, v8::FunctionTemplate) \ + V(secure_context_constructor_template, v8::FunctionTemplate) \ + V(shutdown_wrap_template, v8::ObjectTemplate) \ + V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \ + V(tcp_constructor_template, v8::FunctionTemplate) \ + V(tty_constructor_template, v8::FunctionTemplate) \ + V(write_wrap_template, v8::ObjectTemplate) + +#define ENVIRONMENT_STRONG_PERSISTENT_VALUES(V) \ + V(as_callback_data, v8::Object) \ V(async_hooks_after_function, v8::Function) \ V(async_hooks_before_function, v8::Function) \ V(async_hooks_binding, v8::Object) \ V(async_hooks_destroy_function, v8::Function) \ V(async_hooks_init_function, v8::Function) \ V(async_hooks_promise_resolve_function, v8::Function) \ - V(async_wrap_ctor_template, v8::FunctionTemplate) \ - V(async_wrap_object_ctor_template, v8::FunctionTemplate) \ V(buffer_prototype_object, v8::Object) \ V(coverage_connection, v8::Object) \ - V(context, v8::Context) \ V(crypto_key_object_constructor, v8::Function) \ V(domain_callback, v8::Function) \ V(domexception_function, v8::Function) \ - V(fd_constructor_template, v8::ObjectTemplate) \ - V(fdclose_constructor_template, v8::ObjectTemplate) \ - V(filehandlereadwrap_template, v8::ObjectTemplate) \ V(fs_use_promises_symbol, v8::Symbol) \ - V(fsreqpromise_constructor_template, v8::ObjectTemplate) \ - V(handle_wrap_ctor_template, v8::FunctionTemplate) \ V(host_import_module_dynamically_callback, v8::Function) \ V(host_initialize_import_meta_object_callback, v8::Function) \ - V(http2ping_constructor_template, v8::ObjectTemplate) \ V(http2session_on_altsvc_function, v8::Function) \ V(http2session_on_error_function, v8::Function) \ V(http2session_on_frame_error_function, v8::Function) \ @@ -367,48 +385,37 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(http2session_on_settings_function, v8::Function) \ V(http2session_on_stream_close_function, v8::Function) \ V(http2session_on_stream_trailers_function, v8::Function) \ - V(http2settings_constructor_template, v8::ObjectTemplate) \ - V(http2stream_constructor_template, v8::ObjectTemplate) \ V(internal_binding_loader, v8::Function) \ V(immediate_callback_function, v8::Function) \ V(inspector_console_extension_installer, v8::Function) \ - V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \ V(message_port, v8::Object) \ - V(message_event_object_template, v8::ObjectTemplate) \ - V(message_port_constructor_template, v8::FunctionTemplate) \ V(native_module_require, v8::Function) \ V(on_coverage_message_function, v8::Function) \ V(performance_entry_callback, v8::Function) \ V(performance_entry_template, v8::Function) \ - V(pipe_constructor_template, v8::FunctionTemplate) \ V(process_object, v8::Object) \ V(primordials, v8::Object) \ V(promise_reject_callback, v8::Function) \ - V(promise_wrap_template, v8::ObjectTemplate) \ - V(sab_lifetimepartner_constructor_template, v8::FunctionTemplate) \ - V(script_context_constructor_template, v8::FunctionTemplate) \ V(script_data_constructor_function, v8::Function) \ - V(secure_context_constructor_template, v8::FunctionTemplate) \ - V(shutdown_wrap_template, v8::ObjectTemplate) \ - V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \ - V(tcp_constructor_template, v8::FunctionTemplate) \ V(tick_callback_function, v8::Function) \ V(timers_callback_function, v8::Function) \ V(tls_wrap_constructor_function, v8::Function) \ V(trace_category_state_function, v8::Function) \ - V(tty_constructor_template, v8::FunctionTemplate) \ V(udp_constructor_function, v8::Function) \ - V(url_constructor_function, v8::Function) \ - V(write_wrap_template, v8::ObjectTemplate) + V(url_constructor_function, v8::Function) class Environment; -class IsolateData { +class IsolateData : public MemoryRetainer { public: IsolateData(v8::Isolate* isolate, uv_loop_t* event_loop, MultiIsolatePlatform* platform = nullptr, ArrayBufferAllocator* node_allocator = nullptr); + SET_MEMORY_INFO_NAME(IsolateData); + SET_SELF_SIZE(IsolateData); + void MemoryInfo(MemoryTracker* tracker) const override; + inline uv_loop_t* event_loop() const; inline MultiIsolatePlatform* platform() const; inline std::shared_ptr options(); @@ -563,8 +570,12 @@ namespace per_process { extern std::shared_ptr system_environment; } -class AsyncHooks { +class AsyncHooks : public MemoryRetainer { public: + SET_MEMORY_INFO_NAME(AsyncHooks); + SET_SELF_SIZE(AsyncHooks); + void MemoryInfo(MemoryTracker* tracker) const override; + // Reason for both UidFields and Fields are that one is stored as a double* // and the other as a uint32_t*. enum Fields { @@ -626,7 +637,7 @@ class AsyncHooks { friend class Environment; // So we can call the constructor. inline AsyncHooks(); // Keep a list of all Persistent strings used for Provider types. - v8::Eternal providers_[AsyncWrap::PROVIDERS_LENGTH]; + std::array, AsyncWrap::PROVIDERS_LENGTH> providers_; // Stores the ids of the current execution context stack. AliasedBuffer async_ids_stack_; // Attached to a Uint32Array that tracks the number of active hooks for @@ -650,7 +661,7 @@ class AsyncCallbackScope { Environment* env_; }; -class ImmediateInfo { +class ImmediateInfo : public MemoryRetainer { public: inline AliasedBuffer& fields(); inline uint32_t count() const; @@ -664,6 +675,10 @@ class ImmediateInfo { ImmediateInfo(const ImmediateInfo&) = delete; ImmediateInfo& operator=(const ImmediateInfo&) = delete; + SET_MEMORY_INFO_NAME(ImmediateInfo); + SET_SELF_SIZE(ImmediateInfo); + void MemoryInfo(MemoryTracker* tracker) const override; + private: friend class Environment; // So we can call the constructor. inline explicit ImmediateInfo(v8::Isolate* isolate); @@ -673,12 +688,16 @@ class ImmediateInfo { AliasedBuffer fields_; }; -class TickInfo { +class TickInfo : public MemoryRetainer { public: inline AliasedBuffer& fields(); inline bool has_tick_scheduled() const; inline bool has_rejection_to_warn() const; + SET_MEMORY_INFO_NAME(TickInfo); + SET_SELF_SIZE(TickInfo); + void MemoryInfo(MemoryTracker* tracker) const override; + TickInfo(const TickInfo&) = delete; TickInfo& operator=(const TickInfo&) = delete; @@ -720,11 +739,47 @@ class ShouldNotAbortOnUncaughtScope { Environment* env_; }; -class Environment { +class CleanupHookCallback { + public: + CleanupHookCallback(void (*fn)(void*), + void* arg, + uint64_t insertion_order_counter) + : fn_(fn), arg_(arg), insertion_order_counter_(insertion_order_counter) {} + + // Only hashes `arg_`, since that is usually enough to identify the hook. + struct Hash { + inline size_t operator()(const CleanupHookCallback& cb) const; + }; + + // Compares by `fn_` and `arg_` being equal. + struct Equal { + inline bool operator()(const CleanupHookCallback& a, + const CleanupHookCallback& b) const; + }; + + inline BaseObject* GetBaseObject() const; + + private: + friend class Environment; + void (*fn_)(void*); + void* arg_; + + // We keep track of the insertion order for these objects, so that we can + // call the callbacks in reverse order when we are cleaning up. + uint64_t insertion_order_counter_; +}; + +class Environment : public MemoryRetainer { public: Environment(const Environment&) = delete; Environment& operator=(const Environment&) = delete; + SET_MEMORY_INFO_NAME(Environment); + + inline size_t SelfSize() const override; + bool IsRootNode() const override { return true; } + void MemoryInfo(MemoryTracker* tracker) const override; + inline size_t async_callback_scope_depth() const; inline void PushAsyncCallbackScope(); inline void PopAsyncCallbackScope(); @@ -994,6 +1049,7 @@ class Environment { #define V(PropertyName, TypeName) \ inline v8::Local PropertyName() const; \ inline void set_ ## PropertyName(v8::Local value); + ENVIRONMENT_STRONG_PERSISTENT_VALUES(V) ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) #undef V @@ -1182,28 +1238,6 @@ class Environment { void RunAndClearNativeImmediates(); static void CheckImmediate(uv_check_t* handle); - struct CleanupHookCallback { - void (*fn_)(void*); - void* arg_; - - // We keep track of the insertion order for these objects, so that we can - // call the callbacks in reverse order when we are cleaning up. - uint64_t insertion_order_counter_; - - // Only hashes `arg_`, since that is usually enough to identify the hook. - struct Hash { - inline size_t operator()(const CleanupHookCallback& cb) const; - }; - - // Compares by `fn_` and `arg_` being equal. - struct Equal { - inline bool operator()(const CleanupHookCallback& a, - const CleanupHookCallback& b) const; - }; - - inline BaseObject* GetBaseObject() const; - }; - // Use an unordered_set, so that we have efficient insertion and removal. std::unordered_set PropertyName ## _; + ENVIRONMENT_STRONG_PERSISTENT_VALUES(V) ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) #undef V }; diff --git a/src/memory_tracker-inl.h b/src/memory_tracker-inl.h index 95d623c2b459f8..72117d2f6a0c30 100644 --- a/src/memory_tracker-inl.h +++ b/src/memory_tracker-inl.h @@ -177,6 +177,13 @@ void MemoryTracker::TrackField(const char* edge_name, TrackFieldWithSize(edge_name, value.size() * sizeof(T), "std::basic_string"); } +template +void MemoryTracker::TrackField(const char* edge_name, + const v8::Eternal& value, + const char* node_name) { + TrackField(edge_name, value.Get(isolate_)); +} + template void MemoryTracker::TrackField(const char* edge_name, const v8::Persistent& value, diff --git a/src/memory_tracker.h b/src/memory_tracker.h index c5d9b2106f0826..07b1b472169ce7 100644 --- a/src/memory_tracker.h +++ b/src/memory_tracker.h @@ -35,6 +35,8 @@ namespace crypto { class NodeBIO; } +class CleanupHookCallback; + /* Example: * * class ExampleRetainer : public MemoryRetainer { @@ -179,6 +181,10 @@ class MemoryTracker { inline void TrackField(const char* edge_name, const T& value, const char* node_name = nullptr); + template + void TrackField(const char* edge_name, + const v8::Eternal& value, + const char* node_name); template inline void TrackField(const char* edge_name, const v8::Persistent& value, @@ -191,6 +197,13 @@ class MemoryTracker { inline void TrackField(const char* edge_name, const MallocedBuffer& value, const char* node_name = nullptr); + // We do not implement CleanupHookCallback as MemoryRetainer + // but instead specialize the method here to avoid the cost of + // virtual pointers. + // TODO(joyeecheung): do this for BaseObject and remove WrappedObject() + void TrackField(const char* edge_name, + const CleanupHookCallback& value, + const char* node_name = nullptr); inline void TrackField(const char* edge_name, const uv_buf_t& value, const char* node_name = nullptr); diff --git a/test/pummel/test-heapdump-env.js b/test/pummel/test-heapdump-env.js new file mode 100644 index 00000000000000..8175797cce4f1e --- /dev/null +++ b/test/pummel/test-heapdump-env.js @@ -0,0 +1,66 @@ +// Flags: --expose-internals +'use strict'; + +// This tests that Environment is tracked in heap snapshots. + +require('../common'); +const { validateSnapshotNodes } = require('../common/heap'); +const assert = require('assert'); + +// This is just using ContextifyScript as an example here, it can be replaced +// with any BaseObject that we can easily instantiate here and register in +// cleanup hooks. +// These can all be changed to reflect the status of how these objects +// are captured in the snapshot. +const context = require('vm').createScript('const foo = 123'); + +validateSnapshotNodes('Node / Environment', [{ + children: [ + cleanupHooksFilter, + { node_name: 'Node / cleanup_hooks', edge_name: 'cleanup_hooks' }, + { node_name: 'process', edge_name: 'process_object' }, + { node_name: 'Node / IsolateData', edge_name: 'isolate_data' }, + ] +}]); + +function cleanupHooksFilter(edge) { + if (edge.name !== 'cleanup_hooks') { + return false; + } + if (edge.to.type === 'native') { + verifyCleanupHooksInSnapshot(edge.to); + } else { + verifyCleanupHooksInGraph(edge.to); + } + return true; +} + +function verifyCleanupHooksInSnapshot(node) { + assert.strictEqual(node.name, 'Node / cleanup_hooks'); + const baseObjects = []; + for (const hook of node.outgoingEdges) { + for (const hookEdge of hook.to.outgoingEdges) { + if (hookEdge.name === 'arg') { + baseObjects.push(hookEdge.to); + } + } + } + // Make sure our ContextifyScript show up. + assert(baseObjects.some((node) => node.name === 'Node / ContextifyScript')); +} + +function verifyCleanupHooksInGraph(node) { + assert.strictEqual(node.name, 'Node / cleanup_hooks'); + const baseObjects = []; + for (const hook of node.edges) { + for (const hookEdge of hook.to.edges) { + if (hookEdge.name === 'arg') { + baseObjects.push(hookEdge.to); + } + } + } + // Make sure our ContextifyScript show up. + assert(baseObjects.some((node) => node.name === 'Node / ContextifyScript')); +} + +console.log(context); // Make sure it's not GC'ed