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 + } } }