Add unit tests and smaller updates for DecodeTimePercentileFilter.

Bug: b/493549134
Change-Id: I06e2ab28f31b952cbeb2e938a5f7f3d49026ceb4
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/462841
Commit-Queue: Åsa Persson <asapersson@webrtc.org>
Reviewed-by: Rasmus Brandt <brandtr@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47401}
diff --git a/modules/video_coding/timing/BUILD.gn b/modules/video_coding/timing/BUILD.gn
index 75de611..a9e40f4 100644
--- a/modules/video_coding/timing/BUILD.gn
+++ b/modules/video_coding/timing/BUILD.gn
@@ -120,6 +120,7 @@
 rtc_library("timing_unittests") {
   testonly = true
   sources = [
+    "decode_time_percentile_filter_unittest.cc",
     "frame_delay_variation_kalman_filter_unittest.cc",
     "inter_frame_delay_variation_calculator_unittest.cc",
     "jitter_estimator_unittest.cc",
@@ -128,6 +129,7 @@
     "timing_unittest.cc",
   ]
   deps = [
+    ":decode_time_percentile_filter",
     ":frame_delay_variation_kalman_filter",
     ":inter_frame_delay_variation_calculator",
     ":jitter_estimator",
diff --git a/modules/video_coding/timing/decode_time_percentile_filter.cc b/modules/video_coding/timing/decode_time_percentile_filter.cc
index e40ab5c..821cf0c 100644
--- a/modules/video_coding/timing/decode_time_percentile_filter.cc
+++ b/modules/video_coding/timing/decode_time_percentile_filter.cc
@@ -10,36 +10,37 @@
 
 #include "modules/video_coding/timing/decode_time_percentile_filter.h"
 
+#include <algorithm>
 #include <cstdint>
 
 namespace webrtc {
-
 namespace {
 
-// The first kIgnoredSampleCount samples will be ignored.
-const int kIgnoredSampleCount = 5;
-// Return the `kPercentile` value in RequiredDecodeTimeMs().
-const float kPercentile = 0.95f;
+// The number of initial samples to ignore.
+constexpr int kIgnoredSampleCount = 5;
+// The percentile value used by the filter.
+constexpr float kPercentile = 0.95f;
 // The window size in ms.
-const int64_t kTimeLimitMs = 10000;
+constexpr int64_t kTimeLimitMs = 10000;
 
-}  // anonymous namespace
+}  // namespace
 
 DecodeTimePercentileFilter::DecodeTimePercentileFilter()
-    : ignored_sample_count_(0), filter_(kPercentile) {}
+    : filter_(kPercentile) {}
 DecodeTimePercentileFilter::~DecodeTimePercentileFilter() = default;
 
-void DecodeTimePercentileFilter::AddTiming(int64_t decode_time_ms,
+void DecodeTimePercentileFilter::AddSample(int64_t decode_time_ms,
                                            int64_t now_ms) {
-  // Ignore the first `kIgnoredSampleCount` samples.
+  // Ignore the first samples.
   if (ignored_sample_count_ < kIgnoredSampleCount) {
     ++ignored_sample_count_;
     return;
   }
 
   // Insert new decode time value.
-  filter_.Insert(decode_time_ms);
-  history_.emplace(decode_time_ms, now_ms);
+  const int64_t capped_decode_ms = std::max(int64_t{0}, decode_time_ms);
+  filter_.Insert(capped_decode_ms);
+  history_.emplace(capped_decode_ms, now_ms);
 
   // Pop old decode time values.
   while (!history_.empty() &&
@@ -49,8 +50,7 @@
   }
 }
 
-// Get the 95th percentile observed decode time within a time window.
-int64_t DecodeTimePercentileFilter::RequiredDecodeTimeMs() const {
+int64_t DecodeTimePercentileFilter::GetPercentileMs() const {
   return filter_.GetPercentileValue();
 }
 
diff --git a/modules/video_coding/timing/decode_time_percentile_filter.h b/modules/video_coding/timing/decode_time_percentile_filter.h
index c2c653d..848b3cc 100644
--- a/modules/video_coding/timing/decode_time_percentile_filter.h
+++ b/modules/video_coding/timing/decode_time_percentile_filter.h
@@ -18,8 +18,7 @@
 
 namespace webrtc {
 
-// The `DecodeTimePercentileFilter` filters the actual per-frame decode times
-// and provides an estimate for the 95th percentile of those decode times. This
+// This class estimates the 95th percentile of per-frame decode times. This
 // estimate can be used to determine how large the "decode delay term" should be
 // when determining the render timestamp for a frame.
 class DecodeTimePercentileFilter {
@@ -27,12 +26,12 @@
   DecodeTimePercentileFilter();
   ~DecodeTimePercentileFilter();
 
-  // Add a new decode time to the filter.
-  void AddTiming(int64_t new_decode_time_ms, int64_t now_ms);
+  // Adds a new decode time to the filter.
+  void AddSample(int64_t decode_time_ms, int64_t now_ms);
 
-  // Get the required decode time in ms. It is the 95th percentile observed
-  // decode time within a time window.
-  int64_t RequiredDecodeTimeMs() const;
+  // Returns the 95th percentile of observed decode times within a time window,
+  // in milliseconds.
+  int64_t GetPercentileMs() const;
 
  private:
   struct Sample {
@@ -42,7 +41,7 @@
   };
 
   // The number of samples ignored so far.
-  int ignored_sample_count_;
+  int ignored_sample_count_ = 0;
   // Queue with history of latest decode time values.
   std::queue<Sample> history_;
   // `filter_` contains the same values as `history_`, but in a data structure
diff --git a/modules/video_coding/timing/decode_time_percentile_filter_unittest.cc b/modules/video_coding/timing/decode_time_percentile_filter_unittest.cc
new file mode 100644
index 0000000..12ef007
--- /dev/null
+++ b/modules/video_coding/timing/decode_time_percentile_filter_unittest.cc
@@ -0,0 +1,86 @@
+/*
+ *  Copyright (c) 2026 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 "modules/video_coding/timing/decode_time_percentile_filter.h"
+
+#include <cstdint>
+
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace {
+
+constexpr int64_t kDecodeTimeMs = 44;
+constexpr int64_t kNowMs = 1234;
+// From decode_time_percentile_filter.cc.
+constexpr int kIgnoredSampleCount = 5;
+constexpr int64_t kTimeLimitMs = 10000;
+
+void AddSamples(DecodeTimePercentileFilter& filter,
+                int count,
+                int64_t decode_time_ms,
+                int64_t now_ms) {
+  for (int i = 0; i < count; ++i) {
+    filter.AddSample(decode_time_ms, now_ms);
+  }
+}
+
+TEST(DecodeTimePercentileFilterTest, InitiallyReturnsZero) {
+  DecodeTimePercentileFilter filter;
+  EXPECT_EQ(filter.GetPercentileMs(), 0);
+}
+
+TEST(DecodeTimePercentileFilterTest, IgnoresFirstSamples) {
+  DecodeTimePercentileFilter filter;
+  AddSamples(filter, kIgnoredSampleCount, kDecodeTimeMs, kNowMs);
+  EXPECT_EQ(filter.GetPercentileMs(), 0);
+}
+
+TEST(DecodeTimePercentileFilterTest, IncludesSampleAfterIgnored) {
+  DecodeTimePercentileFilter filter;
+  AddSamples(filter, kIgnoredSampleCount + 1, kDecodeTimeMs, kNowMs);
+  EXPECT_EQ(filter.GetPercentileMs(), kDecodeTimeMs);
+}
+
+TEST(DecodeTimePercentileFilterTest, CapsNegativeSamplesToZero) {
+  DecodeTimePercentileFilter filter;
+  AddSamples(filter, kIgnoredSampleCount + 1, /*decode_time_ms=*/-1, kNowMs);
+  EXPECT_EQ(filter.GetPercentileMs(), 0);
+}
+
+TEST(DecodeTimePercentileFilterTest, Returns95thPercentile) {
+  DecodeTimePercentileFilter filter;
+  AddSamples(filter, kIgnoredSampleCount, kDecodeTimeMs, kNowMs);
+  for (int i = 1; i <= 20; ++i) {
+    filter.AddSample(/*decode_time_ms=*/i, kNowMs);
+  }
+  EXPECT_EQ(filter.GetPercentileMs(), 19);
+}
+
+TEST(DecodeTimePercentileFilterTest, KeepsSamplesWithinTimeWindow) {
+  DecodeTimePercentileFilter filter;
+  AddSamples(filter, kIgnoredSampleCount + 10, kDecodeTimeMs, kNowMs);
+  EXPECT_EQ(filter.GetPercentileMs(), kDecodeTimeMs);
+
+  filter.AddSample(/*decode_time_ms=*/3, kNowMs + kTimeLimitMs);
+  EXPECT_EQ(filter.GetPercentileMs(), kDecodeTimeMs);
+}
+
+TEST(DecodeTimePercentileFilterTest, DiscardsSamplesOutsideTimeWindow) {
+  DecodeTimePercentileFilter filter;
+  AddSamples(filter, kIgnoredSampleCount + 10, kDecodeTimeMs, kNowMs);
+  EXPECT_EQ(filter.GetPercentileMs(), kDecodeTimeMs);
+
+  filter.AddSample(/*decode_time_ms=*/3, kNowMs + kTimeLimitMs + 1);
+  EXPECT_EQ(filter.GetPercentileMs(), 3);
+}
+
+}  // namespace
+}  // namespace webrtc
diff --git a/modules/video_coding/timing/timing.cc b/modules/video_coding/timing/timing.cc
index e9d0ef2..5db6208 100644
--- a/modules/video_coding/timing/timing.cc
+++ b/modules/video_coding/timing/timing.cc
@@ -156,13 +156,11 @@
 
 void VCMTiming::StopDecodeTimer(TimeDelta decode_time, Timestamp now) {
   MutexLock lock(&mutex_);
-  decode_time_filter_->AddTiming(decode_time.ms(), now.ms());
   RTC_DCHECK_GE(decode_time, TimeDelta::Zero());
+  decode_time_filter_->AddSample(decode_time.ms(), now.ms());
   ++timings_.num_decoded_frames;
-
-  int max_decode_ms = decode_time_filter_->RequiredDecodeTimeMs();
-  RTC_DCHECK_GE(max_decode_ms, 0);
-  timings_.estimated_max_decode_time = TimeDelta::Millis(max_decode_ms);
+  timings_.estimated_max_decode_time =
+      TimeDelta::Millis(decode_time_filter_->GetPercentileMs());
 }
 
 void VCMTiming::IncomingTimestamp(uint32_t rtp_timestamp, Timestamp now) {
@@ -210,7 +208,7 @@
     // `render_time` == 0 indicates that the frame should be decoded and
     // rendered as soon as possible. However, the decoder can be choked if too
     // many frames are sent at once. Therefore, limit the interframe delay to
-    // |zero_playout_delay_min_pacing_| unless too many frames are queued in
+    // `zero_playout_delay_min_pacing_` unless too many frames are queued in
     // which case the frames are sent to the decoder at once.
     if (too_many_frames_queued) {
       return TimeDelta::Zero();
@@ -231,12 +229,6 @@
   return timings_.TargetDelay();
 }
 
-VideoFrame::RenderParameters VCMTiming::RenderParameters() const {
-  MutexLock lock(&mutex_);
-  return {.use_low_latency_rendering = timings_.UseLowLatencyRendering(),
-          .max_composition_delay_in_frames = max_composition_delay_in_frames_};
-}
-
 VCMTiming::VideoDelayTimings VCMTiming::GetTimings() const {
   MutexLock lock(&mutex_);
   VideoDelayTimings timings = timings_;
@@ -244,6 +236,12 @@
   return timings;
 }
 
+VideoFrame::RenderParameters VCMTiming::RenderParameters() const {
+  MutexLock lock(&mutex_);
+  return {.use_low_latency_rendering = timings_.UseLowLatencyRendering(),
+          .max_composition_delay_in_frames = max_composition_delay_in_frames_};
+}
+
 void VCMTiming::SetMaxCompositionDelayInFrames(
     std::optional<int> max_composition_delay_in_frames) {
   MutexLock lock(&mutex_);