// Copyright 2023 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef BASE_MOVING_WINDOW_H_ #define BASE_MOVING_WINDOW_H_ #include #include #include #include #include #include #include "base/check_op.h" #include "base/memory/raw_ref.h" #include "base/time/time.h" namespace base { // Class to efficiently calculate statistics in a sliding window. // This class isn't thread safe. // Supported statistics are Min/Max/Mean/Deviation. // You can also iterate through the items in the window. // The class is modular: required features must be specified in the template // arguments. // Non listed features don't consume memory or runtime cycles at all. // // Usage: // base::MovingWindow // moving_window(window_size); // // Following convenience shortcuts are provided with predefined sets of // features: // MovingMax/MovingMin/MovingAverage/MovingAverageDeviation/MovingMinMax. // // Methods: // Constructor: // MovingWindow(size_t window_size); // // Window update (available for all templates): // AddSample(T value) const; // size_t Count() const; // void Reset(); // // Available for MovingWindowFeatures::Min: // T Min() const; // // Available for MovingWindowFeatures::Max: // T Max() const; // // Available for MovingWindowFeatures::Mean: // U Mean() const; // // Available for MovingWindowFeatures::Deviation: // U Deviation() const; // // Available for MovingWindowFeatures::Iteration. Iterating through the window: // iterator begin() const; // iterator begin() const; // size_t size() const; // Features supported by the class. struct MovingWindowFeatures { struct Min { static bool has_min; }; struct Max { static bool has_max; }; // Need to specify a type capable of holding a sum of all elements in the // window. template struct Mean { static SumType has_mean; }; // Need to specify a type capable of holding a sum of squares of all elements // in the window. template struct Deviation { static SumType has_deviation; }; struct Iteration { static bool has_iteration; }; }; // Main template. template class MovingWindow; // Convenience shortcuts. template using MovingMax = MovingWindow; template using MovingMin = MovingWindow; template using MovingMinMax = MovingWindow; template using MovingAverage = MovingWindow>; template using MovingAverageDeviation = MovingWindow, MovingWindowFeatures::Deviation>; namespace internal { // Class responsible only for calculating maximum in the window. // It's reused to calculate both min and max via inverting the comparator. template class MovingExtremumBase { public: explicit MovingExtremumBase(size_t window_size) : window_size_(window_size), values_(window_size), added_at_(window_size), last_idx_(window_size - 1), compare_(Comparator()) {} ~MovingExtremumBase() = default; // Add new sample to the stream. void AddSample(const T& value, size_t total_added) { // Remove old elements from the back of the window; while (size_ > 0 && added_at_[begin_idx_] + window_size_ <= total_added) { ++begin_idx_; if (begin_idx_ == window_size_) { begin_idx_ = 0; } --size_; } // Remove small elements from the front of the window because they can never // become the maximum in the window since the currently added element is // bigger than them and will leave the window later. while (size_ > 0 && compare_(values_[last_idx_], value)) { if (last_idx_ == 0) { last_idx_ = window_size_; } --last_idx_; --size_; } DCHECK_LT(size_, window_size_); ++last_idx_; if (last_idx_ == window_size_) { last_idx_ = 0; } values_[last_idx_] = value; added_at_[last_idx_] = total_added; ++size_; } // Get the maximum of the last `window_size` elements. T Value() const { DCHECK_GT(size_, 0u); return values_[begin_idx_]; } // Clear all samples. void Reset() { size_ = 0; begin_idx_ = 0; last_idx_ = window_size_ - 1; } private: const size_t window_size_; // Circular buffer with some values in the window. // Only possible candidates for maximum are stored: // values form a non-increasing sequence. std::vector values_; // Circular buffer storing when numbers in `values_` were added. std::vector added_at_; // Begin of the circular buffers above. size_t begin_idx_ = 0; // Last occupied position. size_t last_idx_; // How many elements are stored in the circular buffers above. size_t size_ = 0; // Template parameter comparator. const Comparator compare_; }; // Null implementation of the above class to be used when feature is disabled. template struct NullExtremumImpl { explicit NullExtremumImpl(size_t) {} ~NullExtremumImpl() = default; void AddSample(const T&, size_t) {} void Reset() {} }; // Class to hold the moving window. // It's used to calculate replaced element for Mean/Deviation calculations. template class MovingWindowBase { public: explicit MovingWindowBase(size_t window_size) : values_(window_size) {} ~MovingWindowBase() = default; void AddSample(const T& sample) { values_[cur_idx_] = sample; ++cur_idx_; if (cur_idx_ == values_.size()) { cur_idx_ = 0; } } // Is the window filled integer amount of times. bool IsLastIdx() const { return cur_idx_ + 1 == values_.size(); } void Reset() { cur_idx_ = 0; std::fill(values_.begin(), values_.end(), T()); } T GetValue() const { return values_[cur_idx_]; } T operator[](size_t idx) const { return values_[idx]; } size_t Size() const { return values_.size(); } // What index will be overwritten by a new element; size_t CurIdx() const { return cur_idx_; } private: // Circular buffer. std::vector values_; // Where the buffer begins. size_t cur_idx_ = 0; }; // Null implementation of the above class to be used when feature is disabled. template struct NullWindowImpl { explicit NullWindowImpl(size_t) {} ~NullWindowImpl() = default; void AddSample(const T& sample) {} bool IsLastIdx() const { return false; } void Reset() {} T GetValue() const { return T(); } }; // Performs division allowing the class to work with more types. // General template. template struct DivideInternal { static ReturnType Compute(const SumType& sum, const size_t count) { return static_cast(sum) / static_cast(count); } }; // Class to calculate moving mean. template class MovingMeanBase { public: explicit MovingMeanBase(size_t window_size) : sum_() {} ~MovingMeanBase() = default; void AddSample(const T& sample, const T& replaced_value, bool is_last_idx) { sum_ += sample - replaced_value; } template ReturnType Mean(const size_t count) const { if (count == 0) { return ReturnType(); } return DivideInternal::Compute(sum_, count); } void Reset() { sum_ = SumType(); } SumType Sum() const { return sum_; } private: SumType sum_; }; // Class to calculate moving mean. // Variant for float types with running sum to avoid rounding errors // accumulation. template class MovingMeanBase { public: explicit MovingMeanBase(size_t window_size) : sum_(), running_sum_() {} ~MovingMeanBase() = default; void AddSample(const T& sample, const T& replaced_value, bool is_last_idx) { running_sum_ += sample; if (is_last_idx) { // Replace sum with running sum to avoid rounding errors accumulation. sum_ = running_sum_; running_sum_ = SumType(); } else { sum_ += sample - replaced_value; } } template ReturnType Mean(const size_t count) const { if (count == 0) { return ReturnType(); } return DivideInternal::Compute(sum_, count); } void Reset() { sum_ = running_sum_ = SumType(); } SumType Sum() const { return sum_; } private: SumType sum_; SumType running_sum_; }; // Null implementation of the above class to be used when feature is disabled. template struct NullMeanImpl { explicit NullMeanImpl(size_t window_size) {} ~NullMeanImpl() = default; void AddSample(const T& sample, const T&, bool) {} void Reset() {} }; // Computs main Deviation fromula, allowing the class to work with more types. // Deviation is equal to mean of squared values minus squared mean value. // General template. template struct DeivationInternal { static ReturnType Compute(const SumType& sum_squares, const SumType& square_of_sum, const size_t count) { return static_cast( std::sqrt((static_cast(sum_squares) - static_cast(square_of_sum) / count) / count)); } }; // Class to compute square of the number. // General template template struct SquareInternal { static SquareType Compute(const T& sample) { return static_cast(sample) * sample; } }; // Class to calculate moving deviation. template class MovingDeviationBase { public: explicit MovingDeviationBase(size_t window_size) : sum_sq_() {} ~MovingDeviationBase() = default; void AddSample(const T& sample, const T& replaced_value, bool is_last_idx) { sum_sq_ += SquareInternal::Compute(sample) - SquareInternal::Compute(replaced_value); } template ReturnType Deviation(const size_t count, const U& sum) const { if (count == 0) { return ReturnType(); } return DeivationInternal::Compute( sum_sq_, SquareInternal::Compute(sum), count); } void Reset() { sum_sq_ = SumType(); } private: SumType sum_sq_; }; // Class to calculate moving deviation. // Variant for float types with running sum to avoid rounding errors // accumulation. template class MovingDeviationBase { public: explicit MovingDeviationBase(size_t window_size) : sum_sq_(), running_sum_() {} ~MovingDeviationBase() = default; void AddSample(const T& sample, const T& replaced_value, bool is_last_idx) { SumType square = SquareInternal::Compute(sample); running_sum_ += square; if (is_last_idx) { // Replace sum with running sum to avoid rounding errors accumulation. sum_sq_ = running_sum_; running_sum_ = SumType(); } else { sum_sq_ += square - SquareInternal::Compute(replaced_value); } } template ReturnType Deviation(const size_t count, const U& sum) const { if (count == 0) { return ReturnType(); } return DeivationInternal::Compute( sum_sq_, SquareInternal::Compute(sum), count); } void Reset() { running_sum_ = sum_sq_ = SumType(); } private: SumType sum_sq_; SumType running_sum_; }; // Null implementation of the above class to be used when feature is disabled. template struct NullDeviationImpl { public: explicit NullDeviationImpl(size_t window_size) {} ~NullDeviationImpl() = default; void AddSample(const T&, const T&, bool) {} void Reset() {} }; // Template helpers. // Gets all enabled features in one struct. template struct EnabledFeatures : public Features... {}; // Checks if specific member is present. template struct has_member_min : std::false_type {}; template struct has_member_min : std::true_type { }; template struct has_member_max : std::false_type {}; template struct has_member_max : std::true_type { }; template struct has_member_mean : std::false_type {}; template struct has_member_mean : std::true_type {}; template struct has_memeber_deviation : std::false_type {}; template struct has_memeber_deviation : std::true_type {}; template struct has_member_iteration : std::false_type {}; template struct has_member_iteration : std::true_type {}; // Gets the type of the member if present. // Can't just use decltype, because the member might be absent. template struct get_type_mean { typedef void type; }; template struct get_type_mean { typedef decltype(T::has_mean) type; }; template struct get_type_deviation { typedef void type; }; template struct get_type_deviation { typedef decltype(T::has_deviation) type; }; // Performs division allowing the class to work with more types. // Specific template for TimeDelta. template <> struct DivideInternal { static TimeDelta Compute(const TimeDelta& sum, const size_t count) { return sum / count; } }; // Computs main Deviation fromula, allowing the class to work with more types. // Deviation is equal to mean of squared values minus squared mean value. // Specific template for TimeDelta. template <> struct DeivationInternal { static TimeDelta Compute(const double sum_squares, const double square_of_sum, const size_t count) { return Seconds(std::sqrt((sum_squares - square_of_sum / count) / count)); } }; // Class to compute square of the number. // Specific template for TimeDelta. template <> struct SquareInternal { static double Compute(const TimeDelta& sample) { return sample.InSecondsF() * sample.InSecondsF(); } }; } // namespace internal // Implementation of the main class. template class MovingWindow { public: // List of all requested features. using EnabledFeatures = internal::EnabledFeatures; explicit MovingWindow(size_t window_size) : min_impl_(window_size), max_impl_(window_size), mean_impl_(window_size), deviation_impl_(window_size), window_impl_(window_size) {} // Adds sample to the window. void AddSample(const T& sample) { ++total_added_; min_impl_.AddSample(sample, total_added_); max_impl_.AddSample(sample, total_added_); mean_impl_.AddSample(sample, window_impl_.GetValue(), window_impl_.IsLastIdx()); deviation_impl_.AddSample(sample, window_impl_.GetValue(), window_impl_.IsLastIdx()); window_impl_.AddSample(sample); } // Returns amount of elementes so far in the stream (might be bigger than the // window size). size_t Count() const { return total_added_; } // Calculates min in the window. Template to disable when feature isn't // requested. template ::value, int> = 0> T Min() const { return min_impl_.Value(); } // Calculates max in the window. Template to disable when feature isn't // requested. template ::value, int> = 0> T Max() const { return max_impl_.Value(); } // Calculates mean in the window. Template to disable when feature isn't // requested. template ::value, int> = 0> ReturnType Mean() const { return mean_impl_.template Mean( std::min(total_added_, window_impl_.Size())); } // Calculates deviation in the window. Template to disable when feature isn't // requested. template < typename ReturnType = T, typename U = EnabledFeatures, std::enable_if_t::value, int> = 0> ReturnType Deviation() const { const size_t count = std::min(total_added_, window_impl_.Size()); return deviation_impl_.template Deviation(count, mean_impl_.Sum()); } // Resets the state to an empty window. void Reset() { min_impl_.Reset(); max_impl_.Reset(); mean_impl_.Reset(); deviation_impl_.Reset(); window_impl_.Reset(); total_added_ = 0; } // iterator implementation. class iterator { public: ~iterator() = default; const T operator*() { DCHECK_LT(idx_, window_impl_->Size()); return (*window_impl_)[idx_]; } iterator& operator++() { ++idx_; // Wrap around the circular buffer. if (idx_ == window_impl_->Size()) { idx_ = 0; } // The only way to arrive to the current element is to // come around after iterating through the whole window. if (idx_ == window_impl_->CurIdx()) { idx_ = kInvalidIndex; } return *this; } bool operator==(const iterator& other) const { return idx_ == other.idx_; } private: iterator(const internal::MovingWindowBase& window, size_t idx) : window_impl_(window), idx_(idx) {} static const size_t kInvalidIndex = std::numeric_limits::max(); raw_ref> window_impl_; size_t idx_; friend class MovingWindow; }; // Begin iterator. Template to enable only if iteration feature is requested. template ::value, int> = 0> iterator begin() const { if (total_added_ == 0) { return end(); } // Before window is fully filled, the oldest element is at the index 0. size_t idx = (total_added_ < window_impl_.Size()) ? 0 : window_impl_.CurIdx(); return iterator(window_impl_, idx); } // End iterator. Template to enable only if iteration feature is requested. template ::value, int> = 0> iterator end() const { return iterator(window_impl_, iterator::kInvalidIndex); } // Size of the collection. Template to enable only if iteration feature is // requested. template ::value, int> = 0> size_t size() const { return std::min(total_added_, window_impl_.Size()); } private: // Member for calculating min. // Conditionally enabled on Min feature. typename std::conditional::value, internal::MovingExtremumBase>, internal::NullExtremumImpl>::type min_impl_; // Member for calculating min. // Conditionally enabled on Min feature. typename std::conditional::value, internal::MovingExtremumBase>, internal::NullExtremumImpl>::type max_impl_; // Type for sum value in Mean implementation. Might need to reuse deviation // sum type, because enabling only deviation feature will also enable mean // member (because deviation calculation depends on mean calculation). using MeanSumType = typename std::conditional< internal::has_member_mean::value, typename internal::get_type_mean::type, typename internal::get_type_deviation::type>::type; // Member for calculating mean. // Conditionally enabled on Mean or Deviation feature (because deviation // calculation depends on mean calculation). typename std::conditional< internal::has_member_mean::value || internal::has_memeber_deviation::value, internal:: MovingMeanBase>, internal::NullMeanImpl>::type mean_impl_; // Member for calculating deviation. // Conditionally enabled on Deviation feature. typename std::conditional< internal::has_memeber_deviation::value, internal::MovingDeviationBase< T, typename internal::get_type_deviation::type, std::is_floating_point_v< typename internal::get_type_deviation::type>>, internal::NullDeviationImpl>::type deviation_impl_; // Member for storing the moving window. // Conditionally enabled on Mean, Deviation or Iteration feature since // they need the elements in the window. // Min and Max features store elements internally so they don't need this. typename std::conditional< internal::has_member_mean::value || internal::has_memeber_deviation::value || internal::has_member_iteration::value, internal::MovingWindowBase, internal::NullWindowImpl>::type window_impl_; // Total number of added elements. size_t total_added_ = 0; }; } // namespace base #endif // BASE_MOVING_WINDOW_H_