From 69bf45734992a4a593841b2d52809ead30927d6b Mon Sep 17 00:00:00 2001 From: Donghoon Kang Date: Wed, 12 Aug 2026 18:51:21 +0900 Subject: [PATCH 1/4] typings: add credentials internal binding types Add a CredentialsBinding declaration for internalBinding('credentials') and wire it into InternalBindingMap. Signed-off-by: HoonDongKang PR-URL: https://github.com/nodejs/node/pull/65036 Reviewed-By: Daeyeon Jeong --- typings/globals.d.ts | 2 ++ typings/internalBinding/credentials.d.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 typings/internalBinding/credentials.d.ts diff --git a/typings/globals.d.ts b/typings/globals.d.ts index 83fcec8e19a5..536a8c4c2822 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -5,6 +5,7 @@ import { BufferBinding } from './internalBinding/buffer'; import { CJSLexerBinding } from './internalBinding/cjs_lexer'; import { ConfigBinding } from './internalBinding/config'; import { ConstantsBinding } from './internalBinding/constants'; +import { CredentialsBinding } from './internalBinding/credentials'; import { CryptoBinding } from './internalBinding/crypto'; import { DebugBinding } from './internalBinding/debug'; import { EncodingBinding } from './internalBinding/encoding_binding'; @@ -44,6 +45,7 @@ interface InternalBindingMap { cjs_lexer: CJSLexerBinding; config: ConfigBinding; constants: ConstantsBinding; + credentials: CredentialsBinding; crypto: CryptoBinding; debug: DebugBinding; encoding_binding: EncodingBinding; diff --git a/typings/internalBinding/credentials.d.ts b/typings/internalBinding/credentials.d.ts new file mode 100644 index 000000000000..8880e7e38a6f --- /dev/null +++ b/typings/internalBinding/credentials.d.ts @@ -0,0 +1,18 @@ +export interface CredentialsBinding { + implementsPosixCredentials?: true; + safeGetenv(key: string): string | undefined; + getTempDir(): string | undefined; + + getuid?(): number; + geteuid?(): number; + getgid?(): number; + getegid?(): number; + getgroups?(): number[]; + + initgroups?(user: string | number, extraGroup: string | number): 0 | 1 | 2; + setegid?(id: string | number): 0 | 1; + seteuid?(id: string | number): 0 | 1; + setgid?(id: string | number): 0 | 1; + setuid?(id: string | number): 0 | 1; + setgroups?(groups: Array): number; +} From 356ee0a45e5230014f0972439f88155e1e891015 Mon Sep 17 00:00:00 2001 From: greenhead Date: Wed, 12 Aug 2026 22:38:41 +0900 Subject: [PATCH 2/4] doc: fix broken fs.BigIntStats link in vfs.md fs.md describes the bigint variant inside the fs.Stats section and has no separate fs.BigIntStats section to link to. Signed-off-by: greenhead PR-URL: https://github.com/nodejs/node/pull/65045 Reviewed-By: Daeyeon Jeong Reviewed-By: James M Snell --- doc/api/vfs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 7a1695d8b355..1551a18feac7 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -320,6 +320,6 @@ fields use synthetic but stable values: [`RealFSProvider`]: #class-realfsprovider [`VirtualFileSystem`]: #class-virtualfilesystem [`VirtualProvider`]: #class-virtualprovider -[`fs.BigIntStats`]: fs.md#class-fsbigintstats +[`fs.BigIntStats`]: fs.md#class-fsstats [`fs.Stats`]: fs.md#class-fsstats [`node:fs`]: fs.md From 3ad942784a4e2100084a20a5aeb22a0abc9745c0 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 8 Aug 2026 12:05:24 -0700 Subject: [PATCH 3/4] src: shave about 20 bytes off each TLSWrap instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By shifting from individual bool fields to a packed struct we can save 20 bytes per TLSWrap instance Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/65144 Reviewed-By: Tim Perry Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Tobias Nießen --- src/crypto/crypto_tls.cc | 74 +++++++++++++++++++--------------------- src/crypto/crypto_tls.h | 62 +++++++++++++++++++-------------- 2 files changed, 73 insertions(+), 63 deletions(-) diff --git a/src/crypto/crypto_tls.cc b/src/crypto/crypto_tls.cc index 8ef74aee2d0e..936c201de99d 100644 --- a/src/crypto/crypto_tls.cc +++ b/src/crypto/crypto_tls.cc @@ -240,7 +240,7 @@ int SelectALPNCallback( unsigned int inlen, void* arg) { TLSWrap* w = static_cast(SSL_get_app_data(s)); - if (w->alpn_callback_enabled_) { + if (w->get_alpn_callback_enabled()) { Environment* env = w->env(); HandleScope handle_scope(env->isolate()); @@ -275,7 +275,7 @@ int SelectALPNCallback( return SSL_TLSEXT_ERR_OK; } - const std::vector& alpn_protos = w->alpn_protos_; + auto& alpn_protos = w->get_alpn_protos(); if (alpn_protos.empty()) return SSL_TLSEXT_ERR_NOACK; @@ -403,9 +403,9 @@ TLSWrap::TLSWrap(Environment* env, StreamBase(env), env_(env), kind_(kind), - sc_(sc), - has_active_write_issued_by_prev_listener_( - under_stream_ws == UnderlyingStreamWriteStatus::kHasActive) { + sc_(sc) { + flags_.has_active_write_issued_by_prev_listener = + under_stream_ws == UnderlyingStreamWriteStatus::kHasActive; MakeWeak(); CHECK(sc_); ssl_ = sc_->CreateSSL(); @@ -444,8 +444,7 @@ SSL_SESSION* TLSWrap::ReleaseSession() { void TLSWrap::InvokeQueued(int status, const char* error_str) { Debug(this, "Invoking queued write callbacks (%d, %s)", status, error_str); - if (!write_callback_scheduled_) - return; + if (!flags_.write_callback_scheduled) return; if (current_write_) { BaseObjectPtr current_write = std::move(current_write_); @@ -465,8 +464,8 @@ void TLSWrap::NewSessionDoneCb() { bool TLSWrap::OnEarlyClientHello(const unsigned char* session_id, size_t session_id_len, bool has_ticket) { - if (!hello_emitted_) { - hello_emitted_ = true; + if (!flags_.hello_emitted) { + flags_.hello_emitted = true; Debug(this, "Scheduling onclienthello"); // The hello data is only valid inside the library callback, and JS must @@ -480,7 +479,7 @@ bool TLSWrap::OnEarlyClientHello(const unsigned char* session_id, if (ssl_) EmitClientHello(id, has_ticket); }); } - return hello_answered_; + return flags_.hello_answered; } void TLSWrap::EmitClientHello(const std::vector& session_id, @@ -658,8 +657,8 @@ void TLSWrap::Start(const FunctionCallbackInfo& args) { TLSWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); - CHECK(!wrap->started_); - wrap->started_ = true; + CHECK(!wrap->flags_.started); + wrap->flags_.started = true; // Send ClientHello handshake CHECK(wrap->is_client()); @@ -703,7 +702,7 @@ void TLSWrap::SSLInfoCallback(const SSL* ssl_, int where, int ret) { CHECK(!SSL_renegotiate_pending(ssl)); Local callback; - c->established_ = true; + c->flags_.established = true; if (object->Get(env->context(), env->onhandshakedone_string()) .ToLocal(&callback) && callback->IsFunction()) { @@ -727,7 +726,7 @@ void TLSWrap::EncOut() { return; } - if (has_active_write_issued_by_prev_listener_) [[unlikely]] { + if (flags_.has_active_write_issued_by_prev_listener) [[unlikely]] { Debug(this, "Returning from EncOut(), " "has_active_write_issued_by_prev_listener_ is true"); @@ -735,9 +734,9 @@ void TLSWrap::EncOut() { } // Split-off queue - if (established_ && current_write_) { + if (flags_.established && current_write_) { Debug(this, "EncOut() write is scheduled"); - write_callback_scheduled_ = true; + flags_.write_callback_scheduled = true; } if (ssl_ == nullptr) { @@ -750,7 +749,7 @@ void TLSWrap::EncOut() { Debug(this, "No pending encrypted output"); if (!pending_cleartext_input_ || pending_cleartext_input_->ByteLength() == 0) { - if (!in_dowrite_) { + if (!flags_.in_dowrite) { Debug(this, "No pending cleartext input, not inside DoWrite()"); InvokeQueued(0); } else { @@ -805,7 +804,7 @@ void TLSWrap::EncOut() { void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) { Debug(this, "OnStreamAfterWrite(status = %d)", status); - if (has_active_write_issued_by_prev_listener_) [[unlikely]] { + if (flags_.has_active_write_issued_by_prev_listener) [[unlikely]] { Debug(this, "Notify write finish to the previous_listener_"); CHECK_EQ(write_size_, 0); // we must have restrained writes @@ -830,7 +829,7 @@ void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) { // Handle error if (status) { - if (shutdown_) { + if (flags_.shutdown) { Debug(this, "Ignoring error after shutdown"); return; } @@ -855,7 +854,7 @@ void TLSWrap::ClearOut() { Debug(this, "Trying to read cleartext output"); // No reads after EOF - if (eof_) { + if (flags_.eof) { Debug(this, "Returning from ClearOut(), EOF reached"); return; } @@ -911,8 +910,8 @@ void TLSWrap::ClearOut() { int err = SSL_get_error(ssl_.get(), read); switch (err) { case SSL_ERROR_ZERO_RETURN: - if (!eof_) { - eof_ = true; + if (!flags_.eof) { + flags_.eof = true; EmitRead(UV_EOF); } return; @@ -1005,7 +1004,7 @@ void TLSWrap::ClearIn() { int err = SSL_get_error(ssl_.get(), written); if (err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL) { Debug(this, "Got SSL error (%d)", err); - write_callback_scheduled_ = true; + flags_.write_callback_scheduled = true; // TODO(@sam-github) Should forward an error object with // .code/.function/.etc, if possible. InvokeQueued(UV_EPROTO, GetBIOError().c_str()); @@ -1049,7 +1048,7 @@ bool TLSWrap::IsClosing() { int TLSWrap::ReadStart() { Debug(this, "ReadStart()"); - if (underlying_stream() != nullptr && !eof_) + if (underlying_stream() != nullptr && !flags_.eof) return underlying_stream()->ReadStart(); return 0; } @@ -1197,9 +1196,9 @@ int TLSWrap::DoWrite(WriteWrap* w, // Write any encrypted/handshake output that may be ready. // Guard against sync call of current_write_->Done(), its unsupported. - in_dowrite_ = true; + flags_.in_dowrite = true; EncOut(); - in_dowrite_ = false; + flags_.in_dowrite = false; return 0; } @@ -1216,8 +1215,7 @@ void TLSWrap::OnStreamRead(ssize_t nread, const uv_buf_t& buf) { Debug(this, "Read %zd bytes from underlying stream", nread); // Ignore everything after close_notify (rfc5246#section-7.2.1) - if (eof_) - return; + if (flags_.eof) return; if (nread < 0) { // Error should be emitted only after all data was read @@ -1225,7 +1223,7 @@ void TLSWrap::OnStreamRead(ssize_t nread, const uv_buf_t& buf) { if (nread == UV_EOF) { // underlying stream already should have also called ReadStop on itself - eof_ = true; + flags_.eof = true; } EmitRead(nread); @@ -1256,7 +1254,7 @@ int TLSWrap::DoShutdown(ShutdownWrap* req_wrap) { if (ssl_ && SSL_shutdown(ssl_.get()) == 0) SSL_shutdown(ssl_.get()); - shutdown_ = true; + flags_.shutdown = true; EncOut(); return underlying_stream()->DoShutdown(req_wrap); } @@ -1352,7 +1350,7 @@ void TLSWrap::Destroy() { return; // If there is a write happening, mark it as finished. - write_callback_scheduled_ = true; + flags_.write_callback_scheduled = true; // And destroy InvokeQueued(UV_ECANCELED, "Canceled because of SSL destruction"); @@ -1389,7 +1387,7 @@ void TLSWrap::ResumeAfterCertCb(void* arg) { void TLSWrap::EnableALPNCb(const FunctionCallbackInfo& args) { TLSWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); - wrap->alpn_callback_enabled_ = true; + wrap->flags_.alpn_callback_enabled = true; SSL* ssl = wrap->ssl_.get(); SSL_CTX* ssl_ctx = SSL_get_SSL_CTX(ssl); @@ -1418,7 +1416,7 @@ void TLSWrap::SetServername(const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsString()); - CHECK(!wrap->started_); + CHECK(!wrap->flags_.started); CHECK(wrap->is_client()); CHECK(wrap->ssl_); @@ -1633,7 +1631,7 @@ void TLSWrap::CertCbDone(const FunctionCallbackInfo& args) { TLSWrap* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); - CHECK(w->is_waiting_cert_cb() && w->cert_cb_running_); + CHECK(w->is_waiting_cert_cb() && w->flags_.cert_cb_running); Local object = w->object(); Local ctx = object->Get(env->context(), env->sni_context_string()) @@ -1685,7 +1683,7 @@ void TLSWrap::CertCbDone(const FunctionCallbackInfo& args) { cb = w->cert_cb_; arg = w->cert_cb_arg_; - w->cert_cb_running_ = false; + w->flags_.cert_cb_running = false; w->cert_cb_ = nullptr; w->cert_cb_arg_ = nullptr; @@ -2072,7 +2070,7 @@ void TLSWrap::ExportKeyingMaterial(const FunctionCallbackInfo& args) { void TLSWrap::ClientHelloDone(const FunctionCallbackInfo& args) { TLSWrap* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); - w->hello_answered_ = true; + w->flags_.hello_answered = true; w->Cycle(); } @@ -2107,7 +2105,7 @@ void TLSWrap::GetTLSTicket(const FunctionCallbackInfo& args) { void TLSWrap::NewSessionDone(const FunctionCallbackInfo& args) { TLSWrap* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); - w->awaiting_new_session_ = false; + w->flags_.awaiting_new_session = false; w->NewSessionDoneCb(); } @@ -2188,7 +2186,7 @@ void TLSWrap::WritesIssuedByPrevListenerDone( ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); Debug(w, "WritesIssuedByPrevListenerDone is called"); - w->has_active_write_issued_by_prev_listener_ = false; + w->flags_.has_active_write_issued_by_prev_listener = false; w->EncOut(); // resume all of our restrained writes } diff --git a/src/crypto/crypto_tls.h b/src/crypto/crypto_tls.h index a5ded3392915..9a5f59ed472e 100644 --- a/src/crypto/crypto_tls.h +++ b/src/crypto/crypto_tls.h @@ -62,22 +62,26 @@ class TLSWrap : public AsyncWrap, ~TLSWrap() override; - inline bool is_cert_cb_running() const { return cert_cb_running_; } + inline bool is_cert_cb_running() const { return flags_.cert_cb_running; } inline bool is_waiting_cert_cb() const { return cert_cb_ != nullptr; } - inline bool has_session_callbacks() const { return session_callbacks_; } + inline bool has_session_callbacks() const { return flags_.session_callbacks; } // We need to suspend the ClientHello only for server session id // callbacks, and only on the first pass. inline bool should_suspend_for_client_hello() const { - return is_server() && session_callbacks_ && !hello_answered_; + return is_server() && flags_.session_callbacks && !flags_.hello_answered; + } + inline void set_cert_cb_running(bool on = true) { + flags_.cert_cb_running = on; } - inline void set_cert_cb_running(bool on = true) { cert_cb_running_ = on; } inline void set_awaiting_new_session(bool on = true) { - awaiting_new_session_ = on; + flags_.awaiting_new_session = on; } - inline void enable_session_callbacks() { session_callbacks_ = true; } + inline void enable_session_callbacks() { flags_.session_callbacks = true; } inline bool is_server() const { return kind_ == Kind::kServer; } inline bool is_client() const { return kind_ == Kind::kClient; } - inline bool is_awaiting_new_session() const { return awaiting_new_session_; } + inline bool is_awaiting_new_session() const { + return flags_.awaiting_new_session; + } // Implement StreamBase: bool IsAlive() override; @@ -125,6 +129,14 @@ class TLSWrap : public AsyncWrap, std::string diagnostic_name() const override; + bool get_alpn_callback_enabled() const { + return flags_.alpn_callback_enabled; + } + + const std::vector& get_alpn_protos() const { + return alpn_protos_; + } + private: // OpenSSL structures are opaque. Estimate SSL memory size for OpenSSL 1.1.1b: // SSL: 6224 @@ -284,26 +296,30 @@ class TLSWrap : public AsyncWrap, BaseObjectPtr current_empty_write_; std::string error_; - bool session_callbacks_ = false; - bool awaiting_new_session_ = false; - // 'onclienthello' has been emitted for this connection. - bool hello_emitted_ = false; - // JS has answered it by calling clientHelloDone(). - bool hello_answered_ = false; - bool in_dowrite_ = false; - bool started_ = false; - bool shutdown_ = false; - bool cert_cb_running_ = false; - bool eof_ = false; - // TODO(@jasnell): These state flags should be revisited. // The established_ flag indicates that the handshake is // completed. The write_callback_scheduled_ flag is less // clear -- once it is set to true, it is never set to // false and it is only set to true after established_ // is set to true, so it's likely redundant. - bool established_ = false; - bool write_callback_scheduled_ = false; + struct Flags { + bool session_callbacks : 1; + bool awaiting_new_session : 1; + // 'onclienthello' has been emitted for this connection. + bool hello_emitted : 1; + // JS has answered it by calling clientHelloDone(). + bool hello_answered : 1; + bool in_dowrite : 1; + bool started : 1; + bool shutdown : 1; + bool cert_cb_running : 1; + bool eof : 1; + bool established : 1; + bool write_callback_scheduled : 1; + bool has_active_write_issued_by_prev_listener : 1; + bool alpn_callback_enabled : 1; + }; + Flags flags_{}; int cycle_depth_ = 0; @@ -313,11 +329,7 @@ class TLSWrap : public AsyncWrap, ncrypto::BIOPointer bio_trace_; - bool has_active_write_issued_by_prev_listener_ = false; - - public: std::vector alpn_protos_; // Accessed by SelectALPNCallback. - bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback. }; } // namespace crypto From bfa3e982ec3ed8d30b525707948bbf9045967195 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 4 Aug 2026 08:39:49 -0700 Subject: [PATCH 4/4] lib,src: improve histogram implementation Several improvements: 1. In histogram-inl, Add previous locked only this->mutex while reading the other's fields unsafely. 2. In histogram.cc, PrepareCB now uses ContainerOf 3. In histogram.cc, BigInt value range is checked 4. In histogram.js, simplified impl and reduced duplication 5. In event_loop_delay.js, use a more consistent constructor Adds new analytical APIs to Histogram * histogram.ccdf(value) * histogram.cdf(value) * histogram.countAt(value) * histogram.ksTest(other) * histogram.kurtosis * histogram.linearBuckets(stepSize) * histogram.logBuckets(first, base) * histogram.percentilesAt(percentiles) * histogram.shewness On RecordableHistogram * histogram.recordCorrected(val, expectedInterval) * histogram.subtract(other) Signed-off-by: James M Snell Assisted-by: Opencode/Opus PR-URL: https://github.com/nodejs/node/pull/65024 Reviewed-By: Matteo Collina --- doc/api/perf_hooks.md | 240 +++++++++ lib/internal/histogram.js | 212 +++++++- lib/internal/perf/event_loop_delay.js | 23 +- src/histogram-inl.h | 108 +++- src/histogram.cc | 469 +++++++++++++--- src/histogram.h | 133 +++-- .../test-perf-hooks-histogram-analysis.js | 501 ++++++++++++++++++ 7 files changed, 1530 insertions(+), 156 deletions(-) create mode 100644 test/parallel/test-perf-hooks-histogram-analysis.js diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 0eca76ed9842..eb2076eb7921 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1868,6 +1868,45 @@ added: The number of samples recorded by the histogram. +### `histogram.ccdf(value)` + + + +* `value` {number} The value to query. +* Returns: {number} A probability between 0.0 and 1.0. + +Returns the complementary cumulative distribution function (CCDF) value +for the given value, representing the probability that a recorded value +will exceed `value`. Equivalent to `1 - histogram.cdf(value)`. + +### `histogram.cdf(value)` + + + +* `value` {number} The value to query. +* Returns: {number} A probability between 0.0 and 1.0. + +Returns the cumulative distribution function (CDF) value for the given +value, representing the probability that a recorded value will be less +than or equal to `value`. This is the inverse operation of +`histogram.percentile()`. + +### `histogram.countAt(value)` + + + +* `value` {number} The value to query. +* Returns: {number} + +Returns the number of recorded values that fall within the equivalent +value range of the given value. + ### `histogram.exceeds` + +* `other` {Histogram} The histogram to compare against. +* Returns: {number} The KS D-statistic, between 0.0 and 1.0. + +Computes the Kolmogorov-Smirnov test statistic comparing this histogram's +distribution to `other`. A value of 0 indicates identical distributions; +values close to 1 indicate completely disjoint distributions. Useful for +detecting performance regressions by comparing before/after histograms. + +### `histogram.kurtosis` + + + +* Type: {number} + +The excess kurtosis of the recorded values. Measures the heaviness of the +distribution's tails relative to a normal distribution. Positive values +indicate heavier tails (more extreme outliers); negative values indicate +lighter tails. + +### `histogram.linearBuckets(stepSize)` + + + +* `stepSize` {number} The width of each linear bucket. +* Returns: {Map} A map of bucket boundary values to counts. + +Returns the histogram data rebucketed into linearly-spaced intervals +of `stepSize`. Useful for visualization and export. + +### `histogram.logBuckets(firstBucket, base)` + + + +* `firstBucket` {number} The value of the first bucket boundary. +* `base` {number} The logarithmic base for bucket width growth. Must be > 1. +* Returns: {Map} A map of bucket boundary values to counts. + +Returns the histogram data rebucketed into logarithmically-spaced +intervals, where each bucket's width is multiplied by `base`. +Useful for visualization and export. + ### `histogram.max` + +* `percentiles` {number\[]} An array of percentile values in the range (0, 100]. +* Returns: {Map} A map of percentile values to their corresponding histogram + values. + +Returns the values at the specified percentiles, computed in a single +efficient pass over the histogram data. More efficient than calling +`histogram.percentile()` multiple times. + ### `histogram.reset()` + +* Type: {number} + +The skewness of the recorded values. Measures the asymmetry of the +distribution. A positive value indicates a right-skewed distribution +(longer right tail, common for latency data); a negative value +indicates a left-skewed distribution. + ### `histogram.stddev` + +* `val` {number|bigint} The value to record. +* `expectedInterval` {number|bigint} The expected recording interval. + +Records a value with coordinated omission correction. When a system stall +prevents timely recording, this method backfills intermediate values at +`expectedInterval` steps between the previously recorded value and `val`. +This compensates for measurement gaps that would otherwise underrepresent +latency. + +### `histogram.subtract(other)` + + + +* `other` {RecordableHistogram} + +Subtracts the values of `other` from this histogram. Both histograms should +have compatible configurations. Bucket counts that would become negative +are clamped to zero. + +## Histogram analysis examples + +The `Histogram` class provides statistical analysis methods useful for +performance monitoring, SLO enforcement, and regression detection. + +### Distribution shape analysis + +```js +const { createHistogram } = require('node:perf_hooks'); + +const h = createHistogram(); + +// Simulate a right-skewed latency distribution +for (let i = 0; i < 1000; i++) { + h.record(Math.ceil(Math.random() * 100)); +} +// Add some outliers +for (let i = 0; i < 10; i++) { + h.record(500 + Math.ceil(Math.random() * 500)); +} + +console.log('Skewness:', h.skewness.toFixed(4)); // Positive = right-skewed +console.log('Kurtosis:', h.kurtosis.toFixed(4)); // Positive = heavy tails +``` + +### SLO monitoring with CDF + +```js +const { createHistogram } = require('node:perf_hooks'); + +const latency = createHistogram(); + +// Record request latencies (in nanoseconds)... + +// "What fraction of requests complete within 100ms?" +const withinSLO = latency.cdf(100_000_000); +console.log(`${(withinSLO * 100).toFixed(1)}% of requests within SLO`); + +// "What fraction of requests exceed 500ms?" +const violating = latency.ccdf(500_000_000); +console.log(`${(violating * 100).toFixed(1)}% of requests violating SLO`); +``` + +### Regression detection with KS test + +```js +const { createHistogram } = require('node:perf_hooks'); + +const baseline = createHistogram(); +const current = createHistogram(); + +// Record baseline and current latencies... + +// D-statistic: 0 = identical, 1 = completely different +const d = baseline.ksTest(current); +if (d > 0.1) { + console.log(`Possible regression detected (D=${d.toFixed(4)})`); +} +``` + +### Batch percentile queries + +```js +const { createHistogram } = require('node:perf_hooks'); + +const h = createHistogram(); +// Record values... + +// Efficiently query common monitoring percentiles in one pass +const p = h.percentilesAt([50, 75, 90, 95, 99, 99.9]); +console.log('p50:', p.get(50)); +console.log('p99:', p.get(99)); +``` + +### Snapshot diffing with subtract + +```js +const { createHistogram } = require('node:perf_hooks'); + +const total = createHistogram(); +const snapshot = createHistogram(); + +// Record values into total... +// Periodically snapshot for "last interval" analysis: +snapshot.add(total); + +// Later, take a new snapshot and diff: +const newSnapshot = createHistogram(); +newSnapshot.add(total); +newSnapshot.subtract(snapshot); +// newSnapshot now contains only the values recorded since the last snapshot +console.log('Recent p99:', newSnapshot.percentile(99)); +``` + ## Examples ### Measuring the duration of async operations diff --git a/lib/internal/histogram.js b/lib/internal/histogram.js index f2cf3835b9a6..c16c894dd147 100644 --- a/lib/internal/histogram.js +++ b/lib/internal/histogram.js @@ -1,13 +1,13 @@ 'use strict'; const { + ArrayIsArray, + Float64Array, Map, - MapPrototypeClear, MapPrototypeEntries, NumberIsNaN, NumberMAX_SAFE_INTEGER, ObjectFromEntries, - ReflectConstruct, Symbol, } = primordials; @@ -40,7 +40,6 @@ const { const kDestroy = Symbol('kDestroy'); const kHandle = Symbol('kHandle'); -const kMap = Symbol('kMap'); const kRecordable = Symbol('kRecordable'); const { @@ -77,6 +76,8 @@ class Histogram { mean: this.mean, exceeds: this.exceeds, stddev: this.stddev, + skewness: this.skewness, + kurtosis: this.kurtosis, count: this.count, percentiles: this.percentiles, }, opts)}`; @@ -102,6 +103,46 @@ class Histogram { return this[kHandle]?.countBigInt(); } + /** + * Returns the probability that a recorded value will exceed `value` + * (the complement of the cumulative distribution function). + * @param {number} value + * @returns {number} A value between 0.0 and 1.0. + */ + ccdf(value) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + validateNumber(value, 'value'); + return 1 - this[kHandle]?.cdf(value); + } + + /** + * Returns the cumulative distribution function (CDF) value for the + * given value, representing the probability that a recorded value + * will be less than or equal to `value`. + * @param {number} value + * @returns {number} A value between 0.0 and 1.0. + */ + cdf(value) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + validateNumber(value, 'value'); + return this[kHandle]?.cdf(value); + } + + /** + * Returns the number of recorded values that fall within the + * equivalent value range of the given value. + * @param {number} value + * @returns {number} + */ + countAt(value) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + validateNumber(value, 'value'); + return this[kHandle]?.countAt(value); + } + /** * @readonly * @type {number} @@ -172,6 +213,81 @@ class Histogram { return this[kHandle]?.exceedsBigInt(); } + /** + * Returns the Kolmogorov-Smirnov test statistic comparing this + * histogram's distribution to another's. Returns a value between + * 0.0 (identical distributions) and 1.0 (completely disjoint). + * @param {Histogram} other + * @returns {number} + */ + ksTest(other) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + if (!isHistogram(other)) + throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other); + return this[kHandle]?.ksTest(other[kHandle]); + } + + /** + * Returns the excess kurtosis of the recorded values, a measure of + * the heaviness of the distribution's tails. A positive value indicates + * heavier tails (more outliers) than a normal distribution. + * @readonly + * @type {number} + */ + get kurtosis() { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + return this[kHandle]?.kurtosis(); + } + + /** + * Returns a {Map} containing the histogram data bucketed into + * linearly-spaced intervals of `stepSize`. + * @param {number} stepSize The width of each linear bucket. + * @returns {Map} + */ + linearBuckets(stepSize) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + validateInteger(stepSize, 'stepSize', 1); + const map = new Map(); + this[kHandle]?.linearBuckets(stepSize, map); + return map; + } + + /** + * Returns a {Map} containing the histogram data bucketed into + * logarithmically-spaced intervals. + * @param {number} firstBucket The value of the first bucket boundary. + * @param {number} base The logarithmic base for bucket width growth. + * @returns {Map} + */ + logBuckets(firstBucket, base) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + validateInteger(firstBucket, 'firstBucket', 1); + validateNumber(base, 'base'); + if (base <= 1) + throw new ERR_OUT_OF_RANGE('base', '> 1', base); + const map = new Map(); + this[kHandle]?.logBuckets(firstBucket, base, map); + return map; + } + + /** + * Returns the skewness of the recorded values, a measure of the + * asymmetry of the distribution. A positive value indicates a + * right-skewed distribution (longer right tail). + * @readonly + * @type {number} + */ + get skewness() { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + return this[kHandle]?.skewness(); + } + /** * @readonly * @type {number} @@ -217,9 +333,9 @@ class Histogram { get percentiles() { if (!isHistogram(this)) throw new ERR_INVALID_THIS('Histogram'); - MapPrototypeClear(this[kMap]); - this[kHandle]?.percentiles(this[kMap]); - return this[kMap]; + const map = new Map(); + this[kHandle]?.percentiles(map); + return map; } /** @@ -229,9 +345,34 @@ class Histogram { get percentilesBigInt() { if (!isHistogram(this)) throw new ERR_INVALID_THIS('Histogram'); - MapPrototypeClear(this[kMap]); - this[kHandle]?.percentilesBigInt(this[kMap]); - return this[kMap]; + const map = new Map(); + this[kHandle]?.percentilesBigInt(map); + return map; + } + + /** + * Returns a {Map} of values at the specified percentiles, computed + * in a single efficient pass over the histogram. + * @param {number[]} percentiles Array of percentile values (0, 100]. + * @returns {Map} + */ + percentilesAt(percentiles) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + if (!ArrayIsArray(percentiles)) + throw new ERR_INVALID_ARG_TYPE('percentiles', 'Array', percentiles); + for (let i = 0; i < percentiles.length; i++) { + validateNumber(percentiles[i], `percentiles[${i}]`); + if (NumberIsNaN(percentiles[i]) || + percentiles[i] <= 0 || percentiles[i] > 100) + throw new ERR_OUT_OF_RANGE( + `percentiles[${i}]`, '> 0 && <= 100', percentiles[i]); + } + const sorted = [...percentiles].sort((a, b) => a - b); + const input = new Float64Array(sorted); + const map = new Map(); + this[kHandle]?.percentilesAt(map, input); + return map; } /** @@ -263,6 +404,8 @@ class Histogram { mean: this.mean, exceeds: this.exceeds, stddev: this.stddev, + skewness: this.skewness, + kurtosis: this.kurtosis, percentiles: ObjectFromEntries(MapPrototypeEntries(this.percentiles)), }; } @@ -303,6 +446,44 @@ class RecordableHistogram extends Histogram { this[kHandle]?.recordDelta(); } + /** + * Records a value with coordinated omission correction, backfilling + * intermediate values at `expectedInterval` steps between the last + * recorded value and `val`. This compensates for measurement gaps + * caused by the system being stalled. + * @param {number|bigint} val The amount to record. + * @param {number|bigint} expectedInterval The expected recording interval. + * @returns {void} + */ + recordCorrected(val, expectedInterval) { + if (this[kRecordable] === undefined) + throw new ERR_INVALID_THIS('RecordableHistogram'); + if (typeof val === 'bigint') { + if (typeof expectedInterval !== 'bigint') + throw new ERR_INVALID_ARG_TYPE( + 'expectedInterval', 'bigint', expectedInterval); + this[kHandle]?.recordCorrected(val, expectedInterval); + return; + } + validateInteger(val, 'val', 1); + validateInteger(expectedInterval, 'expectedInterval', 1); + this[kHandle]?.recordCorrected(val, expectedInterval); + } + + /** + * Subtracts the values of `other` from this histogram. Both + * histograms must have compatible configurations. Counts that would + * become negative are clamped to zero. + * @param {RecordableHistogram} other + */ + subtract(other) { + if (this[kRecordable] === undefined) + throw new ERR_INVALID_THIS('RecordableHistogram'); + if (other[kRecordable] === undefined) + throw new ERR_INVALID_ARG_TYPE('other', 'RecordableHistogram', other); + this[kHandle]?.subtract(other[kHandle]); + } + /** * @param {RecordableHistogram} other */ @@ -328,12 +509,10 @@ class RecordableHistogram extends Histogram { } function ClonedHistogram(handle) { - return ReflectConstruct( - function() { - markTransferMode(this, true, false); - this[kHandle] = handle; - this[kMap] = new Map(); - }, [], Histogram); + const histogram = new Histogram(kSkipThrow); + markTransferMode(histogram, true, false); + histogram[kHandle] = handle; + return histogram; } ClonedHistogram.prototype[kDeserialize] = () => { }; @@ -343,7 +522,6 @@ function ClonedRecordableHistogram(handle) { markTransferMode(histogram, true, false); histogram[kRecordable] = true; - histogram[kMap] = new Map(); histogram[kHandle] = handle; histogram.constructor = RecordableHistogram; @@ -391,6 +569,6 @@ module.exports = { isHistogram, kDestroy, kHandle, - kMap, + kSkipThrow, createHistogram, }; diff --git a/lib/internal/perf/event_loop_delay.js b/lib/internal/perf/event_loop_delay.js index ebf0017b70df..4d14182c83fa 100644 --- a/lib/internal/perf/event_loop_delay.js +++ b/lib/internal/perf/event_loop_delay.js @@ -1,7 +1,5 @@ 'use strict'; const { - ReflectConstruct, - SafeMap, Symbol, SymbolDispose, } = primordials; @@ -26,7 +24,7 @@ const { const { Histogram, kHandle, - kMap, + kSkipThrow, } = require('internal/histogram'); const { @@ -40,8 +38,11 @@ const { const kEnabled = Symbol('kEnabled'); class ELDHistogram extends Histogram { - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + constructor(skipThrowSymbol = undefined) { + if (skipThrowSymbol !== kSkipThrow) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } + super(skipThrowSymbol); } /** @@ -87,13 +88,11 @@ function monitorEventLoopDelay(options = kEmptyObject) { validateBoolean(samplePerIteration, 'options.samplePerIteration'); validateInteger(resolution, 'options.resolution', 1); - return ReflectConstruct( - function() { - markTransferMode(this, true, false); - this[kEnabled] = false; - this[kHandle] = createELDHistogram(resolution, samplePerIteration); - this[kMap] = new SafeMap(); - }, [], ELDHistogram); + const histogram = new ELDHistogram(kSkipThrow); + markTransferMode(histogram, true, false); + histogram[kEnabled] = false; + histogram[kHandle] = createELDHistogram(resolution, samplePerIteration); + return histogram; } module.exports = monitorEventLoopDelay; diff --git a/src/histogram-inl.h b/src/histogram-inl.h index 3b8712c87879..7c3545f53aad 100644 --- a/src/histogram-inl.h +++ b/src/histogram-inl.h @@ -10,57 +10,80 @@ namespace node { void Histogram::Reset() { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedWriteLock lock(mutex_); hdr_reset(histogram_.get()); exceeds_ = 0; - count_ = 0; prev_ = 0; } double Histogram::Add(const Histogram& other) { - Mutex::ScopedLock lock(mutex_); - count_ += other.count_; - exceeds_ += other.exceeds_; - if (other.prev_ > prev_) - prev_ = other.prev_; - return static_cast(hdr_add(histogram_.get(), other.histogram_.get())); + auto do_add = [&]() { + exceeds_ += other.exceeds_; + if (other.prev_ > prev_) prev_ = other.prev_; + // hdr_add merges all bucket counts and total_count internally. + return static_cast( + hdr_add(histogram_.get(), other.histogram_.get())); + }; + + // When adding a histogram to itself, a single write lock suffices. + if (this == &other) { + RwLock::ScopedWriteLock lock(mutex_); + return do_add(); + } + + // Write-lock this (modified), read-lock other (only read). + // Lock in pointer order to prevent deadlock. + if (this < &other) { + RwLock::ScopedWriteLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_add(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedWriteLock lock2(mutex_); + return do_add(); } size_t Histogram::Count() const { - Mutex::ScopedLock lock(mutex_); - return count_; + RwLock::ScopedReadLock lock(mutex_); + return static_cast(histogram_->total_count); +} + +size_t Histogram::Exceeds() const { + RwLock::ScopedReadLock lock(mutex_); + return exceeds_; } int64_t Histogram::Min() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_min(histogram_.get()); } int64_t Histogram::Max() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_max(histogram_.get()); } double Histogram::Mean() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_mean(histogram_.get()); } double Histogram::Stddev() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_stddev(histogram_.get()); } int64_t Histogram::Percentile(double percentile) const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); CHECK_GT(percentile, 0); CHECK_LE(percentile, 100); return hdr_value_at_percentile(histogram_.get(), percentile); } template -void Histogram::Percentiles(Iterator&& fn) { - Mutex::ScopedLock lock(mutex_); +void Histogram::Percentiles(Iterator&& fn) const { + RwLock::ScopedReadLock lock(mutex_); hdr_iter iter; hdr_iter_percentile_init(&iter, histogram_.get(), 1); while (hdr_iter_next(&iter)) { @@ -69,37 +92,66 @@ void Histogram::Percentiles(Iterator&& fn) { } } +int64_t Histogram::CountAt(int64_t value) const { + RwLock::ScopedReadLock lock(mutex_); + return hdr_count_at_value(histogram_.get(), value); +} + +bool Histogram::RecordCorrected(int64_t value, int64_t expected_interval) { + RwLock::ScopedWriteLock lock(mutex_); + bool recorded = + hdr_record_corrected_value(histogram_.get(), value, expected_interval); + if (!recorded) exceeds_++; + return recorded; +} + bool Histogram::Record(int64_t value) { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedWriteLock lock(mutex_); bool recorded = hdr_record_value(histogram_.get(), value); - if (!recorded) - exceeds_++; - else - count_++; + if (!recorded) exceeds_++; return recorded; } uint64_t Histogram::RecordDelta() { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedWriteLock lock(mutex_); uint64_t time = uv_hrtime(); int64_t delta = 0; if (prev_ > 0) { CHECK_GE(time, prev_); delta = time - prev_; - if (hdr_record_value(histogram_.get(), delta)) - count_++; - else - exceeds_++; + if (!hdr_record_value(histogram_.get(), delta)) exceeds_++; } prev_ = time; return delta; } size_t Histogram::GetMemorySize() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_get_memory_size(histogram_.get()); } +template +void Histogram::LinearBuckets(int64_t step_size, Iterator&& fn) const { + RwLock::ScopedReadLock lock(mutex_); + hdr_iter iter; + hdr_iter_linear_init(&iter, histogram_.get(), step_size); + while (hdr_iter_next(&iter)) { + fn(iter.value, iter.specifics.linear.count_added_in_this_iteration_step); + } +} + +template +void Histogram::LogBuckets(int64_t first_bucket, + double log_base, + Iterator&& fn) const { + RwLock::ScopedReadLock lock(mutex_); + hdr_iter iter; + hdr_iter_log_init(&iter, histogram_.get(), first_bucket, log_base); + while (hdr_iter_next(&iter)) { + fn(iter.value, iter.specifics.log.count_added_in_this_iteration_step); + } +} + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/histogram.cc b/src/histogram.cc index 5dd82c305bf7..3aa451685e86 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -7,6 +7,8 @@ #include "node_external_reference.h" #include "util.h" +#include + namespace node { using v8::BigInt; @@ -52,6 +54,169 @@ void Histogram::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("histogram", GetMemorySize()); } +bool Histogram::IsCompatible(const Histogram& other) const { + return histogram_->counts_len == other.histogram_->counts_len && + histogram_->lowest_discernible_value == + other.histogram_->lowest_discernible_value && + histogram_->highest_trackable_value == + other.histogram_->highest_trackable_value && + histogram_->significant_figures == + other.histogram_->significant_figures; +} + +double Histogram::Cdf(int64_t value) const { + RwLock::ScopedReadLock lock(mutex_); + int64_t total = histogram_->total_count; + if (total == 0) return 0.0; + + hdr_iter iter; + hdr_iter_init(&iter, histogram_.get()); + while (hdr_iter_next(&iter)) { + if (iter.highest_equivalent_value >= value) { + return static_cast(iter.cumulative_count) / + static_cast(total); + } + // All recorded data accounted for; remaining buckets are empty. + if (iter.cumulative_count >= total) break; + } + return 1.0; +} + +double Histogram::Skewness() const { + RwLock::ScopedReadLock lock(mutex_); + int64_t total = histogram_->total_count; + if (total < 3) return 0.0; + + // Compute mean in one pass, then variance and skewness in a second + // pass. This avoids calling hdr_stddev (which internally recomputes + // hdr_mean), reducing the total from 4 iterations to 2. + double mean = hdr_mean(histogram_.get()); + + double m2 = 0.0; + double m3 = 0.0; + hdr_iter iter; + hdr_iter_recorded_init(&iter, histogram_.get()); + while (hdr_iter_next(&iter)) { + double dev = static_cast(hdr_median_equivalent_value( + histogram_.get(), iter.value)) - + mean; + double d2 = dev * dev; + m2 += static_cast(iter.count) * d2; + m3 += static_cast(iter.count) * d2 * dev; + } + + double n = static_cast(total); + double variance = m2 / n; + if (variance == 0.0) return 0.0; + double s3 = variance * std::sqrt(variance); // stddev^3 + return (m3 / n) / s3; +} + +double Histogram::Kurtosis() const { + RwLock::ScopedReadLock lock(mutex_); + int64_t total = histogram_->total_count; + if (total < 4) return 0.0; + + // Same single-pass approach as Skewness: compute mean first, then + // variance and excess kurtosis together in one iteration. + double mean = hdr_mean(histogram_.get()); + + double m2 = 0.0; + double m4 = 0.0; + hdr_iter iter; + hdr_iter_recorded_init(&iter, histogram_.get()); + while (hdr_iter_next(&iter)) { + double dev = static_cast(hdr_median_equivalent_value( + histogram_.get(), iter.value)) - + mean; + double d2 = dev * dev; + m2 += static_cast(iter.count) * d2; + m4 += static_cast(iter.count) * d2 * d2; + } + + double n = static_cast(total); + double variance = m2 / n; + if (variance == 0.0) return 0.0; + double s4 = variance * variance; // stddev^4 + return (m4 / n) / s4 - 3.0; +} + +double Histogram::Subtract(const Histogram& other) { + auto do_subtract = [&]() -> double { + int64_t dropped = 0; + int32_t len = + std::min(histogram_->counts_len, other.histogram_->counts_len); + for (int32_t i = 0; i < len; i++) { + int64_t count = histogram_->counts[i] - other.histogram_->counts[i]; + if (count < 0) { + dropped += -count; + count = 0; + } + histogram_->counts[i] = count; + } + hdr_reset_internal_counters(histogram_.get()); + exceeds_ = (exceeds_ > other.exceeds_) ? exceeds_ - other.exceeds_ : 0; + return static_cast(dropped); + }; + + if (this == &other) { + RwLock::ScopedWriteLock lock(mutex_); + return do_subtract(); + } + + if (this < &other) { + RwLock::ScopedWriteLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_subtract(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedWriteLock lock2(mutex_); + return do_subtract(); +} + +double Histogram::KsTest(const Histogram& other) const { + auto do_ks = [&]() -> double { + int64_t n1 = histogram_->total_count; + int64_t n2 = other.histogram_->total_count; + if (n1 == 0 || n2 == 0) return 0.0; + + double max_d = 0.0; + int64_t cum1 = 0, cum2 = 0; + int32_t len = + std::max(histogram_->counts_len, other.histogram_->counts_len); + + for (int32_t i = 0; i < len; i++) { + if (i < histogram_->counts_len) cum1 += histogram_->counts[i]; + if (i < other.histogram_->counts_len) cum2 += other.histogram_->counts[i]; + double cdf1 = static_cast(cum1) / static_cast(n1); + double cdf2 = static_cast(cum2) / static_cast(n2); + double d = cdf1 > cdf2 ? cdf1 - cdf2 : cdf2 - cdf1; + if (d > max_d) max_d = d; + } + return max_d; + }; + + if (this == &other) return 0.0; + + if (this < &other) { + RwLock::ScopedReadLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_ks(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedReadLock lock2(mutex_); + return do_ks(); +} + +void Histogram::PercentilesAt(const double* percentiles, + int64_t* values, + size_t length) const { + RwLock::ScopedReadLock lock(mutex_); + hdr_value_at_percentiles(histogram_.get(), percentiles, values, length); +} + HistogramImpl::HistogramImpl(const Histogram::Options& options) : histogram_(new Histogram(options)) {} @@ -74,6 +239,14 @@ CFunction HistogramImpl::fast_get_stddev_( CFunction::Make(&HistogramImpl::FastGetStddev)); CFunction HistogramImpl::fast_get_percentile_( CFunction::Make(&HistogramImpl::FastGetPercentile)); +CFunction HistogramImpl::fast_get_skewness_( + CFunction::Make(&HistogramImpl::FastGetSkewness)); +CFunction HistogramImpl::fast_get_kurtosis_( + CFunction::Make(&HistogramImpl::FastGetKurtosis)); +CFunction HistogramImpl::fast_get_cdf_( + CFunction::Make(&HistogramImpl::FastGetCdf)); +CFunction HistogramImpl::fast_get_count_at_( + CFunction::Make(&HistogramImpl::FastGetCountAt)); CFunction HistogramBase::fast_record_( CFunction::Make(&HistogramBase::FastRecord)); CFunction HistogramBase::fast_record_delta_( @@ -112,6 +285,17 @@ void HistogramImpl::AddMethods(Isolate* isolate, Local tmpl) { isolate, instance, "stddev", GetStddev, &fast_get_stddev_); SetFastMethodNoSideEffect( isolate, instance, "percentile", GetPercentile, &fast_get_percentile_); + SetFastMethodNoSideEffect( + isolate, instance, "skewness", GetSkewness, &fast_get_skewness_); + SetFastMethodNoSideEffect( + isolate, instance, "kurtosis", GetKurtosis, &fast_get_kurtosis_); + SetFastMethodNoSideEffect(isolate, instance, "cdf", GetCdf, &fast_get_cdf_); + SetFastMethodNoSideEffect( + isolate, instance, "countAt", GetCountAt, &fast_get_count_at_); + SetProtoMethodNoSideEffect(isolate, tmpl, "ksTest", GetKsTest); + SetProtoMethodNoSideEffect(isolate, tmpl, "percentilesAt", GetPercentilesAt); + SetProtoMethodNoSideEffect(isolate, tmpl, "linearBuckets", GetLinearBuckets); + SetProtoMethodNoSideEffect(isolate, tmpl, "logBuckets", GetLogBuckets); SetFastMethod(isolate, instance, "reset", DoReset, &fast_reset_); } @@ -142,6 +326,18 @@ void HistogramImpl::RegisterExternalReferences( registry->Register(fast_get_exceeds_); registry->Register(fast_get_stddev_); registry->Register(fast_get_percentile_); + registry->Register(GetSkewness); + registry->Register(GetKurtosis); + registry->Register(GetCdf); + registry->Register(GetCountAt); + registry->Register(GetKsTest); + registry->Register(GetPercentilesAt); + registry->Register(GetLinearBuckets); + registry->Register(GetLogBuckets); + registry->Register(fast_get_skewness_); + registry->Register(fast_get_kurtosis_); + registry->Register(fast_get_cdf_); + registry->Register(fast_get_count_at_); is_registered = true; } @@ -223,6 +419,39 @@ void HistogramBase::Add(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(count); } +void HistogramBase::Subtract(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); + + CHECK(GetConstructorTemplate(env->isolate_data())->HasInstance(args[0])); + HistogramBase* other; + ASSIGN_OR_RETURN_UNWRAP(&other, args[0]); + + double dropped = (*histogram)->Subtract(*(other->histogram())); + args.GetReturnValue().Set(dropped); +} + +void HistogramBase::RecordCorrected(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_IMPLIES(!args[0]->IsNumber(), args[0]->IsBigInt()); + CHECK_IMPLIES(!args[1]->IsNumber(), args[1]->IsBigInt()); + bool lossless = true; + int64_t value = args[0]->IsBigInt() + ? args[0].As()->Int64Value(&lossless) + : static_cast(args[0].As()->Value()); + if (!lossless || value < 1) + return THROW_ERR_OUT_OF_RANGE(env, "value is out of range"); + int64_t expected_interval = + args[1]->IsBigInt() ? args[1].As()->Int64Value(&lossless) + : static_cast(args[1].As()->Value()); + if (!lossless || expected_interval < 1) + return THROW_ERR_OUT_OF_RANGE(env, "expected_interval is out of range"); + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); + (*histogram)->RecordCorrected(value, expected_interval); +} + BaseObjectPtr HistogramBase::Create( Environment* env, const Histogram::Options& options) { @@ -261,18 +490,22 @@ void HistogramBase::New(const FunctionCallbackInfo& args) { int64_t lowest = 1; int64_t highest = std::numeric_limits::max(); - bool lossless_ignored; + bool lossless = true; if (args[0]->IsNumber()) { lowest = args[0].As()->Value(); } else if (args[0]->IsBigInt()) { - lowest = args[0].As()->Int64Value(&lossless_ignored); + lowest = args[0].As()->Int64Value(&lossless); + if (!lossless) + return THROW_ERR_OUT_OF_RANGE(env, "options.lowest is out of range"); } if (args[1]->IsNumber()) { highest = args[1].As()->Value(); } else if (args[1]->IsBigInt()) { - highest = args[1].As()->Int64Value(&lossless_ignored); + highest = args[1].As()->Int64Value(&lossless); + if (!lossless) + return THROW_ERR_OUT_OF_RANGE(env, "options.highest is out of range"); } int32_t figures = args[2].As()->Value(); @@ -295,6 +528,8 @@ Local HistogramBase::GetConstructorTemplate( SetFastMethod( isolate, instance, "recordDelta", RecordDelta, &fast_record_delta_); SetProtoMethod(isolate, tmpl, "add", Add); + SetProtoMethod(isolate, tmpl, "subtract", Subtract); + SetProtoMethod(isolate, tmpl, "recordCorrected", RecordCorrected); HistogramImpl::AddMethods(isolate, tmpl); isolate_data->set_histogram_ctor_template(tmpl); } @@ -305,8 +540,10 @@ void HistogramBase::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(New); registry->Register(Add); + registry->Register(Subtract); registry->Register(Record); registry->Register(RecordDelta); + registry->Register(RecordCorrected); registry->Register(fast_record_); registry->Register(fast_record_delta_); HistogramImpl::RegisterExternalReferences(registry); @@ -345,11 +582,7 @@ Local IntervalHistogram::GetConstructorTemplate( tmpl = NewFunctionTemplate(isolate, nullptr); tmpl->Inherit(HandleWrap::GetConstructorTemplate(env)); tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram")); - auto instance = tmpl->InstanceTemplate(); - instance->SetInternalFieldCount(IntervalHistogram::kInternalFieldCount); - HistogramImpl::AddMethods(isolate, tmpl); - SetFastMethod(isolate, instance, "start", Start, &fast_start_); - SetFastMethod(isolate, instance, "stop", Stop, &fast_stop_); + InitTemplate(isolate, tmpl, IntervalHistogram::kInternalFieldCount); env->set_intervalhistogram_constructor_template(tmpl); } return tmpl; @@ -364,21 +597,16 @@ void IntervalHistogram::RegisterExternalReferences( HistogramImpl::RegisterExternalReferences(registry); } -IntervalHistogram::IntervalHistogram( - Environment* env, - Local wrap, - AsyncWrap::ProviderType type, - int32_t interval, - std::function on_interval, - const Histogram::Options& options) - : HandleWrap( - env, - wrap, - reinterpret_cast(&timer_), - type), +IntervalHistogram::IntervalHistogram(Environment* env, + Local wrap, + AsyncWrap::ProviderType type, + int32_t interval, + OnInterval on_interval, + const Histogram::Options& options) + : HandleWrap(env, wrap, reinterpret_cast(&timer_), type), HistogramImpl(options), interval_(interval), - on_interval_(std::move(on_interval)) { + on_interval_(on_interval) { MakeWeak(); wrap->SetAlignedPointerInInternalField( HistogramImpl::InternalFields::kImplField, @@ -390,8 +618,9 @@ IntervalHistogram::IntervalHistogram( BaseObjectPtr IntervalHistogram::Create( Environment* env, int32_t interval, - std::function on_interval, - const Histogram::Options& options) { + OnInterval on_interval, + const Histogram::Options& options, + AsyncWrap::ProviderType type) { Local obj; if (!GetConstructorTemplate(env) ->InstanceTemplate() @@ -400,12 +629,7 @@ BaseObjectPtr IntervalHistogram::Create( } return MakeBaseObject( - env, - obj, - AsyncWrap::PROVIDER_ELDHISTOGRAM, - interval, - std::move(on_interval), - options); + env, obj, type, interval, on_interval, options); } void IntervalHistogram::TimerCB(uv_timer_t* handle) { @@ -436,19 +660,11 @@ void IntervalHistogram::OnStop() { uv_timer_stop(&timer_); } -void IntervalHistogram::Start(const FunctionCallbackInfo& args) { - StartHandleHistogram(args.This(), args[0]->IsTrue()); -} - void IntervalHistogram::FastStart(Local receiver, bool reset) { TRACK_V8_FAST_API_CALL("histogram.start"); StartHandleHistogram(receiver, reset); } -void IntervalHistogram::Stop(const FunctionCallbackInfo& args) { - StopHandleHistogram(args.This()); -} - void IntervalHistogram::FastStop(Local receiver) { TRACK_V8_FAST_API_CALL("histogram.stop"); StopHandleHistogram(receiver); @@ -462,11 +678,7 @@ Local IterationHistogram::GetConstructorTemplate( tmpl = NewFunctionTemplate(isolate, nullptr); tmpl->Inherit(HandleWrap::GetConstructorTemplate(env)); tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram")); - auto instance = tmpl->InstanceTemplate(); - instance->SetInternalFieldCount(IterationHistogram::kInternalFieldCount); - HistogramImpl::AddMethods(isolate, tmpl); - SetFastMethod(isolate, instance, "start", Start, &fast_start_); - SetFastMethod(isolate, instance, "stop", Stop, &fast_stop_); + InitTemplate(isolate, tmpl, IterationHistogram::kInternalFieldCount); env->set_iterationhistogram_constructor_template(tmpl); } return tmpl; @@ -497,11 +709,12 @@ IterationHistogram::IterationHistogram(Environment* env, uv_prepare_init(env->event_loop(), &prepare_handle_); uv_unref(reinterpret_cast(&check_handle_)); uv_unref(reinterpret_cast(&prepare_handle_)); - prepare_handle_.data = this; } BaseObjectPtr IterationHistogram::Create( - Environment* env, const Histogram::Options& options) { + Environment* env, + const Histogram::Options& options, + AsyncWrap::ProviderType type) { Local obj; if (!GetConstructorTemplate(env) ->InstanceTemplate() @@ -510,12 +723,12 @@ BaseObjectPtr IterationHistogram::Create( return nullptr; } - return MakeBaseObject( - env, obj, AsyncWrap::PROVIDER_ELDHISTOGRAM, options); + return MakeBaseObject(env, obj, type, options); } void IterationHistogram::PrepareCB(uv_prepare_t* handle) { - IterationHistogram* self = static_cast(handle->data); + IterationHistogram* self = + ContainerOf(&IterationHistogram::prepare_handle_, handle); if (!self->enabled_) return; self->prepare_time_ = uv_hrtime(); self->timeout_ = uv_backend_timeout(handle->loop); @@ -572,19 +785,11 @@ void IterationHistogram::Close(Local close_callback) { uv_close(reinterpret_cast(&prepare_handle_), nullptr); } -void IterationHistogram::Start(const FunctionCallbackInfo& args) { - StartHandleHistogram(args.This(), args[0]->IsTrue()); -} - void IterationHistogram::FastStart(Local receiver, bool reset) { TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.start"); StartHandleHistogram(receiver, reset); } -void IterationHistogram::Stop(const FunctionCallbackInfo& args) { - StopHandleHistogram(args.This()); -} - void IterationHistogram::FastStop(Local receiver) { TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.stop"); StopHandleHistogram(receiver); @@ -670,12 +875,19 @@ void HistogramImpl::GetPercentiles(const FunctionCallbackInfo& args) { HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); CHECK(args[0]->IsMap()); Local map = args[0].As(); - (*histogram)->Percentiles([map, env](double key, int64_t value) { - USE(map->Set( - env->context(), - Number::New(env->isolate(), key), - Number::New(env->isolate(), static_cast(value)))); + + // Collect percentile data under the histogram lock, then populate the + // V8 Map after releasing it to avoid V8 allocations under the lock. + std::vector> entries; + (*histogram)->Percentiles([&entries](double key, int64_t value) { + entries.emplace_back(key, value); }); + for (const auto& entry : entries) { + USE(map->Set( + env->context(), + Number::New(env->isolate(), entry.first), + Number::New(env->isolate(), static_cast(entry.second)))); + } } void HistogramImpl::GetPercentilesBigInt( @@ -684,12 +896,16 @@ void HistogramImpl::GetPercentilesBigInt( HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); CHECK(args[0]->IsMap()); Local map = args[0].As(); - (*histogram)->Percentiles([map, env](double key, int64_t value) { - USE(map->Set( - env->context(), - Number::New(env->isolate(), key), - BigInt::New(env->isolate(), value))); + + std::vector> entries; + (*histogram)->Percentiles([&entries](double key, int64_t value) { + entries.emplace_back(key, value); }); + for (const auto& entry : entries) { + USE(map->Set(env->context(), + Number::New(env->isolate(), entry.first), + BigInt::New(env->isolate(), entry.second))); + } } void HistogramImpl::DoReset(const FunctionCallbackInfo& args) { @@ -746,6 +962,129 @@ double HistogramImpl::FastGetPercentile(Local receiver, return static_cast((*histogram)->Percentile(percentile)); } +void HistogramImpl::GetSkewness(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + args.GetReturnValue().Set((*histogram)->Skewness()); +} + +double HistogramImpl::FastGetSkewness(Local receiver) { + TRACK_V8_FAST_API_CALL("histogram.skewness"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->Skewness(); +} + +void HistogramImpl::GetKurtosis(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + args.GetReturnValue().Set((*histogram)->Kurtosis()); +} + +double HistogramImpl::FastGetKurtosis(Local receiver) { + TRACK_V8_FAST_API_CALL("histogram.kurtosis"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->Kurtosis(); +} + +void HistogramImpl::GetCdf(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + int64_t value = static_cast(args[0].As()->Value()); + args.GetReturnValue().Set((*histogram)->Cdf(value)); +} + +double HistogramImpl::FastGetCdf(Local receiver, const int64_t value) { + TRACK_V8_FAST_API_CALL("histogram.cdf"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->Cdf(value); +} + +void HistogramImpl::GetCountAt(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + int64_t value = static_cast(args[0].As()->Value()); + double count = static_cast((*histogram)->CountAt(value)); + args.GetReturnValue().Set(count); +} + +double HistogramImpl::FastGetCountAt(Local receiver, + const int64_t value) { + TRACK_V8_FAST_API_CALL("histogram.countAt"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return static_cast((*histogram)->CountAt(value)); +} + +void HistogramImpl::GetKsTest(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + HistogramImpl* other = HistogramImpl::FromJSObject(args[0]); + args.GetReturnValue().Set((*histogram)->KsTest(*(other->histogram()))); +} + +void HistogramImpl::GetPercentilesAt(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsMap()); + Local map = args[0].As(); + CHECK(args[1]->IsFloat64Array()); + Local input = args[1].As(); + size_t length = input->Length(); + auto backing = input->Buffer()->GetBackingStore(); + double* percentiles = reinterpret_cast( + static_cast(backing->Data()) + input->ByteOffset()); + + std::vector values(length); + (*histogram)->PercentilesAt(percentiles, values.data(), length); + + for (size_t i = 0; i < length; i++) { + USE(map->Set(env->context(), + Number::New(env->isolate(), percentiles[i]), + Number::New(env->isolate(), static_cast(values[i])))); + } +} + +void HistogramImpl::GetLinearBuckets(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsMap()); + int64_t step_size = static_cast(args[0].As()->Value()); + Local map = args[1].As(); + + std::vector> entries; + (*histogram) + ->LinearBuckets(step_size, [&entries](int64_t value, int64_t count) { + entries.emplace_back(value, count); + }); + for (const auto& entry : entries) { + USE(map->Set( + env->context(), + Number::New(env->isolate(), static_cast(entry.first)), + Number::New(env->isolate(), static_cast(entry.second)))); + } +} + +void HistogramImpl::GetLogBuckets(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsNumber()); + CHECK(args[2]->IsMap()); + int64_t first_bucket = static_cast(args[0].As()->Value()); + double log_base = args[1].As()->Value(); + Local map = args[2].As(); + + std::vector> entries; + (*histogram) + ->LogBuckets( + first_bucket, log_base, [&entries](int64_t value, int64_t count) { + entries.emplace_back(value, count); + }); + for (const auto& entry : entries) { + USE(map->Set( + env->context(), + Number::New(env->isolate(), static_cast(entry.first)), + Number::New(env->isolate(), static_cast(entry.second)))); + } +} + HistogramImpl* HistogramImpl::FromJSObject(Local value) { auto obj = value.As(); DCHECK_GE(obj->InternalFieldCount(), HistogramImpl::kInternalFieldCount); diff --git a/src/histogram.h b/src/histogram.h index b9f968e8347c..5fbffa2a4879 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -11,10 +11,7 @@ #include "uv.h" #include "v8.h" -#include #include -#include -#include namespace node { @@ -45,7 +42,7 @@ class Histogram : public MemoryRetainer { inline double Mean() const; inline double Stddev() const; inline int64_t Percentile(double percentile) const; - inline size_t Exceeds() const { return exceeds_; } + inline size_t Exceeds() const; inline size_t Count() const; inline uint64_t RecordDelta(); @@ -55,10 +52,31 @@ class Histogram : public MemoryRetainer { // Iterator is a function type that takes two doubles as argument, one for // percentile and one for the value at that percentile. template - inline void Percentiles(Iterator&& fn); + inline void Percentiles(Iterator&& fn) const; inline size_t GetMemorySize() const; + // Analysis methods + inline int64_t CountAt(int64_t value) const; + double Cdf(int64_t value) const; + double Skewness() const; + double Kurtosis() const; + double KsTest(const Histogram& other) const; + double Subtract(const Histogram& other); + void PercentilesAt(const double* percentiles, + int64_t* values, + size_t length) const; + + inline bool RecordCorrected(int64_t value, int64_t expected_interval); + + template + void LinearBuckets(int64_t step_size, Iterator&& fn) const; + + template + void LogBuckets(int64_t first_bucket, double log_base, Iterator&& fn) const; + + bool IsCompatible(const Histogram& other) const; + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Histogram) SET_SELF_SIZE(Histogram) @@ -68,8 +86,7 @@ class Histogram : public MemoryRetainer { HistogramPointer histogram_; uint64_t prev_ = 0; size_t exceeds_ = 0; - size_t count_ = 0; - Mutex mutex_; + RwLock mutex_; }; class HistogramImpl { @@ -106,6 +123,15 @@ class HistogramImpl { static void GetPercentilesBigInt( const v8::FunctionCallbackInfo& args); + static void GetSkewness(const v8::FunctionCallbackInfo& args); + static void GetKurtosis(const v8::FunctionCallbackInfo& args); + static void GetCdf(const v8::FunctionCallbackInfo& args); + static void GetCountAt(const v8::FunctionCallbackInfo& args); + static void GetKsTest(const v8::FunctionCallbackInfo& args); + static void GetPercentilesAt(const v8::FunctionCallbackInfo& args); + static void GetLinearBuckets(const v8::FunctionCallbackInfo& args); + static void GetLogBuckets(const v8::FunctionCallbackInfo& args); + static void FastReset(v8::Local receiver); static double FastGetCount(v8::Local receiver); static double FastGetMin(v8::Local receiver); @@ -115,6 +141,11 @@ class HistogramImpl { static double FastGetStddev(v8::Local receiver); static double FastGetPercentile(v8::Local receiver, const double percentile); + static double FastGetSkewness(v8::Local receiver); + static double FastGetKurtosis(v8::Local receiver); + static double FastGetCdf(v8::Local receiver, const int64_t value); + static double FastGetCountAt(v8::Local receiver, + const int64_t value); static void AddMethods(v8::Isolate* isolate, v8::Local tmpl); @@ -134,6 +165,10 @@ class HistogramImpl { static v8::CFunction fast_get_exceeds_; static v8::CFunction fast_get_stddev_; static v8::CFunction fast_get_percentile_; + static v8::CFunction fast_get_skewness_; + static v8::CFunction fast_get_kurtosis_; + static v8::CFunction fast_get_cdf_; + static v8::CFunction fast_get_count_at_; }; class HistogramBase final : public BaseObject, public HistogramImpl { @@ -165,7 +200,9 @@ class HistogramBase final : public BaseObject, public HistogramImpl { static void Record(const v8::FunctionCallbackInfo& args); static void RecordDelta(const v8::FunctionCallbackInfo& args); + static void RecordCorrected(const v8::FunctionCallbackInfo& args); static void Add(const v8::FunctionCallbackInfo& args); + static void Subtract(const v8::FunctionCallbackInfo& args); static void FastRecord(v8::Local receiver, const int64_t value); static void FastRecordDelta(v8::Local receiver); @@ -211,17 +248,48 @@ class HistogramBase final : public BaseObject, public HistogramImpl { static v8::CFunction fast_record_delta_; }; -class IntervalHistogram final : public HandleWrap, public HistogramImpl { +// CRTP mixin for HandleWrap-based histograms with start/stop support. +// Provides: StartFlags enum, Start/Stop slow-path handlers, enabled_ flag, +// and InitTemplate (shared GetConstructorTemplate body). +// Derived must provide: fast_start_, fast_stop_ (static CFunction), +// FastStart, FastStop, OnStart, OnStop. +template +class HandleHistogramMixin { + public: + enum class StartFlags { NONE, RESET }; + + static void Start(const v8::FunctionCallbackInfo& args) { + StartHandleHistogram(args.This(), args[0]->IsTrue()); + } + + static void Stop(const v8::FunctionCallbackInfo& args) { + StopHandleHistogram(args.This()); + } + + protected: + static void InitTemplate(v8::Isolate* isolate, + v8::Local tmpl, + uint32_t internal_field_count) { + auto instance = tmpl->InstanceTemplate(); + instance->SetInternalFieldCount(internal_field_count); + HistogramImpl::AddMethods(isolate, tmpl); + SetFastMethod(isolate, instance, "start", Start, &Derived::fast_start_); + SetFastMethod(isolate, instance, "stop", Stop, &Derived::fast_stop_); + } + + bool enabled_ = false; +}; + +class IntervalHistogram final : public HandleWrap, + public HistogramImpl, + public HandleHistogramMixin { public: enum InternalFields { kInternalFieldCount = std::max( HandleWrap::kInternalFieldCount, HistogramImpl::kInternalFieldCount), }; - enum class StartFlags { - NONE, - RESET - }; + using OnInterval = void (*)(Histogram&); static void RegisterExternalReferences(ExternalReferenceRegistry* registry); @@ -231,19 +299,16 @@ class IntervalHistogram final : public HandleWrap, public HistogramImpl { static BaseObjectPtr Create( Environment* env, int32_t interval, - std::function on_interval, - const Histogram::Options& options); - - IntervalHistogram( - Environment* env, - v8::Local wrap, - AsyncWrap::ProviderType type, - int32_t interval, - std::function on_interval, - const Histogram::Options& options = Histogram::Options {}); + OnInterval on_interval, + const Histogram::Options& options, + AsyncWrap::ProviderType type = AsyncWrap::PROVIDER_ELDHISTOGRAM); - static void Start(const v8::FunctionCallbackInfo& args); - static void Stop(const v8::FunctionCallbackInfo& args); + IntervalHistogram(Environment* env, + v8::Local wrap, + AsyncWrap::ProviderType type, + int32_t interval, + OnInterval on_interval, + const Histogram::Options& options = Histogram::Options{}); static void FastStart(v8::Local receiver, bool reset); static void FastStop(v8::Local receiver); @@ -262,45 +327,45 @@ class IntervalHistogram final : public HandleWrap, public HistogramImpl { void OnStart(StartFlags flags = StartFlags::RESET); void OnStop(); + friend class HandleHistogramMixin; template friend void StartHandleHistogram(v8::Local, bool); template friend void StopHandleHistogram(v8::Local); - bool enabled_ = false; int32_t interval_ = 0; - std::function on_interval_; + OnInterval on_interval_ = nullptr; uv_timer_t timer_; static v8::CFunction fast_start_; static v8::CFunction fast_stop_; }; -class IterationHistogram final : public HandleWrap, public HistogramImpl { +class IterationHistogram final + : public HandleWrap, + public HistogramImpl, + public HandleHistogramMixin { public: enum InternalFields { kInternalFieldCount = std::max( HandleWrap::kInternalFieldCount, HistogramImpl::kInternalFieldCount), }; - enum class StartFlags { NONE, RESET }; - static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static v8::Local GetConstructorTemplate( Environment* env); static BaseObjectPtr Create( - Environment* env, const Histogram::Options& options); + Environment* env, + const Histogram::Options& options, + AsyncWrap::ProviderType type = AsyncWrap::PROVIDER_ELDHISTOGRAM); IterationHistogram(Environment* env, v8::Local wrap, AsyncWrap::ProviderType type, const Histogram::Options& options = Histogram::Options{}); - static void Start(const v8::FunctionCallbackInfo& args); - static void Stop(const v8::FunctionCallbackInfo& args); - static void FastStart(v8::Local receiver, bool reset); static void FastStop(v8::Local receiver); @@ -322,12 +387,12 @@ class IterationHistogram final : public HandleWrap, public HistogramImpl { void OnStart(StartFlags flags = StartFlags::RESET); void OnStop(); + friend class HandleHistogramMixin; template friend void StartHandleHistogram(v8::Local, bool); template friend void StopHandleHistogram(v8::Local); - bool enabled_ = false; uv_prepare_t prepare_handle_; uv_check_t check_handle_; uint64_t prepare_time_ = 0; diff --git a/test/parallel/test-perf-hooks-histogram-analysis.js b/test/parallel/test-perf-hooks-histogram-analysis.js new file mode 100644 index 000000000000..acc2b5a7eb2d --- /dev/null +++ b/test/parallel/test-perf-hooks-histogram-analysis.js @@ -0,0 +1,501 @@ +// Flags: --expose-internals --no-warnings --allow-natives-syntax +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { createHistogram } = require('perf_hooks'); +const { internalBinding } = require('internal/test/binding'); +const { inspect } = require('util'); + +// --------------------------------------------------------------------------- +// cdf(value) — cumulative distribution function +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Empty histogram returns 0 + assert.strictEqual(h.cdf(1), 0); + + for (let i = 1; i <= 5; i++) h.record(i); + + // Below min → 0 + assert.strictEqual(h.cdf(0), 0); + + // At or above some values → monotonically increasing + assert.ok(h.cdf(1) > 0); + assert.ok(h.cdf(3) >= h.cdf(1)); + assert.ok(h.cdf(5) >= h.cdf(3)); + + // Well above max → 1.0 + assert.strictEqual(h.cdf(1000000), 1.0); + + // Validation + assert.throws(() => h.cdf('hello'), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.cdf(), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.cdf(undefined), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// ccdf(value) — complementary CDF = 1 - cdf +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Empty: cdf=0 so ccdf=1 + assert.strictEqual(h.ccdf(1), 1); + + for (let i = 1; i <= 5; i++) h.record(i); + + // CCDF + CDF === 1 for all values + for (const v of [0, 1, 3, 5, 1000000]) { + const sum = h.ccdf(v) + h.cdf(v); + assert.ok(Math.abs(sum - 1) < 1e-10, `ccdf(${v})+cdf(${v})=${sum}`); + } + + // Well above max → 0 + assert.strictEqual(h.ccdf(1000000), 0); + + // Validation + assert.throws(() => h.ccdf('hello'), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// countAt(value) — count in equivalent bucket +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Empty → 0 + assert.strictEqual(h.countAt(1), 0); + + h.record(1); + h.record(1); + h.record(1); + h.record(100); + + assert.strictEqual(h.countAt(1), 3); + assert.strictEqual(h.countAt(100), 1); + assert.strictEqual(h.countAt(999999), 0); + + // Validation + assert.throws(() => h.countAt('hello'), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.countAt(), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// skewness getter +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Too few values returns 0 + assert.strictEqual(h.skewness, 0); + h.record(1); + assert.strictEqual(h.skewness, 0); + h.record(2); + assert.strictEqual(h.skewness, 0); + + // With 3+ values, returns a number + h.record(3); + assert.strictEqual(typeof h.skewness, 'number'); + assert.ok(!Number.isNaN(h.skewness)); + + // Right-skewed distribution → positive skewness + const right = createHistogram(); + for (let i = 0; i < 100; i++) right.record(1); + for (let i = 0; i < 10; i++) right.record(10000); + assert.ok(right.skewness > 0); + + // Appears in inspect output + assert.ok(inspect(right, { depth: null }).includes('skewness')); + + // Appears in toJSON + const json = right.toJSON(); + assert.ok('skewness' in json); + assert.strictEqual(typeof json.skewness, 'number'); + + // Uniform distribution: zero stddev → returns 0 + const uniform = createHistogram(); + for (let i = 0; i < 10; i++) uniform.record(1); + assert.strictEqual(uniform.skewness, 0); +} + +// --------------------------------------------------------------------------- +// kurtosis getter +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Too few values returns 0 + assert.strictEqual(h.kurtosis, 0); + h.record(1); + h.record(2); + h.record(3); + assert.strictEqual(h.kurtosis, 0); + + // With 4+ values, returns a number + h.record(4); + assert.strictEqual(typeof h.kurtosis, 'number'); + assert.ok(!Number.isNaN(h.kurtosis)); + + // Appears in inspect and toJSON + const h2 = createHistogram(); + for (let i = 1; i <= 100; i++) h2.record(i); + assert.ok(inspect(h2, { depth: null }).includes('kurtosis')); + const json = h2.toJSON(); + assert.ok('kurtosis' in json); + assert.strictEqual(typeof json.kurtosis, 'number'); + + // Uniform distribution: zero stddev → returns 0 + const uniform = createHistogram(); + for (let i = 0; i < 10; i++) uniform.record(1); + assert.strictEqual(uniform.kurtosis, 0); +} + +// --------------------------------------------------------------------------- +// ksTest(other) — Kolmogorov-Smirnov D-statistic +// --------------------------------------------------------------------------- +{ + const h1 = createHistogram(); + const h2 = createHistogram(); + + // Both empty → 0 + assert.strictEqual(h1.ksTest(h2), 0); + + // Identical distributions → 0 + for (let i = 1; i <= 100; i++) { h1.record(i); h2.record(i); } + assert.strictEqual(h1.ksTest(h2), 0); + + // Same histogram against itself → 0 + assert.strictEqual(h1.ksTest(h1), 0); + + // Different distributions → D > 0 + const h3 = createHistogram(); + for (let i = 1000; i <= 2000; i++) h3.record(i); + const d = h1.ksTest(h3); + assert.ok(d > 0); + assert.ok(d <= 1); + + // Symmetry: D(a,b) === D(b,a) + assert.strictEqual(h1.ksTest(h3), h3.ksTest(h1)); + + // Completely disjoint → D close to 1 + const hLow = createHistogram(); + const hHigh = createHistogram(); + for (let i = 0; i < 100; i++) hLow.record(1); + for (let i = 0; i < 100; i++) hHigh.record(100000); + assert.ok(hLow.ksTest(hHigh) > 0.9); + + // One empty → 0 + const empty = createHistogram(); + assert.strictEqual(h1.ksTest(empty), 0); + + // Validation: non-histogram throws + assert.throws(() => h1.ksTest('not a histogram'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.ksTest(42), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.ksTest({}), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// percentilesAt(percentiles) — batch percentile query +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + for (let i = 1; i <= 100; i++) h.record(i); + + // Returns a Map + const result = h.percentilesAt([50, 90, 99]); + assert.ok(result instanceof Map); + assert.strictEqual(result.size, 3); + + // Keys are the requested percentiles + assert.ok(result.has(50)); + assert.ok(result.has(90)); + assert.ok(result.has(99)); + + // Values match individual percentile() calls + assert.strictEqual(result.get(50), h.percentile(50)); + assert.strictEqual(result.get(90), h.percentile(90)); + assert.strictEqual(result.get(99), h.percentile(99)); + + // Single element + const single = h.percentilesAt([50]); + assert.strictEqual(single.size, 1); + + // Unsorted input still works (internally sorted) + const unsorted = h.percentilesAt([99, 50, 90]); + assert.strictEqual(unsorted.get(50), h.percentile(50)); + + // Validation + assert.throws(() => h.percentilesAt('not array'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.percentilesAt([0]), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentilesAt([101]), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentilesAt([NaN]), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentilesAt([-1]), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentilesAt(['hello']), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// linearBuckets(stepSize) — linearly-spaced bucket iteration +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + for (let i = 1; i <= 100; i++) h.record(i); + + const buckets = h.linearBuckets(10); + assert.ok(buckets instanceof Map); + assert.ok(buckets.size > 0); + + // All keys and values are numbers + for (const [key, value] of buckets) { + assert.strictEqual(typeof key, 'number'); + assert.strictEqual(typeof value, 'number'); + assert.ok(value >= 0); + } + + // Total count across buckets equals histogram count + let total = 0; + for (const [, count] of buckets) total += count; + assert.strictEqual(total, h.count); + + // Different step sizes produce different bucket counts + const finer = h.linearBuckets(5); + assert.ok(finer.size >= buckets.size); + + // Validation + assert.throws(() => h.linearBuckets(0), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.linearBuckets(-1), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.linearBuckets('hello'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.linearBuckets(1.5), { code: 'ERR_OUT_OF_RANGE' }); +} + +// --------------------------------------------------------------------------- +// logBuckets(firstBucket, base) — logarithmically-spaced bucket iteration +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + for (let i = 1; i <= 1000; i++) h.record(i); + + const buckets = h.logBuckets(1, 2); + assert.ok(buckets instanceof Map); + assert.ok(buckets.size > 0); + + for (const [key, value] of buckets) { + assert.strictEqual(typeof key, 'number'); + assert.strictEqual(typeof value, 'number'); + assert.ok(value >= 0); + } + + // Total count across buckets equals histogram count + let total = 0; + for (const [, count] of buckets) total += count; + assert.strictEqual(total, h.count); + + // Validation + assert.throws(() => h.logBuckets(0, 2), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets(-1, 2), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets(1, 1), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets(1, 0.5), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets(1, -2), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets('hello', 2), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.logBuckets(1, 'hello'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.logBuckets(1.5, 2), { code: 'ERR_OUT_OF_RANGE' }); +} + +// --------------------------------------------------------------------------- +// subtract(other) — subtract histogram counts +// --------------------------------------------------------------------------- +{ + const h1 = createHistogram(); + const h2 = createHistogram(); + + for (let i = 1; i <= 10; i++) h1.record(i); + for (let i = 1; i <= 5; i++) h2.record(i); + + const countBefore = h1.count; + h1.subtract(h2); + + // Count should decrease + assert.ok(h1.count < countBefore); + + // Subtracting from self zeros out + const h3 = createHistogram(); + for (let i = 1; i <= 10; i++) h3.record(i); + h3.subtract(h3); + assert.strictEqual(h3.count, 0); + + // Clamping: subtracting more than present doesn't go negative + const hSmall = createHistogram(); + const hBig = createHistogram(); + hSmall.record(1); + for (let i = 0; i < 100; i++) hBig.record(1); + hSmall.subtract(hBig); + assert.strictEqual(hSmall.count, 0); + + // Validation + assert.throws(() => h1.subtract('not a histogram'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.subtract(42), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.subtract({}), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// recordCorrected(val, expectedInterval) — coordinated omission correction +// --------------------------------------------------------------------------- +{ + // Basic recording with number args + const h = createHistogram(); + h.recordCorrected(100, 10); + assert.ok(h.count > 0); + + // Should record more values than a plain record (backfilling) + const hPlain = createHistogram(); + hPlain.record(100); + assert.ok(h.count > hPlain.count); + + // BigInt variant + const hBig = createHistogram(); + hBig.recordCorrected(100n, 10n); + assert.ok(hBig.count > 0); + + // Mixed types should throw (bigint val, number interval) + assert.throws(() => h.recordCorrected(100n, 10), + { code: 'ERR_INVALID_ARG_TYPE' }); + + // Validation: non-integer + assert.throws(() => h.recordCorrected('hello', 10), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.recordCorrected(100, 'hello'), + { code: 'ERR_INVALID_ARG_TYPE' }); + + // Out of range + assert.throws(() => h.recordCorrected(0, 10), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.recordCorrected(100, 0), + { code: 'ERR_OUT_OF_RANGE' }); +} + +// --------------------------------------------------------------------------- +// ERR_INVALID_THIS for all new methods on wrong receiver +// --------------------------------------------------------------------------- +{ + const { Histogram } = require('internal/histogram'); + const h = createHistogram(); + const wrongThis = {}; + + // Methods + const methods = [ + ['cdf', [1]], + ['ccdf', [1]], + ['countAt', [1]], + ['ksTest', [h]], + ['linearBuckets', [10]], + ['logBuckets', [1, 2]], + ['percentilesAt', [[50]]], + ]; + + for (const [method, args] of methods) { + assert.throws( + () => Histogram.prototype[method].call(wrongThis, ...args), + { code: 'ERR_INVALID_THIS' }, + `${method} should throw ERR_INVALID_THIS` + ); + } + + // Getters + for (const getter of ['skewness', 'kurtosis']) { + const desc = Object.getOwnPropertyDescriptor( + Histogram.prototype, getter); + assert.throws( + () => desc.get.call(wrongThis), + { code: 'ERR_INVALID_THIS' }, + `${getter} getter should throw ERR_INVALID_THIS` + ); + } +} + +// --------------------------------------------------------------------------- +// Empty histogram edge cases +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + assert.strictEqual(h.cdf(1), 0); + assert.strictEqual(h.ccdf(1), 1); + assert.strictEqual(h.countAt(1), 0); + assert.strictEqual(h.skewness, 0); + assert.strictEqual(h.kurtosis, 0); + + const empty2 = createHistogram(); + assert.strictEqual(h.ksTest(empty2), 0); + + const pctAt = h.percentilesAt([50, 99]); + assert.ok(pctAt instanceof Map); + assert.strictEqual(pctAt.size, 2); + + const linear = h.linearBuckets(10); + assert.ok(linear instanceof Map); + + const log = h.logBuckets(1, 2); + assert.ok(log instanceof Map); +} + +// --------------------------------------------------------------------------- +// Single-value histogram edge cases +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + h.record(42); + + assert.strictEqual(h.skewness, 0); // Needs >= 3 + assert.strictEqual(h.kurtosis, 0); // Needs >= 4 + assert.strictEqual(h.cdf(42), 1); + assert.strictEqual(h.cdf(1), 0); + assert.strictEqual(h.ccdf(42), 0); + assert.strictEqual(h.countAt(42), 1); +} + +// --------------------------------------------------------------------------- +// Fast API call tests for new methods +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + h.record(1); + h.record(100); + + // Prepare cdf and countAt methods for optimization + eval('%PrepareFunctionForOptimization(h.cdf)'); + eval('%PrepareFunctionForOptimization(h.countAt)'); + + // Warmup call + h.cdf(50); + h.countAt(1); + + // Optimize + eval('%OptimizeFunctionOnNextCall(h.cdf)'); + eval('%OptimizeFunctionOnNextCall(h.countAt)'); + + // Fast-path call + h.cdf(50); + h.countAt(1); + + if (common.isDebug) { + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual(getV8FastApiCallCount('histogram.cdf'), 1); + assert.strictEqual(getV8FastApiCallCount('histogram.countAt'), 1); + } +}