From 31980bc151a332bdd2a1d2d70a64163c445cba71 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 26 May 2025 21:18:22 -0700 Subject: [PATCH] perf_hooks.Histogram (#19920) --- Makefile | 2 +- cmake/sources/CxxSources.txt | 3 + cmake/targets/BuildBun.cmake | 2 + cmake/targets/BuildHdrHistogram.cmake | 24 + .../JSNodePerformanceHooksHistogram.cpp | 283 +++++++++ .../JSNodePerformanceHooksHistogram.h | 173 ++++++ ...dePerformanceHooksHistogramConstructor.cpp | 111 ++++ ...NodePerformanceHooksHistogramConstructor.h | 48 ++ ...NodePerformanceHooksHistogramPrototype.cpp | 418 +++++++++++++ ...JSNodePerformanceHooksHistogramPrototype.h | 46 ++ src/bun.js/bindings/ZigGlobalObject.cpp | 6 + src/bun.js/bindings/ZigGlobalObject.h | 1 + .../bindings/webcore/DOMClientIsoSubspaces.h | 1 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 1 + .../webcore/SerializedScriptValue.cpp | 29 + .../bindings/workaround-missing-symbols.cpp | 4 + src/cli/audit_command.zig | 1 - src/js/node/perf_hooks.ts | 60 +- test/js/bun/perf_hooks/histogram.test.ts | 585 ++++++++++++++++++ test/js/node/perf_hooks/perf_hooks.test.ts | 2 - test/regression/issue/18547.test.ts | 1 - 21 files changed, 1794 insertions(+), 7 deletions(-) create mode 100644 cmake/targets/BuildHdrHistogram.cmake create mode 100644 src/bun.js/bindings/JSNodePerformanceHooksHistogram.cpp create mode 100644 src/bun.js/bindings/JSNodePerformanceHooksHistogram.h create mode 100644 src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.cpp create mode 100644 src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.h create mode 100644 src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.cpp create mode 100644 src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.h create mode 100644 test/js/bun/perf_hooks/histogram.test.ts diff --git a/Makefile b/Makefile index b354ec2af6..8826aa4ea7 100644 --- a/Makefile +++ b/Makefile @@ -482,7 +482,7 @@ STATIC_MUSL_FLAG ?= WRAP_SYMBOLS_ON_LINUX = ifeq ($(OS_NAME), linux) -WRAP_SYMBOLS_ON_LINUX = -Wl,--wrap=fcntl -Wl,--wrap=fcntl64 -Wl,--wrap=stat64 -Wl,--wrap=pow -Wl,--wrap=exp -Wl,--wrap=log -Wl,--wrap=log2 \ +WRAP_SYMBOLS_ON_LINUX = -Wl,--wrap=fcntl -Wl,--wrap=fcntl64 -Wl,--wrap=stat64 -Wl,--wrap=pow -Wl,--wrap=exp -Wl,--wrap=exp2 -Wl,--wrap=log -Wl,--wrap=log2 \ -Wl,--wrap=lstat \ -Wl,--wrap=stat \ -Wl,--wrap=fstat \ diff --git a/cmake/sources/CxxSources.txt b/cmake/sources/CxxSources.txt index 318dd51ec9..594b97687b 100644 --- a/cmake/sources/CxxSources.txt +++ b/cmake/sources/CxxSources.txt @@ -80,6 +80,9 @@ src/bun.js/bindings/JSEnvironmentVariableMap.cpp src/bun.js/bindings/JSFFIFunction.cpp src/bun.js/bindings/JSMockFunction.cpp src/bun.js/bindings/JSNextTickQueue.cpp +src/bun.js/bindings/JSNodePerformanceHooksHistogram.cpp +src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.cpp +src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.cpp src/bun.js/bindings/JSPropertyIterator.cpp src/bun.js/bindings/JSS3File.cpp src/bun.js/bindings/JSSocketAddressDTO.cpp diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 06e4b511e7..af179e1d35 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -935,6 +935,7 @@ if(LINUX) if(NOT ABI STREQUAL "musl") target_link_options(${bun} PUBLIC -Wl,--wrap=exp + -Wl,--wrap=exp2 -Wl,--wrap=expf -Wl,--wrap=fcntl64 -Wl,--wrap=log @@ -1057,6 +1058,7 @@ set(BUN_DEPENDENCIES TinyCC Zlib LibArchive # must be loaded after zlib + HdrHistogram # must be loaded after zlib Zstd ) diff --git a/cmake/targets/BuildHdrHistogram.cmake b/cmake/targets/BuildHdrHistogram.cmake new file mode 100644 index 0000000000..bcd4e47c1c --- /dev/null +++ b/cmake/targets/BuildHdrHistogram.cmake @@ -0,0 +1,24 @@ +register_repository( + NAME + hdrhistogram + REPOSITORY + HdrHistogram/HdrHistogram_c + COMMIT + 652d51bcc36744fd1a6debfeb1a8a5f58b14022c +) + +register_cmake_command( + TARGET + hdrhistogram + LIBRARIES + hdr_histogram_static + INCLUDES + include + LIB_PATH + src + ARGS + -DHDR_HISTOGRAM_BUILD_SHARED=OFF + -DHDR_HISTOGRAM_BUILD_STATIC=ON + -DHDR_LOG_REQUIRED=DISABLED + -DHDR_HISTOGRAM_BUILD_PROGRAMS=OFF +) \ No newline at end of file diff --git a/src/bun.js/bindings/JSNodePerformanceHooksHistogram.cpp b/src/bun.js/bindings/JSNodePerformanceHooksHistogram.cpp new file mode 100644 index 0000000000..39705d7782 --- /dev/null +++ b/src/bun.js/bindings/JSNodePerformanceHooksHistogram.cpp @@ -0,0 +1,283 @@ +#include "root.h" + +#include "JSNodePerformanceHooksHistogram.h" +#include "JSNodePerformanceHooksHistogramPrototype.h" +#include "JSNodePerformanceHooksHistogramConstructor.h" +#include "ZigGlobalObject.h" +#include "ErrorCode.h" +#include "BunString.h" +#include "JSDOMExceptionHandling.h" +#include +#include +#include +#include +#include +#include +#include +#include "wtf/text/WTFString.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { + +using namespace JSC; + +const ClassInfo JSNodePerformanceHooksHistogram::s_info = { "RecordableHistogram"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSNodePerformanceHooksHistogram) }; + +void JSNodePerformanceHooksHistogram::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSNodePerformanceHooksHistogram* JSNodePerformanceHooksHistogram::create(VM& vm, Structure* structure, JSGlobalObject* globalObject, int64_t lowest, int64_t highest, int figures) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + struct hdr_histogram* raw_histogram = nullptr; + int result = hdr_init(lowest, highest, figures, &raw_histogram); + if (result != 0 || !raw_histogram) { + throwTypeError(globalObject, scope, "Failed to initialize histogram"_s); + return nullptr; + } + auto histogramData = HistogramData(raw_histogram); + + JSNodePerformanceHooksHistogram* ptr = new (NotNull, allocateCell(vm)) JSNodePerformanceHooksHistogram(vm, structure, std::move(histogramData)); + ptr->finishCreation(vm); + if (ptr->m_histogramData.histogram) { + ptr->m_extraMemorySizeForGC = hdr_get_memory_size(ptr->m_histogramData.histogram); + vm.heap.reportExtraMemoryAllocated(ptr, ptr->m_extraMemorySizeForGC); + } + return ptr; +} + +JSNodePerformanceHooksHistogram* JSNodePerformanceHooksHistogram::create(VM& vm, Structure* structure, JSGlobalObject* globalObject, HistogramData&& existingHistogramData) +{ + JSNodePerformanceHooksHistogram* ptr = new (NotNull, allocateCell(vm)) JSNodePerformanceHooksHistogram(vm, structure, std::move(existingHistogramData)); + ptr->finishCreation(vm); + if (ptr->m_histogramData.histogram) { + ptr->m_extraMemorySizeForGC = hdr_get_memory_size(ptr->m_histogramData.histogram); + vm.heap.reportExtraMemoryAllocated(ptr, ptr->m_extraMemorySizeForGC); + } + return ptr; +} + +void JSNodePerformanceHooksHistogram::destroy(JSCell* cell) +{ + static_cast(cell)->~JSNodePerformanceHooksHistogram(); +} + +JSNodePerformanceHooksHistogram::~JSNodePerformanceHooksHistogram() +{ + // The shared_ptr will handle the destruction of HistogramData, + // which in turn calls hdr_close via HDRHistogramPointer. +} + +template +void JSNodePerformanceHooksHistogram::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSNodePerformanceHooksHistogram* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + + if (!thisObject->m_histogramData.histogram) { + visitor.reportExtraMemoryVisited(thisObject->m_extraMemorySizeForGC); + } +} + +DEFINE_VISIT_CHILDREN(JSNodePerformanceHooksHistogram); + +size_t JSNodePerformanceHooksHistogram::estimatedSize(JSCell* cell, VM& vm) +{ + JSNodePerformanceHooksHistogram* thisObject = jsCast(cell); + size_t selfSize = Base::estimatedSize(cell, vm); + return selfSize + thisObject->m_extraMemorySizeForGC; +} + +void JSNodePerformanceHooksHistogram::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + Base::analyzeHeap(cell, analyzer); +} + +JSC::Structure* JSNodePerformanceHooksHistogram::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +bool JSNodePerformanceHooksHistogram::record(int64_t value) +{ + if (!m_histogramData.histogram) return false; + + // Try to record in the HDR histogram first + bool recorded = hdr_record_value(m_histogramData.histogram, value); + + if (recorded) { + // Value was within range - count it and update min/max + m_histogramData.totalCount++; + + // Update manual min/max tracking for in-range values only + if (value < m_histogramData.manualMin) { + m_histogramData.manualMin = value; + } + if (value > m_histogramData.manualMax) { + m_histogramData.manualMax = value; + } + } else { + // Value was out of range + m_histogramData.exceedsCount++; + } + + return true; +} + +uint64_t JSNodePerformanceHooksHistogram::recordDelta(JSGlobalObject* globalObject) +{ + auto now = WTF::MonotonicTime::now(); + uint64_t nowNs = static_cast(now.secondsSinceEpoch().milliseconds() * 1000000.0); + + uint64_t delta = 0; + if (m_histogramData.prevDeltaTime != 0) { + delta = nowNs - m_histogramData.prevDeltaTime; + record(delta); + } + m_histogramData.prevDeltaTime = nowNs; + return delta; +} + +void JSNodePerformanceHooksHistogram::reset() +{ + if (!m_histogramData.histogram) return; + hdr_reset(m_histogramData.histogram); + m_histogramData.prevDeltaTime = 0; + m_histogramData.totalCount = 0; + m_histogramData.manualMin = std::numeric_limits::max(); + m_histogramData.manualMax = 0; + m_histogramData.exceedsCount = 0; +} + +int64_t JSNodePerformanceHooksHistogram::getMin() const +{ + if (m_histogramData.totalCount == 0) { + // Return the same initial value as Node.js when no values recorded + // Node.js returns 9223372036854776000 which is 0x8000000000000000 + // This is exactly INT64_MIN when interpreted as signed + return INT64_MIN; + } + return m_histogramData.manualMin; +} + +int64_t JSNodePerformanceHooksHistogram::getMax() const +{ + if (m_histogramData.totalCount == 0) { + // Return 0 when no values recorded (Node.js behavior) + return 0; + } + return m_histogramData.manualMax; +} + +double JSNodePerformanceHooksHistogram::getMean() const +{ + if (!m_histogramData.histogram) return NAN; + return hdr_mean(m_histogramData.histogram); +} + +double JSNodePerformanceHooksHistogram::getStddev() const +{ + if (!m_histogramData.histogram) return NAN; + return hdr_stddev(m_histogramData.histogram); +} + +int64_t JSNodePerformanceHooksHistogram::getPercentile(double percentile) const +{ + if (!m_histogramData.histogram) return 0; + return hdr_value_at_percentile(m_histogramData.histogram, percentile); +} + +size_t JSNodePerformanceHooksHistogram::getExceeds() const +{ + return m_histogramData.exceedsCount; +} + +uint64_t JSNodePerformanceHooksHistogram::getCount() const +{ + // Return our manual count of in-range values only + // This matches Node.js behavior + return m_histogramData.totalCount; +} + +double JSNodePerformanceHooksHistogram::add(JSNodePerformanceHooksHistogram* other) +{ + if (!m_histogramData.histogram || !other || !other->m_histogramData.histogram) return 0; + + // Add the manual counts and exceeds + m_histogramData.totalCount += other->m_histogramData.totalCount; + m_histogramData.exceedsCount += other->m_histogramData.exceedsCount; + + // Update manual min/max from the other histogram + if (other->m_histogramData.totalCount > 0) { + if (m_histogramData.totalCount == other->m_histogramData.totalCount) { + // This was empty, so take the other's values + m_histogramData.manualMin = other->m_histogramData.manualMin; + m_histogramData.manualMax = other->m_histogramData.manualMax; + } else { + // Merge min/max values + if (other->m_histogramData.manualMin < m_histogramData.manualMin) { + m_histogramData.manualMin = other->m_histogramData.manualMin; + } + if (other->m_histogramData.manualMax > m_histogramData.manualMax) { + m_histogramData.manualMax = other->m_histogramData.manualMax; + } + } + } + + // hdr_add returns number of dropped values + return hdr_add(m_histogramData.histogram, other->m_histogramData.histogram); +} + +void JSNodePerformanceHooksHistogram::getPercentiles(JSGlobalObject* globalObject, JSC::JSMap* map) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!m_histogramData.histogram) return; + + struct hdr_iter iter; + hdr_iter_percentile_init(&iter, m_histogramData.histogram, 1.0); + + while (hdr_iter_next(&iter)) { + double percentile = iter.specifics.percentiles.percentile; + int64_t value = iter.highest_equivalent_value; + JSValue jsKey = jsNumber(percentile); + JSValue jsValue = JSBigInt::createFrom(globalObject, value); + map->set(globalObject, jsKey, jsValue); + RETURN_IF_EXCEPTION(scope, void()); + } +} + +void JSNodePerformanceHooksHistogram::getPercentilesBigInt(JSGlobalObject* globalObject, JSC::JSMap* map) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!m_histogramData.histogram) return; + + struct hdr_iter iter; + hdr_iter_percentile_init(&iter, m_histogramData.histogram, 1.0); + + while (hdr_iter_next(&iter)) { + double percentile = iter.specifics.percentiles.percentile; + int64_t value = iter.highest_equivalent_value; + JSValue jsKey = jsNumber(percentile); + JSValue jsValue = JSBigInt::createFrom(globalObject, value); + map->set(globalObject, jsKey, jsValue); + RETURN_IF_EXCEPTION(scope, void()); + } +} + +} // namespace Bun diff --git a/src/bun.js/bindings/JSNodePerformanceHooksHistogram.h b/src/bun.js/bindings/JSNodePerformanceHooksHistogram.h new file mode 100644 index 0000000000..51a2bd9604 --- /dev/null +++ b/src/bun.js/bindings/JSNodePerformanceHooksHistogram.h @@ -0,0 +1,173 @@ +#pragma once + +#include "root.h" + +#include "BunClientData.h" +#include "headers-handwritten.h" +#include "wtf/Assertions.h" +#include "wtf/Lock.h" +#include "wtf/FastMalloc.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { + +using namespace JSC; + +// Forward declarations +class JSNodePerformanceHooksHistogram; +class JSNodePerformanceHooksHistogramPrototype; +class JSNodePerformanceHooksHistogramConstructor; + +JSC_DECLARE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncRecord); +JSC_DECLARE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncRecordDelta); +JSC_DECLARE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncAdd); +JSC_DECLARE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncReset); + +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_count); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_countBigInt); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_min); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_minBigInt); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_max); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_maxBigInt); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_mean); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_stddev); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_exceeds); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_exceedsBigInt); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_percentiles); +JSC_DECLARE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_percentilesBigInt); + +JSC_DECLARE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncPercentile); +JSC_DECLARE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncPercentileBigInt); + +JSC_DECLARE_HOST_FUNCTION(jsFunction_createHistogram); + +class HistogramData { +public: + hdr_histogram* histogram; + uint64_t prevDeltaTime = 0; + size_t exceedsCount = 0; + uint64_t totalCount = 0; // Manual count to track all values (Node.js behavior) + int64_t manualMin = std::numeric_limits::max(); // Manual min tracking + int64_t manualMax = 0; // Manual max tracking + + HistogramData(hdr_histogram* histogram) + : histogram(histogram) + { + } + + ~HistogramData() + { + if (histogram) { + hdr_close(histogram); + } + } + + // Move constructor (does not call destructor) + HistogramData(HistogramData&& other) noexcept + : histogram(other.histogram) + , prevDeltaTime(other.prevDeltaTime) + , exceedsCount(other.exceedsCount) + , totalCount(other.totalCount) + , manualMin(other.manualMin) + , manualMax(other.manualMax) + { + // Invalidate other's histogram pointer to avoid double free + other.histogram = nullptr; + other.prevDeltaTime = 0; + other.exceedsCount = 0; + other.totalCount = 0; + other.manualMin = std::numeric_limits::max(); + other.manualMax = 0; + } +}; + +class JSNodePerformanceHooksHistogram final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = NeedsDestruction; + + HistogramData m_histogramData; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype); + + static JSNodePerformanceHooksHistogram* create( + JSC::VM& vm, + JSC::Structure* structure, + JSC::JSGlobalObject* globalObject, + int64_t lowest, + int64_t highest, + int figures); + + static JSNodePerformanceHooksHistogram* create( + JSC::VM& vm, + JSC::Structure* structure, + JSC::JSGlobalObject* globalObject, + HistogramData&& existingHistogramData); + + void finishCreation(JSC::VM& vm); + static void destroy(JSC::JSCell*); + + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + + template + static void visitChildren(JSCell*, Visitor&); + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + 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_clientSubspaceForJSNodePerformanceHooksHistogram.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSNodePerformanceHooksHistogram = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSNodePerformanceHooksHistogram.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSNodePerformanceHooksHistogram = std::forward(space); }); + } + + JSNodePerformanceHooksHistogram(JSC::VM& vm, JSC::Structure* structure, HistogramData&& histogramData) + : Base(vm, structure) + , m_histogramData(std::move(histogramData)) + { + } + + ~JSNodePerformanceHooksHistogram(); + + hdr_histogram& histogram() { return *m_histogramData.histogram; } + + bool record(int64_t value); + uint64_t recordDelta(JSGlobalObject* globalObject); + void reset(); + int64_t getMin() const; + int64_t getMax() const; + double getMean() const; + double getStddev() const; + int64_t getPercentile(double percentile) const; + void getPercentiles(JSGlobalObject* globalObject, JSC::JSMap* map); + void getPercentilesBigInt(JSGlobalObject* globalObject, JSC::JSMap* map); + size_t getExceeds() const; + uint64_t getCount() const; + double add(JSNodePerformanceHooksHistogram* other); + + // std::shared_ptr getHistogramDataForCloning() const; + +private: + uint16_t m_extraMemorySizeForGC = 0; +}; + +void setupJSNodePerformanceHooksHistogramClassStructure(JSC::LazyClassStructure::Initializer& init); + +} // namespace Bun diff --git a/src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.cpp b/src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.cpp new file mode 100644 index 0000000000..009fbb986f --- /dev/null +++ b/src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.cpp @@ -0,0 +1,111 @@ +#include "root.h" + +#include "JSNodePerformanceHooksHistogramConstructor.h" +#include "JSNodePerformanceHooksHistogram.h" +#include "JSNodePerformanceHooksHistogramPrototype.h" +#include "ZigGlobalObject.h" +#include "ErrorCode.h" +#include "BunString.h" +#include "wtf/text/ASCIILiteral.h" +#include "wtf/Vector.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { + +using namespace JSC; + +const ClassInfo JSNodePerformanceHooksHistogramConstructor::s_info = { "Histogram"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSNodePerformanceHooksHistogramConstructor) }; + +void JSNodePerformanceHooksHistogramConstructor::finishCreation(VM& vm, JSGlobalObject* globalObject, JSObject* prototype) +{ + Base::finishCreation(vm, 3, "Histogram"_s, PropertyAdditionMode::WithStructureTransition); // lowest, highest, figures + putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); +} + +static JSNodePerformanceHooksHistogram* createHistogramInternal(JSGlobalObject* globalObject, JSValue lowestVal, JSValue highestVal, JSValue figuresVal) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + int64_t lowest = 1; + int64_t highest = std::numeric_limits::max(); + int figures = 3; + + if (lowestVal.isNumber()) { + double dbl = lowestVal.asNumber(); + if (!std::isnan(dbl)) { + lowest = static_cast(dbl); + } + } else if (lowestVal.isBigInt()) { + auto* bigInt = jsCast(lowestVal); + lowest = JSBigInt::toBigInt64(bigInt); + } + + if (highestVal.isNumber()) { + double dbl = highestVal.asNumber(); + if (!std::isnan(dbl)) { + highest = static_cast(dbl); + } + } else if (highestVal.isBigInt()) { + auto* bigInt = jsCast(highestVal); + highest = JSBigInt::toBigInt64(bigInt); + } + + if (figuresVal.isNumber()) { + double dbl = figuresVal.asNumber(); + if (!std::isnan(dbl)) { + figures = static_cast(dbl); + } + } + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + Structure* structure = zigGlobalObject->m_JSNodePerformanceHooksHistogramClassStructure.get(zigGlobalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + + return JSNodePerformanceHooksHistogram::create(vm, structure, globalObject, lowest, highest, figures); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodePerformanceHooksHistogramConstructorCall, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + Bun::throwError(globalObject, scope, ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, "Histogram constructor cannot be invoked without 'new'"_s); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(jsNodePerformanceHooksHistogramConstructorConstruct, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue lowestArg = callFrame->argument(0); + JSValue highestArg = callFrame->argument(1); + JSValue figuresArg = callFrame->argument(2); + + JSNodePerformanceHooksHistogram* histogram = createHistogramInternal(globalObject, lowestArg, highestArg, figuresArg); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(histogram); +} + +void setupJSNodePerformanceHooksHistogramClassStructure(LazyClassStructure::Initializer& init) +{ + auto* prototypeStructure = JSNodePerformanceHooksHistogramPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()); + auto* prototype = JSNodePerformanceHooksHistogramPrototype::create(init.vm, init.global, prototypeStructure); + + auto* constructorStructure = JSNodePerformanceHooksHistogramConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()); + auto* constructor = JSNodePerformanceHooksHistogramConstructor::create(init.vm, init.global, constructorStructure, prototype); + + auto* structure = JSNodePerformanceHooksHistogram::createStructure(init.vm, init.global, prototype); + + init.setPrototype(prototype); + init.setStructure(structure); + init.setConstructor(constructor); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.h b/src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.h new file mode 100644 index 0000000000..044d1da415 --- /dev/null +++ b/src/bun.js/bindings/JSNodePerformanceHooksHistogramConstructor.h @@ -0,0 +1,48 @@ +#pragma once + +#include "root.h" +#include + +namespace Bun { + +using namespace JSC; + +JSC_DECLARE_HOST_FUNCTION(jsNodePerformanceHooksHistogramConstructorCall); +JSC_DECLARE_HOST_FUNCTION(jsNodePerformanceHooksHistogramConstructorConstruct); + +class JSNodePerformanceHooksHistogramConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSNodePerformanceHooksHistogramConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSObject* prototype) + { + JSNodePerformanceHooksHistogramConstructor* constructor = new (NotNull, JSC::allocateCell(vm)) JSNodePerformanceHooksHistogramConstructor(vm, structure); + constructor->finishCreation(vm, globalObject, prototype); + return constructor; + } + + DECLARE_INFO; + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + 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: + JSNodePerformanceHooksHistogramConstructor(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure, jsNodePerformanceHooksHistogramConstructorCall, jsNodePerformanceHooksHistogramConstructorConstruct) + { + } + + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* prototype); +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.cpp b/src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.cpp new file mode 100644 index 0000000000..e175fb6ee6 --- /dev/null +++ b/src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.cpp @@ -0,0 +1,418 @@ +#include "ErrorCode.h" +#include "JSDOMExceptionHandling.h" +#include "root.h" + +#include "JSNodePerformanceHooksHistogramPrototype.h" +#include "JSNodePerformanceHooksHistogram.h" +#include "wtf/text/ASCIILiteral.h" +#include +#include +#include +#include +#include + +namespace Bun { + +using namespace JSC; + +static const HashTableValue JSNodePerformanceHooksHistogramPrototypeTableValues[] = { + { "record"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodePerformanceHooksHistogramProtoFuncRecord, 1 } }, + { "recordDelta"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodePerformanceHooksHistogramProtoFuncRecordDelta, 0 } }, + { "add"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodePerformanceHooksHistogramProtoFuncAdd, 1 } }, + { "reset"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodePerformanceHooksHistogramProtoFuncReset, 0 } }, + { "percentile"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodePerformanceHooksHistogramProtoFuncPercentile, 1 } }, + { "percentileBigInt"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsNodePerformanceHooksHistogramProtoFuncPercentileBigInt, 1 } }, + + { "count"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_count, 0 } }, + { "countBigInt"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_countBigInt, 0 } }, + { "min"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_min, 0 } }, + { "minBigInt"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_minBigInt, 0 } }, + { "max"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_max, 0 } }, + { "maxBigInt"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_maxBigInt, 0 } }, + { "mean"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_mean, 0 } }, + { "stddev"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_stddev, 0 } }, + { "exceeds"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_exceeds, 0 } }, + { "exceedsBigInt"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_exceedsBigInt, 0 } }, + { "percentiles"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_percentiles, 0 } }, + { "percentilesBigInt"_s, static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodePerformanceHooksHistogramGetter_percentilesBigInt, 0 } }, +}; + +const ClassInfo JSNodePerformanceHooksHistogramPrototype::s_info = { "RecordableHistogram"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSNodePerformanceHooksHistogramPrototype) }; + +void JSNodePerformanceHooksHistogramPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSNodePerformanceHooksHistogram::info(), JSNodePerformanceHooksHistogramPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncRecord, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "record"_s); + return {}; + } + + if (callFrame->argumentCount() < 1) { + Bun::ERR::MISSING_ARGS(scope, globalObject, "record requires at least one argument"_s); + return {}; + } + + JSValue arg = callFrame->uncheckedArgument(0); + int64_t value; + if (arg.isNumber()) { + value = static_cast(arg.asNumber()); + } else if (arg.isBigInt()) { + auto* bigInt = jsCast(arg); + value = JSBigInt::toBigInt64(bigInt); + } else { + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "value"_s, "number or BigInt"_s, arg); + return {}; + } + + if (value < 1) { + Bun::ERR::OUT_OF_RANGE(scope, globalObject, "value is out of range (must be >= 1)"_s); + return {}; + } + + thisObject->record(value); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncRecordDelta, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "recordDelta"_s); + return {}; + } + + thisObject->recordDelta(globalObject); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncAdd, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "add"_s); + return {}; + } + + if (callFrame->argumentCount() < 1) { + Bun::ERR::MISSING_ARGS(scope, globalObject, "add requires at least one argument"_s); + return {}; + } + + JSValue otherArg = callFrame->uncheckedArgument(0); + JSNodePerformanceHooksHistogram* otherHistogram = jsDynamicCast(otherArg); + if (!otherHistogram) [[unlikely]] { + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "argument"_s, "Histogram"_s, otherArg); + return {}; + } + + double dropped = thisObject->add(otherHistogram); + return JSValue::encode(jsNumber(dropped)); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncReset, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "reset"_s); + return {}; + } + + thisObject->reset(); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncPercentile, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "percentile"_s); + return {}; + } + + if (callFrame->argumentCount() < 1) { + Bun::ERR::MISSING_ARGS(scope, globalObject, "percentile requires an argument"_s); + return {}; + } + + double percentile = callFrame->uncheckedArgument(0).toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (percentile <= 0 || percentile > 100 || std::isnan(percentile)) { + Bun::ERR::OUT_OF_RANGE(scope, globalObject, "percentile"_s, "> 0 && <= 100"_s, jsNumber(percentile)); + return {}; + } + + return JSValue::encode(jsNumber(static_cast(thisObject->getPercentile(percentile)))); +} + +JSC_DEFINE_HOST_FUNCTION(jsNodePerformanceHooksHistogramProtoFuncPercentileBigInt, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "percentileBigInt"_s); + return {}; + } + + if (callFrame->argumentCount() < 1) { + Bun::ERR::MISSING_ARGS(scope, globalObject, "percentileBigInt requires an argument"_s); + return {}; + } + + double percentile = callFrame->uncheckedArgument(0).toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (percentile <= 0 || percentile > 100 || std::isnan(percentile)) { + Bun::ERR::OUT_OF_RANGE(scope, globalObject, "percentile"_s, "> 0 && <= 100"_s, jsNumber(percentile)); + return {}; + } + + return JSValue::encode(JSBigInt::createFrom(globalObject, thisObject->getPercentile(percentile))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_count, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "count"_s); + return {}; + } + return JSValue::encode(jsNumber(static_cast(thisObject->getCount()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_countBigInt, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "countBigInt"_s); + return {}; + } + return JSValue::encode(JSBigInt::createFrom(globalObject, thisObject->getCount())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_min, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "min"_s); + return {}; + } + + int64_t minValue = thisObject->getMin(); + + // Node.js returns the value as if it were unsigned when converting to double + // This handles the special case where the initial value is INT64_MIN + return JSValue::encode(jsNumber(static_cast(static_cast(minValue)))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_minBigInt, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "minBigInt"_s); + return {}; + } + + // Node.js returns different initial values for min vs minBigInt + // min returns 9223372036854776000 (as double) + // minBigInt returns 9223372036854775807n (INT64_MAX) + if (thisObject->getCount() == 0) { + return JSValue::encode(JSBigInt::createFrom(globalObject, INT64_MAX)); + } + + return JSValue::encode(JSBigInt::createFrom(globalObject, thisObject->getMin())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_max, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "max"_s); + return {}; + } + return JSValue::encode(jsNumber(static_cast(thisObject->getMax()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_maxBigInt, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "maxBigInt"_s); + return {}; + } + return JSValue::encode(JSBigInt::createFrom(globalObject, thisObject->getMax())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_mean, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "mean"_s); + return {}; + } + return JSValue::encode(jsNumber(thisObject->getMean())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_stddev, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "stddev"_s); + return {}; + } + return JSValue::encode(jsNumber(thisObject->getStddev())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_exceeds, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "exceeds"_s); + return {}; + } + return JSValue::encode(jsNumber(static_cast(thisObject->getExceeds()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_exceedsBigInt, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "exceedsBigInt"_s); + return {}; + } + return JSValue::encode(JSBigInt::createFrom(globalObject, static_cast(thisObject->getExceeds()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_percentiles, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "percentiles"_s); + return {}; + } + + JSMap* map = JSMap::create(vm, globalObject->mapStructure()); + thisObject->getPercentiles(globalObject, map); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(map); +} + +JSC_DEFINE_CUSTOM_GETTER(jsNodePerformanceHooksHistogramGetter_percentilesBigInt, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSNodePerformanceHooksHistogram* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + WebCore::throwThisTypeError(*globalObject, scope, "Histogram"_s, "percentilesBigInt"_s); + return {}; + } + + JSMap* map = JSMap::create(vm, globalObject->mapStructure()); + thisObject->getPercentilesBigInt(globalObject, map); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(map); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunction_createHistogram, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + int64_t lowest = 1; + int64_t highest = std::numeric_limits::max(); + int figures = 3; + + if (callFrame->argumentCount() >= 1) { + JSValue lowestArg = callFrame->uncheckedArgument(0); + if (lowestArg.isNumber()) { + lowest = static_cast(lowestArg.asNumber()); + } else if (lowestArg.isBigInt()) { + auto* bigInt = jsCast(lowestArg); + lowest = JSBigInt::toBigInt64(bigInt); + } + } + + if (callFrame->argumentCount() >= 2) { + JSValue highestArg = callFrame->uncheckedArgument(1); + if (highestArg.isNumber()) { + highest = static_cast(highestArg.asNumber()); + } else if (highestArg.isBigInt()) { + auto* bigInt = jsCast(highestArg); + highest = JSBigInt::toBigInt64(bigInt); + } + } + + if (callFrame->argumentCount() >= 3) { + JSValue figuresArg = callFrame->uncheckedArgument(2); + if (figuresArg.isNumber()) { + figures = static_cast(figuresArg.asNumber()); + } + } + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + Structure* structure = zigGlobalObject->m_JSNodePerformanceHooksHistogramClassStructure.get(zigGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + + JSNodePerformanceHooksHistogram* histogram = JSNodePerformanceHooksHistogram::create(vm, structure, globalObject, lowest, highest, figures); + RETURN_IF_EXCEPTION(scope, {}); + + return JSValue::encode(histogram); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.h b/src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.h new file mode 100644 index 0000000000..ad165d5a3a --- /dev/null +++ b/src/bun.js/bindings/JSNodePerformanceHooksHistogramPrototype.h @@ -0,0 +1,46 @@ +#pragma once + +#include "root.h" +#include "ZigGlobalObject.h" + +namespace Bun { + +using namespace JSC; + +class JSNodePerformanceHooksHistogramPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static JSNodePerformanceHooksHistogramPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSNodePerformanceHooksHistogramPrototype* prototype = new (NotNull, allocateCell(vm)) JSNodePerformanceHooksHistogramPrototype(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: + JSNodePerformanceHooksHistogramPrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM& vm); +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index cf4345d36d..f97ffe3479 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -176,6 +176,7 @@ #include "JSPublicKeyObject.h" #include "JSPrivateKeyObject.h" #include "webcore/JSMIMEParams.h" +#include "JSNodePerformanceHooksHistogram.h" #include "JSS3File.h" #include "S3Error.h" #include "ProcessBindingBuffer.h" @@ -2779,6 +2780,11 @@ void GlobalObject::finishCreation(VM& vm) WebCore::setupJSMIMETypeClassStructure(init); }); + m_JSNodePerformanceHooksHistogramClassStructure.initLater( + [](LazyClassStructure::Initializer& init) { + Bun::setupJSNodePerformanceHooksHistogramClassStructure(init); + }); + m_lazyStackCustomGetterSetter.initLater( [](const Initializer& init) { init.set(CustomGetterSetter::create(init.vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter)); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 1e376a94bb..13ced8fd5f 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -544,6 +544,7 @@ public: V(public, LazyClassStructure, m_JSPrivateKeyObjectClassStructure) \ V(public, LazyClassStructure, m_JSMIMEParamsClassStructure) \ V(public, LazyClassStructure, m_JSMIMETypeClassStructure) \ + V(public, LazyClassStructure, m_JSNodePerformanceHooksHistogramClassStructure) \ \ 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 49e562de5c..4f26cf4906 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -70,6 +70,7 @@ public: std::unique_ptr m_clientSubspaceForJSS3Bucket; std::unique_ptr m_clientSubspaceForJSS3File; std::unique_ptr m_clientSubspaceForJSX509Certificate; + 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 bce6f1bef4..36320772d6 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -67,6 +67,7 @@ public: std::unique_ptr m_subspaceForJSS3Bucket; std::unique_ptr m_subspaceForJSS3File; std::unique_ptr m_subspaceForJSX509Certificate; + std::unique_ptr m_subspaceForJSNodePerformanceHooksHistogram; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index a4a2971dac..e2556d29e4 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -113,6 +113,7 @@ #include "JSPublicKeyObject.h" #include "JSPrivateKeyObject.h" #include "CryptoKeyType.h" +#include "JSNodePerformanceHooksHistogram.h" #if USE(CG) #include @@ -244,6 +245,7 @@ enum SerializationTag { Bun__X509CertificateTag = 253, Bun__KeyObjectTag = 252, Bun__nodenet_BlockList = 251, + Bun__NodePerformanceHooksHistogramTag = 250, ErrorTag = 255 }; @@ -2033,6 +2035,30 @@ private: } } + if (auto* histogram = jsDynamicCast(obj)) { + if (m_context != SerializationContext::WorkerPostMessage && m_context != SerializationContext::WindowPostMessage) { + // Don't allow cloning of histograms if it's not a simple .postMessage(). + code = SerializationReturnCode::DataCloneError; + return true; + } + + // Serialize histogram configuration + hdr_histogram* hdr = histogram->m_histogramData.histogram; + if (!hdr) { + // Histogram is not initialized + code = SerializationReturnCode::DataCloneError; + return true; + } + + write(Bun__NodePerformanceHooksHistogramTag); + // TODO: write the index into the histograms vector on SerializedScriptValue + // make it ThreadSafeRefCounted + // and then we can just write the index + code = SerializationReturnCode::DataCloneError; + + return true; + } + return false; } // Any other types are expected to serialize as null. @@ -5156,6 +5182,9 @@ private: case Bun__KeyObjectTag: return readKeyObject(); + // case Bun__NodePerformanceHooksHistogramTag: + // ? + default: m_ptr--; // Push the tag back return JSValue(); diff --git a/src/bun.js/bindings/workaround-missing-symbols.cpp b/src/bun.js/bindings/workaround-missing-symbols.cpp index e55f4d2991..988529cda5 100644 --- a/src/bun.js/bindings/workaround-missing-symbols.cpp +++ b/src/bun.js/bindings/workaround-missing-symbols.cpp @@ -82,6 +82,7 @@ extern "C" int kill(int pid, int sig) #if defined(__x86_64__) __asm__(".symver exp,exp@GLIBC_2.2.5"); +__asm__(".symver exp2,exp2@GLIBC_2.2.5"); __asm__(".symver expf,expf@GLIBC_2.2.5"); __asm__(".symver log,log@GLIBC_2.2.5"); __asm__(".symver log2,log2@GLIBC_2.2.5"); @@ -92,6 +93,7 @@ __asm__(".symver powf,powf@GLIBC_2.2.5"); #elif defined(__aarch64__) __asm__(".symver expf,expf@GLIBC_2.17"); __asm__(".symver exp,exp@GLIBC_2.17"); +__asm__(".symver exp2,exp2@GLIBC_2.17"); __asm__(".symver log,log@GLIBC_2.17"); __asm__(".symver log2,log2@GLIBC_2.17"); __asm__(".symver log2f,log2f@GLIBC_2.17"); @@ -109,6 +111,7 @@ __asm__(".symver powf,powf@GLIBC_2.17"); extern "C" { double BUN_WRAP_GLIBC_SYMBOL(exp)(double); +double BUN_WRAP_GLIBC_SYMBOL(exp2)(double); float BUN_WRAP_GLIBC_SYMBOL(expf)(float); float BUN_WRAP_GLIBC_SYMBOL(log2f)(float); float BUN_WRAP_GLIBC_SYMBOL(logf)(float); @@ -123,6 +126,7 @@ float __wrap_powf(float x, float y) { return powf(x, y); } float __wrap_logf(float x) { return logf(x); } float __wrap_log2f(float x) { return log2f(x); } double __wrap_exp(double x) { return exp(x); } +double __wrap_exp2(double x) { return exp2(x); } double __wrap_pow(double x, double y) { return pow(x, y); } double __wrap_log(double x) { return log(x); } double __wrap_log2(double x) { return log2(x); } diff --git a/src/cli/audit_command.zig b/src/cli/audit_command.zig index 8fed3632f2..87fc8d90e8 100644 --- a/src/cli/audit_command.zig +++ b/src/cli/audit_command.zig @@ -10,7 +10,6 @@ const HeaderBuilder = http.HeaderBuilder; const MutableString = bun.MutableString; const URL = @import("../url.zig").URL; const logger = bun.logger; -const semver = @import("../semver.zig"); const libdeflate = @import("../deps/libdeflate.zig"); const VulnerabilityInfo = struct { diff --git a/src/js/node/perf_hooks.ts b/src/js/node/perf_hooks.ts index 8ec408c4e8..8c19388bf2 100644 --- a/src/js/node/perf_hooks.ts +++ b/src/js/node/perf_hooks.ts @@ -7,6 +7,12 @@ const createFunctionThatMasqueradesAsUndefined = $newCppFunction( 2, ); +const cppCreateHistogram = $newCppFunction("JSNodePerformanceHooksHistogram.cpp", "jsFunction_createHistogram", 3) as ( + min: number, + max: number, + figures: number, +) => import("node:perf_hooks").RecordableHistogram; + var { Performance, PerformanceEntry, @@ -176,7 +182,57 @@ export default { PerformanceNodeTiming, // TODO: node:perf_hooks.monitorEventLoopDelay -- https://github.com/oven-sh/bun/issues/17650 monitorEventLoopDelay: createFunctionThatMasqueradesAsUndefined("", 0), - // TODO: node:perf_hooks.createHistogram -- https://github.com/oven-sh/bun/issues/8815 - createHistogram: createFunctionThatMasqueradesAsUndefined("", 0), + createHistogram: function createHistogram(options?: { + lowest?: number | bigint; + highest?: number | bigint; + figures?: number; + }): import("node:perf_hooks").RecordableHistogram { + const opts = options || {}; + + let lowest = 1; + let highest = Number.MAX_SAFE_INTEGER; + let figures = 3; + + if (opts.lowest !== undefined) { + if (typeof opts.lowest === "bigint") { + lowest = Number(opts.lowest); + } else if (typeof opts.lowest === "number") { + lowest = opts.lowest; + } else { + throw $ERR_INVALID_ARG_TYPE("options.lowest", ["number", "bigint"], opts.lowest); + } + } + + if (opts.highest !== undefined) { + if (typeof opts.highest === "bigint") { + highest = Number(opts.highest); + } else if (typeof opts.highest === "number") { + highest = opts.highest; + } else { + throw $ERR_INVALID_ARG_TYPE("options.highest", ["number", "bigint"], opts.highest); + } + } + + if (opts.figures !== undefined) { + if (typeof opts.figures !== "number") { + throw $ERR_INVALID_ARG_TYPE("options.figures", "number", opts.figures); + } + if (opts.figures < 1 || opts.figures > 5) { + throw $ERR_OUT_OF_RANGE("options.figures", ">= 1 && <= 5", opts.figures); + } + figures = opts.figures; + } + + // Node.js validation - highest must be >= 2 * lowest + if (lowest < 1) { + throw $ERR_OUT_OF_RANGE("options.lowest", ">= 1 && <= 9007199254740991", lowest); + } + + if (highest < 2 * lowest) { + throw $ERR_OUT_OF_RANGE("options.highest", `>= ${2 * lowest} && <= 9007199254740991`, highest); + } + + return cppCreateHistogram(lowest, highest, figures); + }, PerformanceResourceTiming, }; diff --git a/test/js/bun/perf_hooks/histogram.test.ts b/test/js/bun/perf_hooks/histogram.test.ts new file mode 100644 index 0000000000..1555551f01 --- /dev/null +++ b/test/js/bun/perf_hooks/histogram.test.ts @@ -0,0 +1,585 @@ +import assert from "node:assert"; +import { describe, test } from "node:test"; +import { inspect } from "node:util"; +import { createHistogram } from "perf_hooks"; + +describe("Histogram", () => { + test("basic histogram creation and initial state", () => { + const h = createHistogram(); + + assert.strictEqual(h.min, 9223372036854776000); + assert.strictEqual(h.minBigInt, 9223372036854775807n); + assert.strictEqual(h.max, 0); + assert.strictEqual(h.maxBigInt, 0n); + assert.strictEqual(h.exceeds, 0); + assert.strictEqual(h.exceedsBigInt, 0n); + assert.ok(Number.isNaN(h.mean)); + assert.ok(Number.isNaN(h.stddev)); + assert.strictEqual(h.count, 0); + assert.strictEqual(h.countBigInt, 0n); + }); + + test("recording values", () => { + const h = createHistogram(); + + h.record(1); + assert.strictEqual(h.count, 1); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, 1); + + h.record(5); + assert.strictEqual(h.count, 2); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, 5); + }); + + test("recording multiple values", () => { + const h = createHistogram(); + + for (let i = 1; i <= 10; i++) { + h.record(i); + } + + assert.strictEqual(h.count, 10); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, 10); + assert.strictEqual(h.mean, 5.5); + }); + + test("percentiles", () => { + const h = createHistogram(); + + for (let i = 1; i <= 100; i++) { + h.record(i); + } + + assert.strictEqual(h.percentile(50), 50); + assert.strictEqual(h.percentile(90), 90); + assert.strictEqual(h.percentile(99), 99); + }); + + test("invalid record arguments", () => { + const h = createHistogram(); + + assert.throws(() => h.record(0), /out of range/); + assert.throws(() => h.record(-1), /out of range/); + assert.throws(() => h.record("invalid"), /must be of type number/); + }); + + test("histogram with custom options", () => { + const h = createHistogram({ lowest: 1, highest: 11, figures: 1 }); + + h.record(5); + assert.strictEqual(h.count, 1); + assert.strictEqual(h.min, 5); + assert.strictEqual(h.max, 5); + }); + + test("invalid histogram options", () => { + assert.throws(() => createHistogram({ figures: 6 })); + assert.throws(() => createHistogram({ figures: 0 })); + }); + + test("adding histograms", () => { + const h1 = createHistogram(); + const h2 = createHistogram(); + + h1.record(1); + h1.record(2); + h2.record(3); + h2.record(4); + + const originalCount1 = h1.count; + const originalCount2 = h2.count; + + h1.add(h2); + + assert.strictEqual(h1.count, originalCount1 + originalCount2); + assert.strictEqual(h1.min, 1); + assert.strictEqual(h1.max, 4); + }); + + test("reset functionality", () => { + const h = createHistogram(); + + h.record(1); + h.record(2); + h.record(3); + + assert.strictEqual(h.count, 3); + + h.reset(); + + assert.strictEqual(h.count, 0); + assert.strictEqual(h.exceeds, 0); + assert.ok(Number.isNaN(h.mean)); + assert.ok(Number.isNaN(h.stddev)); + }); + + test("recordDelta functionality", async () => { + const h = createHistogram(); + + h.recordDelta(); + await new Promise(resolve => setTimeout(resolve, 10)); + h.recordDelta(); + + assert.strictEqual(h.count, 1); + }); + + describe("exceeds functionality", () => { + test("basic exceeds counting", () => { + const h = createHistogram({ lowest: 1, highest: 10, figures: 1 }); + + assert.strictEqual(h.exceeds, 0); + + h.record(5); + assert.strictEqual(h.exceeds, 0); + assert.strictEqual(h.count, 1); + + h.record(100); + assert.strictEqual(h.exceeds, 1); + assert.strictEqual(h.count, 1); + + assert.strictEqual(h.min, 5); + assert.strictEqual(h.max, 5); + }); + + test("exceeds with BigInt", () => { + const h = createHistogram({ lowest: 1, highest: 10, figures: 1 }); + + h.record(5); + h.record(100); + + assert.strictEqual(h.exceeds, 1); + assert.strictEqual(h.exceedsBigInt, 1n); + assert.strictEqual(h.count, 1); + }); + + test("exceeds count in add operation", () => { + const h1 = createHistogram({ lowest: 1, highest: 10, figures: 1 }); + const h2 = createHistogram({ lowest: 1, highest: 10, figures: 1 }); + + h1.record(5); + h1.record(100); + assert.strictEqual(h1.exceeds, 1); + assert.strictEqual(h1.count, 1); + + h2.record(8); + h2.record(200); + assert.strictEqual(h2.exceeds, 1); + assert.strictEqual(h2.count, 1); + + h1.add(h2); + assert.strictEqual(h1.exceeds, 2); + assert.strictEqual(h1.count, 2); + }); + + test("exceeds count after reset", () => { + const h = createHistogram({ lowest: 1, highest: 10, figures: 1 }); + + h.record(5); + h.record(100); + assert.strictEqual(h.exceeds, 1); + assert.strictEqual(h.count, 1); + + h.reset(); + assert.strictEqual(h.exceeds, 0); + assert.strictEqual(h.count, 0); + }); + + test("exceeds with very small range", () => { + const h = createHistogram({ lowest: 1, highest: 2, figures: 1 }); + + h.record(1); + h.record(50); + h.record(100); + + assert.strictEqual(h.exceeds, 2); + assert.strictEqual(h.count, 1); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, 1); + }); + }); + + describe("percentiles functionality", () => { + test("percentiles with map", () => { + const h = createHistogram(); + + for (let i = 1; i <= 10; i++) { + h.record(i); + } + + const percentiles = h.percentiles; + assert.strictEqual(typeof percentiles, "object"); + assert.ok(percentiles.size > 0); + assert.ok(percentiles.has(50)); + assert.ok(percentiles.has(100)); + }); + + test("percentilesBigInt with map", () => { + const h = createHistogram(); + + for (let i = 1; i <= 5; i++) { + h.record(i); + } + + const percentiles = h.percentilesBigInt; + assert.strictEqual(typeof percentiles, "object"); + assert.ok(percentiles.size > 0); + + for (const [key, value] of percentiles) { + assert.strictEqual(typeof key, "number"); + assert.strictEqual(typeof value, "bigint"); + } + }); + }); + + describe("edge cases", () => { + test("recording zero", () => { + const h = createHistogram(); + assert.throws(() => h.record(0), /out of range/); + }); + + test("recording negative values", () => { + const h = createHistogram(); + assert.throws(() => h.record(-5), /out of range/); + }); + + test("very large values", () => { + const h = createHistogram(); + h.record(Number.MAX_SAFE_INTEGER); + assert.strictEqual(h.count, 1); + }); + + test("histogram with same lowest and highest", () => { + assert.throws(() => createHistogram({ lowest: 5, highest: 5, figures: 1 }), /out of range/); + }); + + test("multiple add operations", () => { + const h1 = createHistogram(); + const h2 = createHistogram(); + const h3 = createHistogram(); + + h1.record(1); + h2.record(2); + h3.record(3); + + h1.add(h2); + h1.add(h3); + + assert.strictEqual(h1.count, 3); + assert.strictEqual(h1.min, 1); + assert.strictEqual(h1.max, 3); + }); + }); + + describe("BigInt support", () => { + test("recording BigInt values", () => { + const h = createHistogram(); + + h.record(1n); + h.record(5n); + + assert.strictEqual(h.count, 2); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, 5); + }); + + test("BigInt getters", () => { + const h = createHistogram(); + + h.record(42); + + assert.strictEqual(h.countBigInt, 1n); + assert.strictEqual(h.minBigInt, 42n); + assert.strictEqual(h.maxBigInt, 42n); + }); + }); + + describe("comprehensive validation tests", () => { + test("createHistogram with BigInt parameters", () => { + const h = createHistogram({ lowest: 1n, highest: 1000n, figures: 3 }); + h.record(500); + assert.strictEqual(h.count, 1); + assert.strictEqual(h.min, 500); + assert.strictEqual(h.max, 500); + }); + + test("createHistogram parameter validation", () => { + assert.throws( + () => createHistogram({ figures: -1 }), + err => { + return err.code === "ERR_OUT_OF_RANGE" && err.message.includes("options.figures"); + }, + ); + assert.throws( + () => createHistogram({ figures: 6 }), + err => { + return err.code === "ERR_OUT_OF_RANGE" && err.message.includes("options.figures"); + }, + ); + + assert.throws( + () => createHistogram({ lowest: 0 }), + err => { + return err.code === "ERR_OUT_OF_RANGE" && err.message.includes("options.lowest"); + }, + ); + assert.throws( + () => createHistogram({ lowest: -1 }), + err => { + return err.code === "ERR_OUT_OF_RANGE" && err.message.includes("options.lowest"); + }, + ); + + assert.throws( + () => createHistogram({ lowest: 10, highest: 15 }), + err => { + return err.code === "ERR_OUT_OF_RANGE" && err.message.includes("options.highest"); + }, + ); + assert.throws( + () => createHistogram({ lowest: 5, highest: 9 }), + err => { + return err.code === "ERR_OUT_OF_RANGE" && err.message.includes("options.highest"); + }, + ); + + assert.throws( + () => createHistogram({ figures: "invalid" }), + err => { + return err.code === "ERR_INVALID_ARG_TYPE" && err.message.includes("options.figures"); + }, + ); + assert.throws( + () => createHistogram({ lowest: "invalid" }), + err => { + return err.code === "ERR_INVALID_ARG_TYPE" && err.message.includes("options.lowest"); + }, + ); + assert.throws( + () => createHistogram({ highest: "invalid" }), + err => { + return err.code === "ERR_INVALID_ARG_TYPE" && err.message.includes("options.highest"); + }, + ); + + const h = createHistogram({ lowest: 5, highest: 10, figures: 1 }); + assert.strictEqual(h.count, 0); + }); + + test("percentile validation", () => { + const h = createHistogram(); + h.record(50); + + assert.throws(() => h.percentile(0), /out of range/); + assert.throws(() => h.percentile(-1), /out of range/); + assert.throws(() => h.percentile(101), /out of range/); + assert.throws(() => h.percentile(NaN), /out of range/); + + assert.strictEqual(typeof h.percentile(1), "number"); + assert.strictEqual(typeof h.percentile(50), "number"); + assert.strictEqual(typeof h.percentile(100), "number"); + }); + + test("percentileBigInt validation", () => { + const h = createHistogram(); + h.record(50); + + assert.throws(() => h.percentileBigInt(0), /out of range/); + assert.throws(() => h.percentileBigInt(-1), /out of range/); + assert.throws(() => h.percentileBigInt(101), /out of range/); + assert.throws(() => h.percentileBigInt(NaN), /out of range/); + + assert.strictEqual(typeof h.percentileBigInt(1), "bigint"); + assert.strictEqual(typeof h.percentileBigInt(50), "bigint"); + assert.strictEqual(typeof h.percentileBigInt(100), "bigint"); + }); + + test("record with very large BigInt values", () => { + const h = createHistogram(); + + const largeBigInt = BigInt(Number.MAX_SAFE_INTEGER); + + h.record(largeBigInt); + assert.strictEqual(h.count, 1); + assert.strictEqual(h.countBigInt, 1n); + }); + + test("add with empty histograms", () => { + const h1 = createHistogram(); + const h2 = createHistogram(); + + h1.add(h2); + assert.strictEqual(h1.count, 0); + assert.strictEqual(h1.exceeds, 0); + + h2.record(42); + h1.add(h2); + assert.strictEqual(h1.count, 1); + assert.strictEqual(h1.min, 42); + assert.strictEqual(h1.max, 42); + }); + + test("reset preserves initial state", () => { + const h = createHistogram(); + + h.record(10); + h.record(20); + h.record(30); + + h.reset(); + + assert.strictEqual(h.count, 0); + assert.strictEqual(h.countBigInt, 0n); + assert.strictEqual(h.min, 9223372036854776000); + assert.strictEqual(h.minBigInt, 9223372036854775807n); + assert.strictEqual(h.max, 0); + assert.strictEqual(h.maxBigInt, 0n); + assert.strictEqual(h.exceeds, 0); + assert.strictEqual(h.exceedsBigInt, 0n); + assert.ok(Number.isNaN(h.mean)); + assert.ok(Number.isNaN(h.stddev)); + }); + + test("percentiles map properties", () => { + const h = createHistogram(); + + for (let i = 1; i <= 100; i++) { + h.record(i); + } + + const percentiles = h.percentiles; + const percentilesBigInt = h.percentilesBigInt; + + assert.ok(typeof percentiles.size === "number"); + assert.ok(typeof percentiles.has === "function"); + assert.ok(typeof percentiles.get === "function"); + assert.ok(typeof percentiles[Symbol.iterator] === "function"); + + assert.ok(typeof percentilesBigInt.size === "number"); + assert.ok(typeof percentilesBigInt.has === "function"); + assert.ok(typeof percentilesBigInt.get === "function"); + assert.ok(typeof percentilesBigInt[Symbol.iterator] === "function"); + + assert.strictEqual(percentiles.size, percentilesBigInt.size); + + for (const [key, value] of percentiles) { + assert.strictEqual(typeof key, "number"); + assert.strictEqual(typeof value, "bigint"); + + assert.ok(percentilesBigInt.has(key)); + const bigIntValue = percentilesBigInt.get(key); + assert.strictEqual(typeof bigIntValue, "bigint"); + assert.strictEqual(value, bigIntValue); + } + }); + + test("statistical accuracy", () => { + const h = createHistogram(); + + for (let i = 1; i <= 1000; i++) { + h.record(i); + } + + assert.strictEqual(h.count, 1000); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, 1000); + assert.strictEqual(h.mean, 500.5); + + assert.ok(Math.abs(h.percentile(50) - 500) <= 1); + assert.ok(Math.abs(h.percentile(90) - 900) <= 10); + assert.ok(Math.abs(h.percentile(99) - 990) <= 10); + }); + + test("recordDelta timing accuracy", async () => { + const h = createHistogram(); + + h.recordDelta(); + + const start = Date.now(); + await new Promise(resolve => setTimeout(resolve, 50)); + const end = Date.now(); + + h.recordDelta(); + + assert.strictEqual(h.count, 1); + + const expectedNs = (end - start) * 1000000; + const actualValue = h.min; + + assert.ok(actualValue > expectedNs * 0.5); + assert.ok(actualValue < expectedNs * 2); + }); + + test("toJSON method", () => { + const h = createHistogram(); + + h.record(10); + h.record(20); + h.record(30); + + if (typeof h.toJSON === "function") { + const json = h.toJSON(); + + assert.strictEqual(typeof json, "object"); + assert.strictEqual(json.count, 3); + assert.strictEqual(json.min, 10); + assert.strictEqual(json.max, 30); + assert.strictEqual(json.mean, 20); + assert.strictEqual(json.exceeds, 0); + assert.strictEqual(typeof json.stddev, "number"); + assert.strictEqual(typeof json.percentiles, "object"); + + assert.ok(!json.percentiles.has); + assert.ok(typeof json.percentiles === "object"); + } else { + console.log("toJSON method not implemented yet - skipping test"); + } + }); + + test("extreme value handling", () => { + const h = createHistogram(); + + h.record(1); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, 1); + assert.strictEqual(h.count, 1); + + const largeValue = Number.MAX_SAFE_INTEGER; + h.record(largeValue); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, largeValue); + assert.strictEqual(h.count, 2); + }); + + test("concurrent operations", () => { + const h = createHistogram(); + + for (let i = 0; i < 100; i++) { + h.record(i + 1); + if (i % 10 === 0) { + const count = h.count; + const min = h.min; + const max = h.max; + assert.ok(count > 0); + assert.ok(min >= 1); + assert.ok(max >= min); + } + } + + assert.strictEqual(h.count, 100); + assert.strictEqual(h.min, 1); + assert.strictEqual(h.max, 100); + }); + }); + + test("inspect output", () => { + const h = createHistogram(); + h.record(1); + + const inspected = inspect(h); + + assert.ok(inspected.includes("Histogram")); + }); +}); diff --git a/test/js/node/perf_hooks/perf_hooks.test.ts b/test/js/node/perf_hooks/perf_hooks.test.ts index 5a03ba2e42..3e9a8c43cc 100644 --- a/test/js/node/perf_hooks/perf_hooks.test.ts +++ b/test/js/node/perf_hooks/perf_hooks.test.ts @@ -3,9 +3,7 @@ import perf from "perf_hooks"; test("stubs", () => { expect(!!perf.monitorEventLoopDelay).toBeFalse(); - expect(!!perf.createHistogram).toBeFalse(); expect(() => perf.monitorEventLoopDelay()).toThrow(); - expect(() => perf.createHistogram()).toThrow(); expect(perf.performance.nodeTiming).toBeObject(); expect(perf.performance.now()).toBeNumber(); diff --git a/test/regression/issue/18547.test.ts b/test/regression/issue/18547.test.ts index d489177ccd..31980159d0 100644 --- a/test/regression/issue/18547.test.ts +++ b/test/regression/issue/18547.test.ts @@ -16,7 +16,6 @@ test("18547", async () => { expect(request.cookies.get("sessionToken")).toEqual("123456"); expect(clone.cookies.get("sessionToken")).toEqual("654321"); - return new Response("OK"); }, },