Add WindowedMinFilter to rtc_base/numerics

The filter returns the min value within a fixed size window.


Bug: webrtc:447037083
Change-Id: If42ff0e19af8b4a6aaf8f4addc11e326b4244e43
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/411760
Reviewed-by: Danil Chapovalov <danilchap@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Per Kjellander <perkj@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#45741}
diff --git a/rtc_base/BUILD.gn b/rtc_base/BUILD.gn
index ce7896b..49245ac 100644
--- a/rtc_base/BUILD.gn
+++ b/rtc_base/BUILD.gn
@@ -737,6 +737,11 @@
   ]
 }
 
+rtc_library("windowed_min_filter") {
+  sources = [ "numerics/windowed_min_filter.h" ]
+  deps = [ ":checks" ]
+}
+
 rtc_library("rtc_numerics") {
   sources = [
     "numerics/event_based_exponential_moving_average.cc",
@@ -2220,13 +2225,17 @@
         "numerics/running_statistics_unittest.cc",
         "numerics/sequence_number_unwrapper_unittest.cc",
         "numerics/sequence_number_util_unittest.cc",
+        "numerics/windowed_min_filter_unittest.cc",
       ]
       deps = [
         ":mod_ops",
         ":rtc_numerics",
         ":timeutils",
+        ":windowed_min_filter",
+        "../api/units:time_delta",
         "../test:test_main",
         "../test:test_support",
+        "//testing/gtest",
         "//third_party/abseil-cpp/absl/algorithm:container",
       ]
     }
diff --git a/rtc_base/numerics/windowed_min_filter.h b/rtc_base/numerics/windowed_min_filter.h
new file mode 100644
index 0000000..42f4fc1
--- /dev/null
+++ b/rtc_base/numerics/windowed_min_filter.h
@@ -0,0 +1,78 @@
+/*
+ *  Copyright (c) 2025 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef RTC_BASE_NUMERICS_WINDOWED_MIN_FILTER_H_
+#define RTC_BASE_NUMERICS_WINDOWED_MIN_FILTER_H_
+
+#include <deque>
+
+#include "rtc_base/checks.h"
+
+namespace webrtc {
+
+template <typename V>
+class WindowedMinFilter {
+ public:
+  explicit WindowedMinFilter(int window_length) : max_size_(window_length) {
+    RTC_DCHECK_GT(window_length, 1);
+  }
+
+  void Insert(V value) {
+    if (!min_values_.empty()) {
+      if (min_values_.front().index == index_) {
+        // Min value is too old.
+        min_values_.pop_front();
+      }
+
+      // If value <= min_values_.front().value, value is the minimum value and
+      // we can forget all other. The alternative had been to always
+      // check the back value, but then we would also have to check for
+      // empty.
+      if (min_values_.front().value >= value) {
+        min_values_.clear();
+      } else {
+        while (min_values_.back().value >= value) {
+          min_values_.pop_back();
+        }
+      }
+    }
+    RTC_DCHECK_LT(min_values_.size(), max_size_);
+    min_values_.push_back({.value = value, .index = index_});
+    index_ = (index_ + 1) % max_size_;
+  }
+
+  // Returns the min value within the window. If no value has been inserted,
+  // returns the default value of V.
+  V GetMin() const {
+    if (min_values_.empty()) {
+      return V();
+    }
+    return min_values_.front().value;
+  }
+
+  void Reset() {
+    min_values_.clear();
+    index_ = 0;
+  }
+
+ private:
+  const int max_size_;
+  struct ValueAndIndex {
+    V value;
+    int index;
+  };
+
+  int index_ = 0;
+  std::deque<ValueAndIndex> min_values_;
+};
+
+}  // namespace webrtc
+
+#endif  // RTC_BASE_NUMERICS_WINDOWED_MIN_FILTER_H_
diff --git a/rtc_base/numerics/windowed_min_filter_unittest.cc b/rtc_base/numerics/windowed_min_filter_unittest.cc
new file mode 100644
index 0000000..ce6d8ae
--- /dev/null
+++ b/rtc_base/numerics/windowed_min_filter_unittest.cc
@@ -0,0 +1,93 @@
+/*
+ *  Copyright (c) 2025 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "rtc_base/numerics/windowed_min_filter.h"
+
+#include <string>
+
+#include "api/units/time_delta.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace {
+
+TEST(WindowedMinFilterTest, EmptyFilterReturnZero) {
+  WindowedMinFilter<TimeDelta> filter(/*window_length=*/3);
+  EXPECT_EQ(filter.GetMin(), TimeDelta::Zero());
+}
+
+TEST(WindowedMinFilterTest, EmptyFilterReturnEmptyString) {
+  WindowedMinFilter<std::string> filter(/*window_length=*/3);
+  EXPECT_EQ(filter.GetMin(), "");
+}
+
+TEST(WindowedMinFilterTest, GetMinReturnsMin) {
+  WindowedMinFilter<int> filter(/*window_length=*/3);
+
+  filter.Insert(30);
+  EXPECT_EQ(filter.GetMin(), 30);
+  filter.Insert(20);
+  EXPECT_EQ(filter.GetMin(), 20);
+  filter.Insert(10);
+  EXPECT_EQ(filter.GetMin(), 10);
+}
+
+TEST(WindowedMinFilterTest, GetMinReturnsMinNotSortedInput) {
+  WindowedMinFilter<int> filter(/*window_length=*/4);
+
+  filter.Insert(0);
+  filter.Insert(30);
+  EXPECT_EQ(filter.GetMin(), 0);
+  filter.Insert(10);
+  EXPECT_EQ(filter.GetMin(), 0);
+  filter.Insert(40);
+  EXPECT_EQ(filter.GetMin(), 0);
+  filter.Insert(40);
+  EXPECT_EQ(filter.GetMin(), 10);
+}
+
+TEST(WindowedMinFilterTest, GetMinReturnsMinWithStringsNotSorted) {
+  WindowedMinFilter<std::string> filter(/*window_length=*/3);
+
+  filter.Insert("bbb");
+  EXPECT_EQ(filter.GetMin(), "bbb");
+  filter.Insert("ccc");
+  EXPECT_EQ(filter.GetMin(), "bbb");
+  filter.Insert("aaa");
+  EXPECT_EQ(filter.GetMin(), "aaa");
+}
+
+TEST(WindowedMinFilterTest, GetMinReturnsMinInWindow) {
+  WindowedMinFilter<int> filter(/*window_length=*/3);
+
+  filter.Insert(10);
+  filter.Insert(20);
+  filter.Insert(30);
+  EXPECT_EQ(filter.GetMin(), 10);
+  filter.Insert(40);
+  EXPECT_EQ(filter.GetMin(), 20);
+  filter.Insert(50);
+  EXPECT_EQ(filter.GetMin(), 30);
+}
+
+TEST(WindowedMinFilterTest, RestartAfterReset) {
+  WindowedMinFilter<int> filter(/*window_length=*/3);
+
+  filter.Insert(10);
+  filter.Insert(20);
+  ASSERT_EQ(filter.GetMin(), 10);
+  filter.Reset();
+  EXPECT_EQ(filter.GetMin(), 0);
+  filter.Insert(30);
+  EXPECT_EQ(filter.GetMin(), 30);
+}
+
+}  // namespace
+}  // namespace webrtc
diff --git a/test/fuzzers/BUILD.gn b/test/fuzzers/BUILD.gn
index e7a254c..5611c78 100644
--- a/test/fuzzers/BUILD.gn
+++ b/test/fuzzers/BUILD.gn
@@ -829,6 +829,15 @@
   ]
 }
 
