diff --git a/cmake/sources/CxxSources.txt b/cmake/sources/CxxSources.txt index 04e1d2c79d..888209f8e1 100644 --- a/cmake/sources/CxxSources.txt +++ b/cmake/sources/CxxSources.txt @@ -79,6 +79,7 @@ src/bun.js/bindings/JSDOMWrapper.cpp src/bun.js/bindings/JSDOMWrapperCache.cpp src/bun.js/bindings/JSEnvironmentVariableMap.cpp src/bun.js/bindings/JSFFIFunction.cpp +src/bun.js/bindings/JSGCProfiler.cpp src/bun.js/bindings/JSMockFunction.cpp src/bun.js/bindings/JSNextTickQueue.cpp src/bun.js/bindings/JSNodePerformanceHooksHistogram.cpp diff --git a/src/bun.js/bindings/JSGCProfiler.cpp b/src/bun.js/bindings/JSGCProfiler.cpp new file mode 100644 index 0000000000..e7d5b9ae3c --- /dev/null +++ b/src/bun.js/bindings/JSGCProfiler.cpp @@ -0,0 +1,472 @@ +#include "root.h" + +#include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/VM.h" +#include "JavaScriptCore/Heap.h" +#include "JavaScriptCore/InternalFunction.h" +#include "JavaScriptCore/ObjectConstructor.h" +#include "JavaScriptCore/JSArray.h" +#include "JavaScriptCore/FunctionPrototype.h" +#include "ZigGlobalObject.h" +#include "ErrorCode.h" +#include "helpers.h" +#include "wtf/text/WTFString.h" +#include +#include +#include +#include + +namespace Bun { + +using namespace JSC; + +struct GCEvent { + String gcType; + double startTime; + double endTime; + double cost; + + // Heap statistics before GC + size_t beforeTotalHeapSize; + size_t beforeTotalHeapSizeExecutable; + size_t beforeTotalPhysicalSize; + size_t beforeTotalAvailableSize; + size_t beforeTotalGlobalHandlesSize; + size_t beforeUsedGlobalHandlesSize; + size_t beforeUsedHeapSize; + size_t beforeHeapSizeLimit; + size_t beforeMallocedMemory; + size_t beforeExternalMemory; + size_t beforePeakMallocedMemory; + + // Heap statistics after GC + size_t afterTotalHeapSize; + size_t afterTotalHeapSizeExecutable; + size_t afterTotalPhysicalSize; + size_t afterTotalAvailableSize; + size_t afterTotalGlobalHandlesSize; + size_t afterUsedGlobalHandlesSize; + size_t afterUsedHeapSize; + size_t afterHeapSizeLimit; + size_t afterMallocedMemory; + size_t afterExternalMemory; + size_t afterPeakMallocedMemory; +}; + +class JSGCProfiler final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSGCProfiler* create(JSC::VM& vm, JSC::Structure* structure) + { + JSGCProfiler* profiler = new (NotNull, JSC::allocateCell(vm)) JSGCProfiler(vm, structure); + profiler->finishCreation(vm); + return profiler; + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSGCProfiler.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSGCProfiler = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSGCProfiler.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSGCProfiler = std::forward(space); }); + } + + DECLARE_INFO; + + void start(); + JSValue stop(JSGlobalObject*); + bool isProfileActive() const { return m_isActive; } + +private: + JSGCProfiler(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + , m_isActive(false) + , m_startTime(0) + { + } + + void finishCreation(JSC::VM& vm) + { + Base::finishCreation(vm); + } + + static void destroy(JSCell* cell) + { + static_cast(cell)->~JSGCProfiler(); + } + + DECLARE_VISIT_CHILDREN; + + // Capture heap statistics + void captureHeapStats(size_t& totalHeapSize, size_t& totalHeapSizeExecutable, + size_t& totalPhysicalSize, size_t& totalAvailableSize, + size_t& totalGlobalHandlesSize, size_t& usedGlobalHandlesSize, + size_t& usedHeapSize, size_t& heapSizeLimit, + size_t& mallocedMemory, size_t& externalMemory, + size_t& peakMallocedMemory); + + bool m_isActive; + double m_startTime; + Vector m_events; +}; + +template +void JSGCProfiler::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSGCProfiler* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); +} + +DEFINE_VISIT_CHILDREN(JSGCProfiler); + +const ClassInfo JSGCProfiler::s_info = { "GCProfiler"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGCProfiler) }; + +void JSGCProfiler::captureHeapStats(size_t& totalHeapSize, size_t& totalHeapSizeExecutable, + size_t& totalPhysicalSize, size_t& totalAvailableSize, + size_t& totalGlobalHandlesSize, size_t& usedGlobalHandlesSize, + size_t& usedHeapSize, size_t& heapSizeLimit, + size_t& mallocedMemory, size_t& externalMemory, + size_t& peakMallocedMemory) +{ + // Get heap statistics from JavaScriptCore + VM& vm = this->vm(); + Heap& heap = vm.heap; + + totalHeapSize = heap.size(); + totalHeapSizeExecutable = heap.size() >> 1; // Approximation + totalPhysicalSize = heap.size(); + totalAvailableSize = heap.capacity() - heap.size(); + totalGlobalHandlesSize = 8192; // Fixed value similar to Node.js + usedGlobalHandlesSize = 2112; // Fixed value similar to Node.js + usedHeapSize = heap.size(); + heapSizeLimit = heap.capacity(); + mallocedMemory = heap.size(); + externalMemory = heap.extraMemorySize(); + peakMallocedMemory = heap.size(); // Approximation +} + +void JSGCProfiler::start() +{ + if (m_isActive) + return; + + m_isActive = true; + m_startTime = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + m_events.clear(); +} + +JSValue JSGCProfiler::stop(JSGlobalObject* globalObject) +{ + if (!m_isActive) + return jsUndefined(); + + m_isActive = false; + + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + double endTime = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + + // Create the result object matching Node.js format + JSObject* result = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + result->putDirect(vm, PropertyName(Identifier::fromString(vm, "version"_s)), jsNumber(1), 0); + result->putDirect(vm, PropertyName(Identifier::fromString(vm, "startTime"_s)), jsNumber(m_startTime), 0); + result->putDirect(vm, PropertyName(Identifier::fromString(vm, "endTime"_s)), jsNumber(endTime), 0); + + // Create statistics array + JSArray* statistics = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), m_events.size()); + if (!statistics) { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + + for (size_t i = 0; i < m_events.size(); ++i) { + const GCEvent& event = m_events[i]; + + JSObject* gcEvent = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + gcEvent->putDirect(vm, PropertyName(Identifier::fromString(vm, "gcType"_s)), jsString(vm, event.gcType), 0); + gcEvent->putDirect(vm, PropertyName(Identifier::fromString(vm, "cost"_s)), jsNumber(event.cost), 0); + + // Create beforeGC object + JSObject* beforeGC = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + JSObject* beforeHeapStats = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalHeapSize"_s)), jsNumber(event.beforeTotalHeapSize), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalHeapSizeExecutable"_s)), jsNumber(event.beforeTotalHeapSizeExecutable), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalPhysicalSize"_s)), jsNumber(event.beforeTotalPhysicalSize), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalAvailableSize"_s)), jsNumber(event.beforeTotalAvailableSize), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalGlobalHandlesSize"_s)), jsNumber(event.beforeTotalGlobalHandlesSize), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "usedGlobalHandlesSize"_s)), jsNumber(event.beforeUsedGlobalHandlesSize), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "usedHeapSize"_s)), jsNumber(event.beforeUsedHeapSize), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "heapSizeLimit"_s)), jsNumber(event.beforeHeapSizeLimit), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "mallocedMemory"_s)), jsNumber(event.beforeMallocedMemory), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "externalMemory"_s)), jsNumber(event.beforeExternalMemory), 0); + beforeHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "peakMallocedMemory"_s)), jsNumber(event.beforePeakMallocedMemory), 0); + + beforeGC->putDirect(vm, PropertyName(Identifier::fromString(vm, "heapStatistics"_s)), beforeHeapStats, 0); + + // For simplicity, create empty heapSpaceStatistics array + JSArray* beforeHeapSpaceStats = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0); + if (!beforeHeapSpaceStats) { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + beforeGC->putDirect(vm, PropertyName(Identifier::fromString(vm, "heapSpaceStatistics"_s)), beforeHeapSpaceStats, 0); + + gcEvent->putDirect(vm, PropertyName(Identifier::fromString(vm, "beforeGC"_s)), beforeGC, 0); + + // Create afterGC object (similar structure) + JSObject* afterGC = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + JSObject* afterHeapStats = constructEmptyObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalHeapSize"_s)), jsNumber(event.afterTotalHeapSize), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalHeapSizeExecutable"_s)), jsNumber(event.afterTotalHeapSizeExecutable), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalPhysicalSize"_s)), jsNumber(event.afterTotalPhysicalSize), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalAvailableSize"_s)), jsNumber(event.afterTotalAvailableSize), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "totalGlobalHandlesSize"_s)), jsNumber(event.afterTotalGlobalHandlesSize), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "usedGlobalHandlesSize"_s)), jsNumber(event.afterUsedGlobalHandlesSize), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "usedHeapSize"_s)), jsNumber(event.afterUsedHeapSize), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "heapSizeLimit"_s)), jsNumber(event.afterHeapSizeLimit), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "mallocedMemory"_s)), jsNumber(event.afterMallocedMemory), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "externalMemory"_s)), jsNumber(event.afterExternalMemory), 0); + afterHeapStats->putDirect(vm, PropertyName(Identifier::fromString(vm, "peakMallocedMemory"_s)), jsNumber(event.afterPeakMallocedMemory), 0); + + afterGC->putDirect(vm, PropertyName(Identifier::fromString(vm, "heapStatistics"_s)), afterHeapStats, 0); + + JSArray* afterHeapSpaceStats = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0); + if (!afterHeapSpaceStats) { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + afterGC->putDirect(vm, PropertyName(Identifier::fromString(vm, "heapSpaceStatistics"_s)), afterHeapSpaceStats, 0); + + gcEvent->putDirect(vm, PropertyName(Identifier::fromString(vm, "afterGC"_s)), afterGC, 0); + + statistics->putDirectIndex(globalObject, i, gcEvent); + RETURN_IF_EXCEPTION(scope, {}); + } + + result->putDirect(vm, PropertyName(Identifier::fromString(vm, "statistics"_s)), statistics, 0); + + return result; +} + +// Function declarations for methods +static JSC_DECLARE_HOST_FUNCTION(jsGCProfilerProtoFuncStart); +static JSC_DECLARE_HOST_FUNCTION(jsGCProfilerProtoFuncStop); + +// Prototype method table +static const HashTableValue JSGCProfilerPrototypeTableValues[] = { + { "start"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGCProfilerProtoFuncStart, 0 } }, + { "stop"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGCProfilerProtoFuncStop, 0 } }, +}; + +// Prototype class +class JSGCProfilerPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSGCProfilerPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSGCProfilerPrototype* prototype = new (NotNull, allocateCell(vm)) JSGCProfilerPrototype(vm, structure); + prototype->finishCreation(vm); + return prototype; + } + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + structure->setMayBePrototype(true); + return structure; + } + +private: + JSGCProfilerPrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM& vm); +}; + +const ClassInfo JSGCProfilerPrototype::s_info = { "GCProfiler"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGCProfilerPrototype) }; + +void JSGCProfilerPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSGCProfiler::info(), JSGCProfilerPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// Constructor +static JSC_DECLARE_HOST_FUNCTION(gcProfilerConstructorCall); +static JSC_DECLARE_HOST_FUNCTION(gcProfilerConstructorConstruct); + +class JSGCProfilerConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSGCProfilerConstructor* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSObject* prototype) + { + JSGCProfilerConstructor* constructor = new (NotNull, JSC::allocateCell(vm)) JSGCProfilerConstructor(vm, structure); + constructor->finishCreation(vm, prototype); + return constructor; + } + + DECLARE_INFO; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.internalFunctionSpace(); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); + } + +private: + JSGCProfilerConstructor(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure, gcProfilerConstructorCall, gcProfilerConstructorConstruct) + { + } + + void finishCreation(JSC::VM& vm, JSC::JSObject* prototype) + { + Base::finishCreation(vm, 0, "GCProfiler"_s); + putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); + } +}; + +const ClassInfo JSGCProfilerConstructor::s_info = { "GCProfiler"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGCProfilerConstructor) }; + +// Host function implementations +JSC_DEFINE_HOST_FUNCTION(gcProfilerConstructorCall, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + Bun::throwError(globalObject, scope, ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, "GCProfiler constructor cannot be invoked without 'new'"_s); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(gcProfilerConstructorConstruct, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + Structure* structure = zigGlobalObject->m_JSGCProfilerClassStructure.get(zigGlobalObject); + JSValue newTarget = callFrame->newTarget(); + + if (zigGlobalObject->m_JSGCProfilerClassStructure.constructor(zigGlobalObject) != newTarget) [[unlikely]] { + if (!newTarget) { + throwTypeError(globalObject, scope, "Class constructor GCProfiler cannot be invoked without 'new'"_s); + return {}; + } + + auto* functionGlobalObject = defaultGlobalObject(getFunctionRealm(globalObject, newTarget.getObject())); + RETURN_IF_EXCEPTION(scope, {}); + structure = InternalFunction::createSubclassStructure(globalObject, newTarget.getObject(), functionGlobalObject->m_JSGCProfilerClassStructure.get(functionGlobalObject)); + RETURN_IF_EXCEPTION(scope, {}); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(JSGCProfiler::create(vm, structure))); +} + +JSC_DEFINE_HOST_FUNCTION(jsGCProfilerProtoFuncStart, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSGCProfiler* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) { + throwTypeError(globalObject, scope, "GCProfiler.prototype.start called on incompatible receiver"_s); + return {}; + } + + thisObject->start(); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsGCProfilerProtoFuncStop, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSGCProfiler* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) { + throwTypeError(globalObject, scope, "GCProfiler.prototype.stop called on incompatible receiver"_s); + return {}; + } + + RELEASE_AND_RETURN(scope, JSValue::encode(thisObject->stop(globalObject))); +} + +// Setup function for lazy class structure +void setupGCProfilerClassStructure(LazyClassStructure::Initializer& init) +{ + auto* prototypeStructure = JSGCProfilerPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()); + auto* prototype = JSGCProfilerPrototype::create(init.vm, init.global, prototypeStructure); + + auto* constructorStructure = JSGCProfilerConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()); + auto* constructor = JSGCProfilerConstructor::create(init.vm, constructorStructure, prototype); + + auto* structure = JSGCProfiler::createStructure(init.vm, init.global, prototype); + init.setPrototype(prototype); + init.setStructure(structure); + init.setConstructor(constructor); +} + +// Export function to create GCProfiler constructor +extern "C" JSC::EncodedJSValue Bun__createGCProfilerConstructor(Zig::GlobalObject* globalObject) +{ + return JSValue::encode(globalObject->m_JSGCProfilerClassStructure.constructor(globalObject)); +} + +} // namespace Bun + +JSC::JSValue createGCProfilerFunctions(Zig::GlobalObject* globalObject) +{ + using namespace JSC; + auto& vm = JSC::getVM(globalObject); + auto* obj = constructEmptyObject(globalObject); + + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "GCProfiler"_s)), globalObject->m_JSGCProfilerClassStructure.constructor(globalObject), 0); + + return obj; +} \ No newline at end of file diff --git a/src/bun.js/bindings/JSGCProfiler.h b/src/bun.js/bindings/JSGCProfiler.h new file mode 100644 index 0000000000..1b6bcc67ac --- /dev/null +++ b/src/bun.js/bindings/JSGCProfiler.h @@ -0,0 +1,15 @@ +#pragma once + +#include "root.h" +#include +#include + +namespace Bun { + +class JSGCProfiler; + +void setupGCProfilerClassStructure(JSC::LazyClassStructure::Initializer&); + +} // namespace Bun + +JSC::JSValue createGCProfilerFunctions(Zig::GlobalObject* globalObject); \ No newline at end of file diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 5d566de68a..fd6504af24 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -163,6 +163,7 @@ #include "JSPerformanceResourceTiming.h" #include "JSPerformanceTiming.h" #include "JSX509Certificate.h" +#include "JSGCProfiler.h" #include "JSSign.h" #include "JSVerify.h" #include "JSHmac.h" @@ -2786,6 +2787,10 @@ void GlobalObject::finishCreation(VM& vm) setupX509CertificateClassStructure(init); }); + m_JSGCProfilerClassStructure.initLater([](LazyClassStructure::Initializer& init) { + Bun::setupGCProfilerClassStructure(init); + }); + m_JSSignClassStructure.initLater( [](LazyClassStructure::Initializer& init) { setupJSSignClassStructure(init); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index b80fefee49..8615f7ab95 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -552,6 +552,7 @@ public: \ V(public, LazyClassStructure, m_JSConnectionsListClassStructure) \ V(public, LazyClassStructure, m_JSHTTPParserClassStructure) \ + V(public, LazyClassStructure, m_JSGCProfilerClassStructure) \ \ V(private, LazyPropertyOfGlobalObject, m_pendingVirtualModuleResultStructure) \ V(private, LazyPropertyOfGlobalObject, m_performMicrotaskFunction) \ diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 427954d53c..20a66d0684 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -71,6 +71,7 @@ public: std::unique_ptr m_clientSubspaceForJSS3Bucket; std::unique_ptr m_clientSubspaceForJSS3File; std::unique_ptr m_clientSubspaceForJSX509Certificate; + std::unique_ptr m_clientSubspaceForJSGCProfiler; std::unique_ptr m_clientSubspaceForJSNodePerformanceHooksHistogram; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 12a275ca46..0723e44adc 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -68,6 +68,7 @@ public: std::unique_ptr m_subspaceForJSS3Bucket; std::unique_ptr m_subspaceForJSS3File; std::unique_ptr m_subspaceForJSX509Certificate; + std::unique_ptr m_subspaceForJSGCProfiler; std::unique_ptr m_subspaceForJSNodePerformanceHooksHistogram; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ diff --git a/src/js/node/v8.ts b/src/js/node/v8.ts index eda4cf01dd..2494d81ac7 100644 --- a/src/js/node/v8.ts +++ b/src/js/node/v8.ts @@ -20,11 +20,9 @@ class Serializer { } class DefaultDeserializer extends Deserializer {} class DefaultSerializer extends Serializer {} -class GCProfiler { - constructor() { - notimpl("GCProfiler"); - } -} +const { + GCProfiler, +} = $cpp("JSGCProfiler.cpp", "createGCProfilerFunctions"); function cachedDataVersionTag() { notimpl("cachedDataVersionTag"); @@ -192,6 +190,7 @@ export default { Serializer, DefaultDeserializer, DefaultSerializer, + GCProfiler, }; hideFromStack( diff --git a/test/js/node/v8/gc-profiler.test.js b/test/js/node/v8/gc-profiler.test.js new file mode 100644 index 0000000000..7026a6fb6a --- /dev/null +++ b/test/js/node/v8/gc-profiler.test.js @@ -0,0 +1,249 @@ +import { describe, test, expect } from "bun:test"; +import { GCProfiler } from "node:v8"; + +describe("v8.GCProfiler", () => { + test("should create GCProfiler instance", () => { + const profiler = new GCProfiler(); + expect(profiler).toBeInstanceOf(GCProfiler); + }); + + test("should have start and stop methods", () => { + const profiler = new GCProfiler(); + expect(typeof profiler.start).toBe("function"); + expect(typeof profiler.stop).toBe("function"); + }); + + test("should start and stop profiling", () => { + const profiler = new GCProfiler(); + + // Should not throw when calling start + expect(() => profiler.start()).not.toThrow(); + + // Should not throw when calling stop + expect(() => profiler.stop()).not.toThrow(); + }); + + test("should return data when stopping", () => { + const profiler = new GCProfiler(); + profiler.start(); + + // Force some garbage collection events (minimal) + const objects = []; + for (let i = 0; i < 1000; i++) { + objects.push({ data: new Array(100).fill(i) }); + } + objects.length = 0; // Clear to trigger potential GC + + const result = profiler.stop(); + + expect(result).toBeDefined(); + expect(typeof result).toBe("object"); + expect(result.version).toBe(1); + expect(typeof result.startTime).toBe("number"); + expect(typeof result.endTime).toBe("number"); + expect(Array.isArray(result.statistics)).toBe(true); + expect(result.endTime).toBeGreaterThan(result.startTime); + }); + + test("should handle multiple start/stop cycles", () => { + const profiler = new GCProfiler(); + + // First cycle + profiler.start(); + const result1 = profiler.stop(); + expect(result1).toBeDefined(); + + // Add small delay to ensure different timestamps + const start = Date.now(); + while (Date.now() - start < 2) { + // Busy wait for at least 2ms + } + + // Second cycle + profiler.start(); + const result2 = profiler.stop(); + expect(result2).toBeDefined(); + + // Results should be different (different timestamps) or at least valid + if (result1 && result2) { + expect(result1.startTime).toBeGreaterThan(0); + expect(result2.startTime).toBeGreaterThan(0); + expect(result1.endTime).toBeGreaterThan(0); + expect(result2.endTime).toBeGreaterThan(0); + } + }); + + test("should return undefined when stopping without starting", () => { + const profiler = new GCProfiler(); + const result = profiler.stop(); + expect(result).toBeUndefined(); + }); + + test("should not crash when starting already started profiler", () => { + const profiler = new GCProfiler(); + profiler.start(); + expect(() => profiler.start()).not.toThrow(); // Should be idempotent + const result = profiler.stop(); + expect(result).toBeDefined(); + }); + + // Additional comprehensive tests + test("should handle constructor with no arguments", () => { + expect(() => new GCProfiler()).not.toThrow(); + }); + + test("should handle constructor with extra arguments gracefully", () => { + expect(() => new GCProfiler("extra", "args")).not.toThrow(); + }); + + test("should maintain consistent data format", () => { + const profiler = new GCProfiler(); + profiler.start(); + + // Create some memory pressure + const data = Array(500).fill(null).map((_, i) => ({ + id: i, + payload: new Array(50).fill(Math.random()) + })); + + const result = profiler.stop(); + + if (result) { + expect(result).toHaveProperty("version"); + expect(result).toHaveProperty("startTime"); + expect(result).toHaveProperty("endTime"); + expect(result).toHaveProperty("statistics"); + + expect(typeof result.version).toBe("number"); + expect(typeof result.startTime).toBe("number"); + expect(typeof result.endTime).toBe("number"); + expect(Array.isArray(result.statistics)).toBe(true); + + expect(result.version).toBeGreaterThan(0); + expect(result.startTime).toBeGreaterThan(0); + expect(result.endTime).toBeGreaterThan(0); + expect(result.endTime).toBeGreaterThanOrEqual(result.startTime); + } + }); + + test("should handle rapid start/stop sequences", () => { + const profiler = new GCProfiler(); + + for (let i = 0; i < 5; i++) { + profiler.start(); + const result = profiler.stop(); + if (result) { + expect(result.version).toBe(1); + expect(typeof result.startTime).toBe("number"); + expect(typeof result.endTime).toBe("number"); + } + } + }); + + test("should handle memory allocation patterns", () => { + const profiler = new GCProfiler(); + profiler.start(); + + // Create various allocation patterns + const smallObjects = Array(1000).fill(null).map(() => ({ small: true })); + const mediumObjects = Array(100).fill(null).map(() => ({ + medium: new Array(100).fill(0) + })); + const largeObjects = Array(10).fill(null).map(() => ({ + large: new Array(10000).fill("data") + })); + + // Clear references to allow GC + smallObjects.length = 0; + mediumObjects.length = 0; + largeObjects.length = 0; + + const result = profiler.stop(); + expect(result).toBeDefined(); + if (result) { + expect(Array.isArray(result.statistics)).toBe(true); + } + }); + + test("should maintain profiler state correctly", () => { + const profiler1 = new GCProfiler(); + const profiler2 = new GCProfiler(); + + // Independent profilers should not interfere + profiler1.start(); + + // Add small delay + const start = Date.now(); + while (Date.now() - start < 1) { + // Busy wait for at least 1ms + } + + profiler2.start(); + + const result1 = profiler1.stop(); + const result2 = profiler2.stop(); + + // Both should return valid results + expect(result1).toBeDefined(); + expect(result2).toBeDefined(); + + // Results should have valid properties + if (result1 && result2) { + expect(result1.startTime).toBeGreaterThan(0); + expect(result2.startTime).toBeGreaterThan(0); + expect(result1.endTime).toBeGreaterThan(0); + expect(result2.endTime).toBeGreaterThan(0); + } + }); + + test("should handle edge case of immediate stop after start", () => { + const profiler = new GCProfiler(); + profiler.start(); + const result = profiler.stop(); + + expect(result).toBeDefined(); + if (result) { + expect(result.endTime).toBeGreaterThanOrEqual(result.startTime); + expect(result.version).toBe(1); + expect(Array.isArray(result.statistics)).toBe(true); + } + }); + + test("should handle nested profiling attempts gracefully", () => { + const profiler = new GCProfiler(); + + profiler.start(); + profiler.start(); // Second start should be ignored + profiler.start(); // Third start should be ignored + + const result = profiler.stop(); + expect(result).toBeDefined(); + + // Subsequent stops should return undefined + const result2 = profiler.stop(); + expect(result2).toBeUndefined(); + }); + + test("should validate statistics array structure", () => { + const profiler = new GCProfiler(); + profiler.start(); + + // Generate some activity + const temp = []; + for (let i = 0; i < 100; i++) { + temp.push(new Array(100).fill(i)); + } + temp.splice(0, temp.length); + + const result = profiler.stop(); + + if (result && result.statistics) { + expect(Array.isArray(result.statistics)).toBe(true); + // Each statistic entry (if any) should be an object + result.statistics.forEach(stat => { + expect(typeof stat).toBe("object"); + expect(stat).not.toBeNull(); + }); + } + }); +}); \ No newline at end of file