1 // Copyright 2023 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #ifndef BASE_MOVING_WINDOW_H_ 6 #define BASE_MOVING_WINDOW_H_ 7 8 #include <math.h> 9 #include <stddef.h> 10 11 #include <cmath> 12 #include <functional> 13 #include <limits> 14 #include <vector> 15 16 #include "base/check_op.h" 17 #include "base/memory/raw_ref.h" 18 #include "base/time/time.h" 19 20 namespace base { 21 22 // Class to efficiently calculate statistics in a sliding window. 23 // This class isn't thread safe. 24 // Supported statistics are Min/Max/Mean/Deviation. 25 // You can also iterate through the items in the window. 26 // The class is modular: required features must be specified in the template 27 // arguments. 28 // Non listed features don't consume memory or runtime cycles at all. 29 // 30 // Usage: 31 // base::MovingWindow<int, 32 // base::MovingWindowFeatures::Min, 33 // base::MovingWindowFeatures::Max> 34 // moving_window(window_size); 35 // 36 // Following convenience shortcuts are provided with predefined sets of 37 // features: 38 // MovingMax/MovingMin/MovingAverage/MovingAverageDeviation/MovingMinMax. 39 // 40 // Methods: 41 // Constructor: 42 // MovingWindow(size_t window_size); 43 // 44 // Window update (available for all templates): 45 // AddSample(T value) const; 46 // size_t Count() const; 47 // void Reset(); 48 // 49 // Available for MovingWindowFeatures::Min: 50 // T Min() const; 51 // 52 // Available for MovingWindowFeatures::Max: 53 // T Max() const; 54 // 55 // Available for MovingWindowFeatures::Mean: 56 // U Mean<U>() const; 57 // 58 // Available for MovingWindowFeatures::Deviation: 59 // U Deviation<U>() const; 60 // 61 // Available for MovingWindowFeatures::Iteration. Iterating through the window: 62 // iterator begin() const; 63 // iterator begin() const; 64 // size_t size() const; 65 66 // Features supported by the class. 67 struct MovingWindowFeatures { 68 struct Min { 69 static bool has_min; 70 }; 71 72 struct Max { 73 static bool has_max; 74 }; 75 76 // Need to specify a type capable of holding a sum of all elements in the 77 // window. 78 template <typename SumType> 79 struct Mean { 80 static SumType has_mean; 81 }; 82 83 // Need to specify a type capable of holding a sum of squares of all elements 84 // in the window. 85 template <typename SumType> 86 struct Deviation { 87 static SumType has_deviation; 88 }; 89 90 struct Iteration { 91 static bool has_iteration; 92 }; 93 }; 94 95 // Main template. 96 template <typename T, typename... Features> 97 class MovingWindow; 98 99 // Convenience shortcuts. 100 template <typename T> 101 using MovingMax = MovingWindow<T, MovingWindowFeatures::Max>; 102 103 template <typename T> 104 using MovingMin = MovingWindow<T, MovingWindowFeatures::Min>; 105 106 template <typename T> 107 using MovingMinMax = 108 MovingWindow<T, MovingWindowFeatures::Min, MovingWindowFeatures::Max>; 109 110 template <typename T, typename SumType> 111 using MovingAverage = MovingWindow<T, MovingWindowFeatures::Mean<SumType>>; 112 113 template <typename T> 114 using MovingAverageDeviation = 115 MovingWindow<T, 116 MovingWindowFeatures::Mean<T>, 117 MovingWindowFeatures::Deviation<double>>; 118 119 namespace internal { 120 121 // Class responsible only for calculating maximum in the window. 122 // It's reused to calculate both min and max via inverting the comparator. 123 template <typename T, typename Comparator> 124 class MovingExtremumBase { 125 public: MovingExtremumBase(size_t window_size)126 explicit MovingExtremumBase(size_t window_size) 127 : window_size_(window_size), 128 values_(window_size), 129 added_at_(window_size), 130 last_idx_(window_size - 1), 131 compare_(Comparator()) {} 132 ~MovingExtremumBase() = default; 133 134 // Add new sample to the stream. AddSample(const T & value,size_t total_added)135 void AddSample(const T& value, size_t total_added) { 136 // Remove old elements from the back of the window; 137 while (size_ > 0 && added_at_[begin_idx_] + window_size_ <= total_added) { 138 ++begin_idx_; 139 if (begin_idx_ == window_size_) { 140 begin_idx_ = 0; 141 } 142 --size_; 143 } 144 // Remove small elements from the front of the window because they can never 145 // become the maximum in the window since the currently added element is 146 // bigger than them and will leave the window later. 147 while (size_ > 0 && compare_(values_[last_idx_], value)) { 148 if (last_idx_ == 0) { 149 last_idx_ = window_size_; 150 } 151 --last_idx_; 152 --size_; 153 } 154 DCHECK_LT(size_, window_size_); 155 ++last_idx_; 156 if (last_idx_ == window_size_) { 157 last_idx_ = 0; 158 } 159 values_[last_idx_] = value; 160 added_at_[last_idx_] = total_added; 161 ++size_; 162 } 163 164 // Get the maximum of the last `window_size` elements. Value()165 T Value() const { 166 DCHECK_GT(size_, 0u); 167 return values_[begin_idx_]; 168 } 169 170 // Clear all samples. Reset()171 void Reset() { 172 size_ = 0; 173 begin_idx_ = 0; 174 last_idx_ = window_size_ - 1; 175 } 176 177 private: 178 const size_t window_size_; 179 // Circular buffer with some values in the window. 180 // Only possible candidates for maximum are stored: 181 // values form a non-increasing sequence. 182 std::vector<T> values_; 183 // Circular buffer storing when numbers in `values_` were added. 184 std::vector<size_t> added_at_; 185 // Begin of the circular buffers above. 186 size_t begin_idx_ = 0; 187 // Last occupied position. 188 size_t last_idx_; 189 // How many elements are stored in the circular buffers above. 190 size_t size_ = 0; 191 // Template parameter comparator. 192 const Comparator compare_; 193 }; 194 195 // Null implementation of the above class to be used when feature is disabled. 196 template <typename T> 197 struct NullExtremumImpl { NullExtremumImplNullExtremumImpl198 explicit NullExtremumImpl(size_t) {} 199 ~NullExtremumImpl() = default; AddSampleNullExtremumImpl200 void AddSample(const T&, size_t) {} ResetNullExtremumImpl201 void Reset() {} 202 }; 203 204 // Class to hold the moving window. 205 // It's used to calculate replaced element for Mean/Deviation calculations. 206 template <typename T> 207 class MovingWindowBase { 208 public: MovingWindowBase(size_t window_size)209 explicit MovingWindowBase(size_t window_size) : values_(window_size) {} 210 211 ~MovingWindowBase() = default; 212 AddSample(const T & sample)213 void AddSample(const T& sample) { 214 values_[cur_idx_] = sample; 215 ++cur_idx_; 216 if (cur_idx_ == values_.size()) { 217 cur_idx_ = 0; 218 } 219 } 220 221 // Is the window filled integer amount of times. IsLastIdx()222 bool IsLastIdx() const { return cur_idx_ + 1 == values_.size(); } 223 Reset()224 void Reset() { 225 cur_idx_ = 0; 226 std::fill(values_.begin(), values_.end(), T()); 227 } 228 GetValue()229 T GetValue() const { return values_[cur_idx_]; } 230 231 T operator[](size_t idx) const { return values_[idx]; } 232 Size()233 size_t Size() const { return values_.size(); } 234 235 // What index will be overwritten by a new element; CurIdx()236 size_t CurIdx() const { return cur_idx_; } 237 238 private: 239 // Circular buffer. 240 std::vector<T> values_; 241 // Where the buffer begins. 242 size_t cur_idx_ = 0; 243 }; 244 245 // Null implementation of the above class to be used when feature is disabled. 246 template <typename T> 247 struct NullWindowImpl { NullWindowImplNullWindowImpl248 explicit NullWindowImpl(size_t) {} 249 ~NullWindowImpl() = default; AddSampleNullWindowImpl250 void AddSample(const T& sample) {} IsLastIdxNullWindowImpl251 bool IsLastIdx() const { return false; } ResetNullWindowImpl252 void Reset() {} GetValueNullWindowImpl253 T GetValue() const { return T(); } 254 }; 255 256 // Performs division allowing the class to work with more types. 257 // General template. 258 template <typename SumType, typename ReturnType> 259 struct DivideInternal { ComputeDivideInternal260 static ReturnType Compute(const SumType& sum, const size_t count) { 261 return static_cast<ReturnType>(sum) / static_cast<ReturnType>(count); 262 } 263 }; 264 265 // Class to calculate moving mean. 266 template <typename T, typename SumType, bool IsFloating> 267 class MovingMeanBase { 268 public: MovingMeanBase(size_t window_size)269 explicit MovingMeanBase(size_t window_size) : sum_() {} 270 271 ~MovingMeanBase() = default; 272 AddSample(const T & sample,const T & replaced_value,bool is_last_idx)273 void AddSample(const T& sample, const T& replaced_value, bool is_last_idx) { 274 sum_ += sample - replaced_value; 275 } 276 277 template <typename ReturnType = SumType> Mean(const size_t count)278 ReturnType Mean(const size_t count) const { 279 if (count == 0) { 280 return ReturnType(); 281 } 282 return DivideInternal<SumType, ReturnType>::Compute(sum_, count); 283 } Reset()284 void Reset() { sum_ = SumType(); } 285 Sum()286 SumType Sum() const { return sum_; } 287 288 private: 289 SumType sum_; 290 }; 291 292 // Class to calculate moving mean. 293 // Variant for float types with running sum to avoid rounding errors 294 // accumulation. 295 template <typename T, typename SumType> 296 class MovingMeanBase<T, SumType, true> { 297 public: MovingMeanBase(size_t window_size)298 explicit MovingMeanBase(size_t window_size) : sum_(), running_sum_() {} 299 300 ~MovingMeanBase() = default; 301 AddSample(const T & sample,const T & replaced_value,bool is_last_idx)302 void AddSample(const T& sample, const T& replaced_value, bool is_last_idx) { 303 running_sum_ += sample; 304 if (is_last_idx) { 305 // Replace sum with running sum to avoid rounding errors accumulation. 306 sum_ = running_sum_; 307 running_sum_ = SumType(); 308 } else { 309 sum_ += sample - replaced_value; 310 } 311 } 312 313 template <typename ReturnType = SumType> Mean(const size_t count)314 ReturnType Mean(const size_t count) const { 315 if (count == 0) { 316 return ReturnType(); 317 } 318 return DivideInternal<SumType, ReturnType>::Compute(sum_, count); 319 } 320 Reset()321 void Reset() { sum_ = running_sum_ = SumType(); } 322 Sum()323 SumType Sum() const { return sum_; } 324 325 private: 326 SumType sum_; 327 SumType running_sum_; 328 }; 329 330 // Null implementation of the above class to be used when feature is disabled. 331 template <typename T> 332 struct NullMeanImpl { NullMeanImplNullMeanImpl333 explicit NullMeanImpl(size_t window_size) {} 334 ~NullMeanImpl() = default; 335 AddSampleNullMeanImpl336 void AddSample(const T& sample, const T&, bool) {} 337 ResetNullMeanImpl338 void Reset() {} 339 }; 340 341 // Computs main Deviation fromula, allowing the class to work with more types. 342 // Deviation is equal to mean of squared values minus squared mean value. 343 // General template. 344 template <typename SumType, typename ReturnType> 345 struct DeivationInternal { ComputeDeivationInternal346 static ReturnType Compute(const SumType& sum_squares, 347 const SumType& square_of_sum, 348 const size_t count) { 349 return static_cast<ReturnType>( 350 std::sqrt((static_cast<double>(sum_squares) - 351 static_cast<double>(square_of_sum) / count) / 352 count)); 353 } 354 }; 355 356 // Class to compute square of the number. 357 // General template 358 template <typename T, typename SquareType> 359 struct SquareInternal { ComputeSquareInternal360 static SquareType Compute(const T& sample) { 361 return static_cast<SquareType>(sample) * sample; 362 } 363 }; 364 365 // Class to calculate moving deviation. 366 template <typename T, typename SumType, bool IsFloating> 367 class MovingDeviationBase { 368 public: MovingDeviationBase(size_t window_size)369 explicit MovingDeviationBase(size_t window_size) : sum_sq_() {} 370 ~MovingDeviationBase() = default; AddSample(const T & sample,const T & replaced_value,bool is_last_idx)371 void AddSample(const T& sample, const T& replaced_value, bool is_last_idx) { 372 sum_sq_ += SquareInternal<T, SumType>::Compute(sample) - 373 SquareInternal<T, SumType>::Compute(replaced_value); 374 } 375 376 template <typename ReturnType, typename U> Deviation(const size_t count,const U & sum)377 ReturnType Deviation(const size_t count, const U& sum) const { 378 if (count == 0) { 379 return ReturnType(); 380 } 381 return DeivationInternal<SumType, ReturnType>::Compute( 382 sum_sq_, SquareInternal<U, SumType>::Compute(sum), count); 383 } Reset()384 void Reset() { sum_sq_ = SumType(); } 385 386 private: 387 SumType sum_sq_; 388 }; 389 390 // Class to calculate moving deviation. 391 // Variant for float types with running sum to avoid rounding errors 392 // accumulation. 393 template <typename T, typename SumType> 394 class MovingDeviationBase<T, SumType, true> { 395 public: MovingDeviationBase(size_t window_size)396 explicit MovingDeviationBase(size_t window_size) 397 : sum_sq_(), running_sum_() {} 398 ~MovingDeviationBase() = default; AddSample(const T & sample,const T & replaced_value,bool is_last_idx)399 void AddSample(const T& sample, const T& replaced_value, bool is_last_idx) { 400 SumType square = SquareInternal<T, SumType>::Compute(sample); 401 running_sum_ += square; 402 if (is_last_idx) { 403 // Replace sum with running sum to avoid rounding errors accumulation. 404 sum_sq_ = running_sum_; 405 running_sum_ = SumType(); 406 } else { 407 sum_sq_ += square - SquareInternal<T, SumType>::Compute(replaced_value); 408 } 409 } 410 411 template <typename ReturnType, typename U> Deviation(const size_t count,const U & sum)412 ReturnType Deviation(const size_t count, const U& sum) const { 413 if (count == 0) { 414 return ReturnType(); 415 } 416 return DeivationInternal<SumType, ReturnType>::Compute( 417 sum_sq_, SquareInternal<U, SumType>::Compute(sum), count); 418 } Reset()419 void Reset() { running_sum_ = sum_sq_ = SumType(); } 420 421 private: 422 SumType sum_sq_; 423 SumType running_sum_; 424 }; 425 426 // Null implementation of the above class to be used when feature is disabled. 427 template <typename T> 428 struct NullDeviationImpl { 429 public: NullDeviationImplNullDeviationImpl430 explicit NullDeviationImpl(size_t window_size) {} 431 ~NullDeviationImpl() = default; AddSampleNullDeviationImpl432 void AddSample(const T&, const T&, bool) {} ResetNullDeviationImpl433 void Reset() {} 434 }; 435 436 // Template helpers. 437 438 // Gets all enabled features in one struct. 439 template <typename... Features> 440 struct EnabledFeatures : public Features... {}; 441 442 template <typename T> 443 concept has_member_min = requires { T::has_min; }; 444 445 template <typename T> 446 concept has_member_max = requires { T::has_max; }; 447 448 template <typename T> 449 concept has_member_mean = requires { T::has_mean; }; 450 451 template <typename T> 452 concept has_member_deviation = requires { T::has_deviation; }; 453 454 template <typename T> 455 concept has_member_iteration = requires { T::has_iteration; }; 456 457 // Gets the type of the member if present. 458 // Can't just use decltype, because the member might be absent. 459 template <typename T> 460 struct get_type_mean { 461 using type = void; 462 }; 463 template <typename T> 464 requires has_member_mean<T> 465 struct get_type_mean<T> { 466 using type = decltype(T::has_mean); 467 }; 468 template <typename T> 469 using mean_t = typename get_type_mean<T>::type; 470 471 template <typename T> 472 struct get_type_deviation { 473 using type = void; 474 }; 475 template <typename T> 476 requires has_member_deviation<T> 477 struct get_type_deviation<T> { 478 using type = decltype(T::has_deviation); 479 }; 480 template <typename T> 481 using deviation_t = typename get_type_deviation<T>::type; 482 483 // Performs division allowing the class to work with more types. 484 // Specific template for TimeDelta. 485 template <> 486 struct DivideInternal<TimeDelta, TimeDelta> { 487 static TimeDelta Compute(const TimeDelta& sum, const size_t count) { 488 return sum / count; 489 } 490 }; 491 492 // Computs main Deviation fromula, allowing the class to work with more types. 493 // Deviation is equal to mean of squared values minus squared mean value. 494 // Specific template for TimeDelta. 495 template <> 496 struct DeivationInternal<double, TimeDelta> { 497 static TimeDelta Compute(const double sum_squares, 498 const double square_of_sum, 499 const size_t count) { 500 return Seconds(std::sqrt((sum_squares - square_of_sum / count) / count)); 501 } 502 }; 503 504 // Class to compute square of the number. 505 // Specific template for TimeDelta. 506 template <> 507 struct SquareInternal<TimeDelta, double> { 508 static double Compute(const TimeDelta& sample) { 509 return sample.InSecondsF() * sample.InSecondsF(); 510 } 511 }; 512 513 } // namespace internal 514 515 // Implementation of the main class. 516 template <typename T, typename... Features> 517 class MovingWindow { 518 public: 519 // List of all requested features. 520 using EnabledFeatures = internal::EnabledFeatures<Features...>; 521 522 explicit MovingWindow(size_t window_size) 523 : min_impl_(window_size), 524 max_impl_(window_size), 525 mean_impl_(window_size), 526 deviation_impl_(window_size), 527 window_impl_(window_size) {} 528 529 // Adds sample to the window. 530 void AddSample(const T& sample) { 531 ++total_added_; 532 min_impl_.AddSample(sample, total_added_); 533 max_impl_.AddSample(sample, total_added_); 534 mean_impl_.AddSample(sample, window_impl_.GetValue(), 535 window_impl_.IsLastIdx()); 536 deviation_impl_.AddSample(sample, window_impl_.GetValue(), 537 window_impl_.IsLastIdx()); 538 window_impl_.AddSample(sample); 539 } 540 541 // Returns amount of elementes so far in the stream (might be bigger than the 542 // window size). 543 size_t Count() const { return total_added_; } 544 545 // Calculates min in the window. 546 T Min() const 547 requires internal::has_member_min<EnabledFeatures> 548 { 549 return min_impl_.Value(); 550 } 551 552 // Calculates max in the window. 553 T Max() const 554 requires internal::has_member_max<EnabledFeatures> 555 { 556 return max_impl_.Value(); 557 } 558 559 // Calculates mean in the window. 560 // `ReturnType` can be used to adjust the type of the calculated mean value; 561 // if not specified, uses `T` by default. 562 template <typename ReturnType = T> 563 requires internal::has_member_mean<EnabledFeatures> 564 ReturnType Mean() const { 565 return mean_impl_.template Mean<ReturnType>( 566 std::min(total_added_, window_impl_.Size())); 567 } 568 569 // Calculates deviation in the window. 570 // `ReturnType` can be used to adjust the type of the calculated deviation 571 // value; if not specified, uses `T` by default. 572 template <typename ReturnType = T> 573 requires internal::has_member_deviation<EnabledFeatures> 574 ReturnType Deviation() const { 575 const size_t count = std::min(total_added_, window_impl_.Size()); 576 return deviation_impl_.template Deviation<ReturnType>(count, 577 mean_impl_.Sum()); 578 } 579 580 // Resets the state to an empty window. 581 void Reset() { 582 min_impl_.Reset(); 583 max_impl_.Reset(); 584 mean_impl_.Reset(); 585 deviation_impl_.Reset(); 586 window_impl_.Reset(); 587 total_added_ = 0; 588 } 589 590 // iterator implementation. 591 class iterator { 592 public: 593 ~iterator() = default; 594 595 const T operator*() { 596 DCHECK_LT(idx_, window_impl_->Size()); 597 return (*window_impl_)[idx_]; 598 } 599 600 iterator& operator++() { 601 ++idx_; 602 // Wrap around the circular buffer. 603 if (idx_ == window_impl_->Size()) { 604 idx_ = 0; 605 } 606 // The only way to arrive to the current element is to 607 // come around after iterating through the whole window. 608 if (idx_ == window_impl_->CurIdx()) { 609 idx_ = kInvalidIndex; 610 } 611 return *this; 612 } 613 614 bool operator==(const iterator& other) const { return idx_ == other.idx_; } 615 616 private: 617 iterator(const internal::MovingWindowBase<T>& window, size_t idx) 618 : window_impl_(window), idx_(idx) {} 619 620 static const size_t kInvalidIndex = std::numeric_limits<size_t>::max(); 621 622 raw_ref<const internal::MovingWindowBase<T>> window_impl_; 623 size_t idx_; 624 625 friend class MovingWindow<T, Features...>; 626 }; 627 628 // Begin iterator. Template to enable only if iteration feature is requested. 629 iterator begin() const 630 requires internal::has_member_iteration<EnabledFeatures> 631 { 632 if (total_added_ == 0) { 633 return end(); 634 } 635 // Before window is fully filled, the oldest element is at the index 0. 636 size_t idx = 637 (total_added_ < window_impl_.Size()) ? 0 : window_impl_.CurIdx(); 638 639 return iterator(window_impl_, idx); 640 } 641 642 // End iterator. Template to enable only if iteration feature is requested. 643 iterator end() const 644 requires internal::has_member_iteration<EnabledFeatures> 645 { 646 return iterator(window_impl_, iterator::kInvalidIndex); 647 } 648 649 // Size of the collection. Template to enable only if iteration feature is 650 // requested. 651 size_t size() const 652 requires internal::has_member_iteration<EnabledFeatures> 653 { 654 return std::min(total_added_, window_impl_.Size()); 655 } 656 657 private: 658 // Member for calculating min. 659 // Conditionally enabled on Min feature. 660 std::conditional_t<internal::has_member_min<EnabledFeatures>, 661 internal::MovingExtremumBase<T, std::greater<>>, 662 internal::NullExtremumImpl<T>> 663 min_impl_; 664 665 // Member for calculating min. 666 // Conditionally enabled on Min feature. 667 std::conditional_t<internal::has_member_max<EnabledFeatures>, 668 internal::MovingExtremumBase<T, std::less<>>, 669 internal::NullExtremumImpl<T>> 670 max_impl_; 671 672 // Type for sum value in Mean implementation. Might need to reuse deviation 673 // sum type, because enabling only deviation feature will also enable mean 674 // member (because deviation calculation depends on mean calculation). 675 using MeanSumType = 676 std::conditional_t<internal::has_member_mean<EnabledFeatures>, 677 internal::mean_t<EnabledFeatures>, 678 internal::deviation_t<EnabledFeatures>>; 679 // Member for calculating mean. 680 // Conditionally enabled on Mean or Deviation feature (because deviation 681 // calculation depends on mean calculation). 682 std::conditional_t< 683 internal::has_member_mean<EnabledFeatures> || 684 internal::has_member_deviation<EnabledFeatures>, 685 internal:: 686 MovingMeanBase<T, MeanSumType, std::is_floating_point_v<MeanSumType>>, 687 internal::NullMeanImpl<T>> 688 mean_impl_; 689 690 // Member for calculating deviation. 691 // Conditionally enabled on Deviation feature. 692 std::conditional_t< 693 internal::has_member_deviation<EnabledFeatures>, 694 internal::MovingDeviationBase< 695 T, 696 internal::deviation_t<EnabledFeatures>, 697 std::is_floating_point_v<internal::deviation_t<EnabledFeatures>>>, 698 internal::NullDeviationImpl<T>> 699 deviation_impl_; 700 701 // Member for storing the moving window. 702 // Conditionally enabled on Mean, Deviation or Iteration feature since 703 // they need the elements in the window. 704 // Min and Max features store elements internally so they don't need this. 705 std::conditional_t<internal::has_member_mean<EnabledFeatures> || 706 internal::has_member_deviation<EnabledFeatures> || 707 internal::has_member_iteration<EnabledFeatures>, 708 internal::MovingWindowBase<T>, 709 internal::NullWindowImpl<T>> 710 window_impl_; 711 // Total number of added elements. 712 size_t total_added_ = 0; 713 }; 714 715 } // namespace base 716 717 #endif // BASE_MOVING_WINDOW_H_ 718