use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Default)]
pub struct VirtioBlkCounters {
pub(crate) reads_completed: AtomicU64,
pub(crate) writes_completed: AtomicU64,
pub(crate) flushes_completed: AtomicU64,
pub(crate) bytes_read: AtomicU64,
pub(crate) bytes_written: AtomicU64,
pub(crate) throttled_count: AtomicU64,
pub(crate) io_errors: AtomicU64,
pub(crate) currently_throttled_gauge: AtomicU64,
pub(crate) invalid_avail_idx_count: AtomicU64,
}
impl VirtioBlkCounters {
pub(crate) fn record_read(&self, bytes: u64) {
self.reads_completed.fetch_add(1, Ordering::Relaxed);
self.bytes_read.fetch_add(bytes, Ordering::Relaxed);
}
pub(crate) fn record_write(&self, bytes: u64) {
self.writes_completed.fetch_add(1, Ordering::Relaxed);
self.bytes_written.fetch_add(bytes, Ordering::Relaxed);
}
pub(crate) fn record_flush(&self) {
self.flushes_completed.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_io_error(&self) {
self.io_errors.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_throttled(&self) {
self.throttled_count.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_throttle_pending_inc(&self) {
self.currently_throttled_gauge
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_throttle_pending_dec(&self) {
let _ = self.currently_throttled_gauge.fetch_update(
Ordering::Relaxed,
Ordering::Relaxed,
|v| v.checked_sub(1),
);
}
pub(crate) fn record_invalid_avail_idx(&self) {
self.invalid_avail_idx_count.fetch_add(1, Ordering::Relaxed);
}
pub fn reads_completed(&self) -> u64 {
self.reads_completed.load(Ordering::Relaxed)
}
pub fn writes_completed(&self) -> u64 {
self.writes_completed.load(Ordering::Relaxed)
}
pub fn flushes_completed(&self) -> u64 {
self.flushes_completed.load(Ordering::Relaxed)
}
pub fn bytes_read(&self) -> u64 {
self.bytes_read.load(Ordering::Relaxed)
}
pub fn bytes_written(&self) -> u64 {
self.bytes_written.load(Ordering::Relaxed)
}
pub fn throttled_count(&self) -> u64 {
self.throttled_count.load(Ordering::Relaxed)
}
pub fn io_errors(&self) -> u64 {
self.io_errors.load(Ordering::Relaxed)
}
pub fn currently_throttled_gauge(&self) -> u64 {
self.currently_throttled_gauge.load(Ordering::Relaxed)
}
pub fn invalid_avail_idx_count(&self) -> u64 {
self.invalid_avail_idx_count.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_counters_are_all_zero() {
let c = VirtioBlkCounters::default();
assert_eq!(c.reads_completed(), 0, "reads_completed must default to 0");
assert_eq!(
c.writes_completed(),
0,
"writes_completed must default to 0"
);
assert_eq!(
c.flushes_completed(),
0,
"flushes_completed must default to 0"
);
assert_eq!(c.bytes_read(), 0, "bytes_read must default to 0");
assert_eq!(c.bytes_written(), 0, "bytes_written must default to 0");
assert_eq!(c.throttled_count(), 0, "throttled_count must default to 0");
assert_eq!(c.io_errors(), 0, "io_errors must default to 0");
assert_eq!(
c.currently_throttled_gauge(),
0,
"currently_throttled_gauge must default to 0",
);
assert_eq!(
c.invalid_avail_idx_count(),
0,
"invalid_avail_idx_count must default to 0",
);
}
#[test]
fn record_read_bumps_completion_and_bytes_in_lockstep() {
let c = VirtioBlkCounters::default();
c.record_read(512);
assert_eq!(
c.reads_completed(),
1,
"first record_read must bump reads_completed to 1",
);
assert_eq!(
c.bytes_read(),
512,
"first record_read must add bytes to bytes_read",
);
c.record_read(1024);
assert_eq!(
c.reads_completed(),
2,
"second record_read must bump reads_completed to 2",
);
assert_eq!(
c.bytes_read(),
512 + 1024,
"second record_read must accumulate bytes",
);
assert_eq!(
c.writes_completed(),
0,
"record_read must NOT bump writes_completed",
);
assert_eq!(
c.bytes_written(),
0,
"record_read must NOT bump bytes_written",
);
assert_eq!(
c.flushes_completed(),
0,
"record_read must NOT bump flushes_completed",
);
assert_eq!(
c.throttled_count(),
0,
"record_read must NOT bump throttled_count",
);
assert_eq!(c.io_errors(), 0, "record_read must NOT bump io_errors");
assert_eq!(
c.currently_throttled_gauge(),
0,
"record_read must NOT touch the throttle gauge",
);
assert_eq!(
c.invalid_avail_idx_count(),
0,
"record_read must NOT bump invalid_avail_idx_count",
);
}
#[test]
fn record_read_zero_bytes_still_bumps_completion() {
let c = VirtioBlkCounters::default();
c.record_read(0);
assert_eq!(
c.reads_completed(),
1,
"zero-byte read must still increment reads_completed",
);
assert_eq!(
c.bytes_read(),
0,
"zero-byte read must leave bytes_read at 0",
);
}
#[test]
fn record_write_bumps_completion_and_bytes_in_lockstep() {
let c = VirtioBlkCounters::default();
c.record_write(4096);
assert_eq!(
c.writes_completed(),
1,
"first record_write must bump writes_completed to 1",
);
assert_eq!(
c.bytes_written(),
4096,
"first record_write must add bytes to bytes_written",
);
c.record_write(8192);
assert_eq!(
c.writes_completed(),
2,
"second record_write must bump writes_completed to 2",
);
assert_eq!(
c.bytes_written(),
4096 + 8192,
"second record_write must accumulate bytes",
);
assert_eq!(
c.reads_completed(),
0,
"record_write must NOT bump reads_completed",
);
assert_eq!(c.bytes_read(), 0, "record_write must NOT bump bytes_read");
assert_eq!(
c.flushes_completed(),
0,
"record_write must NOT bump flushes_completed",
);
assert_eq!(
c.throttled_count(),
0,
"record_write must NOT bump throttled_count",
);
assert_eq!(c.io_errors(), 0, "record_write must NOT bump io_errors");
assert_eq!(
c.currently_throttled_gauge(),
0,
"record_write must NOT touch the throttle gauge",
);
assert_eq!(
c.invalid_avail_idx_count(),
0,
"record_write must NOT bump invalid_avail_idx_count",
);
}
#[test]
fn record_write_zero_bytes_still_bumps_completion() {
let c = VirtioBlkCounters::default();
c.record_write(0);
assert_eq!(
c.writes_completed(),
1,
"zero-byte write must still increment writes_completed",
);
assert_eq!(
c.bytes_written(),
0,
"zero-byte write must leave bytes_written at 0",
);
}
#[test]
fn record_flush_bumps_only_flushes_completed() {
let c = VirtioBlkCounters::default();
c.record_flush();
assert_eq!(
c.flushes_completed(),
1,
"record_flush must bump flushes_completed to 1",
);
c.record_flush();
c.record_flush();
assert_eq!(
c.flushes_completed(),
3,
"three record_flush calls must accumulate to 3",
);
assert_eq!(
c.reads_completed(),
0,
"record_flush must NOT bump reads_completed",
);
assert_eq!(c.bytes_read(), 0, "record_flush must NOT bump bytes_read");
assert_eq!(
c.writes_completed(),
0,
"record_flush must NOT bump writes_completed",
);
assert_eq!(
c.bytes_written(),
0,
"record_flush must NOT bump bytes_written",
);
assert_eq!(
c.throttled_count(),
0,
"record_flush must NOT bump throttled_count",
);
assert_eq!(c.io_errors(), 0, "record_flush must NOT bump io_errors");
assert_eq!(
c.currently_throttled_gauge(),
0,
"record_flush must NOT touch the throttle gauge",
);
assert_eq!(
c.invalid_avail_idx_count(),
0,
"record_flush must NOT bump invalid_avail_idx_count",
);
}
#[test]
fn record_throttle_pending_inc_increments_each_call() {
let c = VirtioBlkCounters::default();
c.record_throttle_pending_inc();
assert_eq!(
c.currently_throttled_gauge(),
1,
"first inc must bump gauge from 0 to 1",
);
c.record_throttle_pending_inc();
assert_eq!(
c.currently_throttled_gauge(),
2,
"second inc must bump gauge from 1 to 2 (helper itself \
is not idempotent — caller must gate)",
);
c.record_throttle_pending_inc();
assert_eq!(
c.currently_throttled_gauge(),
3,
"third inc must bump gauge from 2 to 3",
);
assert_eq!(
c.throttled_count(),
0,
"record_throttle_pending_inc must NOT bump throttled_count \
(events vs gauge are separate counters with separate helpers)",
);
assert_eq!(
c.reads_completed(),
0,
"record_throttle_pending_inc must NOT bump reads_completed",
);
assert_eq!(
c.io_errors(),
0,
"record_throttle_pending_inc must NOT bump io_errors",
);
}
#[test]
fn record_throttle_pending_dec_decrements_when_positive() {
let c = VirtioBlkCounters::default();
c.record_throttle_pending_inc();
c.record_throttle_pending_inc();
c.record_throttle_pending_inc();
assert_eq!(c.currently_throttled_gauge(), 3, "pre-cond: gauge at 3");
c.record_throttle_pending_dec();
assert_eq!(
c.currently_throttled_gauge(),
2,
"first dec must drop gauge from 3 to 2",
);
c.record_throttle_pending_dec();
assert_eq!(
c.currently_throttled_gauge(),
1,
"second dec must drop gauge from 2 to 1",
);
c.record_throttle_pending_dec();
assert_eq!(
c.currently_throttled_gauge(),
0,
"third dec must drop gauge from 1 to 0",
);
}
#[test]
fn record_throttle_pending_dec_saturates_at_zero() {
let c = VirtioBlkCounters::default();
c.record_throttle_pending_dec();
assert_eq!(
c.currently_throttled_gauge(),
0,
"dec on a zero gauge MUST saturate at 0, not wrap to u64::MAX \
(regression: fetch_sub instead of fetch_update + checked_sub)",
);
for i in 0..5 {
c.record_throttle_pending_dec();
assert_eq!(
c.currently_throttled_gauge(),
0,
"dec on a zero gauge must stay 0 across {} repeated calls",
i + 1,
);
}
}
#[test]
fn record_throttle_pending_inc_then_dec_nets_to_zero() {
let c = VirtioBlkCounters::default();
c.record_throttle_pending_inc();
c.record_throttle_pending_dec();
assert_eq!(
c.currently_throttled_gauge(),
0,
"inc-then-dec must net to 0 on the gauge",
);
for _ in 0..10 {
c.record_throttle_pending_inc();
}
assert_eq!(c.currently_throttled_gauge(), 10, "10 incs → gauge=10");
for _ in 0..10 {
c.record_throttle_pending_dec();
}
assert_eq!(
c.currently_throttled_gauge(),
0,
"10 incs + 10 decs must net to 0",
);
}
#[test]
fn record_io_error_increments_only_io_errors() {
let c = VirtioBlkCounters::default();
c.record_io_error();
assert_eq!(
c.io_errors(),
1,
"first record_io_error must bump io_errors to 1",
);
c.record_io_error();
c.record_io_error();
assert_eq!(
c.io_errors(),
3,
"three record_io_error calls must accumulate to 3 \
(events counter, no per-request dedup)",
);
assert_eq!(
c.reads_completed(),
0,
"record_io_error must NOT bump reads_completed",
);
assert_eq!(
c.writes_completed(),
0,
"record_io_error must NOT bump writes_completed",
);
assert_eq!(
c.flushes_completed(),
0,
"record_io_error must NOT bump flushes_completed",
);
assert_eq!(
c.bytes_read(),
0,
"record_io_error must NOT bump bytes_read"
);
assert_eq!(
c.bytes_written(),
0,
"record_io_error must NOT bump bytes_written",
);
assert_eq!(
c.throttled_count(),
0,
"record_io_error must NOT bump throttled_count \
(events-vs-events distinction — IO errors and \
throttle stalls are separately classified)",
);
assert_eq!(
c.currently_throttled_gauge(),
0,
"record_io_error must NOT touch the throttle gauge",
);
assert_eq!(
c.invalid_avail_idx_count(),
0,
"record_io_error must NOT bump invalid_avail_idx_count",
);
}
#[test]
fn record_throttled_increments_only_throttled_count() {
let c = VirtioBlkCounters::default();
c.record_throttled();
assert_eq!(
c.throttled_count(),
1,
"first record_throttled must bump throttled_count to 1",
);
c.record_throttled();
assert_eq!(
c.throttled_count(),
2,
"second record_throttled must bump throttled_count to 2 \
(events counter — same chain re-stalling produces \
multiple bumps in production)",
);
assert_eq!(
c.currently_throttled_gauge(),
0,
"record_throttled (events counter) must NOT touch \
currently_throttled_gauge (live gauge — separate helper)",
);
assert_eq!(c.io_errors(), 0, "record_throttled must NOT bump io_errors");
assert_eq!(
c.reads_completed(),
0,
"record_throttled must NOT bump reads_completed",
);
assert_eq!(
c.writes_completed(),
0,
"record_throttled must NOT bump writes_completed",
);
assert_eq!(
c.flushes_completed(),
0,
"record_throttled must NOT bump flushes_completed",
);
assert_eq!(
c.invalid_avail_idx_count(),
0,
"record_throttled must NOT bump invalid_avail_idx_count",
);
}
#[test]
fn record_invalid_avail_idx_increments_only_that_field() {
let c = VirtioBlkCounters::default();
c.record_invalid_avail_idx();
assert_eq!(
c.invalid_avail_idx_count(),
1,
"first record_invalid_avail_idx must bump counter to 1",
);
c.record_invalid_avail_idx();
assert_eq!(
c.invalid_avail_idx_count(),
2,
"second record_invalid_avail_idx must bump counter to 2 \
(helper itself does not enforce single-bump; the \
caller's poison gate does)",
);
assert_eq!(
c.io_errors(),
0,
"record_invalid_avail_idx must NOT bump io_errors \
(separate event class — guest spec violation \
vs IO failure)",
);
assert_eq!(
c.throttled_count(),
0,
"record_invalid_avail_idx must NOT bump throttled_count",
);
assert_eq!(
c.currently_throttled_gauge(),
0,
"record_invalid_avail_idx must NOT touch the throttle gauge",
);
assert_eq!(
c.reads_completed(),
0,
"record_invalid_avail_idx must NOT bump reads_completed",
);
assert_eq!(
c.writes_completed(),
0,
"record_invalid_avail_idx must NOT bump writes_completed",
);
assert_eq!(
c.flushes_completed(),
0,
"record_invalid_avail_idx must NOT bump flushes_completed",
);
}
}