diff --git a/src/workerd/jsg/BUILD.bazel b/src/workerd/jsg/BUILD.bazel index 6734e839ac9..38c2dd45be0 100644 --- a/src/workerd/jsg/BUILD.bazel +++ b/src/workerd/jsg/BUILD.bazel @@ -18,6 +18,7 @@ wd_cc_library( exclude = [ "exception.h", "observer.h", + "memory.h", "rtti.h", "url-test-corpus-failures.h", "url-test-corpus-success.h", @@ -26,6 +27,7 @@ wd_cc_library( visibility = ["//visibility:public"], deps = [ ":exception", + ":memory-tracker", ":modules_capnp", ":observer", ":url", @@ -38,6 +40,16 @@ wd_cc_library( ], ) +wd_cc_library( + name = "memory-tracker", + hdrs = ["memory.h"], + visibility = ["//visibility:public"], + deps = [ + "@capnp-cpp//src/kj", + "@workerd-v8//:v8", + ], +) + wd_cc_library( name = "url", srcs = ["url.c++"], @@ -48,6 +60,7 @@ wd_cc_library( deps = [ "@capnp-cpp//src/kj", "@ada-url", + ":memory-tracker", ], ) diff --git a/src/workerd/jsg/async-context.c++ b/src/workerd/jsg/async-context.c++ index 84873fa9515..4271b0ce43b 100644 --- a/src/workerd/jsg/async-context.c++ +++ b/src/workerd/jsg/async-context.c++ @@ -4,6 +4,7 @@ #include "async-context.h" #include "jsg.h" #include "setup.h" +#include #include namespace workerd::jsg { @@ -42,6 +43,13 @@ AsyncContextFrame::AsyncContextFrame(Lock& js, StorageEntry storageEntry) { }); } +AsyncContextFrame::StorageEntry::StorageEntry(kj::Own key, Value value) + : key(kj::mv(key)), value(kj::mv(value)) {} + +AsyncContextFrame::StorageEntry AsyncContextFrame::StorageEntry::clone(Lock& js) { + return StorageEntry(kj::addRef(*key), value.addRef(js)); +} + kj::Maybe AsyncContextFrame::current(Lock& js) { return current(js.v8Isolate); } @@ -172,10 +180,7 @@ AsyncContextFrame::StorageScope::StorageScope( Lock& js, StorageKey& key, Value store) - : frame(AsyncContextFrame::create(js, StorageEntry { - .key = kj::addRef(key), - .value = kj::mv(store) - })), + : frame(AsyncContextFrame::create(js, StorageEntry(kj::addRef(key), kj::mv(store)))), scope(js, *frame) {} v8::Local AsyncContextFrame::getJSWrapper(v8::Isolate* isolate) { diff --git a/src/workerd/jsg/async-context.h b/src/workerd/jsg/async-context.h index 5c9086f41ac..7992993855f 100644 --- a/src/workerd/jsg/async-context.h +++ b/src/workerd/jsg/async-context.h @@ -90,6 +90,8 @@ class AsyncContextFrame final: public Wrappable { return this == &other; } + JSG_MEMORY_INFO(StorageKey) {} + private: uint hash; bool dead = false; @@ -98,12 +100,12 @@ class AsyncContextFrame final: public Wrappable { struct StorageEntry { kj::Own key; Value value; + StorageEntry(kj::Own key, Value value); + StorageEntry clone(Lock& js); - inline StorageEntry clone(Lock& js) { - return { - .key = kj::addRef(*key), - .value = value.addRef(js) - }; + JSG_MEMORY_INFO(StorageEntry) { + tracker.trackField("key", key); + tracker.trackField("value", value); } }; @@ -189,6 +191,13 @@ class AsyncContextFrame final: public Wrappable { KJ_DISALLOW_COPY(StorageScope); }; + kj::StringPtr jsgGetMemoryName() const override { return "AsyncContextFrame"_kjc; } + size_t jsgGetMemorySelfSize() const override { return sizeof(AsyncContextFrame); } + void jsgGetMemoryInfo(MemoryTracker& tracker) const override { + Wrappable::jsgGetMemoryInfo(tracker); + tracker.trackField("storage", storage); + } + private: struct StorageEntryCallbacks { StorageKey& keyForRow(StorageEntry& entry) const { diff --git a/src/workerd/jsg/buffersource.h b/src/workerd/jsg/buffersource.h index cfab9ad1642..ab261105711 100644 --- a/src/workerd/jsg/buffersource.h +++ b/src/workerd/jsg/buffersource.h @@ -207,6 +207,10 @@ class BackingStore { return BackingStore(backingStore, byteLength, byteOffset, elementSize, ctor, integerType); } + JSG_MEMORY_INFO(BackingStore) { + tracker.trackFieldWithSize("buffer", size()); + } + private: std::shared_ptr backingStore; size_t byteLength; @@ -363,6 +367,13 @@ class BufferSource { // to successfully detach the backing store. void setDetachKey(Lock& js, v8::Local key); + JSG_MEMORY_INFO(BufferSource) { + tracker.trackField("handle", handle); + KJ_IF_SOME(backing, maybeBackingStore) { + tracker.trackField("backing", backing); + } + } + private: Value handle; kj::Maybe maybeBackingStore; diff --git a/src/workerd/jsg/dom-exception.c++ b/src/workerd/jsg/dom-exception.c++ index 5216e885a0a..ea703b03d2e 100644 --- a/src/workerd/jsg/dom-exception.c++ +++ b/src/workerd/jsg/dom-exception.c++ @@ -3,6 +3,7 @@ // https://opensource.org/licenses/Apache-2.0 #include "dom-exception.h" +#include #include #include diff --git a/src/workerd/jsg/dom-exception.h b/src/workerd/jsg/dom-exception.h index 26e6a41680f..7ffc780bea9 100644 --- a/src/workerd/jsg/dom-exception.h +++ b/src/workerd/jsg/dom-exception.h @@ -106,6 +106,12 @@ class DOMException: public Object { #undef JSG_DOM_EXCEPTION_CONSTANT_CXX #undef JSG_DOM_EXCEPTION_CONSTANT_JS + void visitForMemoryInfo(MemoryTracker& tracker) const { + tracker.trackField("message", message); + tracker.trackField("name", name); + tracker.trackField("errorForStack", errorForStack); + } + private: kj::String message; kj::String name; diff --git a/src/workerd/jsg/function-test.c++ b/src/workerd/jsg/function-test.c++ index 31099522253..bbee6d850f1 100644 --- a/src/workerd/jsg/function-test.c++ +++ b/src/workerd/jsg/function-test.c++ @@ -8,7 +8,7 @@ namespace workerd::jsg::test { namespace { V8System v8System; -class ContextGlobalObject: public Object, public ContextGlobal { }; +class ContextGlobalObject: public Object, public ContextGlobal {}; struct CallbackContext: public ContextGlobalObject { kj::String callCallback(Lock& js, jsg::Function function) { diff --git a/src/workerd/jsg/function.h b/src/workerd/jsg/function.h index d3f26070235..7a8a7c983e9 100644 --- a/src/workerd/jsg/function.h +++ b/src/workerd/jsg/function.h @@ -23,6 +23,20 @@ class WrappableFunction: public Wrappable { virtual Ret operator()(Lock& js, Args&&... args) = 0; const bool needsGcTracing; + + kj::StringPtr jsgGetMemoryName() const override { + return "WrappableFunction"_kjc; + } + size_t jsgGetMemorySelfSize() const override { + return sizeof(WrappableFunction); + } + void jsgGetMemoryInfo(MemoryTracker& tracker) const override { + Wrappable::jsgGetMemoryInfo(tracker); + visitForMemoryInfo(tracker); + } + virtual void visitForMemoryInfo(MemoryTracker& tracker) const { + // TODO(soon): Implement tracking for WrappableFunction. + } }; template ()> @@ -240,6 +254,17 @@ class Function { } } + JSG_MEMORY_INFO(Function) { + KJ_SWITCH_ONEOF(impl) { + KJ_CASE_ONEOF(ref, Ref) { + tracker.trackField("native", ref); + } + KJ_CASE_ONEOF(impl, JsImpl) { + tracker.trackField("impl", impl); + } + } + } + private: Function(Ref&& func) : impl(kj::mv(func)) {} @@ -247,9 +272,15 @@ class Function { Wrapper* wrapper; Value receiver; V8Ref handle; + + JSG_MEMORY_INFO(JsImpl) { + tracker.trackField("receiver", receiver); + tracker.trackField("handle", handle); + } }; kj::OneOf, JsImpl> impl; + friend class MemoryTracker; }; template diff --git a/src/workerd/jsg/iterator-test.c++ b/src/workerd/jsg/iterator-test.c++ index e61df799031..726c6506e55 100644 --- a/src/workerd/jsg/iterator-test.c++ +++ b/src/workerd/jsg/iterator-test.c++ @@ -10,6 +10,7 @@ namespace { V8System v8System; struct GeneratorContext: public Object, public ContextGlobal { + uint generatorTest(Lock& js, Generator generator) { KJ_DEFER(generator.forEach(js, [](auto& js, auto, auto&) { diff --git a/src/workerd/jsg/iterator.h b/src/workerd/jsg/iterator.h index 4f72bfbac47..cf16ef7427e 100644 --- a/src/workerd/jsg/iterator.h +++ b/src/workerd/jsg/iterator.h @@ -6,6 +6,7 @@ #include "jsg.h" #include "struct.h" +#include #include #include @@ -670,6 +671,14 @@ class IteratorBase: public Object { } } + JSG_MEMORY_INFO(IteratorBase) { + if constexpr (MemoryRetainer) { + tracker.trackField("state", state); + } else { + tracker.trackFieldWithSize("state", sizeof(State)); + } + } + private: State state; @@ -704,6 +713,10 @@ class AsyncIteratorImpl { JSG_STRUCT(done, value); }; + JSG_MEMORY_INFO(AsyncIteratorImpl) { + // TODO(soon): Implement memory tracking + } + private: std::deque> pendingStack; }; @@ -780,10 +793,30 @@ class AsyncIteratorBase: public Object { } } + JSG_MEMORY_INFO(AsyncIteratorBase) { + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(fin, Finished) { + tracker.trackFieldWithSize("state", sizeof(Finished)); + } + KJ_CASE_ONEOF(state, InnerState) { + tracker.trackField("state", state); + } + } + } + private: struct InnerState { State state; AsyncIteratorImpl impl; + + JSG_MEMORY_INFO(InnerState) { + if constexpr (MemoryRetainer) { + tracker.trackField("state", state); + } else { + tracker.trackField("state", sizeof(State)); + } + tracker.trackField("impl", impl); + } }; kj::OneOf state; diff --git a/src/workerd/jsg/jsg-test.c++ b/src/workerd/jsg/jsg-test.c++ index 346642441d6..0bcf509d0cf 100644 --- a/src/workerd/jsg/jsg-test.c++ +++ b/src/workerd/jsg/jsg-test.c++ @@ -35,7 +35,7 @@ static_assert(kj::_::isDisallowedInCoroutine()); // ======================================================================================== V8System v8System; -class ContextGlobalObject: public Object, public ContextGlobal { }; +class ContextGlobalObject: public Object, public ContextGlobal {}; struct TestContext: public ContextGlobalObject { JSG_RESOURCE_TYPE(TestContext) {} diff --git a/src/workerd/jsg/jsg.h b/src/workerd/jsg/jsg.h index c5cbcb43843..2b0287f981a 100644 --- a/src/workerd/jsg/jsg.h +++ b/src/workerd/jsg/jsg.h @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include "macro-meta.h" #include "wrappable.h" #include "util.h" @@ -45,6 +47,13 @@ namespace workerd::jsg { ::workerd::jsg::JsgKind::RESOURCE; \ using jsgSuper = jsgThis; \ using jsgThis = Type; \ + inline kj::StringPtr jsgGetMemoryName() const override { return #Type##_kjc; } \ + inline size_t jsgGetMemorySelfSize() const override { return sizeof(Type); } \ + inline void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override { \ + const Type* self = static_cast(this); \ + jsgSuper::jsgGetMemoryInfo(tracker); \ + ::workerd::jsg::visitSubclassForMemoryInfo(self, tracker); \ + } \ template \ friend constexpr bool ::workerd::jsg::resourceNeedsGcTracing(); \ template \ @@ -745,6 +754,8 @@ class Data { // Implement move constructor when the source of the move has previously been visited for // garbage collection. void moveFromTraced(Data& other, v8::TracedReference& otherTracedRef) noexcept; + + friend class MemoryTracker; }; // A drop-in replacement for v8::Global. Its big feature is that, like jsg::Data, a @@ -789,6 +800,7 @@ class V8Ref: private Data { private: friend class GcVisitor; + friend class MemoryTracker; }; using Value = V8Ref; @@ -820,6 +832,16 @@ class HashableV8Ref: public V8Ref { : V8Ref(isolate, handle), identityHash(identityHash) {} }; +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const V8Ref& value, + kj::Maybe nodeName) { + // Even though we're passing in a template T, casting to a v8::Value is sufficient here. + return trackField(edgeName, value.handle.Get(isolate_) + .template As(), nodeName); +} + // A value of type T, or `undefined`. // // In C++, this has the same usage as kj::Maybe. However, a null kj::Maybe corresponds to @@ -921,9 +943,18 @@ struct Dict { struct Field { Key name; Value value; + + JSG_MEMORY_INFO(Field) { + tracker.trackField(name); + tracker.trackField(value); + } }; kj::Array fields; + + JSG_MEMORY_INFO(Dict) { + tracker.trackField(fields); + } }; template class TypeHandler; @@ -1015,17 +1046,34 @@ class Object: private Wrappable { inline void jsgVisitForGc(GcVisitor& visitor) override {} + // Subclasses should override these to provide appropriate information for + // the heap snapshot process. + inline kj::StringPtr jsgGetMemoryName() const override { return "Object"; } + inline size_t jsgGetMemorySelfSize() const override { return sizeof(Object); } + inline void jsgGetMemoryInfo(MemoryTracker& tracker) const override { + Wrappable::jsgGetMemoryInfo(tracker); + } + inline v8::Local jsgGetMemoryInfoWrapperObject(v8::Isolate* isolate) override { + return Wrappable::jsgGetMemoryInfoWrapperObject(isolate); + } + inline bool jsgGetMemoryInfoIsRootNode() const override { + return Wrappable::jsgGetMemoryInfoIsRootNode(); + } + static constexpr bool jsgHasReflection = false; template inline void jsgInitReflection(TypeWrapper& wrapper) {} private: + inline void visitForMemoryInfo(MemoryTracker& tracker) const {} inline void visitForGc(GcVisitor& visitor) {} template friend constexpr bool ::workerd::jsg::resourceNeedsGcTracing(); template friend void visitSubclassForGc(T* obj, GcVisitor& visitor); template + friend void visitSubclassForMemoryInfo(const T* obj, MemoryTracker& visitor); + template friend class Ref; friend class kj::Refcounted; template @@ -1041,6 +1089,7 @@ class Object: private Wrappable { friend class ObjectWrapper; template friend class SelfPropertyReader; + friend class MemoryTracker; }; // Ref is a reference to a resource type (a type with a JSG_RESOURCE_TYPE block) living on @@ -1171,6 +1220,14 @@ class Ref { friend class GcVisitor; }; +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const Ref& value, + kj::Maybe nodeName) { + return trackField(edgeName, value.get(), nodeName); +} + template Ref alloc(Params&&... params) { return Ref(kj::refcounted(kj::fwd(params)...)); @@ -1203,11 +1260,27 @@ class MemoizedIdentity { void visitForGc(GcVisitor& visitor); + JSG_MEMORY_INFO(MemoizedIdentity) { + KJ_SWITCH_ONEOF(value) { + KJ_CASE_ONEOF(val, T) { + if constexpr (MemoryRetainer) { + tracker.trackField("value", val); + } else { + tracker.trackFieldWithSize("value", sizeof(T)); + } + } + KJ_CASE_ONEOF(val, Value) { + tracker.trackField("value", val); + } + } + } + private: kj::OneOf value; template friend class MemoizedIdentityWrapper; + friend class MemoryTracker; }; // Accept this type from JavaScript when you want to receive an object's identity in addition to @@ -1223,6 +1296,15 @@ struct Identified { // The object's unwrapped value. T unwrapped; + + JSG_MEMORY_INFO(Identified) { + tracker.trackField("identity", identity); + if constexpr (MemoryRetainer) { + tracker.trackField("unwrapped", unwrapped); + } else { + tracker.trackFieldWithSize("unwrapped", sizeof(T)); + } + } }; // jsg::Name represents a value that is either a string or a v8::Symbol. It is most useful for @@ -1244,6 +1326,17 @@ class Name final { kj::String toString(jsg::Lock& js); + JSG_MEMORY_INFO(Name) { + KJ_SWITCH_ONEOF(inner) { + KJ_CASE_ONEOF(str, kj::String) { + tracker.trackField("inner", str); + } + KJ_CASE_ONEOF(sym, V8Ref) { + tracker.trackField("inner", sym); + } + } + } + private: int hash; kj::OneOf> inner; @@ -1254,6 +1347,8 @@ class Name final { friend class NameWrapper; void visitForGc(GcVisitor& visitor); + + friend class MemoryTracker; }; // jsg::Function behaves much like kj::Function, but can be passed to/from JS. It works in diff --git a/src/workerd/jsg/jsvalue-test.c++ b/src/workerd/jsg/jsvalue-test.c++ index 1fbadd9eede..5d7900ffa9c 100644 --- a/src/workerd/jsg/jsvalue-test.c++ +++ b/src/workerd/jsg/jsvalue-test.c++ @@ -8,7 +8,7 @@ namespace workerd::jsg::test { namespace { V8System v8System; -class ContextGlobalObject: public Object, public ContextGlobal { }; +class ContextGlobalObject: public Object, public ContextGlobal {}; struct JsValueContext: public ContextGlobalObject { JsRef persisted; diff --git a/src/workerd/jsg/jsvalue.h b/src/workerd/jsg/jsvalue.h index a2bcaaecf0b..ae5a264a01b 100644 --- a/src/workerd/jsg/jsvalue.h +++ b/src/workerd/jsg/jsvalue.h @@ -455,12 +455,18 @@ class JsRef final { operator V8Ref() && { return kj::mv(value).template cast( Lock::from(v8::Isolate::GetCurrent())); } + JSG_MEMORY_INFO(JsRef) { + tracker.trackField("value", value); + } + private: Value value; friend class JsValue; #define V(Name) friend class Js##Name; JS_TYPE_CLASSES(V) #undef V + + friend class MemoryTracker; }; template diff --git a/src/workerd/jsg/memory-test.c++ b/src/workerd/jsg/memory-test.c++ new file mode 100644 index 00000000000..ba94e234070 --- /dev/null +++ b/src/workerd/jsg/memory-test.c++ @@ -0,0 +1,110 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "jsg-test.h" +#include +#include +#include + +namespace workerd::jsg::test { +namespace { + +V8System v8System; +class ContextGlobalObject: public Object, public ContextGlobal {}; + +struct Foo: public Object { + kj::String bar = kj::str("test"); + + JSG_RESOURCE_TYPE(Foo) {} + void visitForMemoryInfo(MemoryTracker& tracker) const { + tracker.trackField("bar", bar); + } +}; + +struct MemoryTrackerContext: public ContextGlobalObject { + JSG_RESOURCE_TYPE(MemoryTrackerContext) {} +}; +JSG_DECLARE_ISOLATE_TYPE(MemoryTrackerIsolate, MemoryTrackerContext, Foo); + +void runTest(auto callback) { + MemoryTrackerIsolate isolate(v8System, kj::heap()); + isolate.runInLockScope([&](MemoryTrackerIsolate::Lock& lock) { + JSG_WITHIN_CONTEXT_SCOPE(lock, + lock.newContext().getHandle(lock), + [&](jsg::Lock& js) { + callback(js, lock.getTypeHandler>()); + }); + }); +} + +KJ_TEST("MemoryTracker test") { + + // Verifies that workerd details are included in the heapsnapshot. + // This is not a comprehensive test of the heapsnapshot content, + // it is designed just to make sure that we are, in fact, publishing + // internal details to the snapshot. + + runTest([&](jsg::Lock& js, const TypeHandler>& fooHandler) { + kj::Vector serialized; + HeapSnapshotActivity activity([](auto, auto) { + return true; + }); + HeapSnapshotWriter writer([&](kj::Maybe> maybeChunk) { + KJ_IF_SOME(chunk, maybeChunk) { + serialized.addAll(chunk); + } + return true; + }); + + IsolateBase& base = IsolateBase::from(js.v8Isolate); + base.getUuid(); + + auto foo = fooHandler.wrap(js, alloc()); + KJ_ASSERT(foo->IsObject()); + + auto profiler = js.v8Isolate->GetHeapProfiler(); + + HeapSnapshotDeleter deleter; + + auto snapshot = kj::Own( + profiler->TakeHeapSnapshot(&activity, nullptr, true, true), + deleter); + snapshot->Serialize(&writer, v8::HeapSnapshot::kJSON); + + auto parsed = js.parseJson(serialized.asPtr()); + JsValue value = JsValue(parsed.getHandle(js)); + KJ_ASSERT(value.isObject()); + + JsObject obj = KJ_ASSERT_NONNULL(value.tryCast()); + + auto strings = obj.get(js, "strings"); + KJ_ASSERT(strings.isArray()); + + JsArray array = KJ_ASSERT_NONNULL(strings.tryCast()); + + size_t count = 0; + + kj::HashSet checks; + checks.insert(kj::str("workerd / IsolateBase")); + checks.insert(kj::str("workerd / kj::String")); + checks.insert(kj::str("workerd / HeapTracer")); + checks.insert(kj::str("workerd / CppgcShim")); + checks.insert(kj::str("workerd / MemoryTrackerContext")); + checks.insert(kj::str("workerd / Foo")); + + // Find what we're looking for... this is slow but, you know + for (size_t n = 0; n < array.size(); n++) { + JsValue check = array.get(js, n); + auto str = check.toString(js); + if (str.startsWith("workerd /")) { + count++; + KJ_ASSERT(checks.find(str) != kj::none); + } + } + KJ_ASSERT(count == checks.size()); + }); +} + +} // namespace +} // namespace workerd::jsg::test diff --git a/src/workerd/jsg/memory.h b/src/workerd/jsg/memory.h new file mode 100644 index 00000000000..174f723a446 --- /dev/null +++ b/src/workerd/jsg/memory.h @@ -0,0 +1,810 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// Implements mechanism for incorporating details about the native (c++) objects +// in a v8 heap snapshot. The design of the API and implementation were heavily +// influenced by Node.js' implementation of the same feature. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace workerd::jsg { + +class MemoryTracker; +class MemoryRetainerNode; + +template class V8Ref; +template class Ref; + +enum class MemoryInfoDetachedState { + UNKNOWN, + ATTACHED, + DETACHED, +}; + +template +concept MemoryRetainer = requires(const T* a) { + std::is_member_function_pointer_v; + std::is_member_function_pointer_v; + std::is_member_function_pointer_v; +}; + +template +concept MemoryRetainerObject = requires(T a) { + MemoryRetainer; + std::is_member_function_pointer_v; +}; + +template +concept MemoryRetainerDetachedState = requires(T a) { + MemoryRetainer; + std::is_member_function_pointer_v; +}; + +template +concept MemoryRetainerIsRootNode = requires(T a) { + MemoryRetainer; + std::is_member_function_pointer_v; +}; + +template +concept V8Value = requires(T a) { + std::is_assignable_v; +}; + +#define JSG_MEMORY_INFO(Name) \ + kj::StringPtr jsgGetMemoryName() const { \ + return #Name ## _kjc; \ + } \ + size_t jsgGetMemorySelfSize() const { return sizeof(Name); } \ + void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const + +#define JSG_MEMORY_INFO_GET_DETACHED_STATE() \ + MemoryInfoDetachedState jsgGetMemoryInfoDetachedState() const + +#define JSG_MEMORY_INFO_GET_DETACHEDNESS() \ + MemoryInfoDetachedState jsgGetMemoryInfoDetachedState() + +#define JSG_MEMORY_INFO_IS_ROOTNODE() \ + bool jsgGetMemoryInfoIsRootNode() const + +class MemoryTracker final { +public: + inline MemoryTracker& trackFieldWithSize( + kj::StringPtr edgeName, size_t size, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + inline MemoryTracker& trackInlineFieldWithSize( + kj::StringPtr edgeName, size_t size, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const kj::Own& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const std::unique_ptr& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const std::shared_ptr& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const V8Ref& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const Ref& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const T& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const kj::Maybe& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const kj::Maybe& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const kj::String& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template ::is_specialized, bool>::type, + typename dummy = bool> + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const kj::Array& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + MemoryTracker& trackField( + kj::StringPtr edgeName, + const kj::Table& value, + kj::Maybe nodeName = kj::none, + kj::Maybe elementName = kj::none, + bool subtractFromSelf = true) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const T& value, + kj::Maybe nodeName = kj::none, + kj::Maybe elementName = kj::none, + bool subtractFromSelf = true) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const kj::ArrayPtr& value, + kj::Maybe nodeName = kj::none, + kj::Maybe elementName = kj::none, + bool subtractFromSelf = true) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const kj::ArrayPtr& value, + kj::Maybe nodeName = kj::none, + kj::Maybe elementName = kj::none, + bool subtractFromSelf = true) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const T* value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const std::basic_string& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template ::is_specialized, bool>::type, + typename dummy = bool> + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const T& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const v8::Eternal& value, + kj::StringPtr nodeName) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const v8::PersistentBase& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const v8::Local& value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + inline MemoryTracker& trackField( + kj::StringPtr edgeName, + const v8::BackingStore* value, + kj::Maybe nodeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& track( + const T* retainer, + kj::Maybe edgeName = kj::none) KJ_LIFETIMEBOUND; + + template + inline MemoryTracker& trackInlineField( + const T* retainer, + kj::Maybe edgeName = kj::none) KJ_LIFETIMEBOUND; + + inline v8::EmbedderGraph* graph() { return graph_; } + inline v8::Isolate* isolate() { return isolate_; } + + KJ_DISALLOW_COPY_AND_MOVE(MemoryTracker); + +private: + + struct NodeMapEntry { + const void* retainer; + MemoryRetainerNode* node; + }; + + struct NodeMapCallbacks { + const void* keyForRow(const NodeMapEntry& row) const { + return row.retainer; + } + bool matches(const NodeMapEntry& row, const void* key) const { + return row.retainer == key; + } + kj::uint hashCode(const void* row) const { + return kj::hashCode(row); + } + }; + + using NodeMap = kj::Table, + kj::InsertionOrderIndex>; + + v8::Isolate* isolate_; + v8::EmbedderGraph* graph_; + std::stack nodeStack_; + NodeMap seen_; + KJ_DISALLOW_AS_COROUTINE_PARAM; + + inline explicit MemoryTracker(v8::Isolate* isolate, + v8::EmbedderGraph* graph) + : isolate_(isolate), + graph_(graph), + seen_(NodeMapCallbacks(), {}) {} + + inline kj::Maybe getCurrentNode() const; + + template + inline MemoryRetainerNode* addNode(const T* retainer, + kj::Maybe edgeName = kj::none); + + template + inline MemoryRetainerNode* pushNode(const T* retainer, + kj::Maybe edgeName = kj::none); + + inline MemoryRetainerNode* addNode(kj::StringPtr node_name, + size_t size, + kj::Maybe edgeName = kj::none); + inline MemoryRetainerNode* pushNode(kj::StringPtr node_name, + size_t size, + kj::Maybe edgeName = kj::none); + inline void popNode(); + + inline static kj::StringPtr getNodeName(kj::Maybe nodeName, + kj::StringPtr edgeName) { + KJ_IF_SOME(name, nodeName) { return name; } + return edgeName; + } + + friend class IsolateBase; +}; + +class MemoryRetainerNode final : public v8::EmbedderGraph::Node { +public: + static constexpr auto PREFIX = "workerd /"; + + const char* Name() override { return name_.cStr(); } + + const char* NamePrefix() override { return PREFIX; } + + size_t SizeInBytes() override { return size_; } + + bool IsRootNode() override { + KJ_IF_SOME(check, checkIsRootNode) { + return check(); + } + return isRootNode_; + } + + v8::EmbedderGraph::Node::Detachedness GetDetachedness() override { + return detachedness_; + } + + inline Node* JSWrapperNode() { return wrapper_node_; } + + KJ_DISALLOW_COPY_AND_MOVE(MemoryRetainerNode); + ~MemoryRetainerNode() noexcept(true) {} + +private: + static inline v8::EmbedderGraph::Node::Detachedness fromDetachedState( + MemoryInfoDetachedState state) { + switch (state) { + case MemoryInfoDetachedState::UNKNOWN: + return v8::EmbedderGraph::Node::Detachedness::kUnknown; + case MemoryInfoDetachedState::ATTACHED: + return v8::EmbedderGraph::Node::Detachedness::kAttached; + case MemoryInfoDetachedState::DETACHED: + return v8::EmbedderGraph::Node::Detachedness::kDetached; + } + KJ_UNREACHABLE; + } + + template + inline MemoryRetainerNode(MemoryTracker* tracker, + const T* retainer) + : retainer_(retainer) { + KJ_ASSERT(retainer_ != nullptr); + v8::HandleScope handle_scope(tracker->isolate()); + if constexpr (MemoryRetainerObject) { + v8::Local obj = + const_cast(retainer)->jsgGetMemoryInfoWrapperObject(tracker->isolate()); + if (!obj.IsEmpty()) wrapper_node_ = tracker->graph()->V8Node(obj); + } + if constexpr (MemoryRetainerIsRootNode) { + checkIsRootNode = [retainer]() { return retainer->jsgGetMemoryInfoIsRootNode(); }; + } + + name_ = retainer->jsgGetMemoryName(); + size_ = retainer->jsgGetMemorySelfSize(); + if constexpr (MemoryRetainerDetachedState) { + detachedness_ = fromDetachedState(retainer->jsgGetMemoryInfoDetachedState()); + } + } + + inline MemoryRetainerNode(MemoryTracker* tracker, + kj::StringPtr name, + size_t size, + bool isRootNode = false) + : isRootNode_(isRootNode), + name_(name), + size_(size) {} + + const void* retainer_ = nullptr; + v8::EmbedderGraph::Node* wrapper_node_ = nullptr; + + kj::Maybe> checkIsRootNode = kj::none; + + bool isRootNode_ = false; + kj::StringPtr name_; + size_t size_ = 0; + v8::EmbedderGraph::Node::Detachedness detachedness_ = + v8::EmbedderGraph::Node::Detachedness::kUnknown; + + friend class MemoryTracker; +}; + +// ====================================================================================== + +kj::Maybe MemoryTracker::getCurrentNode() const { + if (nodeStack_.empty()) return kj::none; + return *nodeStack_.top(); +} + +MemoryTracker& MemoryTracker::trackFieldWithSize( + kj::StringPtr edgeName, size_t size, + kj::Maybe nodeName) { + if (size > 0) addNode(getNodeName(nodeName, edgeName), size, edgeName); + return *this; +} + +MemoryTracker& MemoryTracker::trackInlineFieldWithSize( + kj::StringPtr edgeName, size_t size, + kj::Maybe nodeName) { + if (size > 0) addNode(getNodeName(nodeName, edgeName), size, edgeName); + KJ_ASSERT_NONNULL(getCurrentNode()).size_ = size; + return *this; +} + +// ====================================================================================== + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const kj::Own& value, + kj::Maybe nodeName) { + if (value.get() == nullptr) return *this; + return trackField(edgeName, value.get(), nodeName); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const std::unique_ptr& value, + kj::Maybe nodeName) { + if (value.get() == nullptr) return *this; + return trackField(edgeName, value.get(), nodeName); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const std::shared_ptr& value, + kj::Maybe nodeName) { + if (value.get() == nullptr) return *this; + return trackField(edgeName, value.get(), nodeName); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const kj::Maybe& value, + kj::Maybe nodeName) { + KJ_IF_SOME(v, value) { + trackField(edgeName, v, nodeName); + } + return *this; +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const kj::Maybe& value, + kj::Maybe nodeName) { + KJ_IF_SOME(v, value) { + trackField(edgeName, v, nodeName); + } + return *this; +} + +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const kj::String& value, + kj::Maybe nodeName) { + return trackFieldWithSize(edgeName, value.size(), "kj::String"_kjc); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const std::basic_string& value, + kj::Maybe nodeName) { + return trackFieldWithSize(edgeName, value.size() * sizeof(T), "std::basic_string"_kjc); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const kj::Array& value, + kj::Maybe nodeName) { + return trackFieldWithSize(edgeName, value.size() * sizeof(T), "kj::Array"_kjc); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const kj::Table& value, + kj::Maybe nodeName, + kj::Maybe elementName, + bool subtractFromSelf) { + if (value.begin() == value.end()) return *this; + KJ_IF_SOME(currentNode, getCurrentNode()) { + if (subtractFromSelf) { + currentNode.size_ -= sizeof(T); + } + } + pushNode(getNodeName(nodeName, edgeName), sizeof(T), edgeName); + for (auto it = value.begin(); it != value.end(); ++it) { + // Use nullptr as edge names so the elements appear as indexed properties + trackField(nullptr, *it, elementName); + } + popNode(); + return *this; +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const T& value, + kj::Maybe nodeName, + kj::Maybe elementName, + bool subtractFromSelf) { + if (value.begin() == value.end()) return *this; + KJ_IF_SOME(currentNode, getCurrentNode()) { + if (subtractFromSelf) { + currentNode.size_ -= sizeof(T); + } + } + pushNode(getNodeName(nodeName, edgeName), sizeof(T), edgeName); + for (Iterator it = value.begin(); it != value.end(); ++it) { + // Use nullptr as edge names so the elements appear as indexed properties + trackField(nullptr, *it, elementName); + } + popNode(); + return *this; +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const kj::ArrayPtr& value, + kj::Maybe nodeName, + kj::Maybe elementName, + bool subtractFromSelf) { + if (value.begin() == value.end()) return *this; + KJ_IF_SOME(currentNode, getCurrentNode()) { + if (subtractFromSelf) { + currentNode.size_ -= sizeof(T); + } + } + pushNode(getNodeName(nodeName, edgeName), sizeof(T), edgeName); + for (const auto item : value) { + // Use nullptr as edge names so the elements appear as indexed properties + trackField(nullptr, item, elementName); + } + popNode(); + return *this; +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const kj::ArrayPtr& value, + kj::Maybe nodeName, + kj::Maybe elementName, + bool subtractFromSelf) { + if (value.begin() == value.end()) return *this; + KJ_IF_SOME(currentNode, getCurrentNode()) { + if (subtractFromSelf) { + currentNode.size_ -= sizeof(T); + } + } + pushNode(getNodeName(nodeName, edgeName), sizeof(T), edgeName); + for (const auto item : value) { + // Use nullptr as edge names so the elements appear as indexed properties + trackField(nullptr, item, elementName); + } + popNode(); + return *this; +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const T& value, + kj::Maybe nodeName) { + return trackField(edgeName, &value, nodeName); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const T* value, + kj::Maybe nodeName) { + if (value == nullptr) return *this; + KJ_IF_SOME(found, seen_.find(value)) { + KJ_IF_SOME(currentNode, getCurrentNode()) { + graph_->AddEdge(¤tNode, found.node, edgeName.cStr()); + } else { + graph_->AddEdge(nullptr, found.node, edgeName.cStr()); + } + return *this; + } + + return track(value, edgeName); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const T& value, + kj::Maybe nodeName) { + KJ_ASSERT_NONNULL(getCurrentNode()).size_ += sizeof(T); + return *this; +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const v8::Eternal& value, + kj::StringPtr nodeName) { + return trackField(edgeName, value.Get(isolate_)); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const v8::PersistentBase& value, + kj::Maybe nodeName) { + if (value.IsWeak()) return *this; + return trackField(edgeName, value.Get(isolate_)); +} + +template +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const v8::Local& value, + kj::Maybe nodeName) { + if (!value.IsEmpty()) { + KJ_IF_SOME(currentNode, getCurrentNode()) { + graph_->AddEdge(¤tNode, graph_->V8Node(value), edgeName.cStr()); + } else { + graph_->AddEdge(nullptr, graph_->V8Node(value), edgeName.cStr()); + } + } + return *this; +} + +MemoryTracker& MemoryTracker::trackField( + kj::StringPtr edgeName, + const v8::BackingStore* value, + kj::Maybe nodeName) { + return trackFieldWithSize(edgeName, value->ByteLength(), "BackingStore"_kjc); +} + +// Put a memory container into the graph, create an edge from +// the current node if there is one on the stack. +template +MemoryTracker& MemoryTracker::track(const T* retainer, kj::Maybe edgeName) { + v8::HandleScope handle_scope(isolate_); + KJ_IF_SOME(found, seen_.find(retainer)) { + KJ_IF_SOME(currentNode, getCurrentNode()) { + KJ_IF_SOME(name, edgeName) { + graph_->AddEdge(¤tNode, found.node, name.cStr()); + } else { + graph_->AddEdge(¤tNode, found.node, nullptr); + } + } + return *this; + } + + MemoryRetainerNode* n = pushNode(retainer, edgeName); + retainer->jsgGetMemoryInfo(*this); + KJ_ASSERT(&KJ_ASSERT_NONNULL(getCurrentNode()) == n); + KJ_ASSERT(n->size_ != 0); + popNode(); + return *this; +} + +// Useful for parents that do not wish to perform manual +// adjustments to its `SelfSize()` when embedding retainer +// objects inline. +// Put a memory container into the graph, create an edge from +// the current node if there is one on the stack - there should +// be one, of the container object which the current field is part of. +// Reduce the size of memory from the container so as to avoid +// duplication in accounting. +template +MemoryTracker& MemoryTracker::trackInlineField( + const T* retainer, + kj::Maybe edgeName) { + track(retainer, edgeName); + KJ_ASSERT_NONNULL(getCurrentNode()).size_ -= retainer->getMemorySelfSize(); + return *this; +} + +template +MemoryRetainerNode* MemoryTracker::addNode(const T* retainer, + kj::Maybe edgeName) { + KJ_IF_SOME(found, seen_.find(retainer)) { return found.node; } + + MemoryRetainerNode* n = new MemoryRetainerNode(this, retainer); + graph_->AddNode(std::unique_ptr(n)); + seen_.upsert({retainer, n},[](auto&,auto&&) {}); + + KJ_IF_SOME(currentNode, getCurrentNode()) { + KJ_IF_SOME(name, edgeName) { + graph_->AddEdge(¤tNode, n, name.cStr()); + } else { + graph_->AddEdge(¤tNode, n, nullptr); + } + } + + if (n->JSWrapperNode() != nullptr) { + graph_->AddEdge(n, n->JSWrapperNode(), "native_to_javascript"); + graph_->AddEdge(n->JSWrapperNode(), n, "javascript_to_native"); + } + + return n; +} + +MemoryRetainerNode* MemoryTracker::addNode(kj::StringPtr nodeName, + size_t size, + kj::Maybe edgeName) { + MemoryRetainerNode* n = new MemoryRetainerNode(this, nodeName, size); + graph_->AddNode(std::unique_ptr(n)); + + KJ_IF_SOME(currentNode, getCurrentNode()) { + KJ_IF_SOME(name, edgeName) { + graph_->AddEdge(¤tNode, n, name.cStr()); + } else { + graph_->AddEdge(¤tNode, n, nullptr); + } + } + + return n; +} + +template +MemoryRetainerNode* MemoryTracker::pushNode(const T* retainer, + kj::Maybe edgeName) { + MemoryRetainerNode* n = addNode(retainer, edgeName); + nodeStack_.push(n); + return n; +} + +MemoryRetainerNode* MemoryTracker::pushNode(kj::StringPtr nodeName, + size_t size, + kj::Maybe edgeName) { + MemoryRetainerNode* n = addNode(nodeName, size, edgeName); + nodeStack_.push(n); + return n; +} + +void MemoryTracker::popNode() { + nodeStack_.pop(); +} + +template +inline void visitSubclassForMemoryInfo(const T* obj, MemoryTracker& tracker) { + if constexpr (&T::visitForMemoryInfo != &T::jsgSuper::visitForMemoryInfo) { + obj->visitForMemoryInfo(tracker); + } +} + +// ====================================================================================== + +class HeapSnapshotActivity final: public v8::ActivityControl { +public: + using Callback = kj::Function; + + inline HeapSnapshotActivity(Callback callback): callback(kj::mv(callback)) {} + ~HeapSnapshotActivity() noexcept(true) = default; + + inline ControlOption ReportProgressValue(uint32_t done, uint32_t total) override { + return callback(done, total) ? + ControlOption::kContinue : + ControlOption::kAbort; + } + +private: + Callback callback; +}; + +class HeapSnapshotWriter final: public v8::OutputStream { +public: + using Callback = kj::Function>)>; + + inline HeapSnapshotWriter(Callback callback, size_t chunkSize = 65536) + : callback(kj::mv(callback)), + chunkSize(chunkSize) {} + inline ~HeapSnapshotWriter() noexcept(true) {} + + inline void EndOfStream() override { + callback(kj::none); + } + + inline int GetChunkSize() override { return chunkSize; } + + inline v8::OutputStream::WriteResult WriteAsciiChunk(char* data, int size) override { + return callback(kj::ArrayPtr(data, size)) ? + v8::OutputStream::WriteResult::kContinue : + v8::OutputStream::WriteResult::kAbort; + } + +private: + Callback callback; + size_t chunkSize; +}; + +struct HeapSnapshotDeleter: public kj::Disposer { + inline void disposeImpl(void* ptr) const override { + auto snapshot = const_cast(static_cast(ptr)); + snapshot->Delete(); + } +}; + +} // namespace workerd::jsg diff --git a/src/workerd/jsg/modules.h b/src/workerd/jsg/modules.h index f832589b47c..35794b1393c 100644 --- a/src/workerd/jsg/modules.h +++ b/src/workerd/jsg/modules.h @@ -27,6 +27,11 @@ class CommonJsModuleObject: public jsg::Object { JSG_RESOURCE_TYPE(CommonJsModuleObject) { JSG_INSTANCE_PROPERTY(exports, getExports, setExports); } + + void visitForMemoryInfo(MemoryTracker& tracker) const { + tracker.trackField("exports", exports); + } + private: jsg::Value exports; }; @@ -52,6 +57,12 @@ class CommonJsModuleContext: public jsg::Object { } jsg::Ref module; + + void visitForMemoryInfo(MemoryTracker& tracker) const { + tracker.trackField("exports", exports); + tracker.trackFieldWithSize("path", path.size()); + } + private: kj::Path path; jsg::Value exports; @@ -90,6 +101,12 @@ class NodeJsModuleObject: public jsg::Object { JSG_INSTANCE_PROPERTY(exports, getExports, setExports); JSG_READONLY_INSTANCE_PROPERTY(path, getPath); } + + void visitForMemoryInfo(MemoryTracker& tracker) const { + tracker.trackField("exports", exports); + tracker.trackField("path", path); + } + private: jsg::Value exports; kj::String path; @@ -131,6 +148,12 @@ class NodeJsModuleContext: public jsg::Object { } jsg::Ref module; + + void visitForMemoryInfo(MemoryTracker& tracker) const { + tracker.trackField("exports", exports); + tracker.trackFieldWithSize("path", path.size()); + } + private: kj::Path path; jsg::Value exports; @@ -185,6 +208,10 @@ class ModuleRegistry { using Type = ModuleType; + JSG_MEMORY_INFO(ModuleRegistry) { + // TODO(soon): Implement memory tracking for ModuleRegistry + } + enum class ResolveOption { // Default resolution. Check the worker bundle first, then builtins. DEFAULT, diff --git a/src/workerd/jsg/promise.c++ b/src/workerd/jsg/promise.c++ index 58f5d00f83e..b4461462a48 100644 --- a/src/workerd/jsg/promise.c++ +++ b/src/workerd/jsg/promise.c++ @@ -203,4 +203,9 @@ void UnhandledRejectionHandler::ensureProcessingWarnings(jsg::Lock& js) { }); } +void UnhandledRejectionHandler::UnhandledRejection::visitForMemoryInfo( + MemoryTracker& tracker) const { + tracker.trackField("asyncContextFrame", asyncContextFrame); +} + } // namespace workerd::jsg diff --git a/src/workerd/jsg/promise.h b/src/workerd/jsg/promise.h index 244db852da8..def94baa2a5 100644 --- a/src/workerd/jsg/promise.h +++ b/src/workerd/jsg/promise.h @@ -30,6 +30,12 @@ struct OpaqueWrappable: public Wrappable { T value; bool movedAway = false; + + kj::StringPtr jsgGetMemoryName() const override { return "OpaqueWrappable"_kjc; } + size_t jsgGetMemorySelfSize() const override { return sizeof(OpaqueWrappable); } + void jsgGetMemoryInfo(MemoryTracker& tracker) const override { + Wrappable::jsgGetMemoryInfo(tracker); + } }; template @@ -38,6 +44,12 @@ struct OpaqueWrappable: public OpaqueWrappable { using OpaqueWrappable::OpaqueWrappable; + kj::StringPtr jsgGetMemoryName() const override { return "OpaqueWrappable"_kjc; } + size_t jsgGetMemorySelfSize() const override { return sizeof(OpaqueWrappable); } + void jsgGetMemoryInfo(MemoryTracker& tracker) const override { + Wrappable::jsgGetMemoryInfo(tracker); + } + void jsgVisitForGc(GcVisitor& visitor) override { if (!this->movedAway) { visitor.visit(this->value); @@ -381,9 +393,14 @@ class Promise { return addRef(Lock::from(deprecatedIsolate)); } + JSG_MEMORY_INFO(Resolver) { + tracker.trackField("resolver", v8Resolver); + } + private: v8::Isolate* deprecatedIsolate; V8Ref v8Resolver; + friend class MemoryTracker; }; void visitForGc(GcVisitor& visitor) { @@ -412,6 +429,12 @@ class Promise { return catch_(Lock::from(deprecatedIsolate), kj::fwd(errorFunc)); } + JSG_MEMORY_INFO(Promise) { + KJ_IF_SOME(promise, v8Promise) { + tracker.trackField("promise", promise); + } + } + private: // We store a copy of the isolate pointer so that `.then()` can be called without passing in // the isolate pointer every time. @@ -479,6 +502,7 @@ class Promise { friend class Lock; template friend class PromiseWrapper; + friend class MemoryTracker; }; template @@ -495,6 +519,11 @@ template struct PromiseResolverPair { Promise promise; typename Promise::Resolver resolver; + + JSG_MEMORY_INFO(PromiseResolverPair) { + tracker.trackField("promise", promise); + tracker.trackField("resolver", resolver); + } }; template @@ -732,6 +761,12 @@ class UnhandledRejectionHandler { void clear(); + JSG_MEMORY_INFO(UnhandledRejectionHandler) { + // TODO (soon): Can we reasonably measure the function handler? + tracker.trackField("unhandledRejections", unhandledRejections); + tracker.trackField("warnedRejections", warnedRejections); + } + private: // Used as part of the book keeping for unhandled rejections. When an // unhandled rejection occurs, the unhandledRejections Table will be updated. @@ -772,6 +807,13 @@ class UnhandledRejectionHandler { size_t rejectionNumber; uint hashCode() const { return hash; } + + JSG_MEMORY_INFO(UnhandledRejection) { + tracker.trackField("promise", promise); + tracker.trackField("value", value); + visitForMemoryInfo(tracker); + } + void visitForMemoryInfo(MemoryTracker& tracker) const; }; // A v8::Promise with memoized hash code. @@ -781,6 +823,10 @@ class UnhandledRejectionHandler { HashedPromise(v8::Local promise) : promise(promise), hash(promise->GetIdentityHash()) {} + + JSG_MEMORY_INFO(HashedPromise) { + tracker.trackField("promise", promise); + } }; struct UnhandledRejectionCallbacks { diff --git a/src/workerd/jsg/resource-test.c++ b/src/workerd/jsg/resource-test.c++ index f84efec4b43..408d73c050d 100644 --- a/src/workerd/jsg/resource-test.c++ +++ b/src/workerd/jsg/resource-test.c++ @@ -9,7 +9,7 @@ namespace workerd::jsg::test { namespace { V8System v8System; -class ContextGlobalObject: public Object, public ContextGlobal { }; +class ContextGlobalObject: public Object, public ContextGlobal {}; struct BoxContext: public ContextGlobalObject { JSG_RESOURCE_TYPE(BoxContext) { @@ -357,6 +357,7 @@ struct StaticContext: public ContextGlobalObject { static void delete_() {} + JSG_RESOURCE_TYPE(StaticMethods) { JSG_STATIC_METHOD(passThrough); JSG_STATIC_METHOD(passThroughWithInfo); diff --git a/src/workerd/jsg/resource.h b/src/workerd/jsg/resource.h index 9adef2b55c4..6c5080ff22d 100644 --- a/src/workerd/jsg/resource.h +++ b/src/workerd/jsg/resource.h @@ -18,6 +18,7 @@ #include "wrappable.h" #include #include "meta.h" +#include #include // The signature of SetAccessor changes in v8 12.1 to drop the v8::AccessControl diff --git a/src/workerd/jsg/setup.c++ b/src/workerd/jsg/setup.c++ index a64da4a4b3b..bad3fd09cec 100644 --- a/src/workerd/jsg/setup.c++ +++ b/src/workerd/jsg/setup.c++ @@ -159,6 +159,19 @@ IsolateBase& IsolateBase::from(v8::Isolate* isolate) { return *reinterpret_cast(isolate->GetData(0)); } +void IsolateBase::buildEmbedderGraph(v8::Isolate* isolate, + v8::EmbedderGraph* graph, + void* data) { + const auto base = reinterpret_cast(data); + MemoryTracker tracker(isolate, graph); + tracker.track(base); +} + +void IsolateBase::jsgGetMemoryInfo(MemoryTracker& tracker) const { + tracker.trackField("uuid", uuid); + tracker.trackField("heapTracer", heapTracer); +} + void IsolateBase::deferDestruction(Item item) { queue.lockExclusive()->push(kj::mv(item)); } @@ -346,6 +359,8 @@ IsolateBase::IsolateBase(const V8System& system, v8::Isolate::CreateParams&& cre } }); + ptr->GetHeapProfiler()->AddBuildEmbedderGraphCallback(buildEmbedderGraph, this); + // Create opaqueTemplate { // We don't need a v8::Locker here since there's no way another thread could be using the diff --git a/src/workerd/jsg/setup.h b/src/workerd/jsg/setup.h index 2d2b5d63ed9..f78833e0021 100644 --- a/src/workerd/jsg/setup.h +++ b/src/workerd/jsg/setup.h @@ -9,6 +9,7 @@ #include "async-context.h" #include "type-wrapper.h" #include "v8-platform-wrapper.h" +#include "v8-profiler.h" #include #include #include @@ -124,10 +125,18 @@ class IsolateBase { IsolateObserver& getObserver() { return *observer; } + // Implementation of MemoryRetainer + void jsgGetMemoryInfo(MemoryTracker& tracker) const; + kj::StringPtr jsgGetMemoryName() const { return "IsolateBase"_kjc; } + size_t jsgGetMemorySelfSize() const { return sizeof(IsolateBase); } + bool jsgGetMemoryInfoIsRootNode() const { return true; } + private: template friend class Isolate; + static void buildEmbedderGraph(v8::Isolate* isolate, v8::EmbedderGraph* graph, void* data); + // The internals of a jsg::Ref to be deleted. class RefToDelete { public: diff --git a/src/workerd/jsg/string.c++ b/src/workerd/jsg/string.c++ index 2f8135b5c5b..5283a6c3c29 100644 --- a/src/workerd/jsg/string.c++ +++ b/src/workerd/jsg/string.c++ @@ -3,6 +3,7 @@ // https://opensource.org/licenses/Apache-2.0 #include "string.h" +#include #include #include #include diff --git a/src/workerd/jsg/string.h b/src/workerd/jsg/string.h index c43166be80b..1d456084cd0 100644 --- a/src/workerd/jsg/string.h +++ b/src/workerd/jsg/string.h @@ -369,6 +369,10 @@ class UsvString { return slice(start.position(), end.position()); } + JSG_MEMORY_INFO(UsvString) { + tracker.trackField("buffer", buffer); + } + private: kj::Array buffer; diff --git a/src/workerd/jsg/type-wrapper-test.c++ b/src/workerd/jsg/type-wrapper-test.c++ index 2136c0457aa..bc95a8b6e8e 100644 --- a/src/workerd/jsg/type-wrapper-test.c++ +++ b/src/workerd/jsg/type-wrapper-test.c++ @@ -8,7 +8,7 @@ namespace workerd::jsg::test { namespace { V8System v8System; -class ContextGlobalObject: public Object, public ContextGlobal { }; +class ContextGlobalObject: public Object, public ContextGlobal {}; struct InfoContext: public ContextGlobalObject { struct WantInfo: public Object { diff --git a/src/workerd/jsg/url.h b/src/workerd/jsg/url.h index 714d2b763d7..6cc8c3bd961 100644 --- a/src/workerd/jsg/url.h +++ b/src/workerd/jsg/url.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include namespace workerd::jsg { @@ -98,6 +99,16 @@ class Url final { // Convert a Unicode hostname to ASCII. static kj::Array idnToAscii(kj::ArrayPtr value) KJ_WARN_UNUSED_RESULT; + JSG_MEMORY_INFO(Url) { + tracker.trackFieldWithSize("inner", getProtocol().size() + + getUsername().size() + + getPassword().size() + + getHost().size() + + getPathname().size() + + getHash().size() + + getSearch().size()); + } + private: Url(kj::Own inner); kj::Own inner; @@ -165,6 +176,10 @@ class UrlSearchParams final { kj::Array toStr() const KJ_WARN_UNUSED_RESULT; + JSG_MEMORY_INFO(Url) { + tracker.trackField("inner", toStr()); + } + private: UrlSearchParams(kj::Own inner); kj::Own inner; diff --git a/src/workerd/jsg/util-test.c++ b/src/workerd/jsg/util-test.c++ index 4a8027ef4aa..aa2acacd93c 100644 --- a/src/workerd/jsg/util-test.c++ +++ b/src/workerd/jsg/util-test.c++ @@ -11,7 +11,7 @@ namespace workerd::jsg::test { namespace { V8System v8System; -class ContextGlobalObject: public Object, public ContextGlobal { }; +class ContextGlobalObject: public Object, public ContextGlobal {}; struct FreezeContext: public ContextGlobalObject { void recursivelyFreeze(v8::Local value, v8::Isolate* isolate) { diff --git a/src/workerd/jsg/value-test.c++ b/src/workerd/jsg/value-test.c++ index 4f7d980823e..ba09a7e3f7c 100644 --- a/src/workerd/jsg/value-test.c++ +++ b/src/workerd/jsg/value-test.c++ @@ -8,7 +8,7 @@ namespace workerd::jsg::test { namespace { V8System v8System; -class ContextGlobalObject: public Object, public ContextGlobal { }; +class ContextGlobalObject: public Object, public ContextGlobal {}; struct BoolContext: public ContextGlobalObject { kj::String takeBool(bool b) { diff --git a/src/workerd/jsg/wrappable.c++ b/src/workerd/jsg/wrappable.c++ index 3a81733a547..d0ea937b384 100644 --- a/src/workerd/jsg/wrappable.c++ +++ b/src/workerd/jsg/wrappable.c++ @@ -129,6 +129,15 @@ public: }; struct Dead {}; + kj::StringPtr jsgGetMemoryName() const { return "CppgcShim"_kjc; } + size_t jsgGetMemorySelfSize() const { return sizeof(CppgcShim); } + void jsgGetMemoryInfo(MemoryTracker& tracker) const { + KJ_IF_SOME(active, state.tryGet()) { + tracker.trackField("wrappable", active.wrappable); + } + } + bool jsgGetMemoryInfoIsRootNode() const { return false; } + mutable kj::OneOf state; // This is `mutable` because `Trace()` is const. We configure V8 to perform traces atomically in // the main thread so concurrency is not a concern. @@ -171,6 +180,13 @@ void HeapTracer::clearFreelistedShims() { } } +void HeapTracer::jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const { + for (const auto& wrapper : wrappers) { + tracker.trackField("wrapper", wrapper); + } + // TODO(soon): Track the other fields here? +} + kj::Own Wrappable::detachWrapper(bool shouldFreelistShim) { KJ_IF_SOME(shim, cppgcShim) { #if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__) @@ -326,6 +342,10 @@ void Wrappable::attachWrapper(v8::Isolate* isolate, } } +void Wrappable::jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const { + tracker.trackField("cppgcshim", cppgcShim); +} + v8::Local Wrappable::attachOpaqueWrapper( v8::Local context, bool needsGcTracing) { auto isolate = context->GetIsolate(); diff --git a/src/workerd/jsg/wrappable.h b/src/workerd/jsg/wrappable.h index 145fe5b9fbb..287de9e6a8c 100644 --- a/src/workerd/jsg/wrappable.h +++ b/src/workerd/jsg/wrappable.h @@ -14,6 +14,7 @@ #include #include #include +#include namespace cppgc { class Visitor; } @@ -116,6 +117,27 @@ class Wrappable: public kj::Refcounted { // namespace of JSG_RESOURCE types. virtual void jsgVisitForGc(GcVisitor& visitor); + virtual kj::StringPtr jsgGetMemoryName() const { + KJ_UNIMPLEMENTED("jsgGetTypeName is not implemented. " + "It must be overridden by subclasses"); + } + + virtual size_t jsgGetMemorySelfSize() const { + KJ_UNIMPLEMENTED("jsgGetMemorySelfSize is not implemented. " + "It must be overridden by subclasses"); + } + + virtual void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const; + + virtual bool jsgGetMemoryInfoIsRootNode() const { return strongRefcount > 0; } + + virtual v8::Local jsgGetMemoryInfoWrapperObject(v8::Isolate* isolate) { + KJ_IF_SOME(handle, tryGetHandle(isolate)) { + return handle; + } + return v8::Local(); + } + // Detaches the wrapper from V8 and returns the reference that V8 had previously held. // (Typically, the caller will ignore the return value, thus dropping the reference.) kj::Own detachWrapper(bool shouldFreelistShim); @@ -160,6 +182,7 @@ class Wrappable: public kj::Refcounted { friend class GcVisitor; friend class HeapTracer; + friend class MemoryTracker; }; // For historical reasons, this is actually implemented in setup.c++. @@ -195,6 +218,11 @@ class HeapTracer: public v8::EmbedderRootsHandler { void ResetRoot(const v8::TracedReference& handle) override; bool TryResetRoot(const v8::TracedReference& handle) override; + kj::StringPtr jsgGetMemoryName() const { return "HeapTracer"_kjc; } + size_t jsgGetMemorySelfSize() const { return sizeof(*this); } + void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const; + bool jsgGetMemoryInfoIsRootNode() const { return false; } + private: v8::Isolate* isolate; kj::Vector wrappersToTrace;