#include "histogram.h" // NOLINT(build/include_inline)
#include "base_object-inl.h"
#include "histogram-inl.h"
#include "memory_tracker-inl.h"
#include "node_debug.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "threadpoolwork-inl.h"
#include "util.h"
#include "v8-typed-array.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <numbers>
#include <vector>
namespace node {
using v8::Array;
using v8::ArrayBuffer;
using v8::BigInt;
using v8::CFunction;
using v8::Context;
using v8::Exception;
using v8::FastApiCallbackOptions;
using v8::Float64Array;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::HandleScope;
using v8::Integer;
using v8::Isolate;
using v8::Local;
using v8::Map;
using v8::Name;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::Promise;
using v8::String;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;
template <typename T>
void StartHandleHistogram(Local<Value> receiver, bool reset) {
T* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
histogram->OnStart(reset ? T::StartFlags::RESET : T::StartFlags::NONE);
}
template <typename T>
void StopHandleHistogram(Local<Value> receiver) {
T* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
histogram->OnStop();
}
Histogram::Histogram(HistogramPointer histogram, const Options& options)
: histogram_(std::move(histogram)) {
// alpha = 1 - 2^(-1/halfLife). With halfLife <= 0, EWMA is disabled.
if (options.half_life > 0) {
ewma_alpha_ = 1.0 - std::exp(-std::log(2.0) / options.half_life);
}
threshold_ = options.threshold;
}
std::shared_ptr<Histogram> Histogram::Create(const Options& options) {
hdr_histogram* histogram;
if (hdr_init(options.lowest, options.highest, options.figures, &histogram) !=
0) {
return {};
}
return std::make_shared<Histogram>(HistogramPointer(histogram), options);
}
namespace {
// Copies the recorded data of `source` into `target`, which must have been
// initialized with the same layout. The caller must hold a lock that prevents
// `source` from being modified during the copy.
void CopyRecordedData(hdr_histogram* target, const hdr_histogram* source) {
CHECK_EQ(target->counts_len, source->counts_len);
target->min_value = source->min_value;
target->max_value = source->max_value;
target->normalizing_index_offset = source->normalizing_index_offset;
target->conversion_ratio = source->conversion_ratio;
target->total_count = source->total_count;
std::memcpy(target->counts,
source->counts,
source->counts_len * sizeof(*source->counts));
}
} // namespace
std::shared_ptr<Histogram> Histogram::CreateWithSameLayout() const {
// The layout is fixed when the histogram is created, so it can be read
// without holding the lock.
hdr_histogram* histogram;
if (hdr_init(histogram_->lowest_discernible_value,
histogram_->highest_trackable_value,
histogram_->significant_figures,
&histogram) != 0) {
return {};
}
return std::make_shared<Histogram>(HistogramPointer(histogram), Options{});
}
std::shared_ptr<Histogram> Histogram::Clone() const {
std::shared_ptr<Histogram> clone = CreateWithSameLayout();
if (!clone) return {};
// Every member that holds recorded or statistical state must be copied
// here. The recorded snapshot cache is not copied; the clone builds its own
// on demand.
RwLock::ScopedReadLock lock(mutex_);
CopyRecordedData(clone->histogram_.get(), histogram_.get());
clone->prev_ = prev_;
clone->exceeds_ = exceeds_;
clone->reset_count_ = reset_count_;
clone->ewma_alpha_ = ewma_alpha_;
clone->ewma_mean_ = ewma_mean_;
clone->ewma_variance_ = ewma_variance_;
clone->ewma_initialized_ = ewma_initialized_;
clone->threshold_ = threshold_;
clone->ewma_error_rate_ = ewma_error_rate_;
return clone;
}
std::shared_ptr<Histogram> Histogram::Diff(const Histogram& other,
DiffError* error) const {
// Counts are subtracted index by index, so both histograms must map values
// to the same indexes. None of these fields change after creation.
if (!IsCompatible(other) || histogram_->normalizing_index_offset !=
other.histogram_->normalizing_index_offset) {
*error = DiffError::kIncompatible;
return {};
}
std::shared_ptr<Histogram> diff = CreateWithSameLayout();
if (!diff) {
*error = DiffError::kOutOfMemory;
return {};
}
// Only the recorded values and the exceeds count carry over. EWMA and timing
// state cannot be subtracted.
uint64_t reset_count;
{
RwLock::ScopedReadLock lock(mutex_);
CopyRecordedData(diff->histogram_.get(), histogram_.get());
diff->exceeds_ = exceeds_;
reset_count = reset_count_;
}
// `diff` is not shared yet, so only the lock of `other` is needed from here
// on. Never holding both locks at once avoids lock ordering issues.
RwLock::ScopedReadLock lock(other.mutex_);
if (reset_count != other.reset_count_) {
*error = DiffError::kReset;
return {};
}
if (diff->exceeds_ < other.exceeds_) {
*error = DiffError::kNotEarlier;
return {};
}
hdr_histogram* target = diff->histogram_.get();
const hdr_histogram* source = other.histogram_.get();
for (int32_t i = 0; i < target->counts_len; i++) {
if (target->counts[i] < source->counts[i]) {
*error = DiffError::kNotEarlier;
return {};
}
target->counts[i] -= source->counts[i];
}
diff->exceeds_ -= other.exceeds_;
hdr_reset_internal_counters(target);
*error = DiffError::kNone;
return diff;
}
void Histogram::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackFieldWithSize("histogram", GetMemorySize());
tracker->TrackFieldWithSize("qrde_snapshot",
GetCachedRecordedSnapshotMemorySize());
}
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;
}
Histogram::RecordedSnapshot Histogram::BuildRecordedSnapshot(
const hdr_histogram* histogram) {
RecordedSnapshot snapshot;
snapshot.total_count = histogram->total_count;
if (snapshot.total_count == 0) return snapshot;
const size_t occupied =
std::count_if(histogram->counts,
histogram->counts + histogram->counts_len,
[](int64_t count) { return count != 0; });
snapshot.buckets.reserve(occupied);
hdr_iter iter;
hdr_iter_recorded_init(&iter, histogram);
while (hdr_iter_next(&iter)) {
snapshot.buckets.push_back({
static_cast<double>(iter.median_equivalent_value),
static_cast<double>(
hdr_size_of_equivalent_value_range(histogram, iter.value)),
iter.count,
iter.cumulative_count,
});
}
return snapshot;
}
Histogram::RecordedSnapshotSource Histogram::GetRecordedSnapshotSource(
bool use_cache) const {
RecordedSnapshotSource source;
int64_t lowest;
int64_t highest;
int figures;
{
RwLock::ScopedReadLock lock(mutex_);
source.generation = mutation_generation_;
if (use_cache && recorded_snapshot_cache_) {
source.snapshot = recorded_snapshot_cache_;
source.cache_hit = true;
return source;
}
if (histogram_->total_count == 0) {
source.snapshot = std::make_shared<const RecordedSnapshot>();
return source;
}
lowest = histogram_->lowest_discernible_value;
highest = histogram_->highest_trackable_value;
figures = histogram_->significant_figures;
}
hdr_histogram* copy;
if (hdr_init(lowest, highest, figures, ©) != 0) return source;
source.histogram.reset(copy);
RwLock::ScopedReadLock lock(mutex_);
source.generation = mutation_generation_;
if (use_cache && recorded_snapshot_cache_) {
source.histogram.reset();
source.snapshot = recorded_snapshot_cache_;
source.cache_hit = true;
return source;
}
if (histogram_->total_count == 0) {
source.histogram.reset();
source.snapshot = std::make_shared<const RecordedSnapshot>();
return source;
}
CopyRecordedData(source.histogram.get(), histogram_.get());
return source;
}
void Histogram::CacheRecordedSnapshot(
uint64_t generation, std::shared_ptr<const RecordedSnapshot> snapshot) {
RwLock::ScopedWriteLock lock(mutex_);
if (generation == mutation_generation_) {
recorded_snapshot_cache_ = std::move(snapshot);
}
}
size_t Histogram::GetCachedRecordedSnapshotMemorySize() const {
RwLock::ScopedReadLock lock(mutex_);
if (!recorded_snapshot_cache_) return 0;
return recorded_snapshot_cache_->buckets.capacity() * sizeof(RecordedBucket);
}
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<double>(iter.cumulative_count) /
static_cast<double>(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<double>(hdr_median_equivalent_value(
histogram_.get(), iter.value)) -
mean;
double d2 = dev * dev;
m2 += static_cast<double>(iter.count) * d2;
m3 += static_cast<double>(iter.count) * d2 * dev;
}
double n = static_cast<double>(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<double>(hdr_median_equivalent_value(
histogram_.get(), iter.value)) -
mean;
double d2 = dev * dev;
m2 += static_cast<double>(iter.count) * d2;
m4 += static_cast<double>(iter.count) * d2 * d2;
}
double n = static_cast<double>(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());
InvalidateRecordedSnapshot();
reset_count_++;
exceeds_ = (exceeds_ > other.exceeds_) ? exceeds_ - other.exceeds_ : 0;
return static_cast<double>(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<double>(cum1) / static_cast<double>(n1);
double cdf2 = static_cast<double>(cum2) / static_cast<double>(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);
}
namespace {
// Continued fraction evaluation for the regularized incomplete beta
// function using Lentz's modified method. Reference: Numerical Recipes
// in C, 2nd edition, section 6.4.
static double BetaContinuedFraction(double a, double b, double x) {
constexpr double FPMIN = 1e-30;
constexpr int MAXIT = 200;
constexpr double EPS = 3e-12;
double qab = a + b;
double qap = a + 1.0;
double qam = a - 1.0;
double c = 1.0;
double d = 1.0 - qab * x / qap;
if (std::fabs(d) < FPMIN) d = FPMIN;
d = 1.0 / d;
double h = d;
for (int m = 1; m <= MAXIT; m++) {
int m2 = 2 * m;
// Even step.
double aa = m * (b - m) * x / ((qam + m2) * (a + m2));
d = 1.0 + aa * d;
if (std::fabs(d) < FPMIN) d = FPMIN;
c = 1.0 + aa / c;
if (std::fabs(c) < FPMIN) c = FPMIN;
d = 1.0 / d;
h *= d * c;
// Odd step.
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
d = 1.0 + aa * d;
if (std::fabs(d) < FPMIN) d = FPMIN;
c = 1.0 + aa / c;
if (std::fabs(c) < FPMIN) c = FPMIN;
d = 1.0 / d;
double del = d * c;
h *= del;
if (std::fabs(del - 1.0) <= EPS) break;
}
return h;
}
struct BetaParameters {
double a;
double b;
double log_normalization;
double mean;
double standard_deviation;
double skewness;
double excess_kurtosis;
double log_scale;
double symmetry_point;
bool use_asymptotic_front;
bool use_asymptotic_cdf;
};
static double StirlingCorrection(double x) {
const double inverse = 1.0 / x;
const double inverse_squared = inverse * inverse;
return inverse * (1.0 / 12.0 +
inverse_squared *
(-1.0 / 360.0 +
inverse_squared *
(1.0 / 1260.0 +
inverse_squared *
(-1.0 / 1680.0 + inverse_squared / 1188.0))));
}
static BetaParameters MakeBetaParameters(double a,
double b,
bool approximate_large_shapes) {
const double sum = a + b;
const double mean = a / sum;
const bool use_asymptotic_front =
approximate_large_shapes && a >= 8.0 && b >= 8.0;
// The continued fraction needs progressively more iterations as both
// beta shapes grow. At this concentration the Edgeworth error is smaller
// than the continued-fraction truncation error.
const bool use_asymptotic_cdf =
approximate_large_shapes && a * b / sum >= 25'000.0;
const double variance = mean * (1.0 - mean) / (sum + 1.0);
const double skewness =
2.0 * (b - a) * std::sqrt(sum + 1.0) / ((sum + 2.0) * std::sqrt(a * b));
const double excess_kurtosis =
6.0 * ((a - b) * (a - b) * (sum + 1.0) - a * b * (sum + 2.0)) /
(a * b * (sum + 2.0) * (sum + 3.0));
return {
a,
b,
use_asymptotic_front ? 0.0
: std::lgamma(sum) - std::lgamma(a) - std::lgamma(b),
mean,
std::sqrt(variance),
skewness,
excess_kurtosis,
use_asymptotic_front ? 0.5 * std::log(sum * mean * (1.0 - mean) /
(2.0 * std::numbers::pi)) +
StirlingCorrection(sum) -
StirlingCorrection(a) - StirlingCorrection(b)
: 0.0,
(a + 1.0) / (sum + 2.0),
use_asymptotic_front,
use_asymptotic_cdf,
};
}
static BetaParameters ReflectBetaParameters(const BetaParameters& params) {
return {
params.b,
params.a,
params.log_normalization,
1.0 - params.mean,
params.standard_deviation,
-params.skewness,
params.excess_kurtosis,
params.log_scale,
1.0 - params.symmetry_point,
params.use_asymptotic_front,
params.use_asymptotic_cdf,
};
}
static double BetaFront(const BetaParameters& params, double x) {
if (x <= 0.0 || x >= 1.0) return 0.0;
double log_front;
if (params.use_asymptotic_front) {
log_front = params.a * std::log1p((x - params.mean) / params.mean) +
params.b * std::log1p((params.mean - x) / (1.0 - params.mean)) +
params.log_scale;
} else {
log_front = params.log_normalization + params.a * std::log(x) +
params.b * std::log(1.0 - x);
}
return std::exp(log_front);
}
static double AsymptoticBetaCdf(const BetaParameters& params,
double x,
double* front_out) {
const double z = (x - params.mean) / params.standard_deviation;
const double normal_cdf = 0.5 * std::erfc(-z / std::numbers::sqrt2);
if (std::fabs(z) >= 8.0) {
if (front_out != nullptr) *front_out = 0.0;
return normal_cdf;
}
const double z2 = z * z;
const double z3 = z2 * z;
const double normal_pdf =
std::exp(-0.5 * z2) / std::sqrt(2.0 * std::numbers::pi);
if (front_out != nullptr) {
const double z4 = z2 * z2;
const double z6 = z3 * z3;
const double density_correction =
1.0 + params.skewness / 6.0 * (z3 - 3.0 * z) +
params.excess_kurtosis / 24.0 * (z4 - 6.0 * z2 + 3.0) +
params.skewness * params.skewness / 72.0 *
(z6 - 15.0 * z4 + 45.0 * z2 - 15.0);
*front_out = x * (1.0 - x) * normal_pdf / params.standard_deviation *
std::max(0.0, density_correction);
}
const double correction = params.skewness / 6.0 * (1.0 - z2) -
params.excess_kurtosis / 24.0 * (z3 - 3.0 * z) -
params.skewness * params.skewness / 72.0 *
(z3 * z2 - 10.0 * z3 + 15.0 * z);
return std::clamp(normal_cdf + normal_pdf * correction, 0.0, 1.0);
}
// Regularized incomplete beta function I_x(a, b).
// Returns the probability that a Beta(a,b) random variable is <= x.
static double RegularizedIncompleteBeta(const BetaParameters& params,
double x,
double* front_out = nullptr) {
if (front_out != nullptr) *front_out = 0.0;
if (x <= 0.0) return 0.0;
if (x >= 1.0) return 1.0;
if (params.use_asymptotic_cdf) {
return AsymptoticBetaCdf(params, x, front_out);
}
const double front = BetaFront(params, x);
if (front_out != nullptr) *front_out = front;
// Use the symmetry relation to ensure the continued fraction
// converges in the region where it is most accurate.
if (x < params.symmetry_point) {
return front * BetaContinuedFraction(params.a, params.b, x) / params.a;
}
return 1.0 -
front * BetaContinuedFraction(params.b, params.a, 1.0 - x) / params.b;
}
static double RegularizedIncompleteBeta(double a, double b, double x) {
return RegularizedIncompleteBeta(MakeBetaParameters(a, b, false), x);
}
// Standard normal CDF: Phi(x) = P(Z <= x).
static double NormalCdf(double x) {
return 0.5 * std::erfc(-x * std::numbers::sqrt2 / 2.0);
}
// Student's t-distribution CDF: P(T <= t) for df degrees of freedom.
static double StudentTCdf(double t, double df) {
double squared = t * t;
double denominator = df + squared;
if (squared < df) {
double x = squared / denominator;
double ibeta = RegularizedIncompleteBeta(0.5, df / 2.0, x);
return t >= 0.0 ? 0.5 + 0.5 * ibeta : 0.5 - 0.5 * ibeta;
}
double x = df / denominator;
double ibeta = RegularizedIncompleteBeta(df / 2.0, 0.5, x);
return t >= 0.0 ? 1.0 - 0.5 * ibeta : 0.5 * ibeta;
}
// Positive Student's t-distribution quantile for an upper-tail probability.
// Using the lower tail avoids loss of precision for probabilities near 1.
static double StudentTUpperQuantile(double p, double df) {
double lo = 0.0;
double hi = 1.0;
while (StudentTCdf(-hi, df) > p) hi *= 2.0;
for (int i = 0; i < 100; i++) {
double mid = (lo + hi) / 2.0;
if (StudentTCdf(-mid, df) > p) {
lo = mid;
} else {
hi = mid;
}
}
return (lo + hi) / 2.0;
}
// Binomial CDF: P(X <= k) for X ~ Binomial(n, p).
// Uses the identity P(X <= k) = I_{1-p}(n-k, k+1).
static double BinomialCdf(int64_t k, int64_t n, double p) {
if (k < 0) return 0.0;
if (k >= n) return 1.0;
return RegularizedIncompleteBeta(
static_cast<double>(n - k), static_cast<double>(k + 1), 1.0 - p);
}
struct QrdeResult {
std::vector<double> quantiles;
std::vector<double> densities;
size_t corrections = 0;
};
enum class QrdeDequantization : uint32_t {
kNone,
kHdr,
kAll,
};
static bool QrdeShouldDequantize(const Histogram::RecordedBucket& bucket,
QrdeDequantization dequantization) {
return bucket.count > 1 && (dequantization == QrdeDequantization::kAll ||
(dequantization == QrdeDequantization::kHdr &&
bucket.resolution > 1.0));
}
static std::pair<double, double> QrdeBucketRange(
const Histogram::RecordedSnapshot& snapshot,
size_t index,
QrdeDequantization dequantization) {
const auto& bucket = snapshot.buckets[index];
if (!QrdeShouldDequantize(bucket, dequantization)) {
return {bucket.value, bucket.value};
}
const double half_resolution = bucket.resolution / 2.0;
if (snapshot.buckets.size() == 1) {
return {bucket.value - half_resolution, bucket.value + half_resolution};
}
if (index == 0) {
return {bucket.value, bucket.value + half_resolution};
}
if (index + 1 == snapshot.buckets.size()) {
return {bucket.value - half_resolution, bucket.value};
}
return {bucket.value - half_resolution, bucket.value + half_resolution};
}
static bool QrdeExactSupport(const Histogram::RecordedSnapshot& snapshot,
const BetaParameters& params,
const BetaParameters& reflected,
double count,
size_t* begin_index,
size_t* end_index) {
// Collapsing both tails changes a quantile by at most 2^-79 times the
// histogram range, which is less than 2^-16 over the int64 value domain.
constexpr double tail_mass = 0x1p-80;
size_t lo = 0;
size_t hi = snapshot.buckets.size();
while (lo < hi) {
const size_t mid = lo + (hi - lo) / 2;
const double rank =
static_cast<double>(snapshot.buckets[mid].cumulative_count) / count;
const double cdf = RegularizedIncompleteBeta(params, rank);
if (!std::isfinite(cdf)) return false;
if (cdf < tail_mass) {
lo = mid + 1;
} else {
hi = mid;
}
}
if (lo == snapshot.buckets.size()) return false;
const size_t first = lo;
lo = first;
hi = snapshot.buckets.size();
while (lo < hi) {
const size_t mid = lo + (hi - lo) / 2;
const double remaining_rank =
static_cast<double>(snapshot.total_count -
snapshot.buckets[mid].cumulative_count) /
count;
const double survival =
RegularizedIncompleteBeta(reflected, remaining_rank);
if (!std::isfinite(survival)) return false;
if (survival > tail_mass) {
lo = mid + 1;
} else {
hi = mid;
}
}
if (lo == snapshot.buckets.size() || lo < first) return false;
*begin_index = first;
*end_index = lo + 1;
return true;
}
static double QrdeQuantile(const Histogram::RecordedSnapshot& snapshot,
double p,
QrdeDequantization dequantization) {
const double count = static_cast<double>(snapshot.total_count);
const double a = (count + 1.0) * p;
const double b = (count + 1.0) * (1.0 - p);
const BetaParameters mass_params = MakeBetaParameters(a, b, true);
const BetaParameters reflected_params = ReflectBetaParameters(mass_params);
size_t begin_index = 0;
size_t end_index = snapshot.buckets.size();
if (mass_params.use_asymptotic_cdf) {
// Outside this interval the normal-tail mass is below 2e-33.
constexpr double support_deviations = 12.0;
const double lower_rank = std::max(
0.0,
mass_params.mean - support_deviations * mass_params.standard_deviation);
const double upper_rank = std::min(
1.0,
mass_params.mean + support_deviations * mass_params.standard_deviation);
const auto first = std::lower_bound(
snapshot.buckets.begin(),
snapshot.buckets.end(),
lower_rank * count,
[](const Histogram::RecordedBucket& bucket, double cumulative) {
return static_cast<double>(bucket.cumulative_count) < cumulative;
});
const auto last = std::lower_bound(
first,
snapshot.buckets.end(),
upper_rank * count,
[](const Histogram::RecordedBucket& bucket, double cumulative) {
return static_cast<double>(bucket.cumulative_count) < cumulative;
});
begin_index = first - snapshot.buckets.begin();
if (last != snapshot.buckets.end()) {
end_index = last - snapshot.buckets.begin() + 1;
}
} else if (snapshot.buckets.size() >= 512) {
QrdeExactSupport(snapshot,
mass_params,
reflected_params,
count,
&begin_index,
&end_index);
}
const bool dequantize = dequantization != QrdeDequantization::kNone;
const auto first_range = QrdeBucketRange(snapshot, 0, dequantization);
const auto last_range =
QrdeBucketRange(snapshot, snapshot.buckets.size() - 1, dequantization);
const double lower_endpoint = first_range.first;
const double upper_endpoint = last_range.second;
double previous_cdf = 0.0;
double previous_survival = 0.0;
double previous_front = 0.0;
int64_t previous_count =
begin_index == 0 ? 0 : snapshot.buckets[begin_index - 1].cumulative_count;
if (previous_count != 0) {
const double previous_rank = static_cast<double>(previous_count) / count;
previous_cdf = RegularizedIncompleteBeta(
mass_params, previous_rank, dequantize ? &previous_front : nullptr);
if (!std::isfinite(previous_cdf) ||
(dequantize && !std::isfinite(previous_front))) {
begin_index = 0;
end_index = snapshot.buckets.size();
previous_count = 0;
previous_cdf = 0.0;
previous_front = 0.0;
} else {
previous_cdf = std::clamp(previous_cdf, 0.0, 1.0);
}
}
double quantile = lower_endpoint * previous_cdf;
bool using_survival = false;
for (size_t i = begin_index; i < end_index; i++) {
const auto& bucket = snapshot.buckets[i];
const double u0 = static_cast<double>(previous_count) / count;
const double u1 = static_cast<double>(bucket.cumulative_count) / count;
double front = 0.0;
double mass;
// Use the reflected CDF above the symmetry point so small upper-tail
// interval weights are not lost to subtraction from one.
if (u1 <= mass_params.symmetry_point) {
const double cdf =
std::clamp(RegularizedIncompleteBeta(
mass_params, u1, dequantize ? &front : nullptr),
previous_cdf,
1.0);
mass = cdf - previous_cdf;
previous_cdf = cdf;
} else {
const double remaining_rank =
static_cast<double>(snapshot.total_count - bucket.cumulative_count) /
count;
double survival = RegularizedIncompleteBeta(
reflected_params, remaining_rank, dequantize ? &front : nullptr);
if (using_survival) {
survival = std::clamp(survival, 0.0, previous_survival);
mass = previous_survival - survival;
} else {
survival = std::clamp(survival, 0.0, 1.0 - previous_cdf);
mass = 1.0 - previous_cdf - survival;
using_survival = true;
}
previous_survival = survival;
}
if (!QrdeShouldDequantize(bucket, dequantization)) {
quantile += bucket.value * mass;
} else {
// I_x(a + 1, b) = I_x(a, b) - front / a, so the first moment
// within this rank interval does not require a second beta CDF.
const double sum = a + b;
const double rank_width = static_cast<double>(bucket.count) / count;
const double local_moment =
std::clamp((a / sum - u0) * mass - (front - previous_front) / sum,
0.0,
rank_width * mass);
const auto [lower, upper] = QrdeBucketRange(snapshot, i, dequantization);
quantile += lower * mass + (upper - lower) * local_moment / rank_width;
}
previous_front = front;
previous_count = bucket.cumulative_count;
}
const double remaining_mass =
using_survival ? previous_survival : 1.0 - previous_cdf;
quantile += upper_endpoint * std::clamp(remaining_mass, 0.0, 1.0);
return quantile;
}
static QrdeResult CalculateQrde(const Histogram::RecordedSnapshot& snapshot,
const std::vector<double>& probabilities,
QrdeDequantization dequantization) {
QrdeResult result;
if (snapshot.buckets.empty()) return result;
result.quantiles.resize(probabilities.size());
result.densities.resize(probabilities.size() - 1);
const auto first_range = QrdeBucketRange(snapshot, 0, dequantization);
const auto last_range =
QrdeBucketRange(snapshot, snapshot.buckets.size() - 1, dequantization);
result.quantiles.front() = first_range.first;
result.quantiles.back() = last_range.second;
for (size_t i = 1; i + 1 < probabilities.size(); i++) {
double value = QrdeQuantile(snapshot, probabilities[i], dequantization);
value =
std::clamp(value, result.quantiles.front(), result.quantiles.back());
if (value < result.quantiles[i - 1]) {
value = result.quantiles[i - 1];
result.corrections++;
}
result.quantiles[i] = value;
}
for (size_t i = 0; i < result.densities.size(); i++) {
const double width = result.quantiles[i + 1] - result.quantiles[i];
const double probability_mass = probabilities[i + 1] - probabilities[i];
result.densities[i] = width == 0.0 ? std::numeric_limits<double>::infinity()
: probability_mass / width;
}
return result;
}
static Local<Float64Array> ToFloat64Array(Isolate* isolate,
const std::vector<double>& values) {
auto store =
ArrayBuffer::NewBackingStore(isolate, values.size() * sizeof(double));
if (!values.empty()) {
memcpy(store->Data(), values.data(), values.size() * sizeof(double));
}
Local<ArrayBuffer> buffer = ArrayBuffer::New(isolate, std::move(store));
return Float64Array::New(buffer, 0, values.size());
}
static const char* QrdeDequantizationName(QrdeDequantization dequantization) {
switch (dequantization) {
case QrdeDequantization::kNone:
return "none";
case QrdeDequantization::kHdr:
return "hdr";
case QrdeDequantization::kAll:
return "all";
}
UNREACHABLE();
}
class QrdeJob final : public ThreadPoolWork {
public:
QrdeJob(Environment* env,
Local<Promise::Resolver> resolver,
std::shared_ptr<Histogram> histogram,
Histogram::RecordedSnapshotSource snapshot_source,
std::vector<double> probabilities,
QrdeDequantization dequantization)
: ThreadPoolWork(env, "histogram.qrde"),
histogram_(std::move(histogram)),
snapshot_source_(std::move(snapshot_source)),
probabilities_(std::move(probabilities)),
dequantization_(dequantization) {
resolver_.Reset(env->isolate(), resolver);
}
void DoThreadPoolWork() override {
if (snapshot_source_.snapshot) {
snapshot_ = std::move(snapshot_source_.snapshot);
} else {
snapshot_ = std::make_shared<const Histogram::RecordedSnapshot>(
Histogram::BuildRecordedSnapshot(snapshot_source_.histogram.get()));
snapshot_source_.histogram.reset();
}
if (histogram_) {
histogram_->CacheRecordedSnapshot(snapshot_source_.generation, snapshot_);
histogram_.reset();
}
result_ = CalculateQrde(*snapshot_, probabilities_, dequantization_);
}
void AfterThreadPoolWork(int status) override {
std::unique_ptr<QrdeJob> self(this);
Environment* env = ThreadPoolWork::env();
CHECK(status == 0 || status == UV_ECANCELED);
if (!env->can_call_into_js()) {
resolver_.Reset();
return;
}
Isolate* isolate = env->isolate();
HandleScope handle_scope(isolate);
Context::Scope context_scope(env->context());
InternalCallbackScope callback_scope(
env, Object::New(isolate), {0, 0}, InternalCallbackScope::kNoFlags);
Local<Promise::Resolver> resolver =
Local<Promise::Resolver>::New(isolate, resolver_);
if (status == UV_ECANCELED) {
USE(resolver->Reject(
env->context(),
Exception::Error(OneByteString(isolate, "QRDE job was canceled"))));
resolver_.Reset();
return;
}
Local<Name> names[] = {
FIXED_ONE_BYTE_STRING(isolate, "probabilities"),
FIXED_ONE_BYTE_STRING(isolate, "quantiles"),
FIXED_ONE_BYTE_STRING(isolate, "densities"),
FIXED_ONE_BYTE_STRING(isolate, "count"),
FIXED_ONE_BYTE_STRING(isolate, "bucketCount"),
FIXED_ONE_BYTE_STRING(isolate, "corrections"),
FIXED_ONE_BYTE_STRING(isolate, "dequantize"),
};
Local<Value> values[] = {
ToFloat64Array(isolate, probabilities_),
ToFloat64Array(isolate, result_.quantiles),
ToFloat64Array(isolate, result_.densities),
BigInt::New(isolate, snapshot_->total_count),
Number::New(isolate, static_cast<double>(snapshot_->buckets.size())),
Number::New(isolate, static_cast<double>(result_.corrections)),
OneByteString(isolate, QrdeDequantizationName(dequantization_)),
};
Local<Object> value =
Object::New(isolate, Null(isolate), names, values, arraysize(names));
USE(resolver->Resolve(env->context(), value));
resolver_.Reset();
}
private:
std::shared_ptr<Histogram> histogram_;
Histogram::RecordedSnapshotSource snapshot_source_;
std::shared_ptr<const Histogram::RecordedSnapshot> snapshot_;
const std::vector<double> probabilities_;
const QrdeDequantization dequantization_;
QrdeResult result_;
Global<Promise::Resolver> resolver_;
};
// -----------------------------------------------------------------------
// Minimal CBOR encoder/decoder (RFC 8949) -- just enough types for
// histogram export/import: unsigned int, float64, array, and map.
// -----------------------------------------------------------------------
// CBOR major types (upper 3 bits of the initial byte).
constexpr uint8_t kCborUint = 0 << 5; // Major 0: unsigned integer
constexpr uint8_t kCborArray = 4 << 5; // Major 4: array
constexpr uint8_t kCborMap = 5 << 5; // Major 5: map
constexpr uint8_t kCborFloat64 = 0xfb; // Major 7, additional 27
static void CborWriteUint(std::vector<uint8_t>& out,
uint8_t major,
uint64_t val) {
if (val <= 23) {
out.push_back(major | static_cast<uint8_t>(val));
} else if (val <= 0xff) {
out.push_back(major | 24);
out.push_back(static_cast<uint8_t>(val));
} else if (val <= 0xffff) {
out.push_back(major | 25);
out.push_back(static_cast<uint8_t>(val >> 8));
out.push_back(static_cast<uint8_t>(val));
} else if (val <= 0xffffffff) {
out.push_back(major | 26);
out.push_back(static_cast<uint8_t>(val >> 24));
out.push_back(static_cast<uint8_t>(val >> 16));
out.push_back(static_cast<uint8_t>(val >> 8));
out.push_back(static_cast<uint8_t>(val));
} else {
out.push_back(major | 27);
out.push_back(static_cast<uint8_t>(val >> 56));
out.push_back(static_cast<uint8_t>(val >> 48));
out.push_back(static_cast<uint8_t>(val >> 40));
out.push_back(static_cast<uint8_t>(val >> 32));
out.push_back(static_cast<uint8_t>(val >> 24));
out.push_back(static_cast<uint8_t>(val >> 16));
out.push_back(static_cast<uint8_t>(val >> 8));
out.push_back(static_cast<uint8_t>(val));
}
}
static void CborWriteFloat64(std::vector<uint8_t>& out, double val) {
out.push_back(kCborFloat64);
uint64_t bits;
memcpy(&bits, &val, sizeof(bits));
// Network byte order (big-endian).
out.push_back(static_cast<uint8_t>(bits >> 56));
out.push_back(static_cast<uint8_t>(bits >> 48));
out.push_back(static_cast<uint8_t>(bits >> 40));
out.push_back(static_cast<uint8_t>(bits >> 32));
out.push_back(static_cast<uint8_t>(bits >> 24));
out.push_back(static_cast<uint8_t>(bits >> 16));
out.push_back(static_cast<uint8_t>(bits >> 8));
out.push_back(static_cast<uint8_t>(bits));
}
static bool CborReadUint(const uint8_t*& p, const uint8_t* end, uint64_t* val) {
if (p >= end) return false;
uint8_t info = *p++ & 0x1f;
if (info <= 23) {
*val = info;
} else if (info == 24) {
if (p + 1 > end) return false;
*val = p[0];
p += 1;
} else if (info == 25) {
if (p + 2 > end) return false;
*val = (static_cast<uint64_t>(p[0]) << 8) | p[1];
p += 2;
} else if (info == 26) {
if (p + 4 > end) return false;
*val = (static_cast<uint64_t>(p[0]) << 24) |
(static_cast<uint64_t>(p[1]) << 16) |
(static_cast<uint64_t>(p[2]) << 8) | p[3];
p += 4;
} else if (info == 27) {
if (p + 8 > end) return false;
*val = (static_cast<uint64_t>(p[0]) << 56) |
(static_cast<uint64_t>(p[1]) << 48) |
(static_cast<uint64_t>(p[2]) << 40) |
(static_cast<uint64_t>(p[3]) << 32) |
(static_cast<uint64_t>(p[4]) << 24) |
(static_cast<uint64_t>(p[5]) << 16) |
(static_cast<uint64_t>(p[6]) << 8) | p[7];
p += 8;
} else {
return false; // Indefinite length or reserved -- not supported.
}
return true;
}
static bool CborReadFloat64(const uint8_t*& p,
const uint8_t* end,
double* val) {
if (p >= end || *p != kCborFloat64) return false;
p++;
if (p + 8 > end) return false;
uint64_t bits = (static_cast<uint64_t>(p[0]) << 56) |
(static_cast<uint64_t>(p[1]) << 48) |
(static_cast<uint64_t>(p[2]) << 40) |
(static_cast<uint64_t>(p[3]) << 32) |
(static_cast<uint64_t>(p[4]) << 24) |
(static_cast<uint64_t>(p[5]) << 16) |
(static_cast<uint64_t>(p[6]) << 8) | p[7];
p += 8;
memcpy(val, &bits, sizeof(*val));
return true;
}
// Read the argument of a data item that must have the given major type
// (kCborUint, kCborArray, or kCborMap). CborReadUint() alone decodes the
// argument of any major type.
static bool CborReadArgument(const uint8_t*& p,
const uint8_t* end,
uint8_t major,
uint64_t* val) {
if (p >= end || (*p & 0xe0) != major) return false;
return CborReadUint(p, end, val);
}
// Read an unsigned integer that must fit into a non-negative int64_t.
static bool CborReadInt64(const uint8_t*& p, const uint8_t* end, int64_t* val) {
uint64_t v;
if (!CborReadArgument(p, end, kCborUint, &v) ||
v > static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) {
return false;
}
*val = static_cast<int64_t>(v);
return true;
}
// Read an unsigned integer that must fit into a non-negative int32_t.
static bool CborReadInt32(const uint8_t*& p, const uint8_t* end, int32_t* val) {
uint64_t v;
if (!CborReadArgument(p, end, kCborUint, &v) ||
v > static_cast<uint64_t>(std::numeric_limits<int32_t>::max())) {
return false;
}
*val = static_cast<int32_t>(v);
return true;
}
// Read a value that may be either a uint or float64.
static bool CborReadNumber(const uint8_t*& p, const uint8_t* end, double* val) {
if (p >= end) return false;
if (*p == kCborFloat64) return CborReadFloat64(p, end, val);
uint64_t u;
if (!CborReadArgument(p, end, kCborUint, &u)) return false;
*val = static_cast<double>(u);
return true;
}
// Maximum nesting depth of the values that CborSkipItem() skips over.
constexpr int kCborMaxSkipDepth = 16;
// Skip over one well-formed data item, including any items nested within it.
// Indefinite-length items are not supported.
static bool CborSkipItem(const uint8_t*& p, const uint8_t* end, int depth = 0) {
if (p >= end || depth > kCborMaxSkipDepth) return false;
const uint8_t major = *p >> 5;
const uint8_t info = *p & 0x1f;
if (major == 7) {
// Simple values and floats. Additional information 24 to 27 is followed
// by 1, 2, 4, or 8 bytes; 28 to 30 are reserved, and 31 is a break.
size_t extra;
if (info <= 23) {
extra = 0;
} else if (info <= 27) {
extra = size_t{1} << (info - 24);
} else {
return false;
}
p++;
if (static_cast<size_t>(end - p) < extra) return false;
p += extra;
return true;
}
uint64_t arg;
if (!CborReadUint(p, end, &arg)) return false;
switch (major) {
case 0: // Unsigned integer.
case 1: // Negative integer.
return true;
case 2: // Byte string.
case 3: // Text string.
if (arg > static_cast<uint64_t>(end - p)) return false;
p += arg;
return true;
case 4: // Array.
case 5: { // Map.
// Each item takes at least one byte, which also bounds the loop.
if (arg > static_cast<uint64_t>(end - p)) return false;
const uint64_t items = major == 4 ? arg : arg * 2;
for (uint64_t i = 0; i < items; i++) {
if (!CborSkipItem(p, end, depth + 1)) return false;
}
return true;
}
case 6: // Tag.
return CborSkipItem(p, end, depth + 1);
}
return false;
}
// Histogram export format version. Version 2 has the same layout as
// version 1, but importers ignore unknown keys in version 2 data, so fields
// can be added without changing the version. Version 1 data is imported
// with its original semantics, which reject unknown keys.
constexpr uint64_t kExportVersion = 2;
constexpr uint64_t kStrictExportVersion = 1;
// Integer keys for the top-level CBOR map.
constexpr uint64_t kKeyVersion = 0;
constexpr uint64_t kKeyLowest = 1;
constexpr uint64_t kKeyHighest = 2;
constexpr uint64_t kKeyFigures = 3;
constexpr uint64_t kKeyTotalCount = 4;
constexpr uint64_t kKeyMin = 5;
constexpr uint64_t kKeyMax = 6;
constexpr uint64_t kKeyNormOffset = 7;
constexpr uint64_t kKeyConvRatio = 8;
constexpr uint64_t kKeyCountsLen = 9;
constexpr uint64_t kKeyCounts = 10;
constexpr uint64_t kKeyEwma = 11;
// Integer keys for the EWMA sub-map.
constexpr uint64_t kEwmaAlpha = 0;
constexpr uint64_t kEwmaMean = 1;
constexpr uint64_t kEwmaVariance = 2;
constexpr uint64_t kEwmaErrorRate = 3;
constexpr uint64_t kEwmaThreshold = 4;
} // namespace
Histogram::MeanCIResult Histogram::MeanCI(double confidence) const {
RwLock::ScopedReadLock lock(mutex_);
int64_t count = histogram_->total_count;
double mean = hdr_mean(histogram_.get());
if (count < 2) {
double nan = std::numeric_limits<double>::quiet_NaN();
return {mean, nan, nan};
}
double stddev = hdr_stddev(histogram_.get());
if (stddev == 0.0) return {mean, mean, mean};
// HdrHistogram computes population stddev (divides by N). The confidence
// interval requires sample variance (divides by N-1).
double variance = stddev * stddev * static_cast<double>(count) /
static_cast<double>(count - 1);
double standard_error = std::sqrt(variance / static_cast<double>(count));
double alpha = 1.0 - confidence;
double t_crit =
StudentTUpperQuantile(alpha / 2.0, static_cast<double>(count - 1));
double margin = t_crit * standard_error;
return {mean, mean - margin, mean + margin};
}
Histogram::WelchTestResult Histogram::WelchTest(const Histogram& other,
double confidence) const {
auto do_welch = [&]() -> WelchTestResult {
int64_t n1 = histogram_->total_count;
int64_t n2 = other.histogram_->total_count;
if (n1 < 2 || n2 < 2) return {0, 0, 1, 0, 0};
double mean1 = hdr_mean(histogram_.get());
double mean2 = hdr_mean(other.histogram_.get());
double sd1 = hdr_stddev(histogram_.get());
double sd2 = hdr_stddev(other.histogram_.get());
// HdrHistogram computes population stddev (divides by N).
// Welch's t-test requires sample variance (divides by N-1).
double var1 =
sd1 * sd1 * static_cast<double>(n1) / static_cast<double>(n1 - 1);
double var2 =
sd2 * sd2 * static_cast<double>(n2) / static_cast<double>(n2 - 1);
double se1 = var1 / static_cast<double>(n1);
double se2 = var2 / static_cast<double>(n2);
double se_sum = se1 + se2;
if (se_sum == 0.0) return {0, 0, 1, 0, 0};
double t = (mean1 - mean2) / std::sqrt(se_sum);
// Welch-Satterthwaite degrees of freedom.
double df = (se_sum * se_sum) / (se1 * se1 / static_cast<double>(n1 - 1) +
se2 * se2 / static_cast<double>(n2 - 1));
// Two-tailed p-value.
double p = 2.0 * StudentTCdf(-std::fabs(t), df);
// Confidence interval on the difference of means.
double alpha = 1.0 - confidence;
double t_crit = StudentTUpperQuantile(alpha / 2.0, df);
double margin = t_crit * std::sqrt(se_sum);
double diff = mean1 - mean2;
return {t, df, p, diff - margin, diff + margin};
};
if (this == &other) return {0, 0, 1, 0, 0};
if (this < &other) {
RwLock::ScopedReadLock lock1(mutex_);
RwLock::ScopedReadLock lock2(other.mutex_);
return do_welch();
}
RwLock::ScopedReadLock lock1(other.mutex_);
RwLock::ScopedReadLock lock2(mutex_);
return do_welch();
}
Histogram::MannWhitneyResult Histogram::MannWhitneyTest(
const Histogram& other) const {
auto do_mw = [&]() -> MannWhitneyResult {
int64_t n1 = histogram_->total_count;
int64_t n2 = other.histogram_->total_count;
if (n1 == 0 || n2 == 0) return {0, 0, 1};
// Walk the counts arrays to compute the U statistic.
// At each bucket index, values from histogram 1 at index i "beat"
// all values from histogram 2 at indices < i (concordant pairs).
int32_t len =
std::max(histogram_->counts_len, other.histogram_->counts_len);
// Forward pass: count concordant pairs (h1 values > h2 values).
int64_t cum2 = 0;
double concordant = 0.0;
double tied = 0.0;
for (int32_t i = 0; i < len; i++) {
int64_t c1 = (i < histogram_->counts_len) ? histogram_->counts[i] : 0;
int64_t c2 =
(i < other.histogram_->counts_len) ? other.histogram_->counts[i] : 0;
concordant += static_cast<double>(c1) * static_cast<double>(cum2);
tied += static_cast<double>(c1) * static_cast<double>(c2);
cum2 += c2;
}
// U statistic for sample 1: concordant + half of ties.
double u = concordant + 0.5 * tied;
double dn1 = static_cast<double>(n1);
double dn2 = static_cast<double>(n2);
double mu = dn1 * dn2 / 2.0;
// Tie correction for the variance.
// sigma^2 = n1*n2/12 * (N+1 - sum(t_k^3 - t_k) / (N*(N-1)))
// where t_k is the number of observations tied at rank k.
double n_total = dn1 + dn2;
double tie_correction = 0.0;
for (int32_t i = 0; i < len; i++) {
int64_t c1 = (i < histogram_->counts_len) ? histogram_->counts[i] : 0;
int64_t c2 =
(i < other.histogram_->counts_len) ? other.histogram_->counts[i] : 0;
double tk = static_cast<double>(c1 + c2);
if (tk > 1) {
tie_correction += tk * tk * tk - tk;
}
}
double sigma_sq =
(dn1 * dn2 / 12.0) *
(n_total + 1.0 - tie_correction / (n_total * (n_total - 1.0)));
if (sigma_sq <= 0.0) return {u, 0, 1};
// Continuity-corrected z-score.
double z = (u - mu) / std::sqrt(sigma_sq);
// Two-tailed p-value using normal approximation.
double p = 2.0 * NormalCdf(-std::fabs(z));
return {u, z, p};
};
if (this == &other) return {0, 0, 1};
if (this < &other) {
RwLock::ScopedReadLock lock1(mutex_);
RwLock::ScopedReadLock lock2(other.mutex_);
return do_mw();
}
RwLock::ScopedReadLock lock1(other.mutex_);
RwLock::ScopedReadLock lock2(mutex_);
return do_mw();
}
double Histogram::CohensD(const Histogram& other) const {
auto do_cohens = [&]() -> double {
int64_t n1 = histogram_->total_count;
int64_t n2 = other.histogram_->total_count;
if (n1 < 2 || n2 < 2) return 0.0;
double mean1 = hdr_mean(histogram_.get());
double mean2 = hdr_mean(other.histogram_.get());
double sd1 = hdr_stddev(histogram_.get());
double sd2 = hdr_stddev(other.histogram_.get());
// Convert population variance to sample variance (Bessel's correction).
double var1 =
sd1 * sd1 * static_cast<double>(n1) / static_cast<double>(n1 - 1);
double var2 =
sd2 * sd2 * static_cast<double>(n2) / static_cast<double>(n2 - 1);
double pooled_sd = std::sqrt((static_cast<double>(n1 - 1) * var1 +
static_cast<double>(n2 - 1) * var2) /
static_cast<double>(n1 + n2 - 2));
if (pooled_sd == 0.0) return 0.0;
return (mean1 - mean2) / pooled_sd;
};
if (this == &other) return 0.0;
if (this < &other) {
RwLock::ScopedReadLock lock1(mutex_);
RwLock::ScopedReadLock lock2(other.mutex_);
return do_cohens();
}
RwLock::ScopedReadLock lock1(other.mutex_);
RwLock::ScopedReadLock lock2(mutex_);
return do_cohens();
}
double Histogram::CliffsD(const Histogram& other) const {
auto do_cliffs = [&]() -> double {
int64_t n1 = histogram_->total_count;
int64_t n2 = other.histogram_->total_count;
if (n1 == 0 || n2 == 0) return 0.0;
int32_t len =
std::max(histogram_->counts_len, other.histogram_->counts_len);
// Forward pass: count pairs where h1 value > h2 value (concordant).
int64_t cum2 = 0;
double concordant = 0.0;
double tied = 0.0;
for (int32_t i = 0; i < len; i++) {
int64_t c1 = (i < histogram_->counts_len) ? histogram_->counts[i] : 0;
int64_t c2 =
(i < other.histogram_->counts_len) ? other.histogram_->counts[i] : 0;
concordant += static_cast<double>(c1) * static_cast<double>(cum2);
tied += static_cast<double>(c1) * static_cast<double>(c2);
cum2 += c2;
}
double discordant =
static_cast<double>(n1) * static_cast<double>(n2) - concordant - tied;
return (concordant - discordant) /
(static_cast<double>(n1) * static_cast<double>(n2));
};
if (this == &other) return 0.0;
if (this < &other) {
RwLock::ScopedReadLock lock1(mutex_);
RwLock::ScopedReadLock lock2(other.mutex_);
return do_cliffs();
}
RwLock::ScopedReadLock lock1(other.mutex_);
RwLock::ScopedReadLock lock2(mutex_);
return do_cliffs();
}
Histogram::PercentileCIResult Histogram::PercentileCI(double percentile,
double confidence) const {
RwLock::ScopedReadLock lock(mutex_);
int64_t value = hdr_value_at_percentile(histogram_.get(), percentile);
int64_t n = histogram_->total_count;
if (n < 2) {
return {value, value, value};
}
double p = percentile / 100.0;
double alpha = 1.0 - confidence;
// Lower rank: largest j such that BinomialCdf(j-1, n, p) <= alpha/2.
// Binary search over [0, n].
int64_t lo = 0;
int64_t hi = n;
while (lo < hi) {
int64_t mid = lo + (hi - lo + 1) / 2;
if (BinomialCdf(mid - 1, n, p) <= alpha / 2.0) {
lo = mid;
} else {
hi = mid - 1;
}
}
double lower_pct = static_cast<double>(lo) / static_cast<double>(n) * 100.0;
// Upper rank: smallest k such that BinomialCdf(k-1, n, p) >= 1 - alpha/2.
lo = 0;
hi = n;
while (lo < hi) {
int64_t mid = lo + (hi - lo) / 2;
if (BinomialCdf(mid - 1, n, p) >= 1.0 - alpha / 2.0) {
hi = mid;
} else {
lo = mid + 1;
}
}
double upper_pct = static_cast<double>(lo) / static_cast<double>(n) * 100.0;
int64_t lower_val = hdr_value_at_percentile(histogram_.get(), lower_pct);
int64_t upper_val = hdr_value_at_percentile(histogram_.get(), upper_pct);
return {value, lower_val, upper_val};
}
// Serialize the histogram to a CBOR (RFC 8949) byte sequence. There is no
// standard for serializing histograms, so this format is specific to this
// implementation. But, it has been designed to be compact, extensible, and
// portable across languages and platforms. Should be easily parseable in
// other languages with or without a CBOR library.
//
// The delta-encoded sparse counts is a space-saving optimization in the
// common case.
//
// Layout: a CBOR map with integer keys:
// 0 -> uint format version (currently 2)
// 1 -> uint lowest discernible value
// 2 -> uint highest trackable value
// 3 -> uint significant figures
// 4 -> uint total count
// 5 -> uint min value
// 6 -> uint max value
// 7 -> uint normalizing index offset
// 8 -> float64 conversion ratio
// 9 -> uint counts array length
// 10 -> array delta-encoded sparse counts as flat [delta, count, ...]
// (first delta is the absolute index)
// 11 -> map EWMA state (omitted when alpha = 0):
// 0 -> float64 alpha
// 1 -> float64 mean
// 2 -> float64 variance
// 3 -> float64 error rate
// 4 -> uint threshold
//
// Compatibility: Import() accepts format versions 1 and 2, whose layouts are
// identical. In version 2 data, keys that the importer does not recognize are
// skipped, so new fields can be added without changing the version. Version 1
// data keeps its original semantics, in which unknown keys are rejected. Any
// field may be absent; the total count, min, and max are then derived from
// the counts.
std::vector<uint8_t> Histogram::Export() const {
RwLock::ScopedReadLock lock(mutex_);
// Count non-zero buckets for the sparse encoding.
int32_t non_zero = 0;
for (int32_t i = 0; i < histogram_->counts_len; i++) {
if (histogram_->counts[i] != 0) non_zero++;
}
bool has_ewma = ewma_alpha_ > 0;
uint64_t map_size = has_ewma ? 12 : 11;
std::vector<uint8_t> out;
out.reserve(64 + non_zero * 10);
// Top-level map.
CborWriteUint(out, kCborMap, map_size);
// 0: version
CborWriteUint(out, kCborUint, kKeyVersion);
CborWriteUint(out, kCborUint, kExportVersion);
// 1: lowest
CborWriteUint(out, kCborUint, kKeyLowest);
CborWriteUint(out,
kCborUint,
static_cast<uint64_t>(histogram_->lowest_discernible_value));
// 2: highest
CborWriteUint(out, kCborUint, kKeyHighest);
CborWriteUint(out,
kCborUint,
static_cast<uint64_t>(histogram_->highest_trackable_value));
// 3: figures
CborWriteUint(out, kCborUint, kKeyFigures);
CborWriteUint(
out, kCborUint, static_cast<uint64_t>(histogram_->significant_figures));
// 4: total_count
CborWriteUint(out, kCborUint, kKeyTotalCount);
CborWriteUint(out, kCborUint, static_cast<uint64_t>(histogram_->total_count));
// 5: min
CborWriteUint(out, kCborUint, kKeyMin);
CborWriteUint(out, kCborUint, static_cast<uint64_t>(histogram_->min_value));
// 6: max
CborWriteUint(out, kCborUint, kKeyMax);
CborWriteUint(out, kCborUint, static_cast<uint64_t>(histogram_->max_value));
// 7: normalizing_index_offset
CborWriteUint(out, kCborUint, kKeyNormOffset);
CborWriteUint(out,
kCborUint,
static_cast<uint64_t>(histogram_->normalizing_index_offset));
// 8: conversion_ratio
CborWriteUint(out, kCborUint, kKeyConvRatio);
CborWriteFloat64(out, histogram_->conversion_ratio);
// 9: counts_len
CborWriteUint(out, kCborUint, kKeyCountsLen);
CborWriteUint(out, kCborUint, static_cast<uint64_t>(histogram_->counts_len));
// 10: sparse counts -- array of [delta, count, ...] pairs.
// Indices are delta-encoded: the first value is the absolute index,
// each subsequent value is the difference from the previous index.
CborWriteUint(out, kCborUint, kKeyCounts);
CborWriteUint(out, kCborArray, static_cast<uint64_t>(non_zero) * 2);
int32_t prev_idx = 0;
for (int32_t i = 0; i < histogram_->counts_len; i++) {
if (histogram_->counts[i] != 0) {
CborWriteUint(out, kCborUint, static_cast<uint64_t>(i - prev_idx));
CborWriteUint(
out, kCborUint, static_cast<uint64_t>(histogram_->counts[i]));
prev_idx = i;
}
}
// 11: EWMA state (optional)
if (has_ewma) {
CborWriteUint(out, kCborUint, kKeyEwma);
CborWriteUint(out, kCborMap, 5);
CborWriteUint(out, kCborUint, kEwmaAlpha);
CborWriteFloat64(out, ewma_alpha_);
CborWriteUint(out, kCborUint, kEwmaMean);
CborWriteFloat64(out, ewma_mean_);
CborWriteUint(out, kCborUint, kEwmaVariance);
CborWriteFloat64(out, ewma_variance_);
CborWriteUint(out, kCborUint, kEwmaErrorRate);
CborWriteFloat64(out, ewma_error_rate_);
CborWriteUint(out, kCborUint, kEwmaThreshold);
CborWriteUint(out, kCborUint, static_cast<uint64_t>(threshold_));
}
return out;
}
std::shared_ptr<Histogram> Histogram::Import(const uint8_t* data, size_t len) {
const uint8_t* p = data;
const uint8_t* end = data + len;
// Read top-level map header.
uint64_t map_size;
if (!CborReadArgument(p, end, kCborMap, &map_size)) return nullptr;
int64_t lowest = 1;
int64_t highest = std::numeric_limits<int64_t>::max();
int32_t figures = 3;
int64_t total_count = 0;
int64_t min_value = std::numeric_limits<int64_t>::max();
int64_t max_value = 0;
int32_t norm_offset = 0;
double conv_ratio = 1.0;
int32_t counts_len = 0;
// Data without a version key is imported as version 1.
uint64_t version = kStrictExportVersion;
// Bitsets of the keys read so far, used to reject duplicate keys and to
// tell whether a field was present. All known keys are less than 64.
uint64_t seen_keys = 0;
uint64_t seen_ewma_keys = 0;
// Unknown keys are skipped while parsing, because the version key that
// determines whether they are allowed can appear anywhere in the map.
bool has_unknown_keys = false;
auto mark_seen = [](uint64_t* seen, uint64_t key) {
const uint64_t bit = uint64_t{1} << key;
if (*seen & bit) return false;
*seen |= bit;
return true;
};
auto has_key = [&seen_keys](uint64_t key) {
return (seen_keys & (uint64_t{1} << key)) != 0;
};
// Sparse counts storage.
std::vector<std::pair<int32_t, int64_t>> sparse_counts;
// EWMA state.
double ewma_alpha = 0;
double ewma_mean = 0;
double ewma_variance = 0;
double ewma_error_rate = 0;
int64_t threshold = 0;
for (uint64_t i = 0; i < map_size; i++) {
// Read key (unsigned int).
uint64_t key;
if (!CborReadArgument(p, end, kCborUint, &key)) return nullptr;
if (key > kKeyEwma) {
// Unknown key.
if (!CborSkipItem(p, end)) return nullptr;
has_unknown_keys = true;
continue;
}
if (!mark_seen(&seen_keys, key)) return nullptr; // Duplicate key.
switch (key) {
case kKeyVersion:
if (!CborReadArgument(p, end, kCborUint, &version)) return nullptr;
if (version < kStrictExportVersion || version > kExportVersion) {
return nullptr;
}
break;
case kKeyLowest:
if (!CborReadInt64(p, end, &lowest)) return nullptr;
break;
case kKeyHighest:
if (!CborReadInt64(p, end, &highest)) return nullptr;
break;
case kKeyFigures:
if (!CborReadInt32(p, end, &figures)) return nullptr;
break;
case kKeyTotalCount:
if (!CborReadInt64(p, end, &total_count)) return nullptr;
break;
case kKeyMin:
if (!CborReadInt64(p, end, &min_value)) return nullptr;
break;
case kKeyMax:
if (!CborReadInt64(p, end, &max_value)) return nullptr;
break;
case kKeyNormOffset:
// Reject values that cannot be represented as int32_t; casting them
// would wrap and produce an arbitrary offset.
if (!CborReadInt32(p, end, &norm_offset)) return nullptr;
break;
case kKeyConvRatio:
if (!CborReadNumber(p, end, &conv_ratio)) return nullptr;
break;
case kKeyCountsLen:
if (!CborReadInt32(p, end, &counts_len)) return nullptr;
break;
case kKeyCounts: {
// Array of flat [delta, count, ...] pairs. Indices are
// delta-encoded: accumulate to recover absolute indices.
uint64_t arr_len;
if (!CborReadArgument(p, end, kCborArray, &arr_len)) return nullptr;
if (arr_len % 2 != 0) return nullptr;
// Each element needs at least 1 byte of CBOR encoding, so
// arr_len can't exceed the remaining buffer. Without this
// check, a crafted buffer claiming arr_len=2^60 would cause
// reserve() to OOM-crash before the loop catches the error.
if (arr_len > static_cast<uint64_t>(end - p)) return nullptr;
sparse_counts.reserve(static_cast<size_t>(arr_len / 2));
int64_t acc_idx = 0;
for (uint64_t j = 0; j < arr_len; j += 2) {
int32_t delta;
int64_t cnt;
if (!CborReadInt32(p, end, &delta)) return nullptr;
if (!CborReadInt64(p, end, &cnt)) return nullptr;
// Indices are strictly increasing, so only the first delta (the
// absolute index of the first non-empty bucket) may be zero.
if (j > 0 && delta == 0) return nullptr;
acc_idx += delta;
if (acc_idx > std::numeric_limits<int32_t>::max()) return nullptr;
sparse_counts.emplace_back(static_cast<int32_t>(acc_idx), cnt);
}
break;
}
case kKeyEwma: {
// Sub-map for EWMA state.
uint64_t sub_size;
if (!CborReadArgument(p, end, kCborMap, &sub_size)) return nullptr;
for (uint64_t j = 0; j < sub_size; j++) {
uint64_t sub_key;
if (!CborReadArgument(p, end, kCborUint, &sub_key)) return nullptr;
if (sub_key > kEwmaThreshold) {
// Unknown EWMA key.
if (!CborSkipItem(p, end)) return nullptr;
has_unknown_keys = true;
continue;
}
if (!mark_seen(&seen_ewma_keys, sub_key)) return nullptr;
switch (sub_key) {
case kEwmaAlpha:
if (!CborReadNumber(p, end, &ewma_alpha)) return nullptr;
break;
case kEwmaMean:
if (!CborReadNumber(p, end, &ewma_mean)) return nullptr;
break;
case kEwmaVariance:
if (!CborReadNumber(p, end, &ewma_variance)) return nullptr;
break;
case kEwmaErrorRate:
if (!CborReadNumber(p, end, &ewma_error_rate)) return nullptr;
break;
case kEwmaThreshold:
if (!CborReadInt64(p, end, &threshold)) return nullptr;
break;
}
}
break;
}
}
}
// Version 1 data keeps its original semantics: unknown keys are rejected.
if (version == kStrictExportVersion && has_unknown_keys) return nullptr;
// Reconstruct the histogram.
Options opts;
opts.lowest = lowest;
opts.highest = highest;
opts.figures = figures;
// Compute half_life from alpha: alpha = 1 - 2^(-1/halfLife)
// => halfLife = -1 / log2(1 - alpha)
if (ewma_alpha > 0 && ewma_alpha < 1) {
opts.half_life = -1.0 / std::log2(1.0 - ewma_alpha);
}
opts.threshold = threshold;
auto histogram = Histogram::Create(opts);
if (!histogram) return nullptr;
// Validate counts_len matches what the options produce.
if (histogram->histogram_->counts_len != counts_len) return nullptr;
// The normalization offset must index into the allocated counts array.
// normalize_index() in hdr_histogram.c applies at most one wrap
// adjustment of +/-counts_len, so any offset outside [0, counts_len)
// can leave normalized_index out of bounds and a later record() would
// perform an out-of-bounds write in counts_inc_normalised().
if (norm_offset < 0 || norm_offset >= counts_len) return nullptr;
// Restore counts directly.
int64_t observed_total_count = 0;
for (const auto& [idx, cnt] : sparse_counts) {
if (idx < 0 || idx >= counts_len) return nullptr;
// The counts must add up without overflowing int64_t.
if (cnt > std::numeric_limits<int64_t>::max() - observed_total_count) {
return nullptr;
}
observed_total_count += cnt;
histogram->histogram_->counts[idx] = cnt;
}
histogram->histogram_->normalizing_index_offset = norm_offset;
histogram->histogram_->conversion_ratio = conv_ratio;
// Derive the total count, min, and max from the counts, as
// Histogram::Subtract() does. This keeps the histogram consistent when
// any of them are absent. A total count that is present must match the
// counts. Min and max values that are present are restored as recorded.
hdr_reset_internal_counters(histogram->histogram_.get());
if (has_key(kKeyTotalCount) &&
total_count != histogram->histogram_->total_count) {
return nullptr;
}
if (has_key(kKeyMin)) histogram->histogram_->min_value = min_value;
if (has_key(kKeyMax)) histogram->histogram_->max_value = max_value;
// Restore EWMA state.
if (ewma_alpha > 0) {
histogram->ewma_mean_ = ewma_mean;
histogram->ewma_variance_ = ewma_variance;
histogram->ewma_error_rate_ = ewma_error_rate;
histogram->ewma_initialized_ = true;
}
return histogram;
}
HistogramImpl::HistogramImpl(const Histogram::Options& options)
: histogram_(Histogram::Create(options)) {
CHECK(histogram_);
}
HistogramImpl::HistogramImpl(std::shared_ptr<Histogram> histogram)
: histogram_(std::move(histogram)) {}
CFunction HistogramImpl::fast_reset_(
CFunction::Make(&HistogramImpl::FastReset));
CFunction HistogramImpl::fast_get_count_(
CFunction::Make(&HistogramImpl::FastGetCount));
CFunction HistogramImpl::fast_get_min_(
CFunction::Make(&HistogramImpl::FastGetMin));
CFunction HistogramImpl::fast_get_max_(
CFunction::Make(&HistogramImpl::FastGetMax));
CFunction HistogramImpl::fast_get_mean_(
CFunction::Make(&HistogramImpl::FastGetMean));
CFunction HistogramImpl::fast_get_exceeds_(
CFunction::Make(&HistogramImpl::FastGetExceeds));
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 HistogramImpl::fast_get_ewma_mean_(
CFunction::Make(&HistogramImpl::FastGetEwmaMean));
CFunction HistogramImpl::fast_get_ewma_stddev_(
CFunction::Make(&HistogramImpl::FastGetEwmaStddev));
CFunction HistogramImpl::fast_get_ewma_error_rate_(
CFunction::Make(&HistogramImpl::FastGetEwmaErrorRate));
CFunction HistogramBase::fast_record_(
CFunction::Make(&HistogramBase::FastRecord));
CFunction HistogramBase::fast_record_delta_(
CFunction::Make(&HistogramBase::FastRecordDelta));
CFunction SlidingWindowHistogram::fast_record_(
CFunction::Make(&SlidingWindowHistogram::FastRecord));
CFunction IntervalHistogram::fast_start_(
CFunction::Make(&IntervalHistogram::FastStart));
CFunction IntervalHistogram::fast_stop_(
CFunction::Make(&IntervalHistogram::FastStop));
CFunction IterationHistogram::fast_start_(
CFunction::Make(&IterationHistogram::FastStart));
CFunction IterationHistogram::fast_stop_(
CFunction::Make(&IterationHistogram::FastStop));
void HistogramImpl::AddMethods(Isolate* isolate, Local<FunctionTemplate> tmpl) {
// TODO(@jasnell): The bigint API variations do not yet support fast
// variations since v8 will not return a bigint value from a fast method.
SetProtoMethodNoSideEffect(isolate, tmpl, "countBigInt", GetCountBigInt);
SetProtoMethodNoSideEffect(isolate, tmpl, "exceedsBigInt", GetExceedsBigInt);
SetProtoMethodNoSideEffect(isolate, tmpl, "minBigInt", GetMinBigInt);
SetProtoMethodNoSideEffect(isolate, tmpl, "maxBigInt", GetMaxBigInt);
SetProtoMethodNoSideEffect(
isolate, tmpl, "percentileBigInt", GetPercentileBigInt);
SetProtoMethodNoSideEffect(isolate, tmpl, "percentiles", GetPercentiles);
SetProtoMethodNoSideEffect(
isolate, tmpl, "percentilesBigInt", GetPercentilesBigInt);
auto instance = tmpl->InstanceTemplate();
SetFastMethodNoSideEffect(
isolate, instance, "count", GetCount, &fast_get_count_);
SetFastMethodNoSideEffect(
isolate, instance, "exceeds", GetExceeds, &fast_get_exceeds_);
SetFastMethodNoSideEffect(isolate, instance, "min", GetMin, &fast_get_min_);
SetFastMethodNoSideEffect(isolate, instance, "max", GetMax, &fast_get_max_);
SetFastMethodNoSideEffect(
isolate, instance, "mean", GetMean, &fast_get_mean_);
SetFastMethodNoSideEffect(
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);
SetProtoMethodNoSideEffect(isolate, tmpl, "meanCI", GetMeanCI);
SetProtoMethodNoSideEffect(isolate, tmpl, "welchTest", GetWelchTest);
SetProtoMethodNoSideEffect(
isolate, tmpl, "mannWhitneyTest", GetMannWhitneyTest);
SetProtoMethodNoSideEffect(isolate, tmpl, "cohensD", GetCohensD);
SetProtoMethodNoSideEffect(isolate, tmpl, "cliffsD", GetCliffsD);
SetProtoMethodNoSideEffect(isolate, tmpl, "percentileCI", GetPercentileCI);
SetProtoMethod(isolate, tmpl, "qrde", GetQrde);
SetFastMethodNoSideEffect(
isolate, instance, "ewmaMean", GetEwmaMean, &fast_get_ewma_mean_);
SetFastMethodNoSideEffect(
isolate, instance, "ewmaStddev", GetEwmaStddev, &fast_get_ewma_stddev_);
SetFastMethodNoSideEffect(isolate,
instance,
"ewmaErrorRate",
GetEwmaErrorRate,
&fast_get_ewma_error_rate_);
SetProtoMethodNoSideEffect(isolate, tmpl, "export", DoExport);
SetProtoMethodNoSideEffect(isolate, tmpl, "snapshot", DoSnapshot);
SetProtoMethodNoSideEffect(isolate, tmpl, "diff", DoDiff);
SetProtoMethodNoSideEffect(isolate, tmpl, "resetCount", GetResetCount);
SetFastMethod(isolate, instance, "reset", DoReset, &fast_reset_);
}
void HistogramImpl::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
static bool is_registered = false;
if (is_registered) return;
registry->Register(GetCount);
registry->Register(GetCountBigInt);
registry->Register(GetExceeds);
registry->Register(GetExceedsBigInt);
registry->Register(GetMin);
registry->Register(GetMinBigInt);
registry->Register(GetMax);
registry->Register(GetMaxBigInt);
registry->Register(GetMean);
registry->Register(GetStddev);
registry->Register(GetPercentile);
registry->Register(GetPercentileBigInt);
registry->Register(GetPercentiles);
registry->Register(GetPercentilesBigInt);
registry->Register(DoReset);
registry->Register(fast_reset_);
registry->Register(fast_get_count_);
registry->Register(fast_get_min_);
registry->Register(fast_get_max_);
registry->Register(fast_get_mean_);
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(GetMeanCI);
registry->Register(GetWelchTest);
registry->Register(GetMannWhitneyTest);
registry->Register(GetCohensD);
registry->Register(GetCliffsD);
registry->Register(GetPercentileCI);
registry->Register(GetQrde);
registry->Register(GetEwmaMean);
registry->Register(GetEwmaStddev);
registry->Register(GetEwmaErrorRate);
registry->Register(DoExport);
registry->Register(DoSnapshot);
registry->Register(DoDiff);
registry->Register(GetResetCount);
registry->Register(fast_get_ewma_mean_);
registry->Register(fast_get_ewma_stddev_);
registry->Register(fast_get_ewma_error_rate_);
registry->Register(fast_get_skewness_);
registry->Register(fast_get_kurtosis_);
registry->Register(fast_get_cdf_);
registry->Register(fast_get_count_at_);
is_registered = true;
}
HistogramBase::HistogramBase(Environment* env,
Local<Object> wrap,
const Histogram::Options& options)
: BaseObject(env, wrap), HistogramImpl(options) {
MakeWeak();
wrap->SetAlignedPointerInInternalField(
HistogramImpl::InternalFields::kImplField,
static_cast<HistogramImpl*>(this),
EmbedderDataTag::kDefault);
ReportExternalMemory();
}
HistogramBase::HistogramBase(Environment* env,
Local<Object> wrap,
std::shared_ptr<Histogram> histogram)
: BaseObject(env, wrap), HistogramImpl(std::move(histogram)) {
MakeWeak();
wrap->SetAlignedPointerInInternalField(
HistogramImpl::InternalFields::kImplField,
static_cast<HistogramImpl*>(this),
EmbedderDataTag::kDefault);
ReportExternalMemory();
}
HistogramBase::~HistogramBase() {
env()->external_memory_accounter()->Decrease(env()->isolate(),
external_memory_);
}
// Reports the size of the native histogram to V8 so that the garbage
// collector accounts for it. Every object that refers to a native histogram
// reports its full size, including objects that share one after cloning.
void HistogramBase::ReportExternalMemory() {
external_memory_ = histogram()->GetMemorySize();
env()->external_memory_accounter()->Increase(env()->isolate(),
external_memory_);
}
void HistogramBase::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("histogram", histogram());
}
void HistogramBase::RecordDelta(const FunctionCallbackInfo<Value>& args) {
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
(*histogram)->RecordDelta();
}
void HistogramBase::FastRecordDelta(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.recordDelta");
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
(*histogram)->RecordDelta();
}
void HistogramBase::Record(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_IMPLIES(!args[0]->IsNumber(), args[0]->IsBigInt());
bool lossless = true;
int64_t value = args[0]->IsBigInt()
? args[0].As<BigInt>()->Int64Value(&lossless)
: static_cast<int64_t>(args[0].As<Number>()->Value());
if (!lossless || value < 0)
return THROW_ERR_OUT_OF_RANGE(env, "value is out of range");
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
(*histogram)->Record(value);
}
void HistogramBase::FastRecord(Local<Value> receiver, const int64_t value) {
CHECK_GE(value, 0);
TRACK_V8_FAST_API_CALL("histogram.record");
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
(*histogram)->Record(value);
}
void HistogramBase::Add(const FunctionCallbackInfo<Value>& 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 count = (*histogram)->Add(*(other->histogram()));
args.GetReturnValue().Set(count);
}
void HistogramBase::Subtract(const FunctionCallbackInfo<Value>& 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<Value>& 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<BigInt>()->Int64Value(&lossless)
: static_cast<int64_t>(args[0].As<Number>()->Value());
if (!lossless || value < 0)
return THROW_ERR_OUT_OF_RANGE(env, "value is out of range");
int64_t expected_interval =
args[1]->IsBigInt() ? args[1].As<BigInt>()->Int64Value(&lossless)
: static_cast<int64_t>(args[1].As<Number>()->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> HistogramBase::Create(
Environment* env, const Histogram::Options& options) {
Local<Object> obj;
if (!GetConstructorTemplate(env->isolate_data())
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return nullptr;
}
return MakeBaseObject<HistogramBase>(env, obj, options);
}
BaseObjectPtr<HistogramBase> HistogramBase::Create(
Environment* env, std::shared_ptr<Histogram> histogram) {
Local<Object> obj;
if (!GetConstructorTemplate(env->isolate_data())
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return nullptr;
}
return MakeBaseObject<HistogramBase>(env, obj, std::move(histogram));
}
void HistogramBase::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
Environment* env = Environment::GetCurrent(args);
CHECK_IMPLIES(!args[0]->IsNumber(), args[0]->IsBigInt());
CHECK_IMPLIES(!args[1]->IsNumber(), args[1]->IsBigInt());
CHECK(args[2]->IsUint32());
int64_t lowest = 1;
int64_t highest = std::numeric_limits<int64_t>::max();
bool lossless = true;
if (args[0]->IsNumber()) {
lowest = args[0].As<Integer>()->Value();
} else if (args[0]->IsBigInt()) {
lowest = args[0].As<BigInt>()->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<Integer>()->Value();
} else if (args[1]->IsBigInt()) {
highest = args[1].As<BigInt>()->Int64Value(&lossless);
if (!lossless)
return THROW_ERR_OUT_OF_RANGE(env, "options.highest is out of range");
}
int32_t figures = args[2].As<Uint32>()->Value();
double half_life = 0;
if (args.Length() > 3 && args[3]->IsNumber()) {
half_life = args[3].As<Number>()->Value();
}
int64_t threshold = 0;
if (args.Length() > 4 && args[4]->IsNumber()) {
threshold = static_cast<int64_t>(args[4].As<Number>()->Value());
}
auto histogram = Histogram::Create(
Histogram::Options{lowest, highest, figures, half_life, threshold});
if (!histogram) {
return THROW_ERR_INVALID_ARG_VALUE(env, "Invalid histogram options");
}
new HistogramBase(env, args.This(), std::move(histogram));
}
Local<FunctionTemplate> HistogramBase::GetConstructorTemplate(
IsolateData* isolate_data) {
Local<FunctionTemplate> tmpl = isolate_data->histogram_ctor_template();
if (tmpl.IsEmpty()) {
Isolate* isolate = isolate_data->isolate();
tmpl = NewFunctionTemplate(isolate, New);
Local<String> classname = FIXED_ONE_BYTE_STRING(isolate, "Histogram");
tmpl->SetClassName(classname);
auto instance = tmpl->InstanceTemplate();
instance->SetInternalFieldCount(HistogramBase::kInternalFieldCount);
SetFastMethod(isolate, instance, "record", Record, &fast_record_);
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);
}
return tmpl;
}
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_);
registry->Register(HistogramImpl::DoImport);
HistogramImpl::RegisterExternalReferences(registry);
}
void HistogramBase::Initialize(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Local<FunctionTemplate> tmpl = GetConstructorTemplate(isolate_data);
SetMethodNoSideEffect(isolate, tmpl, "import", HistogramImpl::DoImport);
SetConstructorFunction(
isolate, target, "Histogram", tmpl, SetConstructorFunctionFlag::NONE);
}
BaseObjectPtr<BaseObject> HistogramBase::HistogramTransferData::Deserialize(
Environment* env,
Local<Context> context,
std::unique_ptr<worker::TransferData> self) {
return Create(env, std::move(histogram_));
}
std::unique_ptr<worker::TransferData> HistogramBase::CloneForMessaging() const {
return std::make_unique<HistogramTransferData>(this);
}
void HistogramBase::HistogramTransferData::MemoryInfo(
MemoryTracker* tracker) const {
tracker->TrackField("histogram", histogram_);
}
SlidingWindowHistogram::SlidingWindowHistogram(
Environment* env,
Local<Object> wrap,
const Histogram::Options& options,
size_t chunk_count,
bool time_based,
uint64_t rotate_at,
std::shared_ptr<Histogram> spare)
: BaseObject(env, wrap),
options_(options),
chunks_(chunk_count),
generations_(chunk_count, kNoGeneration),
spare_(std::move(spare)),
time_based_(time_based),
rotate_at_(rotate_at),
origin_(uv_hrtime()) {
MakeWeak();
external_memory_ = spare_->GetMemorySize();
env->external_memory_accounter()->Increase(env->isolate(), external_memory_);
}
SlidingWindowHistogram::~SlidingWindowHistogram() {
env()->external_memory_accounter()->Decrease(env()->isolate(),
external_memory_);
}
void SlidingWindowHistogram::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("chunks", chunks_);
tracker->TrackField("generations", generations_);
tracker->TrackField("spare", spare_);
}
uint64_t SlidingWindowHistogram::CurrentTimeGeneration() const {
const uint64_t now = uv_hrtime();
CHECK_GE(now, origin_);
return (now - origin_) / rotate_at_;
}
Histogram* SlidingWindowHistogram::GetChunk(uint64_t generation) {
const size_t index = generation % chunks_.size();
if (generations_[index] == generation) {
CHECK(chunks_[index]);
return chunks_[index].get();
}
if (chunks_[index]) {
chunks_[index]->Reset();
} else if (spare_) {
chunks_[index] = std::move(spare_);
} else {
chunks_[index] = Histogram::Create(options_);
if (!chunks_[index]) return nullptr;
const size_t size = chunks_[index]->GetMemorySize();
external_memory_ += size;
env()->external_memory_accounter()->Increase(env()->isolate(), size);
}
generations_[index] = generation;
return chunks_[index].get();
}
bool SlidingWindowHistogram::RecordValue(int64_t value) {
uint64_t generation;
if (time_based_) {
generation = CurrentTimeGeneration();
} else if (records_in_current_chunk_ == rotate_at_) {
CHECK_LT(current_generation_, kNoGeneration - 1);
generation = current_generation_ + 1;
} else {
generation = current_generation_;
}
Histogram* chunk = GetChunk(generation);
if (chunk == nullptr) return false;
chunk->Record(value);
if (!time_based_) {
if (generation != current_generation_) {
current_generation_ = generation;
records_in_current_chunk_ = 0;
}
records_in_current_chunk_++;
has_count_records_ = true;
}
return true;
}
std::shared_ptr<Histogram> SlidingWindowHistogram::CreateSnapshot() const {
std::shared_ptr<Histogram> snapshot = Histogram::Create(options_);
if (!snapshot) return {};
uint64_t current_generation;
if (time_based_) {
current_generation = CurrentTimeGeneration();
} else {
if (!has_count_records_) return snapshot;
current_generation = current_generation_;
}
for (size_t i = 0; i < chunks_.size(); i++) {
const uint64_t generation = generations_[i];
if (generation == kNoGeneration || generation > current_generation ||
current_generation - generation >= chunks_.size()) {
continue;
}
CHECK(chunks_[i]);
CHECK_EQ(snapshot->Add(*chunks_[i]), 0);
}
return snapshot;
}
void SlidingWindowHistogram::ResetWindow() {
std::fill(generations_.begin(), generations_.end(), kNoGeneration);
origin_ = uv_hrtime();
current_generation_ = 0;
records_in_current_chunk_ = 0;
has_count_records_ = false;
}
void SlidingWindowHistogram::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
CHECK_IMPLIES(!args[0]->IsNumber(), args[0]->IsBigInt());
CHECK_IMPLIES(!args[1]->IsNumber(), args[1]->IsBigInt());
CHECK(args[2]->IsUint32());
CHECK(args[3]->IsUint32());
CHECK(args[4]->IsBoolean());
CHECK(args[5]->IsBigInt());
Environment* env = Environment::GetCurrent(args);
bool lossless = true;
int64_t lowest = 1;
int64_t highest = std::numeric_limits<int64_t>::max();
if (args[0]->IsNumber()) {
lowest = args[0].As<Integer>()->Value();
} else {
lowest = args[0].As<BigInt>()->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<Integer>()->Value();
} else {
highest = args[1].As<BigInt>()->Int64Value(&lossless);
if (!lossless)
return THROW_ERR_OUT_OF_RANGE(env, "options.highest is out of range");
}
const int figures = args[2].As<Uint32>()->Value();
const uint32_t chunk_count = args[3].As<Uint32>()->Value();
if (chunk_count == 0)
return THROW_ERR_OUT_OF_RANGE(env, "options.chunks is out of range");
lossless = true;
const uint64_t rotate_at = args[5].As<BigInt>()->Uint64Value(&lossless);
if (!lossless || rotate_at == 0) {
return THROW_ERR_OUT_OF_RANGE(env, "rotation interval is out of range");
}
Histogram::Options options{lowest, highest, figures};
std::shared_ptr<Histogram> spare = Histogram::Create(options);
if (!spare)
return THROW_ERR_INVALID_ARG_VALUE(env, "Invalid histogram options");
new SlidingWindowHistogram(env,
args.This(),
options,
chunk_count,
args[4]->IsTrue(),
rotate_at,
std::move(spare));
}
void SlidingWindowHistogram::Record(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_IMPLIES(!args[0]->IsNumber(), args[0]->IsBigInt());
bool lossless = true;
const int64_t value =
args[0]->IsBigInt() ? args[0].As<BigInt>()->Int64Value(&lossless)
: static_cast<int64_t>(args[0].As<Number>()->Value());
if (!lossless || value < 0)
return THROW_ERR_OUT_OF_RANGE(env, "value is out of range");
SlidingWindowHistogram* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
if (!histogram->RecordValue(value)) THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
}
void SlidingWindowHistogram::FastRecord(Local<Value> receiver,
int64_t value,
// NOLINTNEXTLINE(runtime/references)
FastApiCallbackOptions& options) {
CHECK_GE(value, 0);
TRACK_V8_FAST_API_CALL("histogram.slidingWindow.record");
SlidingWindowHistogram* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
if (!histogram->RecordValue(value)) {
HandleScope scope(options.isolate);
THROW_ERR_MEMORY_ALLOCATION_FAILED(histogram->env());
}
}
void SlidingWindowHistogram::Snapshot(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
SlidingWindowHistogram* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
std::shared_ptr<Histogram> snapshot = histogram->CreateSnapshot();
if (!snapshot) return THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
BaseObjectPtr<HistogramBase> result =
HistogramBase::Create(env, std::move(snapshot));
if (result) args.GetReturnValue().Set(result->object());
}
void SlidingWindowHistogram::Reset(const FunctionCallbackInfo<Value>& args) {
SlidingWindowHistogram* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
histogram->ResetWindow();
}
void SlidingWindowHistogram::Initialize(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Local<FunctionTemplate> tmpl = NewFunctionTemplate(isolate, New);
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "SlidingWindowHistogram"));
auto instance = tmpl->InstanceTemplate();
instance->SetInternalFieldCount(BaseObject::kInternalFieldCount);
SetFastMethod(isolate, instance, "record", Record, &fast_record_);
SetProtoMethod(isolate, tmpl, "snapshot", Snapshot);
SetProtoMethod(isolate, tmpl, "reset", Reset);
SetConstructorFunction(isolate,
target,
"SlidingWindowHistogram",
tmpl,
SetConstructorFunctionFlag::NONE);
}
void SlidingWindowHistogram::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(New);
registry->Register(Record);
registry->Register(fast_record_);
registry->Register(Snapshot);
registry->Register(Reset);
}
Local<FunctionTemplate> IntervalHistogram::GetConstructorTemplate(
Environment* env) {
Local<FunctionTemplate> tmpl = env->intervalhistogram_constructor_template();
if (tmpl.IsEmpty()) {
Isolate* isolate = env->isolate();
tmpl = NewFunctionTemplate(isolate, nullptr);
tmpl->Inherit(HandleWrap::GetConstructorTemplate(env));
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram"));
InitTemplate(isolate, tmpl, IntervalHistogram::kInternalFieldCount);
env->set_intervalhistogram_constructor_template(tmpl);
}
return tmpl;
}
void IntervalHistogram::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(Start);
registry->Register(Stop);
registry->Register(fast_start_);
registry->Register(fast_stop_);
HistogramImpl::RegisterExternalReferences(registry);
}
IntervalHistogram::IntervalHistogram(Environment* env,
Local<Object> wrap,
AsyncWrap::ProviderType type,
uint64_t interval,
OnInterval on_interval,
std::shared_ptr<Histogram> histogram)
: HandleWrap(env, wrap, reinterpret_cast<uv_handle_t*>(&timer_), type),
HistogramImpl(std::move(histogram)),
interval_(interval),
on_interval_(on_interval) {
MakeWeak();
wrap->SetAlignedPointerInInternalField(
HistogramImpl::InternalFields::kImplField,
static_cast<HistogramImpl*>(this),
EmbedderDataTag::kDefault);
uv_timer_init(env->event_loop(), &timer_);
}
BaseObjectPtr<IntervalHistogram> IntervalHistogram::Create(
Environment* env,
uint64_t interval,
OnInterval on_interval,
std::shared_ptr<Histogram> histogram,
AsyncWrap::ProviderType type) {
Local<Object> obj;
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return nullptr;
}
return MakeBaseObject<IntervalHistogram>(
env, obj, type, interval, on_interval, std::move(histogram));
}
void IntervalHistogram::TimerCB(uv_timer_t* handle) {
IntervalHistogram* histogram =
ContainerOf(&IntervalHistogram::timer_, handle);
Histogram* h = histogram->histogram().get();
histogram->on_interval_(*h);
}
void IntervalHistogram::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("histogram", histogram());
}
void IntervalHistogram::OnStart(StartFlags flags) {
if (enabled_ || IsHandleClosing()) return;
enabled_ = true;
if (flags == StartFlags::RESET) histogram()->Reset();
uv_timer_start(&timer_, TimerCB, interval_, interval_);
uv_unref(reinterpret_cast<uv_handle_t*>(&timer_));
}
void IntervalHistogram::OnStop() {
if (!enabled_ || IsHandleClosing()) return;
enabled_ = false;
uv_timer_stop(&timer_);
}
void IntervalHistogram::FastStart(Local<Value> receiver, bool reset) {
TRACK_V8_FAST_API_CALL("histogram.start");
StartHandleHistogram<IntervalHistogram>(receiver, reset);
}
void IntervalHistogram::FastStop(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.stop");
StopHandleHistogram<IntervalHistogram>(receiver);
}
Local<FunctionTemplate> IterationHistogram::GetConstructorTemplate(
Environment* env) {
Local<FunctionTemplate> tmpl = env->iterationhistogram_constructor_template();
if (tmpl.IsEmpty()) {
Isolate* isolate = env->isolate();
tmpl = NewFunctionTemplate(isolate, nullptr);
tmpl->Inherit(HandleWrap::GetConstructorTemplate(env));
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram"));
InitTemplate(isolate, tmpl, IterationHistogram::kInternalFieldCount);
env->set_iterationhistogram_constructor_template(tmpl);
}
return tmpl;
}
void IterationHistogram::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(Start);
registry->Register(Stop);
registry->Register(fast_start_);
registry->Register(fast_stop_);
HistogramImpl::RegisterExternalReferences(registry);
}
IterationHistogram::IterationHistogram(Environment* env,
Local<Object> wrap,
AsyncWrap::ProviderType type,
std::shared_ptr<Histogram> histogram)
: HandleWrap(
env, wrap, reinterpret_cast<uv_handle_t*>(&check_handle_), type),
HistogramImpl(std::move(histogram)) {
MakeWeak();
wrap->SetAlignedPointerInInternalField(
HistogramImpl::InternalFields::kImplField,
static_cast<HistogramImpl*>(this),
EmbedderDataTag::kDefault);
uv_check_init(env->event_loop(), &check_handle_);
uv_prepare_init(env->event_loop(), &prepare_handle_);
uv_unref(reinterpret_cast<uv_handle_t*>(&check_handle_));
uv_unref(reinterpret_cast<uv_handle_t*>(&prepare_handle_));
}
BaseObjectPtr<IterationHistogram> IterationHistogram::Create(
Environment* env,
std::shared_ptr<Histogram> histogram,
AsyncWrap::ProviderType type) {
Local<Object> obj;
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return nullptr;
}
return MakeBaseObject<IterationHistogram>(
env, obj, type, std::move(histogram));
}
void IterationHistogram::PrepareCB(uv_prepare_t* handle) {
IterationHistogram* self =
ContainerOf(&IterationHistogram::prepare_handle_, handle);
if (!self->enabled_) return;
self->prepare_time_ = uv_hrtime();
self->timeout_ = uv_backend_timeout(handle->loop);
}
void IterationHistogram::CheckCB(uv_check_t* handle) {
IterationHistogram* self =
ContainerOf(&IterationHistogram::check_handle_, handle);
if (!self->enabled_) return;
uint64_t check_time = uv_hrtime();
uint64_t poll_time = check_time - self->prepare_time_;
uint64_t latency = self->prepare_time_ - self->check_time_;
if (self->timeout_ >= 0) {
uint64_t timeout_ns = static_cast<uint64_t>(self->timeout_) * 1000 * 1000;
if (poll_time > timeout_ns) {
latency += poll_time - timeout_ns;
}
}
self->histogram()->Record(latency == 0 ? 1 : latency);
self->check_time_ = check_time;
}
void IterationHistogram::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("histogram", histogram());
}
void IterationHistogram::OnStart(StartFlags flags) {
if (enabled_ || IsHandleClosing()) return;
enabled_ = true;
if (flags == StartFlags::RESET) histogram()->Reset();
check_time_ = uv_hrtime();
prepare_time_ = check_time_;
timeout_ = 0;
uv_check_start(&check_handle_, CheckCB);
uv_prepare_start(&prepare_handle_, PrepareCB);
uv_unref(reinterpret_cast<uv_handle_t*>(&check_handle_));
uv_unref(reinterpret_cast<uv_handle_t*>(&prepare_handle_));
}
void IterationHistogram::OnStop() {
if (!enabled_ || IsHandleClosing()) return;
enabled_ = false;
uv_check_stop(&check_handle_);
uv_prepare_stop(&prepare_handle_);
}
void IterationHistogram::Close(Local<Value> close_callback) {
if (IsHandleClosing()) return;
OnStop();
HandleWrap::Close(close_callback);
uv_close(reinterpret_cast<uv_handle_t*>(&prepare_handle_), nullptr);
}
void IterationHistogram::FastStart(Local<Value> receiver, bool reset) {
TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.start");
StartHandleHistogram<IterationHistogram>(receiver, reset);
}
void IterationHistogram::FastStop(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.stop");
StopHandleHistogram<IterationHistogram>(receiver);
}
void HistogramImpl::GetCount(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
double value = static_cast<double>((*histogram)->Count());
args.GetReturnValue().Set(value);
}
void HistogramImpl::GetCountBigInt(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set(
BigInt::NewFromUnsigned(args.GetIsolate(), (*histogram)->Count()));
}
void HistogramImpl::GetMin(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
double value = static_cast<double>((*histogram)->Min());
args.GetReturnValue().Set(value);
}
void HistogramImpl::GetMinBigInt(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set(
BigInt::New(args.GetIsolate(), (*histogram)->Min()));
}
void HistogramImpl::GetMax(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
double value = static_cast<double>((*histogram)->Max());
args.GetReturnValue().Set(value);
}
void HistogramImpl::GetMaxBigInt(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set(
BigInt::New(args.GetIsolate(), (*histogram)->Max()));
}
void HistogramImpl::GetMean(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set((*histogram)->Mean());
}
void HistogramImpl::GetExceeds(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
double value = static_cast<double>((*histogram)->Exceeds());
args.GetReturnValue().Set(value);
}
void HistogramImpl::GetExceedsBigInt(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set(
BigInt::New(args.GetIsolate(), (*histogram)->Exceeds()));
}
void HistogramImpl::GetStddev(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set((*histogram)->Stddev());
}
void HistogramImpl::GetPercentile(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsNumber());
double percentile = args[0].As<Number>()->Value();
double value = static_cast<double>((*histogram)->Percentile(percentile));
args.GetReturnValue().Set(value);
}
void HistogramImpl::GetPercentileBigInt(
const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsNumber());
double percentile = args[0].As<Number>()->Value();
int64_t value = (*histogram)->Percentile(percentile);
args.GetReturnValue().Set(BigInt::New(args.GetIsolate(), value));
}
void HistogramImpl::GetPercentiles(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsMap());
Local<Map> map = args[0].As<Map>();
// Collect percentile data under the histogram lock, then populate the
// V8 Map after releasing it to avoid V8 allocations under the lock.
std::vector<std::pair<double, int64_t>> 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<double>(entry.second))));
}
}
void HistogramImpl::GetPercentilesBigInt(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsMap());
Local<Map> map = args[0].As<Map>();
std::vector<std::pair<double, int64_t>> 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<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
(*histogram)->Reset();
}
void HistogramImpl::FastReset(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.reset");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
(*histogram)->Reset();
}
double HistogramImpl::FastGetCount(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.count");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return static_cast<double>((*histogram)->Count());
}
double HistogramImpl::FastGetMin(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.min");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return static_cast<double>((*histogram)->Min());
}
double HistogramImpl::FastGetMax(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.max");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return static_cast<double>((*histogram)->Max());
}
double HistogramImpl::FastGetMean(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.mean");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return (*histogram)->Mean();
}
double HistogramImpl::FastGetExceeds(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.exceeds");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return static_cast<double>((*histogram)->Exceeds());
}
double HistogramImpl::FastGetStddev(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.stddev");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return (*histogram)->Stddev();
}
double HistogramImpl::FastGetPercentile(Local<Value> receiver,
const double percentile) {
TRACK_V8_FAST_API_CALL("histogram.percentile");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return static_cast<double>((*histogram)->Percentile(percentile));
}
void HistogramImpl::GetSkewness(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set((*histogram)->Skewness());
}
double HistogramImpl::FastGetSkewness(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.skewness");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return (*histogram)->Skewness();
}
void HistogramImpl::GetKurtosis(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set((*histogram)->Kurtosis());
}
double HistogramImpl::FastGetKurtosis(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.kurtosis");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return (*histogram)->Kurtosis();
}
void HistogramImpl::GetCdf(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsNumber());
int64_t value = static_cast<int64_t>(args[0].As<Number>()->Value());
args.GetReturnValue().Set((*histogram)->Cdf(value));
}
double HistogramImpl::FastGetCdf(Local<Value> 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<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsNumber());
int64_t value = static_cast<int64_t>(args[0].As<Number>()->Value());
double count = static_cast<double>((*histogram)->CountAt(value));
args.GetReturnValue().Set(count);
}
double HistogramImpl::FastGetCountAt(Local<Value> receiver,
const int64_t value) {
TRACK_V8_FAST_API_CALL("histogram.countAt");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return static_cast<double>((*histogram)->CountAt(value));
}
void HistogramImpl::GetKsTest(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
HistogramImpl* other = HistogramImpl::FromJSObject(args[0]);
args.GetReturnValue().Set((*histogram)->KsTest(*(other->histogram())));
}
void HistogramImpl::GetMeanCI(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsNumber());
double confidence = args[0].As<Number>()->Value();
auto result = (*histogram)->MeanCI(confidence);
Local<Value> values[] = {Number::New(isolate, result.mean),
Number::New(isolate, result.lower),
Number::New(isolate, result.upper)};
Local<Array> arr = Array::New(isolate, &values[0], arraysize(values));
args.GetReturnValue().Set(arr);
}
void HistogramImpl::GetWelchTest(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
HistogramImpl* other = HistogramImpl::FromJSObject(args[0]);
CHECK(args[1]->IsNumber());
double confidence = args[1].As<Number>()->Value();
auto result = (*histogram)->WelchTest(*(other->histogram()), confidence);
Local<Value> values[] = {Number::New(isolate, result.t_statistic),
Number::New(isolate, result.degrees_of_freedom),
Number::New(isolate, result.p_value),
Number::New(isolate, result.ci_lower),
Number::New(isolate, result.ci_upper)};
Local<Array> arr = Array::New(isolate, &values[0], arraysize(values));
args.GetReturnValue().Set(arr);
}
void HistogramImpl::GetMannWhitneyTest(
const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
HistogramImpl* other = HistogramImpl::FromJSObject(args[0]);
auto result = (*histogram)->MannWhitneyTest(*(other->histogram()));
Local<Value> values[] = {Number::New(isolate, result.u_statistic),
Number::New(isolate, result.z_score),
Number::New(isolate, result.p_value)};
Local<Array> arr = Array::New(isolate, &values[0], arraysize(values));
args.GetReturnValue().Set(arr);
}
void HistogramImpl::GetCohensD(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
HistogramImpl* other = HistogramImpl::FromJSObject(args[0]);
args.GetReturnValue().Set((*histogram)->CohensD(*(other->histogram())));
}
void HistogramImpl::GetCliffsD(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
HistogramImpl* other = HistogramImpl::FromJSObject(args[0]);
args.GetReturnValue().Set((*histogram)->CliffsD(*(other->histogram())));
}
void HistogramImpl::GetPercentileCI(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsNumber());
CHECK(args[1]->IsNumber());
double percentile = args[0].As<Number>()->Value();
double confidence = args[1].As<Number>()->Value();
auto result = (*histogram)->PercentileCI(percentile, confidence);
Local<Value> values[] = {
Number::New(isolate, static_cast<double>(result.value)),
Number::New(isolate, static_cast<double>(result.lower)),
Number::New(isolate, static_cast<double>(result.upper))};
Local<Array> arr = Array::New(isolate, &values[0], arraysize(values));
args.GetReturnValue().Set(arr);
}
void HistogramImpl::GetQrde(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsFloat64Array());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsBoolean());
Local<Float64Array> input = args[0].As<Float64Array>();
CHECK_GE(input->Length(), 2);
CHECK_LE(input->Length(), 1001);
auto backing = input->Buffer()->GetBackingStore();
const double* data = reinterpret_cast<const double*>(
static_cast<const char*>(backing->Data()) + input->ByteOffset());
std::vector<double> probabilities(data, data + input->Length());
const uint32_t dequantization = args[1].As<Uint32>()->Value();
CHECK_LE(dequantization, static_cast<uint32_t>(QrdeDequantization::kAll));
const bool cache_snapshot = args[2]->IsTrue();
Local<Promise::Resolver> resolver;
if (!Promise::Resolver::New(env->context()).ToLocal(&resolver)) return;
auto histogram_ptr = histogram->histogram();
auto snapshot_source =
histogram_ptr->GetRecordedSnapshotSource(cache_snapshot);
if (!snapshot_source.histogram && !snapshot_source.snapshot) {
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
return;
}
std::shared_ptr<Histogram> cache_target;
if (cache_snapshot && !snapshot_source.cache_hit) {
cache_target = histogram_ptr;
}
auto* job = new QrdeJob(env,
resolver,
std::move(cache_target),
std::move(snapshot_source),
std::move(probabilities),
static_cast<QrdeDequantization>(dequantization));
args.GetReturnValue().Set(resolver->GetPromise());
job->ScheduleWork();
}
void HistogramImpl::GetEwmaMean(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set((*histogram)->EwmaMean());
}
double HistogramImpl::FastGetEwmaMean(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.ewmaMean");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return (*histogram)->EwmaMean();
}
void HistogramImpl::GetEwmaStddev(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set((*histogram)->EwmaStddev());
}
double HistogramImpl::FastGetEwmaStddev(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.ewmaStddev");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return (*histogram)->EwmaStddev();
}
void HistogramImpl::GetEwmaErrorRate(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set((*histogram)->EwmaErrorRate());
}
double HistogramImpl::FastGetEwmaErrorRate(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.ewmaErrorRate");
HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver);
return (*histogram)->EwmaErrorRate();
}
void HistogramImpl::DoExport(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
std::vector<uint8_t> data = (*histogram)->Export();
auto store = ArrayBuffer::NewBackingStore(env->isolate(), data.size());
memcpy(store->Data(), data.data(), data.size());
auto buf = ArrayBuffer::New(env->isolate(), std::move(store));
auto arr = Uint8Array::New(buf, 0, data.size());
args.GetReturnValue().Set(arr);
}
void HistogramImpl::DoImport(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args[0]->IsUint8Array()) {
THROW_ERR_INVALID_ARG_TYPE(env, "data must be a Uint8Array");
return;
}
Local<Uint8Array> input = args[0].As<Uint8Array>();
auto backing = input->Buffer()->GetBackingStore();
const uint8_t* data =
static_cast<const uint8_t*>(backing->Data()) + input->ByteOffset();
size_t len = input->ByteLength();
auto histogram = Histogram::Import(data, len);
if (!histogram) {
THROW_ERR_INVALID_ARG_VALUE(env, "Invalid histogram export data");
return;
}
// Create a new HistogramBase wrapping the imported histogram.
Local<FunctionTemplate> tmpl =
HistogramBase::GetConstructorTemplate(env->isolate_data());
Local<Object> obj;
if (!tmpl->InstanceTemplate()->NewInstance(env->context()).ToLocal(&obj))
return;
new HistogramBase(env, obj, std::move(histogram));
args.GetReturnValue().Set(obj);
}
void HistogramImpl::DoSnapshot(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
std::shared_ptr<Histogram> snapshot = (*histogram)->Clone();
if (!snapshot) return THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
BaseObjectPtr<HistogramBase> result =
HistogramBase::Create(env, std::move(snapshot));
if (result) args.GetReturnValue().Set(result->object());
}
void HistogramImpl::DoDiff(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
HistogramImpl* other = HistogramImpl::FromJSObject(args[0]);
Histogram::DiffError error;
std::shared_ptr<Histogram> diff =
(*histogram)->Diff(*(other->histogram()), &error);
switch (error) {
case Histogram::DiffError::kNone:
break;
case Histogram::DiffError::kOutOfMemory:
return THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
case Histogram::DiffError::kIncompatible:
return THROW_ERR_INVALID_ARG_VALUE(
env, "other must have the same configuration as the histogram");
case Histogram::DiffError::kReset:
return THROW_ERR_INVALID_STATE(
env, "Values were removed from the histogram after other was taken");
case Histogram::DiffError::kNotEarlier:
return THROW_ERR_INVALID_ARG_VALUE(
env, "other contains values that are not in the histogram");
}
BaseObjectPtr<HistogramBase> result =
HistogramBase::Create(env, std::move(diff));
if (result) args.GetReturnValue().Set(result->object());
}
void HistogramImpl::GetResetCount(const FunctionCallbackInfo<Value>& args) {
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
args.GetReturnValue().Set(static_cast<double>((*histogram)->ResetCount()));
}
void HistogramImpl::GetPercentilesAt(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
CHECK(args[0]->IsMap());
Local<Map> map = args[0].As<Map>();
CHECK(args[1]->IsFloat64Array());
Local<Float64Array> input = args[1].As<Float64Array>();
size_t length = input->Length();
auto backing = input->Buffer()->GetBackingStore();
double* percentiles = reinterpret_cast<double*>(
static_cast<char*>(backing->Data()) + input->ByteOffset());
std::vector<int64_t> 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<double>(values[i]))));
}
}
void HistogramImpl::GetLinearBuckets(const FunctionCallbackInfo<Value>& 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<int64_t>(args[0].As<Number>()->Value());
Local<Map> map = args[1].As<Map>();
std::vector<std::pair<int64_t, int64_t>> 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<double>(entry.first)),
Number::New(env->isolate(), static_cast<double>(entry.second))));
}
}
void HistogramImpl::GetLogBuckets(const FunctionCallbackInfo<Value>& 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<int64_t>(args[0].As<Number>()->Value());
double log_base = args[1].As<Number>()->Value();
Local<Map> map = args[2].As<Map>();
std::vector<std::pair<int64_t, int64_t>> 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<double>(entry.first)),
Number::New(env->isolate(), static_cast<double>(entry.second))));
}
}
HistogramImpl* HistogramImpl::FromJSObject(Local<Value> value) {
auto obj = value.As<Object>();
DCHECK_GE(obj->InternalFieldCount(), HistogramImpl::kInternalFieldCount);
return static_cast<HistogramImpl*>(obj->GetAlignedPointerFromInternalField(
HistogramImpl::kImplField, EmbedderDataTag::kDefault));
}
std::unique_ptr<worker::TransferData> IterationHistogram::CloneForMessaging()
const {
return std::make_unique<HistogramBase::HistogramTransferData>(histogram());
}
std::unique_ptr<worker::TransferData> IntervalHistogram::CloneForMessaging()
const {
return std::make_unique<HistogramBase::HistogramTransferData>(histogram());
}
} // namespace node