blob: e982c0facad18cd44bcb617fb6624eb835f99f46 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SRC_CONNECTIVITY_BLUETOOTH_CORE_BT_HOST_COMMON_RETIRE_LOG_H_
#define SRC_CONNECTIVITY_BLUETOOTH_CORE_BT_HOST_COMMON_RETIRE_LOG_H_
#include <lib/async/cpp/time.h>
#include <zircon/assert.h>
#include <algorithm>
#include <array>
#include <functional>
#include <optional>
#include <tuple>
#include <utility>
#include <vector>
namespace bt::internal {
// This class is not thread-safe.
// TODO(fxbug.dev/71341): Store each retirement's timestamp in order to provide information like how
// much time the log depth represents and overall throughput (bytes/sec and packets/sec)
class RetireLog final {
public:
// Create a bounded buffer intended to hold recently retired PipelineMonitor tokens. It supports
// efficient querying of statistics about logged events. |min_depth| specifies the number of
// entries that must be logged before queries return meaningful results and must be non-zero.
// |max_depth| limits the number of recent entries that are kept in memory. Each entry is
// represented by Retired, so the log memory use is roughly sizeof(Retired) * max_depth. The
// designed range is between 100 and 100,000 entries deep.
explicit RetireLog(size_t min_depth, size_t max_depth);
~RetireLog() = default;
// Add an entry to the log. If depth() is less than max_depth, the log is expanded. Otherwise, the
// oldest entry is replaced. This is a fast operation that does not allocate.
void Retire(size_t byte_count, zx::duration age);
// The current number of log entries.
[[nodiscard]] size_t depth() const { return buffer_.size(); }
// Compute the quantiles at cut points specified in |partitions| as numbers between 0 and 1. Each
// partition specifies a point in the distribution of |bytes_count|s in the log. Returns an array
// of |byte_count| entries corresponding to those points, if |depth()| is at least min_depth as
// provided to the ctor. Otherwise, returns std::nullopt.
//
// Cut points may, but do not need to, be evenly distributed, e.g. {.25, .5, .75} for quartiles.
// If a cut point is "between" log entry values, the nearest value is chosen without interpolation
// (e.g. for 0.5 with an even log depth, a biased median is returned rather than the average of
// the true median samples).
//
// TODO(fxbug.dev/71341): Add a |max_age| parameter to window to only samples that are recent
// enough to be relevant
template <size_t NumQuantiles>
[[nodiscard]] std::optional<std::array<size_t, NumQuantiles>> ComputeByteCountQuantiles(
std::array<double, NumQuantiles> partitions) const {
return ComputeQuantiles(partitions, &Retired::byte_count);
}
// Similar to ComputeByteCountQuantiles, but for the |age| durations logged in |Retire|.
template <size_t NumQuantiles>
[[nodiscard]] std::optional<std::array<zx::duration, NumQuantiles>> ComputeAgeQuantiles(
std::array<double, NumQuantiles> partitions) const {
return ComputeQuantiles(partitions, &Retired::age);
}
private:
struct Retired {
size_t byte_count;
zx::duration age;
};
// Helper function to build the index_sequence
template <typename ArrayT, typename PointerT>
[[nodiscard]] auto ComputeQuantiles(ArrayT partitions, PointerT element_ptr) const {
return ComputeQuantilesImpl(partitions, element_ptr,
std::make_index_sequence<partitions.size()>());
}
// Computes the indexes to sample the retire log as if it were sorted, then returns those samples
// in the same order as specified. |element_ptr| specifies which field in Retired to sort by.
// The log isn't actually fully sorted; instead, a k-selection algorithm is used for each sample.
// Assuming partitions.size() << depth(), this runs on average linearly to their product.
template <size_t NumQuantiles, typename ElementT, size_t... Indexes>
[[nodiscard]] std::optional<std::array<ElementT, NumQuantiles>> ComputeQuantilesImpl(
std::array<double, NumQuantiles> partitions, ElementT Retired::*element_ptr,
std::index_sequence<Indexes...> /*unused*/) const {
if (depth() < min_depth_) {
return std::nullopt;
}
// Computing quantiles is done in-place with k-selection routines, so use a working copy that is
// reused across invocations to prevent re-allocation with each call
std::vector<ElementT>& elements = std::get<std::vector<ElementT>>(quantile_scratchpads_);
elements.resize(depth());
std::transform(buffer_.begin(), buffer_.end(), elements.begin(), std::mem_fn(element_ptr));
// The k-selection we use is std::nth_element, which conveniently does a partial sort about k.
// By pre-sorting values of k, each invocation of nth_element selects only from the elements
// that are greater than or equal to the previous selection. The values are sorted with their
// corresponding indexes to remember the original order for the output
std::array partitions_and_indexes = {std::pair{partitions[Indexes], Indexes}...};
std::sort(partitions_and_indexes.begin(), partitions_and_indexes.end());
std::array<ElementT, NumQuantiles> quantiles; // output
auto unsorted_begin = elements.begin();
for (auto [partition, index] : partitions_and_indexes) {
ZX_ASSERT(partition >= 0.);
ZX_ASSERT(partition <= 1.);
// Even though the last element is at index depth()-1, use depth() here instead to ensure the
// final (max) element has sufficient range representation.
const size_t cut_point = static_cast<size_t>(static_cast<double>(depth()) * partition);
ZX_ASSERT(cut_point <= depth());
// In the case that partition = 1.0, cut_point = depth(). Saturate to the final (max) element.
const size_t selection_offset = std::min(cut_point, depth() - 1);
const auto cut_iter = std::next(elements.begin(), selection_offset);
std::nth_element(unsorted_begin, cut_iter, elements.end());
// Post-condition: the element at cut_iter is what it would be if |elements| were sorted, with
// all preceding elements no greater than *cut_iter and all successive elements no less than
// *cut_iter.
quantiles[index] = *cut_iter;
// Technically the next unsorted element is at cut_iter+1 but moving unsorted_begin past
// cut_iter causes problems if multiple values of |partition| are the same.
unsorted_begin = cut_iter;
}
return quantiles;
}
const size_t min_depth_;
const size_t max_depth_;
// Circular buffer of recently retired entries, kept in increasing retirement time order starting
// at the oldest at |next_insertion_index_|. Bounded to |max_depth_| in size.
std::vector<Retired> buffer_;
size_t next_insertion_index_ = 0;
// Used by ComputeQuantilesImpl to store type-dependent k-selection computation working data. We
// could probably save memory by using the same scratchpad for both byte_count and age, but it's
// not worth the extra code complexity at this time.
mutable std::tuple<std::vector<size_t>, std::vector<zx::duration>> quantile_scratchpads_;
};
} // namespace bt::internal
#endif // SRC_CONNECTIVITY_BLUETOOTH_CORE_BT_HOST_COMMON_RETIRE_LOG_H_