blob: 73ccbae34100f6769835182e04d8135920d28811 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "paimon/common/metrics/histogram.h"
#include <algorithm>
#include <cmath>
namespace paimon {
HistogramImpl::HistogramImpl() : bucket_counts_(BucketLimits().size(), 0) {}
void HistogramImpl::Add(double value) {
if (std::isnan(value)) {
return;
}
std::lock_guard<std::mutex> guard(mu_);
const auto& limits = BucketLimits();
size_t idx = BucketIndex(value, limits);
bucket_counts_[idx] += 1;
num_ += 1;
sum_ += value;
sum_squares_ += value * value;
if (value < min_) {
min_ = value;
}
if (value > max_) {
max_ = value;
}
}
HistogramStats HistogramImpl::GetStats() const {
return ToStats(GetSnapshot());
}
HistogramImpl::Snapshot HistogramImpl::GetSnapshot() const {
std::lock_guard<std::mutex> guard(mu_);
Snapshot s;
s.count = num_;
s.sum = sum_;
s.sum_squares = sum_squares_;
s.min = (num_ == 0) ? 0 : min_;
s.max = (num_ == 0) ? 0 : max_;
s.bucket_counts = bucket_counts_;
return s;
}
void HistogramImpl::Merge(const Histogram& other) {
const auto* other_impl = dynamic_cast<const HistogramImpl*>(&other);
if (!other_impl) {
return;
}
MergeFrom(*other_impl);
}
void HistogramImpl::MergeFrom(const HistogramImpl& other) {
Snapshot other_snapshot = other.GetSnapshot();
std::lock_guard<std::mutex> guard(mu_);
if (bucket_counts_.size() != other_snapshot.bucket_counts.size()) {
// Should never happen unless bucket config changes.
return;
}
if (other_snapshot.count == 0) {
return;
}
for (size_t i = 0; i < bucket_counts_.size(); ++i) {
bucket_counts_[i] += other_snapshot.bucket_counts[i];
}
if (num_ == 0) {
min_ = other_snapshot.min;
max_ = other_snapshot.max;
} else {
min_ = std::min(min_, other_snapshot.min);
max_ = std::max(max_, other_snapshot.max);
}
num_ += other_snapshot.count;
sum_ += other_snapshot.sum;
sum_squares_ += other_snapshot.sum_squares;
}
std::shared_ptr<Histogram> HistogramImpl::Clone() const {
Snapshot s = GetSnapshot();
auto cloned = std::make_shared<HistogramImpl>();
std::lock_guard<std::mutex> guard(cloned->mu_);
cloned->num_ = s.count;
cloned->sum_ = s.sum;
cloned->sum_squares_ = s.sum_squares;
cloned->min_ = (s.count == 0) ? std::numeric_limits<double>::infinity() : s.min;
cloned->max_ = s.max;
cloned->bucket_counts_ = std::move(s.bucket_counts);
return cloned;
}
HistogramStats HistogramImpl::ToStats(const Snapshot& s) {
HistogramStats stats;
stats.count = s.count;
stats.sum = s.sum;
stats.min = s.min;
stats.max = s.max;
stats.average = (s.count == 0) ? 0 : (s.sum / static_cast<double>(s.count));
stats.p50 = EstimatePercentile(s, 0.50);
stats.p90 = EstimatePercentile(s, 0.90);
stats.p95 = EstimatePercentile(s, 0.95);
stats.p99 = EstimatePercentile(s, 0.99);
stats.p999 = EstimatePercentile(s, 0.999);
stats.stddev = EstimateStddev(s);
return stats;
}
const std::vector<double>& HistogramImpl::BucketLimits() {
static const std::vector<double> limits = []() {
// Keep bucket generation aligned with RocksDB's HistogramBucketMapper
// (see rocksdb/monitoring/histogram.cc).
//
// The mapper starts from {1,2}, then grows by *1.5 and rounds each bucket boundary to
// be human-readable by keeping the two most significant digits.
std::vector<double> v;
v.reserve(256);
v.push_back(1);
v.push_back(2);
double bucket_val = v.back();
const auto max_u64 = static_cast<double>(std::numeric_limits<uint64_t>::max());
while ((bucket_val = 1.5 * bucket_val) <= max_u64) {
auto rounded = static_cast<uint64_t>(bucket_val);
// Keep two most significant digits (e.g., 172 -> 170).
uint64_t pow_of_ten = 1;
while (rounded / 10 > 10) {
rounded /= 10;
pow_of_ten *= 10;
}
rounded *= pow_of_ten;
v.push_back(static_cast<double>(rounded));
}
return v;
}();
return limits;
}
size_t HistogramImpl::BucketIndex(double value, const std::vector<double>& limits) {
if (value <= limits.front()) {
return 0;
}
auto it = std::lower_bound(limits.begin(), limits.end(), value);
if (it == limits.end()) {
return limits.size() - 1;
}
return static_cast<size_t>(std::distance(limits.begin(), it));
}
double HistogramImpl::EstimatePercentile(const Snapshot& s, double p) {
if (s.count == 0) {
return 0;
}
if (p <= 0) {
return s.min;
}
if (p >= 1) {
return s.max;
}
const auto& limits = BucketLimits();
const auto rank = static_cast<uint64_t>(std::ceil(p * static_cast<double>(s.count)));
uint64_t cum = 0;
for (size_t i = 0; i < s.bucket_counts.size(); ++i) {
uint64_t bcnt = s.bucket_counts[i];
if (bcnt == 0) {
continue;
}
uint64_t prev = cum;
cum += bcnt;
if (cum < rank) {
continue;
}
// Interpolate inside bucket: estimate that samples are uniformly distributed
// between bucket lower and upper bounds.
double upper = (i < limits.size()) ? limits[i] : s.max;
double lower = (i == 0) ? std::min(s.min, 0.0) : limits[i - 1];
if (std::isinf(upper)) {
return s.max;
}
const double pos_in_bucket = static_cast<double>(rank - prev) / static_cast<double>(bcnt);
double est = lower + (upper - lower) * pos_in_bucket;
if (est < s.min) {
est = s.min;
}
if (est > s.max) {
est = s.max;
}
return est;
}
return s.max;
}
double HistogramImpl::EstimateStddev(const Snapshot& s) {
if (s.count == 0) {
return 0;
}
const auto n = static_cast<double>(s.count);
const double mean = s.sum / n;
const double ex2 = s.sum_squares / n;
const double var = std::max(0.0, ex2 - mean * mean);
return std::sqrt(var);
}
} // namespace paimon