+webrtc_fuzzer_test("windowed_min_filter_fuzzer") {
+  sources = [ "windowed_min_filter_fuzzer.cc" ]
+  deps = [
+    "../../rtc_base:checks",
+    "../../rtc_base:windowed_min_filter",
+    "//third_party/abseil-cpp/absl/algorithm:container",
+  ]
+}
+
 group("fuzzers") {
   testonly = true
   deps = [
@@ -889,6 +898,7 @@
     ":vp9_replay_fuzzer",
     ":webrtc_base64_decode_fuzzer",
     ":webrtc_base64_encode_fuzzer",
+    ":windowed_min_filter_fuzzer",
   ]
   if (rtc_use_h265) {
     deps += [
diff --git a/test/fuzzers/windowed_min_filter_fuzzer.cc b/test/fuzzers/windowed_min_filter_fuzzer.cc
new file mode 100644
index 0000000..a79f6bc
--- /dev/null
+++ b/test/fuzzers/windowed_min_filter_fuzzer.cc
@@ -0,0 +1,47 @@
+/*
+ *  Copyright (c) 2025 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include <deque>
+#include "absl/algorithm/container.h"
+#include "rtc_base/numerics/windowed_min_filter.h"
+#include "rtc_base/checks.h"
+#include "test/fuzzers/fuzz_data_helper.h"
+
+namespace webrtc {
+
+void FuzzOneInput(const uint8_t* data, size_t size) {
+  class ReferenceFilter {
+   public:
+    explicit ReferenceFilter(int window_length) : max_size_(window_length) {}
+    void Insert(int value) {
+      buffer_.push_back(value);
+      if (buffer_.size() > max_size_) {
+        buffer_.pop_front();
+      }
+    }
+    int GetMin() const { return *absl::c_min_element(buffer_); }
+
+   private:
+    const size_t max_size_;
+    std::deque<int> buffer_;
+  };
+
+  ReferenceFilter reference_filter(/*window_length=*/10);
+  WindowedMinFilter<int> filter(/*window_length=*/10);
+  test::FuzzDataHelper fuzz_data(MakeArrayView(data, size));
+
+  while (fuzz_data.CanReadBytes(sizeof(int))) {
+    int value = fuzz_data.Read<int>();
+    reference_filter.Insert(value);
+    filter.Insert(value);
+    RTC_CHECK_EQ(filter.GetMin(), reference_filter.GetMin());
+  }
+}
+}  // namespace webrtc