Adds rust version of webrtc::RateTracker 

This includes some updates to how units FFI is exposed and removes the
previous bridge tests as now we have real tests for cxx bridges.

Bug: webrtc:416446214
Change-Id: I027e68b4521e14d5a1687dd0f0d03ed16a6a6964
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/470180
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Auto-Submit: Evan Shrubsole <eshr@webrtc.org>
Commit-Queue: Evan Shrubsole <eshr@webrtc.org>
Reviewed-by: Victor Boivie <boivie@webrtc.org>
Commit-Queue: Mirko Bonadei <mbonadei@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47663}
diff --git a/api/units/BUILD.gn b/api/units/BUILD.gn
index f48cd0b..e004878 100644
--- a/api/units/BUILD.gn
+++ b/api/units/BUILD.gn
@@ -107,20 +107,25 @@
       "../../test:test_support",
       "//third_party/abseil-cpp/absl/strings",
     ]
-    if (rtc_rust) {
-      deps += [ ":units_rs_bridge_unittests" ]
-    }
   }
 }
 
 if (rtc_rust) {
-  rtc_rust_cxx_bridge("units_ffi") {
-    source = "units_ffi.rs"
+  rtc_rust_library("time_delta_ffi_rust") {
+    crate_root = "time_delta_ffi.rs"
+    sources = [ "time_delta_ffi.rs" ]
     deps = [
       ":time_delta",
       ":time_delta_rs",
-      ":timestamp",
-      ":timestamp_rs",
+      "//build/rust:cxx_rustdeps",
+    ]
+  }
+  rtc_rust_cxx_bridge("time_delta_ffi") {
+    source = "time_delta_ffi.rs"
+    deps = [
+      ":time_delta",
+      ":time_delta_ffi_rust",
+      ":time_delta_rs",
       "//build/rust:cxx_rustdeps",
     ]
   }
@@ -135,6 +140,27 @@
     ]
   }
 
+  rtc_rust_library("timestamp_ffi_rust") {
+    allow_unsafe = true
+    crate_root = "timestamp_ffi.rs"
+    sources = [ "timestamp_ffi.rs" ]
+    deps = [
+      ":timestamp",
+      ":timestamp_rs",
+      "//build/rust:cxx_rustdeps",
+    ]
+  }
+
+  rtc_rust_cxx_bridge("timestamp_ffi") {
+    source = "timestamp_ffi.rs"
+    deps = [
+      ":timestamp",
+      ":timestamp_ffi_rust",
+      ":timestamp_rs",
+      "//build/rust:cxx_rustdeps",
+    ]
+  }
+
   rtc_rust_library("timestamp_rs") {
     allow_unsafe = true
     crate_root = "timestamp.rs"
@@ -176,40 +202,5 @@
         ":timestamp_rs_unittests",
       ]
     }
-
-    rtc_rust_library("demo_unit_interop_impl") {
-      allow_unsafe = true
-      crate_root = "demo_unit_interop.rs"
-      sources = [ "demo_unit_interop.rs" ]
-      deps = [
-        ":time_delta_rs",
-        ":timestamp_rs",
-        "//build/rust:cxx_rustdeps",
-      ]
-    }
-
-    rtc_rust_cxx_bridge("demo_unit_interop") {
-      allow_unsafe = true
-      source = "demo_unit_interop.rs"
-      deps = [
-        ":demo_unit_interop_impl",
-        ":time_delta",
-        ":time_delta_rs",
-        ":timestamp",
-        ":timestamp_rs",
-        "//third_party/abseil-cpp/absl/strings",
-      ]
-    }
-
-    rtc_library("units_rs_bridge_unittests") {
-      testonly = true
-      sources = [ "units_rs_bridge_unittest.cc" ]
-      deps = [
-        ":demo_unit_interop",
-        ":time_delta",
-        ":timestamp",
-        "../../test:test_support",
-      ]
-    }
   }
 }
