diff --git a/common.gypi b/common.gypi index 9d74b46a8d5c70..a782cfbecb85e2 100644 --- a/common.gypi +++ b/common.gypi @@ -28,7 +28,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.14', + 'v8_embedder_string': '-node.15', # Enable disassembler for `--print-code` v8 options 'v8_enable_disassembler': 1, diff --git a/deps/v8/include/v8-profiler.h b/deps/v8/include/v8-profiler.h index 5df9b2a217ea8e..c61027b3b94e45 100644 --- a/deps/v8/include/v8-profiler.h +++ b/deps/v8/include/v8-profiler.h @@ -636,7 +636,7 @@ class V8_EXPORT AllocationProfile { * Usage: * 1) Define derived class of EmbedderGraph::Node for embedder objects. * 2) Set the build embedder graph callback on the heap profiler using - * HeapProfiler::SetBuildEmbedderGraphCallback. + * HeapProfiler::AddBuildEmbedderGraphCallback. * 3) In the callback use graph->AddEdge(node1, node2) to add an edge from * node1 to node2. * 4) To represent references from/to V8 object, construct V8 nodes using @@ -736,7 +736,12 @@ class V8_EXPORT HeapProfiler { * The callback must not trigger garbage collection in V8. */ typedef void (*BuildEmbedderGraphCallback)(v8::Isolate* isolate, - v8::EmbedderGraph* graph); + v8::EmbedderGraph* graph, + void* data); + + /** TODO(addaleax): Remove */ + typedef void (*LegacyBuildEmbedderGraphCallback)(v8::Isolate* isolate, + v8::EmbedderGraph* graph); /** Returns the number of snapshots taken. */ int GetSnapshotCount(); @@ -878,15 +883,22 @@ class V8_EXPORT HeapProfiler { /** Binds a callback to embedder's class ID. */ V8_DEPRECATED( - "Use SetBuildEmbedderGraphCallback to provide info about embedder nodes", + "Use AddBuildEmbedderGraphCallback to provide info about embedder nodes", void SetWrapperClassInfoProvider(uint16_t class_id, WrapperInfoCallback callback)); V8_DEPRECATED( - "Use SetBuildEmbedderGraphCallback to provide info about embedder nodes", + "Use AddBuildEmbedderGraphCallback to provide info about embedder nodes", void SetGetRetainerInfosCallback(GetRetainerInfosCallback callback)); - void SetBuildEmbedderGraphCallback(BuildEmbedderGraphCallback callback); + V8_DEPRECATE_SOON( + "Use AddBuildEmbedderGraphCallback to provide info about embedder nodes", + void SetBuildEmbedderGraphCallback( + LegacyBuildEmbedderGraphCallback callback)); + void AddBuildEmbedderGraphCallback(BuildEmbedderGraphCallback callback, + void* data); + void RemoveBuildEmbedderGraphCallback(BuildEmbedderGraphCallback callback, + void* data); /** * Default value of persistent handle class ID. Must not be used to diff --git a/deps/v8/src/api.cc b/deps/v8/src/api.cc index a7f6d00f6fabf7..192ad90f83e55c 100644 --- a/deps/v8/src/api.cc +++ b/deps/v8/src/api.cc @@ -10558,9 +10558,25 @@ void HeapProfiler::SetGetRetainerInfosCallback( } void HeapProfiler::SetBuildEmbedderGraphCallback( - BuildEmbedderGraphCallback callback) { - reinterpret_cast(this)->SetBuildEmbedderGraphCallback( - callback); + LegacyBuildEmbedderGraphCallback callback) { + reinterpret_cast(this)->AddBuildEmbedderGraphCallback( + [](v8::Isolate* isolate, v8::EmbedderGraph* graph, void* data) { + reinterpret_cast(data)(isolate, + graph); + }, + reinterpret_cast(callback)); +} + +void HeapProfiler::AddBuildEmbedderGraphCallback( + BuildEmbedderGraphCallback callback, void* data) { + reinterpret_cast(this)->AddBuildEmbedderGraphCallback( + callback, data); +} + +void HeapProfiler::RemoveBuildEmbedderGraphCallback( + BuildEmbedderGraphCallback callback, void* data) { + reinterpret_cast(this)->RemoveBuildEmbedderGraphCallback( + callback, data); } v8::Testing::StressType internal::Testing::stress_type_ = diff --git a/deps/v8/src/profiler/heap-profiler.cc b/deps/v8/src/profiler/heap-profiler.cc index 7e0bcec97ae75e..2496e24b91cf18 100644 --- a/deps/v8/src/profiler/heap-profiler.cc +++ b/deps/v8/src/profiler/heap-profiler.cc @@ -69,16 +69,25 @@ v8::HeapProfiler::RetainerInfos HeapProfiler::GetRetainerInfos( return infos; } -void HeapProfiler::SetBuildEmbedderGraphCallback( - v8::HeapProfiler::BuildEmbedderGraphCallback callback) { - build_embedder_graph_callback_ = callback; +void HeapProfiler::AddBuildEmbedderGraphCallback( + v8::HeapProfiler::BuildEmbedderGraphCallback callback, void* data) { + build_embedder_graph_callbacks_.push_back({callback, data}); +} + +void HeapProfiler::RemoveBuildEmbedderGraphCallback( + v8::HeapProfiler::BuildEmbedderGraphCallback callback, void* data) { + auto it = std::find(build_embedder_graph_callbacks_.begin(), + build_embedder_graph_callbacks_.end(), + std::make_pair(callback, data)); + if (it != build_embedder_graph_callbacks_.end()) + build_embedder_graph_callbacks_.erase(it); } void HeapProfiler::BuildEmbedderGraph(Isolate* isolate, v8::EmbedderGraph* graph) { - if (build_embedder_graph_callback_ != nullptr) - build_embedder_graph_callback_(reinterpret_cast(isolate), - graph); + for (const auto& cb : build_embedder_graph_callbacks_) { + cb.first(reinterpret_cast(isolate), graph, cb.second); + } } HeapSnapshot* HeapProfiler::TakeSnapshot( diff --git a/deps/v8/src/profiler/heap-profiler.h b/deps/v8/src/profiler/heap-profiler.h index 507dd579bffb65..fc0b005e1c67ce 100644 --- a/deps/v8/src/profiler/heap-profiler.h +++ b/deps/v8/src/profiler/heap-profiler.h @@ -71,11 +71,13 @@ class HeapProfiler : public HeapObjectAllocationTracker { v8::HeapProfiler::GetRetainerInfosCallback callback); v8::HeapProfiler::RetainerInfos GetRetainerInfos(Isolate* isolate); - void SetBuildEmbedderGraphCallback( - v8::HeapProfiler::BuildEmbedderGraphCallback callback); + void AddBuildEmbedderGraphCallback( + v8::HeapProfiler::BuildEmbedderGraphCallback callback, void* data); + void RemoveBuildEmbedderGraphCallback( + v8::HeapProfiler::BuildEmbedderGraphCallback callback, void* data); void BuildEmbedderGraph(Isolate* isolate, v8::EmbedderGraph* graph); bool HasBuildEmbedderGraphCallback() { - return build_embedder_graph_callback_ != nullptr; + return !build_embedder_graph_callbacks_.empty(); } bool is_tracking_object_moves() const { return is_tracking_object_moves_; } @@ -103,8 +105,8 @@ class HeapProfiler : public HeapObjectAllocationTracker { std::unique_ptr sampling_heap_profiler_; v8::HeapProfiler::GetRetainerInfosCallback get_retainer_infos_callback_ = nullptr; - v8::HeapProfiler::BuildEmbedderGraphCallback build_embedder_graph_callback_ = - nullptr; + std::vector> + build_embedder_graph_callbacks_; DISALLOW_COPY_AND_ASSIGN(HeapProfiler); }; diff --git a/deps/v8/test/cctest/test-heap-profiler.cc b/deps/v8/test/cctest/test-heap-profiler.cc index 4372aa3d2e5205..eec739d109dcf1 100644 --- a/deps/v8/test/cctest/test-heap-profiler.cc +++ b/deps/v8/test/cctest/test-heap-profiler.cc @@ -1541,8 +1541,8 @@ class EmbedderGraphBuilder : public v8::PersistentHandleVisitor { graph->AddNode(std::unique_ptr(new Group("ccc-group"))); } - static void BuildEmbedderGraph(v8::Isolate* isolate, - v8::EmbedderGraph* graph) { + static void BuildEmbedderGraph(v8::Isolate* isolate, v8::EmbedderGraph* graph, + void* data) { EmbedderGraphBuilder builder(isolate, graph); isolate->VisitHandlesWithClassIds(&builder); } @@ -1604,8 +1604,8 @@ TEST(HeapSnapshotRetainedObjectInfo) { v8::HandleScope scope(isolate); v8::HeapProfiler* heap_profiler = isolate->GetHeapProfiler(); - heap_profiler->SetBuildEmbedderGraphCallback( - EmbedderGraphBuilder::BuildEmbedderGraph); + heap_profiler->AddBuildEmbedderGraphCallback( + EmbedderGraphBuilder::BuildEmbedderGraph, nullptr); v8::Persistent p_AAA(isolate, v8_str("AAA")); p_AAA.SetWrapperClassId(1); v8::Persistent p_BBB(isolate, v8_str("BBB")); @@ -2932,7 +2932,8 @@ class EmbedderRootNode : public EmbedderNode { // global object. v8::Local* global_object_pointer; -void BuildEmbedderGraph(v8::Isolate* v8_isolate, v8::EmbedderGraph* graph) { +void BuildEmbedderGraph(v8::Isolate* v8_isolate, v8::EmbedderGraph* graph, + void* data) { using Node = v8::EmbedderGraph::Node; Node* global_node = graph->V8Node(*global_object_pointer); Node* embedder_node_A = graph->AddNode( @@ -2979,12 +2980,92 @@ TEST(EmbedderGraph) { (isolate->context()->native_context()->global_object()))); global_object_pointer = &global_object; v8::HeapProfiler* heap_profiler = env->GetIsolate()->GetHeapProfiler(); - heap_profiler->SetBuildEmbedderGraphCallback(BuildEmbedderGraph); + heap_profiler->AddBuildEmbedderGraphCallback(BuildEmbedderGraph, nullptr); const v8::HeapSnapshot* snapshot = heap_profiler->TakeHeapSnapshot(); CHECK(ValidateSnapshot(snapshot)); CheckEmbedderGraphSnapshot(env->GetIsolate(), snapshot); } +struct GraphBuildingContext { + int counter = 0; +}; + +void CheckEmbedderGraphSnapshotWithContext( + v8::Isolate* isolate, const v8::HeapSnapshot* snapshot, + const GraphBuildingContext* context) { + const v8::HeapGraphNode* global = GetGlobalObject(snapshot); + CHECK_GE(context->counter, 1); + CHECK_LE(context->counter, 2); + + const v8::HeapGraphNode* embedder_node_A = + GetChildByName(global, "EmbedderNodeA"); + CHECK_EQ(10, GetSize(embedder_node_A)); + + const v8::HeapGraphNode* embedder_node_B = + GetChildByName(global, "EmbedderNodeB"); + if (context->counter == 2) { + CHECK_NOT_NULL(embedder_node_B); + CHECK_EQ(20, GetSize(embedder_node_B)); + } else { + CHECK_NULL(embedder_node_B); + } +} + +void BuildEmbedderGraphWithContext(v8::Isolate* v8_isolate, + v8::EmbedderGraph* graph, void* data) { + using Node = v8::EmbedderGraph::Node; + GraphBuildingContext* context = static_cast(data); + Node* global_node = graph->V8Node(*global_object_pointer); + + CHECK_GE(context->counter, 0); + CHECK_LE(context->counter, 1); + switch (context->counter++) { + case 0: { + Node* embedder_node_A = graph->AddNode( + std::unique_ptr(new EmbedderNode("EmbedderNodeA", 10))); + graph->AddEdge(global_node, embedder_node_A); + break; + } + case 1: { + Node* embedder_node_B = graph->AddNode( + std::unique_ptr(new EmbedderNode("EmbedderNodeB", 20))); + graph->AddEdge(global_node, embedder_node_B); + break; + } + } +} + +TEST(EmbedderGraphMultipleCallbacks) { + i::FLAG_heap_profiler_use_embedder_graph = true; + LocalContext env; + v8::HandleScope scope(env->GetIsolate()); + i::Isolate* isolate = reinterpret_cast(env->GetIsolate()); + v8::Local global_object = + v8::Utils::ToLocal(i::Handle( + (isolate->context()->native_context()->global_object()))); + global_object_pointer = &global_object; + v8::HeapProfiler* heap_profiler = env->GetIsolate()->GetHeapProfiler(); + GraphBuildingContext context; + + heap_profiler->AddBuildEmbedderGraphCallback(BuildEmbedderGraphWithContext, + &context); + heap_profiler->AddBuildEmbedderGraphCallback(BuildEmbedderGraphWithContext, + &context); + const v8::HeapSnapshot* snapshot = heap_profiler->TakeHeapSnapshot(); + CHECK_EQ(context.counter, 2); + CHECK(ValidateSnapshot(snapshot)); + CheckEmbedderGraphSnapshotWithContext(env->GetIsolate(), snapshot, &context); + + heap_profiler->RemoveBuildEmbedderGraphCallback(BuildEmbedderGraphWithContext, + &context); + context.counter = 0; + + snapshot = heap_profiler->TakeHeapSnapshot(); + CHECK_EQ(context.counter, 1); + CHECK(ValidateSnapshot(snapshot)); + CheckEmbedderGraphSnapshotWithContext(env->GetIsolate(), snapshot, &context); +} + TEST(StrongHandleAnnotation) { LocalContext env; v8::HandleScope scope(env->GetIsolate()); @@ -3010,7 +3091,7 @@ TEST(StrongHandleAnnotation) { } void BuildEmbedderGraphWithWrapperNode(v8::Isolate* v8_isolate, - v8::EmbedderGraph* graph) { + v8::EmbedderGraph* graph, void* data) { using Node = v8::EmbedderGraph::Node; Node* global_node = graph->V8Node(*global_object_pointer); Node* wrapper_node = graph->AddNode( @@ -3041,8 +3122,8 @@ TEST(EmbedderGraphWithWrapperNode) { (isolate->context()->native_context()->global_object()))); global_object_pointer = &global_object; v8::HeapProfiler* heap_profiler = env->GetIsolate()->GetHeapProfiler(); - heap_profiler->SetBuildEmbedderGraphCallback( - BuildEmbedderGraphWithWrapperNode); + heap_profiler->AddBuildEmbedderGraphCallback( + BuildEmbedderGraphWithWrapperNode, nullptr); const v8::HeapSnapshot* snapshot = heap_profiler->TakeHeapSnapshot(); CHECK(ValidateSnapshot(snapshot)); const v8::HeapGraphNode* global = GetGlobalObject(snapshot); @@ -3080,7 +3161,7 @@ class EmbedderNodeWithPrefix : public v8::EmbedderGraph::Node { }; void BuildEmbedderGraphWithPrefix(v8::Isolate* v8_isolate, - v8::EmbedderGraph* graph) { + v8::EmbedderGraph* graph, void* data) { using Node = v8::EmbedderGraph::Node; Node* global_node = graph->V8Node(*global_object_pointer); Node* node = graph->AddNode( @@ -3098,7 +3179,8 @@ TEST(EmbedderGraphWithPrefix) { (isolate->context()->native_context()->global_object()))); global_object_pointer = &global_object; v8::HeapProfiler* heap_profiler = env->GetIsolate()->GetHeapProfiler(); - heap_profiler->SetBuildEmbedderGraphCallback(BuildEmbedderGraphWithPrefix); + heap_profiler->AddBuildEmbedderGraphCallback(BuildEmbedderGraphWithPrefix, + nullptr); const v8::HeapSnapshot* snapshot = heap_profiler->TakeHeapSnapshot(); CHECK(ValidateSnapshot(snapshot)); const v8::HeapGraphNode* global = GetGlobalObject(snapshot); diff --git a/lib/internal/test/heap.js b/lib/internal/test/heap.js new file mode 100644 index 00000000000000..a9260f651b9c1a --- /dev/null +++ b/lib/internal/test/heap.js @@ -0,0 +1,87 @@ +'use strict'; + +process.emitWarning( + 'These APIs are exposed only for testing and are not ' + + 'tracked by any versioning system or deprecation process.', + 'internal/test/heap'); + +const { internalBinding } = require('internal/bootstrap/loaders'); +const { createHeapDump, buildEmbedderGraph } = internalBinding('heap_utils'); +const assert = require('assert'); + +// This is not suitable for production code. It creates a full V8 heap dump, +// parses it as JSON, and then creates complex objects from it, leading +// to significantly increased memory usage. +function createJSHeapDump() { + const dump = createHeapDump(); + const meta = dump.snapshot.meta; + + const nodes = + readHeapInfo(dump.nodes, meta.node_fields, meta.node_types, dump.strings); + const edges = + readHeapInfo(dump.edges, meta.edge_fields, meta.edge_types, dump.strings); + + for (const node of nodes) { + node.incomingEdges = []; + node.outgoingEdges = []; + } + + let fromNodeIndex = 0; + let edgeIndex = 0; + for (const { type, name_or_index, to_node } of edges) { + while (edgeIndex === nodes[fromNodeIndex].edge_count) { + edgeIndex = 0; + fromNodeIndex++; + } + const toNode = nodes[to_node / meta.node_fields.length]; + const fromNode = nodes[fromNodeIndex]; + const edge = { + type, + toNode, + fromNode, + name: typeof name_or_index === 'string' ? name_or_index : null + }; + toNode.incomingEdges.push(edge); + fromNode.outgoingEdges.push(edge); + edgeIndex++; + } + + for (const node of nodes) + assert.strictEqual(node.edge_count, node.outgoingEdges.length); + + return nodes; +} + +function readHeapInfo(raw, fields, types, strings) { + const items = []; + + for (var i = 0; i < raw.length; i += fields.length) { + const item = {}; + for (var j = 0; j < fields.length; j++) { + const name = fields[j]; + let type = types[j]; + if (Array.isArray(type)) { + item[name] = type[raw[i + j]]; + } else if (name === 'name_or_index') { // type === 'string_or_number' + if (item.type === 'element' || item.type === 'hidden') + type = 'number'; + else + type = 'string'; + } + + if (type === 'string') { + item[name] = strings[raw[i + j]]; + } else if (type === 'number' || type === 'node') { + item[name] = raw[i + j]; + } + } + items.push(item); + } + + return items; +} + +module.exports = { + createJSHeapDump, + buildEmbedderGraph +}; diff --git a/node.gyp b/node.gyp index d3bebba8cec4f1..75288114a8a4a1 100644 --- a/node.gyp +++ b/node.gyp @@ -148,6 +148,7 @@ 'lib/internal/repl/await.js', 'lib/internal/socket_list.js', 'lib/internal/test/binding.js', + 'lib/internal/test/heap.js', 'lib/internal/test/unicode.js', 'lib/internal/timers.js', 'lib/internal/tls.js', @@ -330,6 +331,7 @@ 'src/exceptions.cc', 'src/fs_event_wrap.cc', 'src/handle_wrap.cc', + 'src/heap_utils.cc', 'src/js_stream.cc', 'src/module_wrap.cc', 'src/node.cc', diff --git a/src/async_wrap.cc b/src/async_wrap.cc index e98dca3c56651b..7ef3dafdf992c5 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -32,7 +32,6 @@ using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; -using v8::HeapProfiler; using v8::Integer; using v8::Isolate; using v8::Local; @@ -43,7 +42,6 @@ using v8::ObjectTemplate; using v8::Promise; using v8::PromiseHookType; using v8::PropertyCallbackInfo; -using v8::RetainedObjectInfo; using v8::String; using v8::Uint32; using v8::Undefined; @@ -61,87 +59,6 @@ static const char* const provider_names[] = { }; -// Report correct information in a heapdump. - -class RetainedAsyncInfo: public RetainedObjectInfo { - public: - explicit RetainedAsyncInfo(uint16_t class_id, AsyncWrap* wrap); - - void Dispose() override; - bool IsEquivalent(RetainedObjectInfo* other) override; - intptr_t GetHash() override; - const char* GetLabel() override; - intptr_t GetSizeInBytes() override; - - private: - const char* label_; - const AsyncWrap* wrap_; - const size_t length_; -}; - - -static int OwnMemory(AsyncWrap* async_wrap) { - MemoryTracker tracker; - tracker.set_track_only_self(true); - tracker.Track(async_wrap); - return tracker.accumulated_size(); -} - - -RetainedAsyncInfo::RetainedAsyncInfo(uint16_t class_id, AsyncWrap* wrap) - : label_(provider_names[class_id - NODE_ASYNC_ID_OFFSET]), - wrap_(wrap), - length_(OwnMemory(wrap)) { -} - - -void RetainedAsyncInfo::Dispose() { - delete this; -} - - -bool RetainedAsyncInfo::IsEquivalent(RetainedObjectInfo* other) { - return label_ == other->GetLabel() && - wrap_ == static_cast(other)->wrap_; -} - - -intptr_t RetainedAsyncInfo::GetHash() { - return reinterpret_cast(wrap_); -} - - -const char* RetainedAsyncInfo::GetLabel() { - return label_; -} - - -intptr_t RetainedAsyncInfo::GetSizeInBytes() { - return length_; -} - - -RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local wrapper) { - // No class_id should be the provider type of NONE. - CHECK_GT(class_id, NODE_ASYNC_ID_OFFSET); - // And make sure the class_id doesn't extend past the last provider. - CHECK_LE(class_id - NODE_ASYNC_ID_OFFSET, AsyncWrap::PROVIDERS_LENGTH); - CHECK(wrapper->IsObject()); - CHECK(!wrapper.IsEmpty()); - - Local object = wrapper.As(); - CHECK_GT(object->InternalFieldCount(), 0); - - AsyncWrap* wrap; - ASSIGN_OR_RETURN_UNWRAP(&wrap, object, nullptr); - - return new RetainedAsyncInfo(class_id, wrap); -} - - -// end RetainedAsyncInfo - - struct AsyncWrapObject : public AsyncWrap { static inline void New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -616,16 +533,6 @@ void AsyncWrap::Initialize(Local target, } -void LoadAsyncWrapperInfo(Environment* env) { - HeapProfiler* heap_profiler = env->isolate()->GetHeapProfiler(); -#define V(PROVIDER) \ - heap_profiler->SetWrapperClassInfoProvider( \ - (NODE_ASYNC_ID_OFFSET + AsyncWrap::PROVIDER_ ## PROVIDER), WrapperInfo); - NODE_ASYNC_PROVIDER_TYPES(V) -#undef V -} - - AsyncWrap::AsyncWrap(Environment* env, Local object, ProviderType provider, @@ -814,9 +721,12 @@ void EmitAsyncDestroy(Isolate* isolate, async_context asyncContext) { Environment::GetCurrent(isolate), asyncContext.async_id); } +std::string AsyncWrap::MemoryInfoName() const { + return provider_names[provider_type()]; +} + std::string AsyncWrap::diagnostic_name() const { - return std::string(provider_names[provider_type()]) + - " (" + std::to_string(env()->thread_id()) + ":" + + return MemoryInfoName() + " (" + std::to_string(env()->thread_id()) + ":" + std::to_string(static_cast(async_id_)) + ")"; } diff --git a/src/async_wrap.h b/src/async_wrap.h index ef3a5934893d7f..f748dc801dab77 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -174,6 +174,7 @@ class AsyncWrap : public BaseObject { v8::Local* argv); virtual std::string diagnostic_name() const; + std::string MemoryInfoName() const override; static void WeakCallback(const v8::WeakCallbackInfo &info); @@ -204,8 +205,6 @@ class AsyncWrap : public BaseObject { double trigger_async_id_; }; -void LoadAsyncWrapperInfo(Environment* env); - } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/env-inl.h b/src/env-inl.h index b47af181115d4d..fccd45070c4e07 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -809,6 +809,22 @@ bool Environment::CleanupHookCallback::Equal::operator()( return a.fn_ == b.fn_ && a.arg_ == b.arg_; } +BaseObject* Environment::CleanupHookCallback::GetBaseObject() const { + if (fn_ == BaseObject::DeleteMe) + return static_cast(arg_); + else + return nullptr; +} + +template +void Environment::ForEachBaseObject(T&& iterator) { + for (const auto& hook : cleanup_hooks_) { + BaseObject* obj = hook.GetBaseObject(); + if (obj != nullptr) + iterator(obj); + } +} + #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) #define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) diff --git a/src/env.cc b/src/env.cc index 30bc85559a9a0a..5a6e765681f24b 100644 --- a/src/env.cc +++ b/src/env.cc @@ -144,9 +144,15 @@ Environment::Environment(IsolateData* isolate_data, std::string debug_cats; SafeGetenv("NODE_DEBUG_NATIVE", &debug_cats); set_debug_categories(debug_cats, true); + + isolate()->GetHeapProfiler()->AddBuildEmbedderGraphCallback( + BuildEmbedderGraph, this); } Environment::~Environment() { + isolate()->GetHeapProfiler()->RemoveBuildEmbedderGraphCallback( + BuildEmbedderGraph, this); + // Make sure there are no re-used libuv wrapper objects. // CleanupHandles() should have removed all of them. CHECK(file_handle_read_wrap_freelist_.empty()); @@ -217,7 +223,6 @@ void Environment::Start(int argc, set_process_object(process_object); SetupProcessObject(this, argc, argv, exec_argc, exec_argv); - LoadAsyncWrapperInfo(this); static uv_once_t init_once = UV_ONCE_INIT; uv_once(&init_once, InitThreadLocalOnce); @@ -734,6 +739,16 @@ void Environment::stop_sub_worker_contexts() { } } +void Environment::BuildEmbedderGraph(v8::Isolate* isolate, + v8::EmbedderGraph* graph, + void* data) { + MemoryTracker tracker(isolate, graph); + static_cast(data)->ForEachBaseObject([&](BaseObject* obj) { + tracker.Track(obj); + }); +} + + // Not really any better place than env.cc at this moment. void BaseObject::DeleteMe(void* data) { BaseObject* self = static_cast(data); diff --git a/src/env.h b/src/env.h index f6725e8e53d6b9..120048fe009b03 100644 --- a/src/env.h +++ b/src/env.h @@ -861,6 +861,10 @@ class Environment { inline void RemoveCleanupHook(void (*fn)(void*), void* arg); void RunCleanup(); + static void BuildEmbedderGraph(v8::Isolate* isolate, + v8::EmbedderGraph* graph, + void* data); + private: inline void CreateImmediate(native_immediate_callback cb, void* data, @@ -981,6 +985,8 @@ class Environment { 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. @@ -993,6 +999,9 @@ class Environment { v8::Local promise, v8::Local parent); + template + void ForEachBaseObject(T&& iterator); + #define V(PropertyName, TypeName) Persistent PropertyName ## _; ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) #undef V diff --git a/src/heap_utils.cc b/src/heap_utils.cc new file mode 100644 index 00000000000000..2d339c580fa076 --- /dev/null +++ b/src/heap_utils.cc @@ -0,0 +1,232 @@ +#include "node_internals.h" +#include "env.h" + +using v8::Array; +using v8::Boolean; +using v8::Context; +using v8::EmbedderGraph; +using v8::EscapableHandleScope; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::HeapSnapshot; +using v8::Isolate; +using v8::JSON; +using v8::Local; +using v8::MaybeLocal; +using v8::Number; +using v8::Object; +using v8::String; +using v8::Value; + +namespace node { +namespace heap { + +class JSGraphJSNode : public EmbedderGraph::Node { + public: + const char* Name() override { return ""; } + size_t SizeInBytes() override { return 0; } + bool IsEmbedderNode() override { return false; } + Local JSValue() { return StrongPersistentToLocal(persistent_); } + + int IdentityHash() { + Local v = JSValue(); + if (v->IsObject()) return v.As()->GetIdentityHash(); + if (v->IsName()) return v.As()->GetIdentityHash(); + if (v->IsInt32()) return v.As()->Value(); + return 0; + } + + JSGraphJSNode(Isolate* isolate, Local val) + : persistent_(isolate, val) { + CHECK(!val.IsEmpty()); + } + + struct Hash { + inline size_t operator()(JSGraphJSNode* n) const { + return n->IdentityHash(); + } + }; + + struct Equal { + inline bool operator()(JSGraphJSNode* a, JSGraphJSNode* b) const { + return a->JSValue()->SameValue(b->JSValue()); + } + }; + + private: + Persistent persistent_; +}; + +class JSGraph : public EmbedderGraph { + public: + explicit JSGraph(Isolate* isolate) : isolate_(isolate) {} + + Node* V8Node(const Local& value) override { + std::unique_ptr n { new JSGraphJSNode(isolate_, value) }; + auto it = engine_nodes_.find(n.get()); + if (it != engine_nodes_.end()) + return *it; + engine_nodes_.insert(n.get()); + return AddNode(std::unique_ptr(n.release())); + } + + Node* AddNode(std::unique_ptr node) override { + Node* n = node.get(); + nodes_.emplace(std::move(node)); + return n; + } + + void AddEdge(Node* from, Node* to) override { + edges_[from].insert(to); + } + + MaybeLocal CreateObject() const { + EscapableHandleScope handle_scope(isolate_); + Local context = isolate_->GetCurrentContext(); + + std::unordered_map> info_objects; + Local nodes = Array::New(isolate_, nodes_.size()); + Local edges_string = FIXED_ONE_BYTE_STRING(isolate_, "edges"); + Local is_root_string = FIXED_ONE_BYTE_STRING(isolate_, "isRoot"); + Local name_string = FIXED_ONE_BYTE_STRING(isolate_, "name"); + Local size_string = FIXED_ONE_BYTE_STRING(isolate_, "size"); + Local value_string = FIXED_ONE_BYTE_STRING(isolate_, "value"); + Local wraps_string = FIXED_ONE_BYTE_STRING(isolate_, "wraps"); + + for (const std::unique_ptr& n : nodes_) + info_objects[n.get()] = Object::New(isolate_); + + { + HandleScope handle_scope(isolate_); + size_t i = 0; + for (const std::unique_ptr& n : nodes_) { + Local obj = info_objects[n.get()]; + Local value; + if (!String::NewFromUtf8(isolate_, n->Name(), + v8::NewStringType::kNormal).ToLocal(&value) || + obj->Set(context, name_string, value).IsNothing() || + obj->Set(context, is_root_string, + Boolean::New(isolate_, n->IsRootNode())).IsNothing() || + obj->Set(context, size_string, + Number::New(isolate_, n->SizeInBytes())).IsNothing() || + obj->Set(context, edges_string, + Array::New(isolate_)).IsNothing()) { + return MaybeLocal(); + } + if (nodes->Set(context, i++, obj).IsNothing()) + return MaybeLocal(); + if (!n->IsEmbedderNode()) { + value = static_cast(n.get())->JSValue(); + if (obj->Set(context, value_string, value).IsNothing()) + return MaybeLocal(); + } + } + } + + for (const std::unique_ptr& n : nodes_) { + Node* wraps = n->WrapperNode(); + if (wraps == nullptr) continue; + Local from = info_objects[n.get()]; + Local to = info_objects[wraps]; + if (from->Set(context, wraps_string, to).IsNothing()) + return MaybeLocal(); + } + + for (const auto& edge_info : edges_) { + Node* source = edge_info.first; + Local edges; + if (!info_objects[source]->Get(context, edges_string).ToLocal(&edges) || + !edges->IsArray()) { + return MaybeLocal(); + } + + size_t i = 0; + for (Node* target : edge_info.second) { + if (edges.As()->Set(context, + i++, + info_objects[target]).IsNothing()) { + return MaybeLocal(); + } + } + } + + return handle_scope.Escape(nodes); + } + + private: + Isolate* isolate_; + std::unordered_set> nodes_; + std::unordered_set + engine_nodes_; + std::unordered_map> edges_; +}; + +void BuildEmbedderGraph(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + JSGraph graph(env->isolate()); + Environment::BuildEmbedderGraph(env->isolate(), &graph, env); + Local ret; + if (graph.CreateObject().ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + + +class BufferOutputStream : public v8::OutputStream { + public: + BufferOutputStream() : buffer_(new JSString()) {} + + void EndOfStream() override {} + int GetChunkSize() override { return 1024 * 1024; } + WriteResult WriteAsciiChunk(char* data, int size) override { + buffer_->Append(data, size); + return kContinue; + } + + Local ToString(Isolate* isolate) { + return String::NewExternalOneByte(isolate, + buffer_.release()).ToLocalChecked(); + } + + private: + class JSString : public String::ExternalOneByteStringResource { + public: + void Append(char* data, size_t count) { + store_.append(data, count); + } + + const char* data() const override { return store_.data(); } + size_t length() const override { return store_.size(); } + + private: + std::string store_; + }; + + std::unique_ptr buffer_; +}; + +void CreateHeapDump(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + const HeapSnapshot* snapshot = isolate->GetHeapProfiler()->TakeHeapSnapshot(); + BufferOutputStream out; + snapshot->Serialize(&out, HeapSnapshot::kJSON); + const_cast(snapshot)->Delete(); + Local ret; + if (JSON::Parse(isolate->GetCurrentContext(), + out.ToString(isolate)).ToLocal(&ret)) { + args.GetReturnValue().Set(ret); + } +} + +void Initialize(Local target, + Local unused, + Local context) { + Environment* env = Environment::GetCurrent(context); + + env->SetMethodNoSideEffect(target, "buildEmbedderGraph", BuildEmbedderGraph); + env->SetMethodNoSideEffect(target, "createHeapDump", CreateHeapDump); +} + +} // namespace heap +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(heap_utils, node::heap::Initialize) diff --git a/src/memory_tracker-inl.h b/src/memory_tracker-inl.h index 758223492f6e71..568a4364f9c64d 100644 --- a/src/memory_tracker-inl.h +++ b/src/memory_tracker-inl.h @@ -7,13 +7,54 @@ namespace node { +class MemoryRetainerNode : public v8::EmbedderGraph::Node { + public: + explicit inline MemoryRetainerNode(MemoryTracker* tracker, + const MemoryRetainer* retainer, + const char* name) + : retainer_(retainer) { + if (retainer_ != nullptr) { + v8::HandleScope handle_scope(tracker->isolate()); + v8::Local obj = retainer_->WrappedObject(); + if (!obj.IsEmpty()) + wrapper_node_ = tracker->graph()->V8Node(obj); + + name_ = retainer_->MemoryInfoName(); + } + if (name_.empty() && name != nullptr) { + name_ = name; + } + } + + const char* Name() override { return name_.c_str(); } + const char* NamePrefix() override { return "Node /"; } + size_t SizeInBytes() override { return size_; } + // TODO(addaleax): Merging this with the "official" WrapperNode() method + // seems to lose accuracy, e.g. SizeInBytes() is disregarded. + // Figure out whether to do anything about that. + Node* JSWrapperNode() { return wrapper_node_; } + + bool IsRootNode() override { + return retainer_ != nullptr && retainer_->IsRootNode(); + } + + private: + friend class MemoryTracker; + + Node* wrapper_node_ = nullptr; + const MemoryRetainer* retainer_; + std::string name_; + size_t size_ = 0; +}; + template void MemoryTracker::TrackThis(const T* obj) { - accumulated_size_ += sizeof(T); + CurrentNode()->size_ = sizeof(T); } void MemoryTracker::TrackFieldWithSize(const char* name, size_t size) { - accumulated_size_ += size; + if (size > 0) + AddNode(name)->size_ = size; } void MemoryTracker::TrackField(const char* name, const MemoryRetainer& value) { @@ -21,9 +62,13 @@ void MemoryTracker::TrackField(const char* name, const MemoryRetainer& value) { } void MemoryTracker::TrackField(const char* name, const MemoryRetainer* value) { - if (track_only_self_ || value == nullptr || seen_.count(value) > 0) return; - seen_.insert(value); - Track(value); + if (track_only_self_ || value == nullptr) return; + auto it = seen_.find(value); + if (it != seen_.end()) { + graph_->AddEdge(CurrentNode(), it->second); + } else { + Track(value, name); + } } template @@ -36,8 +81,10 @@ template void MemoryTracker::TrackField(const char* name, const T& value) { if (value.begin() == value.end()) return; size_t index = 0; + PushNode(name); for (Iterator it = value.begin(); it != value.end(); ++it) TrackField(std::to_string(index++).c_str(), *it); + PopNode(); } template @@ -56,13 +103,15 @@ void MemoryTracker::TrackField(const char* name, const std::queue& value) { template void MemoryTracker::TrackField(const char* name, const T& value) { // For numbers, creating new nodes is not worth the overhead. - TrackFieldWithSize(name, sizeof(T)); + CurrentNode()->size_ += sizeof(T); } template void MemoryTracker::TrackField(const char* name, const std::pair& value) { + PushNode(name); TrackField("first", value.first); TrackField("second", value.second); + PopNode(); } template @@ -74,10 +123,13 @@ void MemoryTracker::TrackField(const char* name, template void MemoryTracker::TrackField(const char* name, const v8::Persistent& value) { + TrackField(name, value.Get(isolate_)); } template void MemoryTracker::TrackField(const char* name, const v8::Local& value) { + if (!value.IsEmpty()) + graph_->AddEdge(CurrentNode(), graph_->V8Node(value)); } template @@ -96,8 +148,47 @@ void MemoryTracker::TrackField(const char* name, TrackField(name, value.GetJSArray()); } -void MemoryTracker::Track(const MemoryRetainer* value) { +void MemoryTracker::Track(const MemoryRetainer* value, const char* name) { + v8::HandleScope handle_scope(isolate_); + MemoryRetainerNode* n = PushNode(name, value); value->MemoryInfo(this); + CHECK_EQ(CurrentNode(), n); + CHECK_NE(n->size_, 0); + PopNode(); +} + +MemoryRetainerNode* MemoryTracker::CurrentNode() const { + if (node_stack_.empty()) return nullptr; + return node_stack_.top(); +} + +MemoryRetainerNode* MemoryTracker::AddNode( + const char* name, const MemoryRetainer* retainer) { + MemoryRetainerNode* n = new MemoryRetainerNode(this, retainer, name); + graph_->AddNode(std::unique_ptr(n)); + if (retainer != nullptr) + seen_[retainer] = n; + + if (CurrentNode() != nullptr) + graph_->AddEdge(CurrentNode(), n); + + if (n->JSWrapperNode() != nullptr) { + graph_->AddEdge(n, n->JSWrapperNode()); + graph_->AddEdge(n->JSWrapperNode(), n); + } + + return n; +} + +MemoryRetainerNode* MemoryTracker::PushNode( + const char* name, const MemoryRetainer* retainer) { + MemoryRetainerNode* n = AddNode(name, retainer); + node_stack_.push(n); + return n; +} + +void MemoryTracker::PopNode() { + node_stack_.pop(); } } // namespace node diff --git a/src/memory_tracker.h b/src/memory_tracker.h index 18822651f67873..d0f9e0dcad8f1e 100644 --- a/src/memory_tracker.h +++ b/src/memory_tracker.h @@ -3,15 +3,19 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#include +#include #include +#include +#include #include #include -#include +#include "aliased_buffer.h" +#include "v8-profiler.h" namespace node { class MemoryTracker; +class MemoryRetainerNode; namespace crypto { class NodeBIO; @@ -29,6 +33,8 @@ class MemoryRetainer { } virtual bool IsRootNode() const { return false; } + + virtual std::string MemoryInfoName() const { return std::string(); } }; class MemoryTracker { @@ -67,17 +73,32 @@ class MemoryTracker { inline void TrackField(const char* name, const AliasedBuffer& value); - inline void Track(const MemoryRetainer* value); - inline size_t accumulated_size() const { return accumulated_size_; } + inline void Track(const MemoryRetainer* value, const char* name = nullptr); inline void set_track_only_self(bool value) { track_only_self_ = value; } + inline v8::EmbedderGraph* graph() { return graph_; } + inline v8::Isolate* isolate() { return isolate_; } - inline MemoryTracker() {} + inline explicit MemoryTracker(v8::Isolate* isolate, + v8::EmbedderGraph* graph) + : isolate_(isolate), graph_(graph) {} private: + typedef std::unordered_map + NodeMap; + + inline MemoryRetainerNode* CurrentNode() const; + inline MemoryRetainerNode* AddNode(const char* name, + const MemoryRetainer* retainer = nullptr); + inline MemoryRetainerNode* PushNode(const char* name, + const MemoryRetainer* retainer = nullptr); + inline void PopNode(); + bool track_only_self_ = false; - size_t accumulated_size_ = 0; - std::unordered_set seen_; + v8::Isolate* isolate_; + v8::EmbedderGraph* graph_; + std::stack node_stack_; + NodeMap seen_; }; } // namespace node diff --git a/src/node_internals.h b/src/node_internals.h index 860566e314cb98..f29812e26e2298 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -111,6 +111,7 @@ struct sockaddr; V(domain) \ V(fs) \ V(fs_event_wrap) \ + V(heap_utils) \ V(http2) \ V(http_parser) \ V(inspector) \ diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc index 0d0791b710c860..3efa6adb4edb0e 100644 --- a/src/tls_wrap.cc +++ b/src/tls_wrap.cc @@ -758,6 +758,8 @@ void TLSWrap::DestroySSL(const FunctionCallbackInfo& args) { // Destroy the SSL structure and friends wrap->SSLWrap::DestroySSL(); + wrap->enc_in_ = nullptr; + wrap->enc_out_ = nullptr; if (wrap->stream_ != nullptr) wrap->stream_->RemoveStreamListener(wrap); @@ -868,8 +870,10 @@ void TLSWrap::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackThis(this); tracker->TrackField("error", error_); tracker->TrackField("pending_cleartext_input", pending_cleartext_input_); - tracker->TrackField("enc_in", crypto::NodeBIO::FromBIO(enc_in_)); - tracker->TrackField("enc_out", crypto::NodeBIO::FromBIO(enc_out_)); + if (enc_in_ != nullptr) + tracker->TrackField("enc_in", crypto::NodeBIO::FromBIO(enc_in_)); + if (enc_out_ != nullptr) + tracker->TrackField("enc_out", crypto::NodeBIO::FromBIO(enc_out_)); } diff --git a/src/tls_wrap.h b/src/tls_wrap.h index b45e379ca3f61c..5f4fd3f7073305 100644 --- a/src/tls_wrap.h +++ b/src/tls_wrap.h @@ -143,8 +143,8 @@ class TLSWrap : public AsyncWrap, static int SelectSNIContextCallback(SSL* s, int* ad, void* arg); crypto::SecureContext* sc_; - BIO* enc_in_; - BIO* enc_out_; + BIO* enc_in_ = nullptr; + BIO* enc_out_ = nullptr; std::vector pending_cleartext_input_; size_t write_size_; WriteWrap* current_write_ = nullptr; diff --git a/test/common/README.md b/test/common/README.md index 111ce45b00360c..c0051ad9f7ca6e 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -10,6 +10,7 @@ This directory contains modules used to test the Node.js implementation. * [DNS module](#dns-module) * [Duplex pair helper](#duplex-pair-helper) * [Fixtures module](#fixtures-module) +* [Heap dump checker module](#heap-dump-checker-module) * [HTTP2 module](#http2-module) * [Internet module](#internet-module) * [tmpdir module](#tmpdir-module) @@ -538,6 +539,42 @@ Returns the result of Returns the result of `fs.readFileSync(path.join(fixtures.fixturesDir, 'keys', arg), 'enc')`. +## Heap dump checker module + +This provides utilities for checking the validity of heap dumps. +This requires the usage of `--expose-internals`. + +### heap.recordState() + +Create a heap dump and an embedder graph copy for inspection. +The returned object has a `validateSnapshotNodes` function similar to the +one listed below. (`heap.validateSnapshotNodes(...)` is a shortcut for +`heap.recordState().validateSnapshotNodes(...)`.) + +### heap.validateSnapshotNodes(name, expected, options) + +* `name` [<string>] Look for this string as the name of heap dump nodes. +* `expected` [<Array>] A list of objects, possibly with an `children` + property that points to expected other adjacent nodes. +* `options` [<Array>] + * `loose` [<boolean>] Do not expect an exact listing of occurrences + of nodes with name `name` in `expected`. + +Create a heap dump and an embedder graph copy and validate occurrences. + + +```js +validateSnapshotNodes('TLSWRAP', [ + { + children: [ + { name: 'enc_out' }, + { name: 'enc_in' }, + { name: 'TLSWrap' } + ] + } +]); +``` + ## HTTP/2 Module The http2.js module provides a handful of utilities for creating mock HTTP/2 diff --git a/test/common/heap.js b/test/common/heap.js new file mode 100644 index 00000000000000..a02de9a60651f4 --- /dev/null +++ b/test/common/heap.js @@ -0,0 +1,80 @@ +/* eslint-disable node-core/required-modules */ +'use strict'; +const assert = require('assert'); +const util = require('util'); + +let internalTestHeap; +try { + internalTestHeap = require('internal/test/heap'); +} catch (e) { + console.log('using `test/common/heap.js` requires `--expose-internals`'); + throw e; +} +const { createJSHeapDump, buildEmbedderGraph } = internalTestHeap; + +class State { + constructor() { + this.snapshot = createJSHeapDump(); + this.embedderGraph = buildEmbedderGraph(); + } + + validateSnapshotNodes(name, expected, { loose = false } = {}) { + const snapshot = this.snapshot.filter( + (node) => node.name === 'Node / ' + name && node.type !== 'string'); + if (loose) + assert(snapshot.length >= expected.length); + else + assert.strictEqual(snapshot.length, expected.length); + for (const expectedNode of expected) { + if (expectedNode.children) { + for (const expectedChild of expectedNode.children) { + const check = typeof expectedChild === 'function' ? + expectedChild : + (node) => [expectedChild.name, 'Node / ' + expectedChild.name] + .includes(node.name); + + assert(snapshot.some((node) => { + return node.outgoingEdges.map((edge) => edge.toNode).some(check); + }), `expected to find child ${util.inspect(expectedChild)} ` + + `in ${util.inspect(snapshot)}`); + } + } + } + + const graph = this.embedderGraph.filter((node) => node.name === name); + if (loose) + assert(graph.length >= expected.length); + else + assert.strictEqual(graph.length, expected.length); + for (const expectedNode of expected) { + if (expectedNode.edges) { + for (const expectedChild of expectedNode.children) { + const check = typeof expectedChild === 'function' ? + expectedChild : (node) => { + return node.name === expectedChild.name || + (node.value && + node.value.constructor && + node.value.constructor.name === expectedChild.name); + }; + + assert(graph.some((node) => node.edges.some(check)), + `expected to find child ${util.inspect(expectedChild)} ` + + `in ${util.inspect(snapshot)}`); + } + } + } + } +} + +function recordState() { + return new State(); +} + +function validateSnapshotNodes(...args) { + return recordState().validateSnapshotNodes(...args); +} + +module.exports = { + recordState, + validateSnapshotNodes +}; diff --git a/test/parallel/test-heapdump-dns.js b/test/parallel/test-heapdump-dns.js new file mode 100644 index 00000000000000..011503f5874d5a --- /dev/null +++ b/test/parallel/test-heapdump-dns.js @@ -0,0 +1,17 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const { validateSnapshotNodes } = require('../common/heap'); + +validateSnapshotNodes('DNSCHANNEL', []); +const dns = require('dns'); +validateSnapshotNodes('DNSCHANNEL', [{}]); +dns.resolve('localhost', () => {}); +validateSnapshotNodes('DNSCHANNEL', [ + { + children: [ + { name: 'task list' }, + { name: 'ChannelWrap' } + ] + } +]); diff --git a/test/parallel/test-heapdump-fs-promise.js b/test/parallel/test-heapdump-fs-promise.js new file mode 100644 index 00000000000000..be44b3d8731bc1 --- /dev/null +++ b/test/parallel/test-heapdump-fs-promise.js @@ -0,0 +1,16 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const { validateSnapshotNodes } = require('../common/heap'); +const fs = require('fs').promises; + +validateSnapshotNodes('FSREQPROMISE', []); +fs.stat(__filename); +validateSnapshotNodes('FSREQPROMISE', [ + { + children: [ + { name: 'FSReqPromise' }, + { name: 'Float64Array' } // Stat array + ] + } +]); diff --git a/test/parallel/test-heapdump-http2.js b/test/parallel/test-heapdump-http2.js new file mode 100644 index 00000000000000..19a70d8c44b15d --- /dev/null +++ b/test/parallel/test-heapdump-http2.js @@ -0,0 +1,76 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const { recordState } = require('../common/heap'); +const http2 = require('http2'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +{ + const state = recordState(); + state.validateSnapshotNodes('HTTP2SESSION', []); + state.validateSnapshotNodes('HTTP2STREAM', []); +} + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFile(__filename); +}); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall(() => { + const state = recordState(); + state.validateSnapshotNodes('HTTP2STREAM', [ + { + children: [ + { name: 'Http2Stream' } + ] + }, + ], { loose: true }); + state.validateSnapshotNodes('FILEHANDLE', [ + { + children: [ + { name: 'FileHandle' } + ] + } + ]); + state.validateSnapshotNodes('TCPWRAP', [ + { + children: [ + { name: 'TCP' } + ] + } + ], { loose: true }); + state.validateSnapshotNodes('TCPSERVERWRAP', [ + { + children: [ + { name: 'TCP' } + ] + } + ], { loose: true }); + state.validateSnapshotNodes('STREAMPIPE', [ + { + children: [ + { name: 'StreamPipe' } + ] + } + ]); + state.validateSnapshotNodes('HTTP2SESSION', [ + { + children: [ + { name: 'Http2Session' }, + { name: 'streams' } + ] + } + ], { loose: true }); + })); + + req.resume(); + req.on('end', common.mustCall(() => { + client.close(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-heapdump-inspector.js b/test/parallel/test-heapdump-inspector.js new file mode 100644 index 00000000000000..355b8d0d0a1d51 --- /dev/null +++ b/test/parallel/test-heapdump-inspector.js @@ -0,0 +1,21 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const { validateSnapshotNodes } = require('../common/heap'); +const inspector = require('inspector'); + +const session = new inspector.Session(); +validateSnapshotNodes('INSPECTORJSBINDING', []); +session.connect(); +validateSnapshotNodes('INSPECTORJSBINDING', [ + { + children: [ + { name: 'session' }, + { name: 'Connection' }, + (node) => node.type === 'closure' || typeof node.value === 'function' + ] + } +]); diff --git a/test/parallel/test-heapdump-tls.js b/test/parallel/test-heapdump-tls.js new file mode 100644 index 00000000000000..be14b7b5f7ca64 --- /dev/null +++ b/test/parallel/test-heapdump-tls.js @@ -0,0 +1,33 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { validateSnapshotNodes } = require('../common/heap'); +const net = require('net'); +const tls = require('tls'); + +validateSnapshotNodes('TLSWRAP', []); + +const server = net.createServer(common.mustCall((c) => { + c.end(); +})).listen(0, common.mustCall(() => { + const c = tls.connect({ port: server.address().port }); + + c.on('error', common.mustCall(() => { + server.close(); + })); + c.write('hello'); + + validateSnapshotNodes('TLSWRAP', [ + { + children: [ + { name: 'enc_out' }, + { name: 'enc_in' }, + { name: 'TLSWrap' } + ] + } + ]); +})); diff --git a/test/parallel/test-heapdump-worker.js b/test/parallel/test-heapdump-worker.js new file mode 100644 index 00000000000000..68d2ccd1abbc29 --- /dev/null +++ b/test/parallel/test-heapdump-worker.js @@ -0,0 +1,27 @@ +// Flags: --expose-internals --experimental-worker +'use strict'; +require('../common'); +const { validateSnapshotNodes } = require('../common/heap'); +const { Worker } = require('worker_threads'); + +validateSnapshotNodes('WORKER', []); +const worker = new Worker('setInterval(() => {}, 100);', { eval: true }); +validateSnapshotNodes('WORKER', [ + { + children: [ + { name: 'thread_exit_async' }, + { name: 'env' }, + { name: 'MESSAGEPORT' }, + { name: 'Worker' } + ] + } +]); +validateSnapshotNodes('MESSAGEPORT', [ + { + children: [ + { name: 'data' }, + { name: 'MessagePort' } + ] + } +], { loose: true }); +worker.terminate(); diff --git a/test/parallel/test-heapdump-zlib.js b/test/parallel/test-heapdump-zlib.js new file mode 100644 index 00000000000000..7a749902f5aaf6 --- /dev/null +++ b/test/parallel/test-heapdump-zlib.js @@ -0,0 +1,17 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const { validateSnapshotNodes } = require('../common/heap'); +const zlib = require('zlib'); + +validateSnapshotNodes('ZLIB', []); +// eslint-disable-next-line no-unused-vars +const gunzip = zlib.createGunzip(); +validateSnapshotNodes('ZLIB', [ + { + children: [ + { name: 'Zlib' }, + { name: 'zlib memory' } + ] + } +]);