diff options
-rw-r--r-- | src/base_object.h | 3 | ||||
-rw-r--r-- | src/env-inl.h | 7 | ||||
-rw-r--r-- | src/env.cc | 119 | ||||
-rw-r--r-- | src/env.h | 143 | ||||
-rw-r--r-- | src/memory_tracker-inl.h | 7 | ||||
-rw-r--r-- | src/memory_tracker.h | 13 | ||||
-rw-r--r-- | test/pummel/test-heapdump-env.js | 66 |
7 files changed, 292 insertions, 66 deletions
diff --git a/src/base_object.h b/src/base_object.h index 091c3d5af0..f1c666224f 100644 --- a/src/base_object.h +++ b/src/base_object.h @@ -86,7 +86,6 @@ class BaseObject : public MemoryRetainer { private: v8::Local<v8::Object> 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<v8::Object> persistent_handle_; Environment* env_; diff --git a/src/env-inl.h b/src/env-inl.h index bce36c0f69..ef054be4cb 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<void*>()(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<BaseObject*>(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 cc8554032e..1542638e77 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<Value> 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<Environment*>(data)->ForEachBaseObject([&](BaseObject* obj) { - tracker.Track(obj); - }); + Environment* env = static_cast<Environment*>(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<Object> BaseObject::WrappedObject() const { return object(); } -bool BaseObject::IsRootNode() const { - return !persistent_handle_.IsWeak(); -} - } // namespace node @@ -38,6 +38,7 @@ #include "uv.h" #include "v8.h" +#include <array> #include <atomic> #include <cstdint> #include <functional> @@ -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<PerIsolateOptions> options(); @@ -563,8 +570,12 @@ namespace per_process { extern std::shared_ptr<KVStore> 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<v8::String> providers_[AsyncWrap::PROVIDERS_LENGTH]; + std::array<v8::Eternal<v8::String>, AsyncWrap::PROVIDERS_LENGTH> providers_; // Stores the ids of the current execution context stack. AliasedBuffer<double, v8::Float64Array> 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<uint32_t, v8::Uint32Array>& 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<uint32_t, v8::Uint32Array> fields_; }; -class TickInfo { +class TickInfo : public MemoryRetainer { public: inline AliasedBuffer<uint8_t, v8::Uint8Array>& 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<TypeName> PropertyName() const; \ inline void set_ ## PropertyName(v8::Local<TypeName> 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<CleanupHookCallback, CleanupHookCallback::Hash, @@ -1219,6 +1253,7 @@ class Environment { void ForEachBaseObject(T&& iterator); #define V(PropertyName, TypeName) Persistent<TypeName> 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 95d623c2b4..72117d2f6a 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 <typename T> +void MemoryTracker::TrackField(const char* edge_name, + const v8::Eternal<T>& value, + const char* node_name) { + TrackField(edge_name, value.Get(isolate_)); +} + template <typename T, typename Traits> void MemoryTracker::TrackField(const char* edge_name, const v8::Persistent<T, Traits>& value, diff --git a/src/memory_tracker.h b/src/memory_tracker.h index c5d9b2106f..07b1b47216 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 <typename T> + void TrackField(const char* edge_name, + const v8::Eternal<T>& value, + const char* node_name); template <typename T, typename Traits> inline void TrackField(const char* edge_name, const v8::Persistent<T, Traits>& value, @@ -191,6 +197,13 @@ class MemoryTracker { inline void TrackField(const char* edge_name, const MallocedBuffer<T>& 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 0000000000..8175797cce --- /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 |