use std::cell::Cell;
thread_local! {
static H2D_TRANSFERS: Cell<u64> = const { Cell::new(0) };
static D2H_TRANSFERS: Cell<u64> = const { Cell::new(0) };
static H2D_BYTES: Cell<u64> = const { Cell::new(0) };
static D2H_BYTES: Cell<u64> = const { Cell::new(0) };
}
#[must_use]
pub fn total_h2d_transfers() -> u64 {
H2D_TRANSFERS.with(Cell::get)
}
#[must_use]
pub fn total_d2h_transfers() -> u64 {
D2H_TRANSFERS.with(Cell::get)
}
#[must_use]
pub fn total_h2d_bytes() -> u64 {
H2D_BYTES.with(Cell::get)
}
#[must_use]
pub fn total_d2h_bytes() -> u64 {
D2H_BYTES.with(Cell::get)
}
pub fn reset_transfer_counters() {
H2D_TRANSFERS.with(|c| c.set(0));
D2H_TRANSFERS.with(|c| c.set(0));
H2D_BYTES.with(|c| c.set(0));
D2H_BYTES.with(|c| c.set(0));
}
pub(crate) fn record_h2d_transfer(bytes: u64) {
H2D_TRANSFERS.with(|c| c.set(c.get() + 1));
H2D_BYTES.with(|c| c.set(c.get() + bytes));
}
pub(crate) fn record_d2h_transfer(bytes: u64) {
D2H_TRANSFERS.with(|c| c.set(c.get() + 1));
D2H_BYTES.with(|c| c.set(c.get() + bytes));
}
#[derive(Debug, Clone, Default)]
pub struct TransferStats {
pub h2d_transfers: u64,
pub d2h_transfers: u64,
pub h2d_bytes: u64,
pub d2h_bytes: u64,
}
impl TransferStats {
#[must_use]
pub fn capture() -> Self {
Self {
h2d_transfers: total_h2d_transfers(),
d2h_transfers: total_d2h_transfers(),
h2d_bytes: total_h2d_bytes(),
d2h_bytes: total_d2h_bytes(),
}
}
#[must_use]
pub fn delta_from(&self, prev: &Self) -> Self {
Self {
h2d_transfers: self.h2d_transfers.saturating_sub(prev.h2d_transfers),
d2h_transfers: self.d2h_transfers.saturating_sub(prev.d2h_transfers),
h2d_bytes: self.h2d_bytes.saturating_sub(prev.h2d_bytes),
d2h_bytes: self.d2h_bytes.saturating_sub(prev.d2h_bytes),
}
}
#[must_use]
pub const fn total_transfers(&self) -> u64 {
self.h2d_transfers + self.d2h_transfers
}
#[must_use]
pub const fn total_bytes(&self) -> u64 {
self.h2d_bytes + self.d2h_bytes
}
}
impl std::fmt::Display for TransferStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"H2D: {} ({:.2} MB), D2H: {} ({:.2} MB)",
self.h2d_transfers,
self.h2d_bytes as f64 / (1024.0 * 1024.0),
self.d2h_transfers,
self.d2h_bytes as f64 / (1024.0 * 1024.0)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counters_are_not_polluted_by_other_threads() {
reset_transfer_counters();
record_h2d_transfer(32);
let neighbour = std::thread::spawn(|| {
reset_transfer_counters();
for _ in 0..1000 {
record_h2d_transfer(4096);
record_d2h_transfer(4096);
}
assert_eq!(total_h2d_transfers(), 1000);
assert_eq!(total_d2h_transfers(), 1000);
});
neighbour.join().expect("neighbour thread must not panic");
assert_eq!(
total_h2d_transfers(),
1,
"another thread's H2D transfers were attributed to this thread"
);
assert_eq!(
total_h2d_bytes(),
32,
"another thread's H2D bytes were attributed to this thread"
);
assert_eq!(
total_d2h_transfers(),
0,
"another thread's D2H transfers were attributed to this thread"
);
assert_eq!(
total_d2h_bytes(),
0,
"another thread's D2H bytes were attributed to this thread"
);
}
#[test]
fn test_reset_on_another_thread_does_not_zero_ours() {
reset_transfer_counters();
record_h2d_transfer(64);
record_d2h_transfer(64);
std::thread::spawn(|| {
reset_transfer_counters();
})
.join()
.expect("neighbour thread must not panic");
assert_eq!(
(total_h2d_transfers(), total_d2h_transfers()),
(1, 1),
"another thread's reset_transfer_counters() wiped this thread's window"
);
}
#[test]
fn test_transfer_counter_reset() {
reset_transfer_counters();
assert_eq!(total_h2d_transfers(), 0);
assert_eq!(total_d2h_transfers(), 0);
assert_eq!(total_h2d_bytes(), 0);
assert_eq!(total_d2h_bytes(), 0);
}
#[test]
fn test_transfer_counter_increment() {
reset_transfer_counters();
record_h2d_transfer(1024);
record_h2d_transfer(2048);
record_d2h_transfer(512);
assert_eq!(total_h2d_transfers(), 2);
assert_eq!(total_d2h_transfers(), 1);
assert_eq!(total_h2d_bytes(), 3072);
assert_eq!(total_d2h_bytes(), 512);
}
#[test]
fn test_transfer_stats_capture() {
reset_transfer_counters();
record_h2d_transfer(100);
record_d2h_transfer(200);
let stats = TransferStats::capture();
assert_eq!(stats.h2d_transfers, 1);
assert_eq!(stats.d2h_transfers, 1);
assert_eq!(stats.h2d_bytes, 100);
assert_eq!(stats.d2h_bytes, 200);
}
#[test]
fn test_transfer_stats_delta() {
let prev = TransferStats {
h2d_transfers: 10,
d2h_transfers: 5,
h2d_bytes: 1000,
d2h_bytes: 500,
};
let curr = TransferStats {
h2d_transfers: 15,
d2h_transfers: 8,
h2d_bytes: 2500,
d2h_bytes: 1200,
};
let delta = curr.delta_from(&prev);
assert_eq!(delta.h2d_transfers, 5);
assert_eq!(delta.d2h_transfers, 3);
assert_eq!(delta.h2d_bytes, 1500);
assert_eq!(delta.d2h_bytes, 700);
}
#[test]
fn test_transfer_stats_totals() {
let stats = TransferStats {
h2d_transfers: 10,
d2h_transfers: 5,
h2d_bytes: 1000,
d2h_bytes: 500,
};
assert_eq!(stats.total_transfers(), 15);
assert_eq!(stats.total_bytes(), 1500);
}
#[test]
fn test_transfer_stats_display() {
let stats = TransferStats {
h2d_transfers: 100,
d2h_transfers: 50,
h2d_bytes: 1024 * 1024, d2h_bytes: 512 * 1024, };
let display = format!("{}", stats);
assert!(display.contains("H2D: 100"));
assert!(display.contains("D2H: 50"));
assert!(display.contains("1.00 MB"));
assert!(display.contains("0.50 MB"));
}
}