diff --git a/api/units/time_delta.rs b/api/units/time_delta.rs
index 510f932..5f22478 100644
--- a/api/units/time_delta.rs
+++ b/api/units/time_delta.rs
@@ -26,27 +26,27 @@
 }
 
 impl TimeDelta {
-    pub fn ms(&self) -> i64 {
+    pub const fn ms(&self) -> i64 {
         self.microseconds / 1000
     }
 
-    pub fn us(&self) -> i64 {
+    pub const fn us(&self) -> i64 {
         self.microseconds
     }
 
-    pub fn from_millis(value: i64) -> Self {
+    pub const fn from_millis(value: i64) -> Self {
         Self { microseconds: value * 1000 }
     }
 
-    pub fn from_micros(value: i64) -> Self {
+    pub const fn from_micros(value: i64) -> Self {
         Self { microseconds: value }
     }
 
-    pub fn zero() -> Self {
+    pub const fn zero() -> Self {
         Self { microseconds: 0 }
     }
 
-    pub fn seconds_f64(&self) -> f64 {
+    pub const fn seconds_f64(&self) -> f64 {
         self.microseconds as f64 / 1_000_000.0
     }
 }
diff --git a/api/units/time_delta_ffi.rs b/api/units/time_delta_ffi.rs
new file mode 100644
index 0000000..4729235
--- /dev/null
+++ b/api/units/time_delta_ffi.rs
@@ -0,0 +1,20 @@
+// 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.
+
+webrtc::import! {
+  "//api/units:time_delta_rs" as time_delta;
+}
+
+#[cxx::bridge(namespace = "webrtc")]
+pub mod ffi {
+    unsafe extern "C++" {
+        include!("api/units/time_delta.h");
+
+        type TimeDelta = super::time_delta::TimeDelta;
+    }
+}
diff --git a/api/units/timestamp.rs b/api/units/timestamp.rs
index f3a00d7..55269be 100644
--- a/api/units/timestamp.rs
+++ b/api/units/timestamp.rs
@@ -24,19 +24,19 @@
 }
 
 impl Timestamp {
-    pub fn ms(&self) -> i64 {
+    pub const fn ms(&self) -> i64 {
         self.microseconds / 1000
     }
 
-    pub fn us(&self) -> i64 {
+    pub const fn us(&self) -> i64 {
         self.microseconds
     }
 
-    pub fn from_millis(value: i64) -> Self {
+    pub const fn from_millis(value: i64) -> Self {
         Self { microseconds: value * 1000 }
     }
 
-    pub fn from_micros(value: i64) -> Self {
+    pub const fn from_micros(value: i64) -> Self {
         Self { microseconds: value }
     }
 }
diff --git a/api/units/timestamp_ffi.rs b/api/units/timestamp_ffi.rs
new file mode 100644
index 0000000..90752ff
--- /dev/null
+++ b/api/units/timestamp_ffi.rs
@@ -0,0 +1,20 @@
+// 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.
+
+webrtc::import! {
+  "//api/units:timestamp_rs" as timestamp;
+}
+
+#[cxx::bridge(namespace = "webrtc")]
+pub mod ffi {
+    unsafe extern "C++" {
+        include!("api/units/timestamp.h");
+
+        type Timestamp = super::timestamp::Timestamp;
+    }
+}
diff --git a/rtc_base/BUILD.gn b/rtc_base/BUILD.gn
index 6277570..f156680 100644
--- a/rtc_base/BUILD.gn
+++ b/rtc_base/BUILD.gn
@@ -338,6 +338,71 @@
   ]
 }
 
+if (rtc_rust) {
+  rtc_rust_library("rate_tracker_rust") {
+    crate_root = "rate_tracker.rs"
+    sources = [ "rate_tracker.rs" ]
+    deps = [
+      ":checks",
+      "../api/units:time_delta",
+      "../api/units:time_delta_rs",
+      "../api/units:timestamp",
+      "../api/units:timestamp_rs",
+      "//build/rust:cxx_rustdeps",
+    ]
+    visibility = [ "*" ]
+  }
+
+  rtc_rust_library("rate_tracker_ffi_rust") {
+    allow_unsafe = true
+    crate_root = "rate_tracker_ffi.rs"
+    sources = [ "rate_tracker_ffi.rs" ]
+    deps = [
+      ":rate_tracker_rust",
+      "../api/units:time_delta_rs",
+      "../api/units:timestamp_rs",
+      "//build/rust:cxx_rustdeps",
+    ]
+  }
+
+  rtc_rust_cxx_bridge("rate_tracker_ffi") {
+    allow_unsafe = true
+    source = "rate_tracker_ffi.rs"
+    deps = [
+      ":rate_tracker_ffi_rust",
+      ":rate_tracker_rust",
+      "../api/units:time_delta",
+      "../api/units:time_delta_ffi",
+      "../api/units:time_delta_rs",
+      "../api/units:timestamp",
+      "../api/units:timestamp_ffi",
+      "../api/units:timestamp_rs",
+      "//build/rust:cxx_rustdeps",
+    ]
+  }
+
+  rtc_rust_unittest("rate_tracker_rust_tests") {
+    crate_root = "rate_tracker.rs"
+    sources = [ "rate_tracker.rs" ]
+    deps = [
+      ":checks",
+      "../api/units:time_delta",
+      "../api/units:time_delta_rs",
+      "../api/units:timestamp",
+      "../api/units:timestamp_rs",
+    ]
+  }
+
+  rtc_rust_test_suite("rust_unit_tests") {
+    deps = [ ":rate_tracker_rust_tests" ]
+  }
+} else {
+  # This target is required for the bot configuration as it is always
+  # present in the more_configs_tests test suite even if Rust is disabled.
+  group("rust_unit_tests") {
+  }
+}
+
 rtc_library("rate_tracker") {
   visibility = [ "*" ]
   sources = [
@@ -2195,6 +2260,13 @@
       if (is_win) {
         deps += [ "win:windows_version_unittest" ]
       }
+      if (rtc_rust) {
+        sources += [ "rate_tracker_comparison_unittest.cc" ]
+        deps += [
+          ":rate_tracker_ffi",
+          "//third_party/abseil-cpp/absl/random",
+        ]
+      }
     }
 
     rtc_library("rtc_task_queue_unittests") {
diff --git a/rtc_base/DEPS b/rtc_base/DEPS
index c28d27a..2f81863 100644
--- a/rtc_base/DEPS
+++ b/rtc_base/DEPS
@@ -49,4 +49,7 @@
   "base64_rust\\.cc": [
     "+third_party/rust/chromium_crates_io/vendor/cxx-v1/include/cxx.h",
   ],
+  "rate_tracker_comparison_unittest\\.cc": [
+    "+absl/random/random.h",
+  ],
 }
diff --git a/rtc_base/rate_tracker.cc b/rtc_base/rate_tracker.cc
index 517bd50..abd6a9b 100644
--- a/rtc_base/rate_tracker.cc
+++ b/rtc_base/rate_tracker.cc
@@ -137,4 +137,9 @@
   return (bucket_index + 1u) % (bucket_count_ + 1u);
 }
 
+double RateTracker::Rate(Timestamp current_time) const {
+  return ComputeRateForInterval(
+      current_time, TimeDelta::Millis(bucket_milliseconds_) * bucket_count_);
+}
+
 }  // namespace webrtc
diff --git a/rtc_base/rate_tracker.h b/rtc_base/rate_tracker.h
index 39ec6e7..89240fa 100644
--- a/rtc_base/rate_tracker.h
+++ b/rtc_base/rate_tracker.h
@@ -33,12 +33,7 @@
   double ComputeRateForInterval(Timestamp current_time,
                                 TimeDelta interval) const;
 
-  // Computes the average rate over the rate tracker's recording interval
-  // of bucket_milliseconds * bucket_count.
-  double Rate(Timestamp current_time) const {
-    return ComputeRateForInterval(
-        current_time, TimeDelta::Millis(bucket_milliseconds_) * bucket_count_);
-  }
+  double Rate(Timestamp current_time) const;
 
   // The total number of samples added.
   int64_t TotalSampleCount() const;
@@ -61,5 +56,4 @@
 
 }  //  namespace webrtc
 
-
 #endif  // RTC_BASE_RATE_TRACKER_H_
diff --git a/rtc_base/rate_tracker.rs b/rtc_base/rate_tracker.rs
new file mode 100644
index 0000000..72439af
--- /dev/null
+++ b/rtc_base/rate_tracker.rs
@@ -0,0 +1,342 @@
+/*
+ *  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.
+ */
+
+webrtc::import! {
+  "//api/units:time_delta_rs" as time_delta;
+  "//api/units:timestamp_rs" as timestamp;
+}
+use std::cmp;
+use time_delta::TimeDelta;
+use timestamp::Timestamp;
+
+/// Computes units per second over a given interval by tracking the units over
+/// each bucket of a given size and calculating the instantaneous rate assuming
+/// that over each bucket the rate was constant.
+pub struct RustRateTracker {
+    bucket: TimeDelta,
+    bucket_count: usize,
+    sample_buckets: Vec<i64>,
+    total_sample_count: i64,
+    current_bucket: usize,
+    bucket_start_time: Option<Timestamp>,
+    initialization_time: Option<Timestamp>,
+}
+
+impl RustRateTracker {
+    pub fn new(bucket: TimeDelta, bucket_count: usize) -> Self {
+        assert!(bucket > TimeDelta::zero());
+        Self {
+            bucket,
+            bucket_count,
+            sample_buckets: vec![0; bucket_count + 1],
+            total_sample_count: 0,
+            current_bucket: 0,
+            bucket_start_time: None,
+            initialization_time: None,
+        }
+    }
+
+    /// Computes the average rate over the most recent `interval_ms`,
+    /// or if the first sample was added within this period, computes the rate
+    /// since the first sample was added.
+    pub fn compute_rate_for_interval(&self, current_time: Timestamp, interval: TimeDelta) -> f64 {
+        let bucket_start_time = match self.bucket_start_time {
+            Some(time) => time,
+            None => return 0.0,
+        };
+
+        // Calculate which buckets to sum up given the current time. If the time
+        // has passed to a new bucket then we have to skip some of the oldest buckets.
+        let mut available_interval = cmp::min(interval, self.bucket * self.bucket_count);
+
+        let buckets_to_skip;
+        let duration_to_skip;
+
+        if current_time > self.initialization_time.unwrap() + available_interval {
+            let time_to_skip = current_time - bucket_start_time + self.bucket * self.bucket_count
+                - available_interval;
+            buckets_to_skip = (time_to_skip / self.bucket) as usize;
+            duration_to_skip = time_to_skip % self.bucket;
+        } else {
+            buckets_to_skip = self.bucket_count - self.current_bucket;
+            duration_to_skip = TimeDelta::zero();
+            available_interval = current_time - self.initialization_time.unwrap();
+            // Let one bucket interval pass after initialization before reporting.
+            if available_interval < self.bucket {
+                return 0.0;
+            }
+        }
+
+        // If we're skipping all buckets that means that there have been no samples
+        // within the sampling interval so report 0.
+        if buckets_to_skip > self.bucket_count || available_interval == TimeDelta::zero() {
+            return 0.0;
+        }
+
+        let start_bucket = self.next_bucket_index(self.current_bucket + buckets_to_skip);
+        // Only count a portion of the first bucket according to how much of the
+        // first bucket is within the current interval.
+        // We use i128 to avoid overflow during the multiplication.
+        let mut total_samples = (((self.sample_buckets[start_bucket] as i128
+            * (self.bucket.us() - duration_to_skip.us()) as i128)
+            + (self.bucket.us() / 2) as i128)
+            / self.bucket.us() as i128) as i64;
+
+        // All other buckets in the interval are counted in their entirety.
+        let mut i = self.next_bucket_index(start_bucket);
+        while i != self.next_bucket_index(self.current_bucket) {
+            total_samples += self.sample_buckets[i];
+            i = self.next_bucket_index(i);
+        }
+
+        // Convert to samples per second.
+        total_samples as f64 / available_interval.seconds_f64()
+    }
+
+    /// Computes the average rate over the rate tracker's recording interval
+    /// of `bucket` * `bucket_count`.
+    pub fn rate(&self, current_time: Timestamp) -> f64 {
+        self.compute_rate_for_interval(current_time, self.bucket * self.bucket_count)
+    }
+
+    /// The total number of samples added.
+    pub fn total_sample_count(&self) -> i64 {
+        self.total_sample_count
+    }
+
+    /// Increment count for bucket at `current_time`.
+    pub fn update(&mut self, sample_count: i64, current_time: Timestamp) {
+        debug_assert!(sample_count >= 0);
+        self.ensure_initialized(current_time);
+
+        let mut bucket_start_time = self.bucket_start_time.unwrap();
+
+        // Advance the current bucket as needed for the current time, and reset
+        // bucket counts as we advance.
+        let mut i = 0;
+        while i <= self.bucket_count && current_time >= bucket_start_time + self.bucket {
+            bucket_start_time += self.bucket;
+            self.current_bucket = self.next_bucket_index(self.current_bucket);
+            self.sample_buckets[self.current_bucket] = 0;
+            i += 1;
+        }
+
+        // Ensure that bucket_start_time is updated appropriately if
+        // the entire buffer of samples has been expired.
+        bucket_start_time += self.bucket * ((current_time - bucket_start_time) / self.bucket);
+
+        self.bucket_start_time = Some(bucket_start_time);
+
+        // Add all samples in the bucket that includes the current time.
+        self.sample_buckets[self.current_bucket] += sample_count;
+        self.total_sample_count += sample_count;
+    }
+
+    fn ensure_initialized(&mut self, current_time: Timestamp) {
+        if self.bucket_start_time.is_none() {
+            self.initialization_time = Some(current_time);
+            self.bucket_start_time = Some(current_time);
+            self.current_bucket = 0;
+            // We only need to initialize the first bucket because we reset buckets when
+            // current_bucket increments.
+            self.sample_buckets[self.current_bucket] = 0;
+        }
+    }
+
+    fn next_bucket_index(&self, bucket_index: usize) -> usize {
+        (bucket_index + 1) % (self.bucket_count + 1)
+    }
+}
+
+pub fn create_rate_tracker(bucket: TimeDelta, bucket_count: usize) -> Box<RustRateTracker> {
+    Box::new(RustRateTracker::new(bucket, bucket_count))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const BUCKET_INTERVAL: TimeDelta = TimeDelta::from_millis(100);
+
+    struct RateTrackerForTest {
+        rate_tracker: RustRateTracker,
+        time: Timestamp,
+    }
+
+    impl RateTrackerForTest {
+        fn new() -> Self {
+            Self {
+                rate_tracker: RustRateTracker::new(BUCKET_INTERVAL, 10),
+                time: Timestamp::from_millis(0),
+            }
+        }
+
+        fn advance_time(&mut self, delta: TimeDelta) {
+            self.time += delta;
+        }
+
+        fn compute_rate(&self) -> f64 {
+            self.rate_tracker.rate(self.time)
+        }
+
+        fn compute_rate_for_interval(&self, interval: TimeDelta) -> f64 {
+            self.rate_tracker.compute_rate_for_interval(self.time, interval)
+        }
+
+        fn total_sample_count(&self) -> i64 {
+            self.rate_tracker.total_sample_count()
+        }
+
+        fn add_samples(&mut self, samples_count: i64) {
+            self.rate_tracker.update(samples_count, self.time);
+        }
+    }
+
+    #[test]
+    fn test_30_fps() {
+        let mut tracker = RateTrackerForTest::new();
+        for i in 0..300 {
+            tracker.add_samples(1);
+            tracker.advance_time(TimeDelta::from_millis(33));
+            if i % 3 == 0 {
+                tracker.advance_time(TimeDelta::from_millis(1));
+            }
+        }
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(50000)), 30.0);
+    }
+
+    #[test]
+    fn test_60_fps() {
+        let mut tracker = RateTrackerForTest::new();
+        for i in 0..300 {
+            tracker.add_samples(1);
+            tracker.advance_time(TimeDelta::from_millis(16));
+            if i % 3 != 0 {
+                tracker.advance_time(TimeDelta::from_millis(1));
+            }
+        }
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 60.0);
+    }
+
+    #[test]
+    fn test_rate_tracker_basics() {
+        let mut tracker = RateTrackerForTest::new();
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 0.0);
+
+        // Add a sample.
+        tracker.add_samples(1234);
+        // Advance the clock by less than one bucket interval (no rate returned).
+        tracker.advance_time(BUCKET_INTERVAL - TimeDelta::from_millis(1));
+        assert_eq!(tracker.compute_rate(), 0.0);
+        // Advance the clock by 100 ms (one bucket interval).
+        tracker.advance_time(TimeDelta::from_millis(1));
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 12340.0);
+        assert_eq!(tracker.compute_rate(), 12340.0);
+        assert_eq!(tracker.total_sample_count(), 1234);
+
+        // Repeat.
+        tracker.add_samples(1234);
+        tracker.advance_time(TimeDelta::from_millis(100));
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 12340.0);
+        assert_eq!(tracker.compute_rate(), 12340.0);
+        assert_eq!(tracker.total_sample_count(), 1234 * 2);
+
+        // Advance the clock by 800 ms, so we've elapsed a full second.
+        tracker.advance_time(TimeDelta::from_millis(800));
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 1234.0 * 2.0);
+        assert_eq!(tracker.compute_rate(), 1234.0 * 2.0);
+        assert_eq!(tracker.total_sample_count(), 1234 * 2);
+
+        // Poll the tracker again immediately. The reported rate should stay the same.
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 1234.0 * 2.0);
+        assert_eq!(tracker.compute_rate(), 1234.0 * 2.0);
+        assert_eq!(tracker.total_sample_count(), 1234 * 2);
+
+        // Do nothing and advance by a second. We should drop down to zero.
+        tracker.advance_time(TimeDelta::from_millis(1000));
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 0.0);
+        assert_eq!(tracker.compute_rate(), 0.0);
+        assert_eq!(tracker.total_sample_count(), 1234 * 2);
+
+        // Send a bunch of data at a constant rate for 5.5 "seconds".
+        for _ in (0..5500).step_by(100) {
+            tracker.add_samples(9876);
+            tracker.advance_time(TimeDelta::from_millis(100));
+        }
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 9876.0 * 10.0);
+        assert_eq!(tracker.compute_rate(), 9876.0 * 10.0);
+        assert_eq!(tracker.total_sample_count(), 1234 * 2 + 9876 * 55);
+
+        // Advance the clock by 500 ms.
+        tracker.advance_time(TimeDelta::from_millis(500));
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 9876.0 * 5.0);
+        assert_eq!(tracker.compute_rate(), 9876.0 * 5.0);
+        assert_eq!(tracker.total_sample_count(), 1234 * 2 + 9876 * 55);
+
+        // Rate over the last half second should be zero.
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(500)), 0.0);
+    }
+
+    #[test]
+    fn test_long_period_between_samples() {
+        let mut tracker = RateTrackerForTest::new();
+        tracker.add_samples(1);
+        tracker.advance_time(TimeDelta::from_millis(1000));
+        assert_eq!(tracker.compute_rate(), 1.0);
+
+        tracker.advance_time(TimeDelta::from_millis(2000));
+        assert_eq!(tracker.compute_rate(), 0.0);
+
+        tracker.advance_time(TimeDelta::from_millis(2000));
+        tracker.add_samples(1);
+        assert_eq!(tracker.compute_rate(), 1.0);
+    }
+
+    #[test]
+    fn test_rolloff() {
+        let mut tracker = RateTrackerForTest::new();
+        for _ in 0..10 {
+            tracker.add_samples(1);
+            tracker.advance_time(TimeDelta::from_millis(100));
+        }
+        assert_eq!(tracker.compute_rate(), 10.0);
+
+        for _ in 0..10 {
+            tracker.add_samples(1);
+            tracker.advance_time(TimeDelta::from_millis(50));
+        }
+        assert_eq!(tracker.compute_rate(), 15.0);
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(500)), 20.0);
+
+        for _ in 0..10 {
+            tracker.add_samples(1);
+            tracker.advance_time(TimeDelta::from_millis(50));
+        }
+        assert_eq!(tracker.compute_rate(), 20.0);
+    }
+
+    #[test]
+    fn test_get_unit_seconds_after_initial_value() {
+        let mut tracker = RateTrackerForTest::new();
+        tracker.add_samples(1234);
+        tracker.advance_time(TimeDelta::from_millis(1000));
+        assert_eq!(tracker.compute_rate_for_interval(TimeDelta::from_millis(1000)), 1234.0);
+    }
+
+    #[test]
+    fn test_large_numbers() {
+        let mut tracker = RateTrackerForTest::new();
+        let large_number = 0x100000000i64;
+        tracker.add_samples(large_number);
+        tracker.advance_time(TimeDelta::from_millis(1000));
+        tracker.add_samples(large_number);
+        assert_eq!(tracker.compute_rate(), (large_number * 2) as f64);
+    }
+}
diff --git a/rtc_base/rate_tracker_comparison_unittest.cc b/rtc_base/rate_tracker_comparison_unittest.cc
new file mode 100644
index 0000000..ce9cea4
--- /dev/null
+++ b/rtc_base/rate_tracker_comparison_unittest.cc
@@ -0,0 +1,116 @@
+/*
+ *  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 <cstddef>
+#include <cstdint>
+#include <vector>
+
+#include "absl/random/random.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "rtc_base/rate_tracker.h"
+#include "rtc_base/rate_tracker_ffi.rs.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace {
+
+struct RateTrackerUpdate {
+  int64_t samples;
+  TimeDelta delta;
+  TimeDelta interval;
+};
+
+void RustAndCppMatch(TimeDelta bucket,
+                     size_t bucket_count,
+                     Timestamp start_time,
+                     const std::vector<RateTrackerUpdate>& updates) {
+  RateTracker cpp_tracker(bucket.ms(), bucket_count);
+  rust::Box<RustRateTracker> rust_tracker =
+      create_rate_tracker(bucket, bucket_count);
+
+  Timestamp current_time = start_time;
+
+  for (const auto& update : updates) {
+    current_time += update.delta;
+    cpp_tracker.Update(update.samples, current_time);
+    rust_tracker->update(update.samples, current_time);
+
+    EXPECT_EQ(cpp_tracker.TotalSampleCount(),
+              rust_tracker->total_sample_count());
+    EXPECT_NEAR(cpp_tracker.Rate(current_time),
+                rust_tracker->rate(current_time), 1e-7);
+
+    EXPECT_NEAR(
+        cpp_tracker.ComputeRateForInterval(current_time, update.interval),
+        rust_tracker->compute_rate_for_interval(current_time, update.interval),
+        1e-6);
+  }
+}
+
+TEST(RateTrackerComparisonTest, RustVsCppFuzzTest) {
+  // This should use FuzzTest instead, but for now we use rng.
+  absl::BitGen gen;
+  for (int i = 0; i < 100; ++i) {
+    TimeDelta bucket = TimeDelta::Millis(absl::Uniform<int64_t>(gen, 1, 1000));
+    size_t bucket_count = absl::Uniform<size_t>(gen, 1, 1000);
+    Timestamp start_time =
+        Timestamp::Millis(absl::Uniform<int64_t>(gen, 0, 1000000));
+
+    size_t num_updates = absl::Uniform<size_t>(gen, 0, 100);
+    std::vector<RateTrackerUpdate> updates;
+    for (size_t j = 0; j < num_updates; ++j) {
+      RateTrackerUpdate update;
+      update.samples = absl::Uniform<int64_t>(gen, 0, 1000000);
+      update.delta = TimeDelta::Millis(absl::Uniform<int64_t>(gen, 1, 1000));
+      update.interval =
+          TimeDelta::Millis(absl::Uniform<int64_t>(gen, 1, 10000));
+      updates.push_back(update);
+    }
+
+    RustAndCppMatch(bucket, bucket_count, start_time, updates);
+  }
+}
+
+TEST(RateTrackerComparisonTest, LargeSamples) {
+  const TimeDelta kBucket = TimeDelta::Millis(100);
+  const size_t kBucketCount = 10;
+
+  RateTracker cpp_tracker(kBucket.ms(), kBucketCount);
+  rust::Box<RustRateTracker> rust_tracker =
+      create_rate_tracker(kBucket, kBucketCount);
+
+  // Use a large sample count that might cause overflow if not handled.
+  // microseconds * samples: 100,000 * 1,000,000,000,000 = 10^17 (fits in i64,
+  // max i64 is ~9.2 * 10^18) But let's go larger: 10^15 samples. 100,000 *
+  // 10^15 = 10^20 (EXCEEDS i64!)
+  const int64_t kLargeSamples = 1000000000000000LL;  // 10^15
+  Timestamp current_time = Timestamp::Millis(1000);
+
+  cpp_tracker.Update(kLargeSamples, current_time);
+  rust_tracker->update(kLargeSamples, current_time);
+
+  // Advance by half a bucket.
+  current_time += kBucket / 2;
+
+  // Rate should be (samples / 2) / (bucket / 2) * 1000 = samples * 1000 /
+  // bucket available_interval is current_time - initialization_time = 50ms.
+  // Wait, C++'s available_interval_milliseconds would be 50ms.
+  // total_samples calculation:
+  // start_bucket = current_bucket (0)
+  // samples_to_count = samples * (100 - 50) + 50 / 100 = samples * 50 / 100 =
+  // samples / 2. rate = (samples / 2 * 1000) / 50 = samples * 10.
+
+  EXPECT_NEAR(cpp_tracker.Rate(current_time), rust_tracker->rate(current_time),
+              1e-6);
+}
+
+}  // namespace
+}  // namespace webrtc
diff --git a/rtc_base/rate_tracker_ffi.rs b/rtc_base/rate_tracker_ffi.rs
new file mode 100644
index 0000000..5538120
--- /dev/null
+++ b/rtc_base/rate_tracker_ffi.rs
@@ -0,0 +1,67 @@
+// 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.
+
+webrtc::import! {
+  "//api/units:time_delta_rs" as time_delta;
+  "//api/units:timestamp_rs" as timestamp;
+  "//rtc_base:rate_tracker_rust" as rate_tracker;
+}
+
+use time_delta::TimeDelta;
+use timestamp::Timestamp;
+
+pub struct RustRateTracker {
+    inner: rate_tracker::RustRateTracker,
+}
+
+#[cxx::bridge(namespace = "webrtc")]
+mod ffi {
+    unsafe extern "C++" {
+        include!("api/units/time_delta.h");
+        include!("api/units/timestamp.h");
+
+        type TimeDelta = super::TimeDelta;
+        type Timestamp = super::Timestamp;
+    }
+
+    extern "Rust" {
+        type RustRateTracker;
+
+        fn create_rate_tracker(bucket: TimeDelta, bucket_count: usize) -> Box<RustRateTracker>;
+        fn compute_rate_for_interval(
+            self: &RustRateTracker,
+            current_time: Timestamp,
+            interval: TimeDelta,
+        ) -> f64;
+        fn rate(self: &RustRateTracker, current_time: Timestamp) -> f64;
+        fn total_sample_count(self: &RustRateTracker) -> i64;
+        fn update(self: &mut RustRateTracker, sample_count: i64, current_time: Timestamp);
+    }
+}
+
+pub fn create_rate_tracker(bucket: TimeDelta, bucket_count: usize) -> Box<RustRateTracker> {
+    Box::new(RustRateTracker { inner: rate_tracker::RustRateTracker::new(bucket, bucket_count) })
+}
+
+impl RustRateTracker {
+    pub fn compute_rate_for_interval(&self, current_time: Timestamp, interval: TimeDelta) -> f64 {
+        self.inner.compute_rate_for_interval(current_time, interval)
+    }
+
+    pub fn rate(&self, current_time: Timestamp) -> f64 {
+        self.inner.rate(current_time)
+    }
+
+    pub fn total_sample_count(&self) -> i64 {
+        self.inner.total_sample_count()
+    }
+
+    pub fn update(&mut self, sample_count: i64, current_time: Timestamp) {
+        self.inner.update(sample_count, current_time);
+    }
+}
diff --git a/webrtc.gni b/webrtc.gni
index 5d358ef..39d9346 100644
--- a/webrtc.gni
+++ b/webrtc.gni
@@ -1439,6 +1439,9 @@
   assert(
       !defined(invoker.crate_name),
       "Custom crate_name is not allowed in WebRTC. Use webrtc::import! instead.")
+  assert(
+      !defined(invoker.cxx_bindings),
+      "cxx_bindings are not allowed in rtc_rust_library. Use rtc_rust_cxx_bridge instead.")
   rust_static_library(target_name) {
     forward_variables_from(invoker,
                            "*",
@@ -1464,6 +1467,9 @@
 # Defines a Rust unit test.
 template("rtc_rust_unittest") {
   if (can_build_rust_unit_tests) {
+    assert(
+        !defined(invoker.crate_name),
+        "Custom crate_name is not allowed in WebRTC. Use webrtc::import! instead.")
     rust_unit_test(target_name) {
       forward_variables_from(invoker,
                              "*",
@@ -1498,6 +1504,15 @@
   assert(defined(invoker.source), "source must be specified")
   assert(!defined(invoker.sources), "Use source instead of sources")
 
+  # Explicitly assert that //build/rust:cxx_rustdeps is listed in deps.
+  assert(defined(invoker.deps) &&
+             filter_labels_include(invoker.deps,
+                                   [ "//build/rust:cxx_rustdeps" ]) != [],
+         "rtc_rust_cxx_bridge requires //build/rust:cxx_rustdeps in deps.")
+  assert(
+      !defined(invoker.crate_name),
+      "Custom crate_name is not allowed in WebRTC. Use webrtc::import! instead.")
+
   rust_static_library(target_name) {
     forward_variables_from(invoker,
                            [
@@ -1505,23 +1520,43 @@
                              "features",
                              "edition",
                              "crate_root",
-                             "crate_name",
                              "testonly",
                              "visibility",
+                             "deps",
                            ])
     rustenv = [ "WEBRTC_GN_PREFIX=$webrtc_root" ]
     if (defined(invoker.rustenv)) {
       rustenv += invoker.rustenv
     }
-    deps = [ "$webrtc_root/rust/webrtc_import" ]
-    if (defined(invoker.deps)) {
-      deps += invoker.deps
+    if (!defined(deps)) {
+      deps = []
     }
+    deps += [ "$webrtc_root/rust/webrtc_import" ]
     if (!defined(crate_root)) {
       crate_root = invoker.source
     }
     sources = [ invoker.source ]
     cxx_bindings = [ invoker.source ]
+
+    # WebRTC and Abseil include paths/defines are explicitly populated so that
+    # auto-generated bridge C++ source files (.cc) can safely locate required
+    # headers during compilation, and propagate header visibility cleanly via public_configs.
+    if (defined(invoker.configs)) {
+      configs += invoker.configs
+    }
+    configs += [
+      rtc_common_inherited_config,
+      absl_include_config,
+      absl_define_config,
+    ]
+    public_configs = [
+      rtc_common_inherited_config,
+      absl_include_config,
+      absl_define_config,
+    ]
+    if (defined(invoker.public_configs)) {
+      public_configs += invoker.public_configs
+    }
   }
 }