| /* |
| * 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 |