Parallel agent work: bandwidth fix, CPU platforms, packaging
All checks were successful
CI / test (push) Successful in 2m8s

5 agents ran in parallel:

1. Fix bandwidth limit (-b): new advance_next_send() prevents drift
   bursts by resetting when >2x interval behind (bandwidth.rs, client.rs, server.rs)

2. Windows + FreeBSD CPU support (cpu.rs):
   - Windows: GetSystemTimes via raw FFI
   - FreeBSD: sysctl kern.cp_time parsing

3. Ubuntu .deb packaging (deploy/deb/):
   - build-deb.sh: creates .deb from pre-built binary
   - test-deb.sh: tests in Ubuntu Docker container

4. Fedora/RHEL RPM packaging (deploy/rpm/):
   - btest-rs.spec: full RPM spec with systemd unit
   - build-rpm.sh + test-rpm.sh

5. Alpine Linux apk packaging (deploy/alpine/):
   - APKBUILD with OpenRC init script
   - test-alpine.sh

58 tests pass, zero warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-01 14:04:00 +04:00
parent fe28c04c19
commit 8c853c3605
12 changed files with 816 additions and 15 deletions

View File

@@ -80,6 +80,34 @@ pub fn calc_send_interval(tx_speed_bps: u32, tx_size: u16) -> Option<Duration> {
}
}
/// Advance `next_send` by one interval and clamp drift.
///
/// When the sender falls behind (e.g., the write blocked longer than the
/// inter-packet interval), `next_send` accumulates a debt. Once the path
/// clears, the loop would fire packets with *no* delay until the debt is
/// repaid, producing a burst that overshoots the target rate.
///
/// This helper resets `next_send` to `now` whenever it has drifted more
/// than 2x the interval behind the current wall-clock time, bounding the
/// maximum burst to at most one extra interval's worth of packets.
pub fn advance_next_send(
next_send: &mut std::time::Instant,
iv: Duration,
now: std::time::Instant,
) -> Option<Duration> {
*next_send += iv;
// If we have fallen more than 2x the interval behind, reset to now
// to prevent a compensating burst.
if *next_send + iv < now {
*next_send = now;
}
if *next_send > now {
Some(*next_send - now)
} else {
None
}
}
/// Format a bandwidth value in human-readable form.
pub fn format_bandwidth(bits_per_sec: f64) -> String {
if bits_per_sec >= 1_000_000_000.0 {

View File

@@ -167,10 +167,9 @@ async fn tcp_client_tx_loop(
match interval {
Some(iv) => {
next_send += iv;
let now = Instant::now();
if next_send > now {
tokio::time::sleep(next_send - now).await;
if let Some(delay) = bandwidth::advance_next_send(&mut next_send, iv, now) {
tokio::time::sleep(delay).await;
}
}
None => {
@@ -317,10 +316,9 @@ async fn udp_client_tx_loop(
match interval {
Some(iv) => {
next_send += iv;
let now = Instant::now();
if next_send > now {
tokio::time::sleep(next_send - now).await;
if let Some(delay) = bandwidth::advance_next_send(&mut next_send, iv, now) {
tokio::time::sleep(delay).await;
}
}
None => {

View File

@@ -1,7 +1,7 @@
//! Lightweight CPU usage measurement.
//!
//! Returns the system-wide CPU usage as a percentage (0-100).
//! Works on macOS and Linux without external dependencies.
//! Works on macOS, Linux, Windows, and FreeBSD without external dependencies.
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Duration;
@@ -93,7 +93,82 @@ fn get_cpu_times() -> (u64, u64) {
(0, 0)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
#[cfg(target_os = "windows")]
fn get_cpu_times() -> (u64, u64) {
#[repr(C)]
#[derive(Default)]
struct FILETIME {
dwLowDateTime: u32,
dwHighDateTime: u32,
}
impl FILETIME {
fn to_u64(&self) -> u64 {
(self.dwHighDateTime as u64) << 32 | self.dwLowDateTime as u64
}
}
extern "system" {
fn GetSystemTimes(
lpIdleTime: *mut FILETIME,
lpKernelTime: *mut FILETIME,
lpUserTime: *mut FILETIME,
) -> i32;
}
let mut idle = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
// SAFETY: We pass valid pointers to stack-allocated FILETIME structs.
// GetSystemTimes is a well-documented Win32 API that writes into these
// output parameters. A non-zero return value indicates success.
let ret = unsafe { GetSystemTimes(&mut idle, &mut kernel, &mut user) };
if ret != 0 {
let idle_ticks = idle.to_u64();
// Kernel time includes idle time on Windows, so total = kernel + user.
let total_ticks = kernel.to_u64() + user.to_u64();
(total_ticks, idle_ticks)
} else {
(0, 0)
}
}
#[cfg(target_os = "freebsd")]
fn get_cpu_times() -> (u64, u64) {
// kern.cp_time returns: user nice system interrupt idle
if let Ok(output) = std::process::Command::new("sysctl")
.arg("-n")
.arg("kern.cp_time")
.output()
{
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
let parts: Vec<u64> = text
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if parts.len() >= 5 {
let user = parts[0];
let nice = parts[1];
let system = parts[2];
let interrupt = parts[3];
let idle = parts[4];
let total = user + nice + system + interrupt + idle;
return (total, idle);
}
}
}
(0, 0)
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
)))]
fn get_cpu_times() -> (u64, u64) {
(0, 0) // Unsupported platform
}
@@ -116,7 +191,12 @@ mod tests {
fn test_cpu_times_returns_nonzero() {
let (total, idle) = get_cpu_times();
// On supported platforms, total should be > 0
if cfg!(any(target_os = "linux", target_os = "macos")) {
if cfg!(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
)) {
assert!(total > 0, "CPU total ticks should be > 0");
assert!(idle <= total, "idle should be <= total");
}

View File

@@ -565,10 +565,9 @@ async fn tcp_tx_loop_inner(
match interval {
Some(iv) => {
next_send += iv;
let now = Instant::now();
if next_send > now {
tokio::time::sleep(next_send - now).await;
if let Some(delay) = bandwidth::advance_next_send(&mut next_send, iv, now) {
tokio::time::sleep(delay).await;
}
}
None => {
@@ -805,10 +804,9 @@ async fn udp_tx_loop(
match interval {
Some(iv) => {
next_send += iv;
let now = Instant::now();
if next_send > now {
tokio::time::sleep(next_send - now).await;
if let Some(delay) = bandwidth::advance_next_send(&mut next_send, iv, now) {
tokio::time::sleep(delay).await;
}
}
None => {