use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::io;
use std::sync::{Arc, OnceLock, Weak};
#[cfg(feature = "io-stats")]
pub(crate) mod tally;
#[cfg(not(feature = "io-stats"))]
#[path = "counters/tally_off.rs"]
pub(crate) mod tally;
use super::shard::Shared;
use crate::IoBackend;
use crate::stats::{ErrorCounts, ShardStats, Stats};
pub(crate) const KINDS: [io::ErrorKind; 39] = {
use io::ErrorKind::*;
[
NotFound,
PermissionDenied,
ConnectionRefused,
ConnectionReset,
HostUnreachable,
NetworkUnreachable,
ConnectionAborted,
NotConnected,
AddrInUse,
AddrNotAvailable,
NetworkDown,
BrokenPipe,
AlreadyExists,
WouldBlock,
NotADirectory,
IsADirectory,
DirectoryNotEmpty,
ReadOnlyFilesystem,
StaleNetworkFileHandle,
InvalidInput,
InvalidData,
TimedOut,
WriteZero,
StorageFull,
NotSeekable,
QuotaExceeded,
FileTooLarge,
ResourceBusy,
ExecutableFileBusy,
Deadlock,
CrossesDevices,
TooManyLinks,
InvalidFilename,
ArgumentListTooLong,
Interrupted,
Unsupported,
UnexpectedEof,
OutOfMemory,
Other,
]
};
pub(crate) fn kind_index(kind: io::ErrorKind) -> usize {
KINDS
.iter()
.position(|&k| k == kind)
.unwrap_or(KINDS.len() - 1)
}
#[repr(align(128))]
pub(crate) struct Counters {
polls: AtomicU64,
parks: AtomicU64,
parked_nanos: AtomicU64,
queue_depth: AtomicUsize,
born: std::time::Instant,
remote_wakes: AtomicU64,
timers: AtomicUsize,
buffer_bytes: AtomicUsize,
buffer_exhausted: AtomicU64,
ops_in_flight: AtomicUsize,
cq_overflows: AtomicU64,
handoffs: AtomicU64,
longest_poll_nanos: AtomicU64,
budget_yields: AtomicU64,
poll_times: OnceLock<Box<Buckets>>,
#[cfg(feature = "io-stats")]
io_times: OnceLock<Arc<IoTimes>>,
#[cfg(feature = "io-stats")]
io: OnceLock<Box<IoTotals>>,
owner: Option<(u64, usize)>,
gone: AtomicBool,
backend: IoBackend,
buffer_cap: usize,
errors: Errors,
}
pub(crate) struct Published {
pub(crate) polls: u64,
pub(crate) parks: u64,
pub(crate) parked: std::time::Duration,
pub(crate) queue_depth: usize,
pub(crate) remote_wakes: u64,
pub(crate) timers: usize,
pub(crate) buffer_bytes: usize,
pub(crate) buffer_exhausted: u64,
pub(crate) ops_in_flight: usize,
pub(crate) cq_overflows: u64,
}
impl Counters {
pub(crate) fn new(backend: IoBackend, buffer_cap: usize, owner: Option<(u64, usize)>) -> Self {
Self {
polls: AtomicU64::new(0),
parks: AtomicU64::new(0),
parked_nanos: AtomicU64::new(0),
queue_depth: AtomicUsize::new(0),
born: std::time::Instant::now(),
remote_wakes: AtomicU64::new(0),
timers: AtomicUsize::new(0),
buffer_bytes: AtomicUsize::new(0),
buffer_exhausted: AtomicU64::new(0),
ops_in_flight: AtomicUsize::new(0),
cq_overflows: AtomicU64::new(0),
handoffs: AtomicU64::new(0),
longest_poll_nanos: AtomicU64::new(0),
budget_yields: AtomicU64::new(0),
poll_times: OnceLock::new(),
#[cfg(feature = "io-stats")]
io_times: OnceLock::new(),
#[cfg(feature = "io-stats")]
io: OnceLock::new(),
owner,
gone: AtomicBool::new(false),
backend,
buffer_cap,
errors: Errors::new(),
}
}
pub(crate) fn publish(&self, now: Published) {
self.polls.store(now.polls, Ordering::Relaxed);
self.parks.store(now.parks, Ordering::Relaxed);
let parked = u64::try_from(now.parked.as_nanos()).unwrap_or(u64::MAX);
self.parked_nanos.store(parked, Ordering::Relaxed);
self.queue_depth.store(now.queue_depth, Ordering::Relaxed);
self.remote_wakes.store(now.remote_wakes, Ordering::Relaxed);
self.timers.store(now.timers, Ordering::Relaxed);
self.buffer_bytes.store(now.buffer_bytes, Ordering::Relaxed);
self.buffer_exhausted
.store(now.buffer_exhausted, Ordering::Relaxed);
self.ops_in_flight
.store(now.ops_in_flight, Ordering::Relaxed);
self.cq_overflows.store(now.cq_overflows, Ordering::Relaxed);
#[cfg(feature = "io-stats")]
if tally::counting() {
let io = tally::take();
if let Some(totals) = self.io.get() {
totals.add(&io);
}
}
}
#[cfg(feature = "io-stats")]
pub(crate) fn count_io(&self) {
tally::start();
let _ = self.io.set(Box::default());
}
pub(crate) fn handed_off(&self) {
self.handoffs.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn longest_poll(&self, length: std::time::Duration) {
let nanos = u64::try_from(length.as_nanos()).unwrap_or(u64::MAX);
self.longest_poll_nanos.fetch_max(nanos, Ordering::Relaxed);
}
pub(crate) fn record_latencies(&self) {
let _ = self.poll_times.set(Box::new(Buckets::new()));
#[cfg(feature = "io-stats")]
let _ = self.io_times.set(Arc::new(IoTimes::new()));
}
#[cfg(all(completion, feature = "io-stats"))]
pub(crate) fn io_times(&self) -> Option<&Arc<IoTimes>> {
self.io_times.get()
}
pub(crate) fn poll_time(&self, length: std::time::Duration) {
if let Some(buckets) = self.poll_times.get() {
buckets.add(length);
}
}
pub(crate) fn budget_yield(&self) {
let n = self.budget_yields.load(Ordering::Relaxed);
self.budget_yields.store(n + 1, Ordering::Relaxed);
}
pub(crate) fn runtime(&self) -> Option<u64> {
self.owner.map(|(runtime, _)| runtime)
}
pub(crate) fn went(&self) {
self.gone.store(true, Ordering::Relaxed);
WENT.fetch_add(1, Ordering::Release);
}
fn is_gone(&self) -> bool {
self.gone.load(Ordering::Relaxed)
}
pub(crate) fn refused_registration(&self) {
self.errors
.refused_registrations
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn read(&self, tasks: usize) -> ShardStats {
ShardStats {
worker: self.owner.map(|(_, place)| place),
runtime: self.runtime().map(crate::runtime::RuntimeId::from_raw),
backend: self.backend,
tasks,
polls: self.polls.load(Ordering::Relaxed),
parks: self.parks.load(Ordering::Relaxed),
parked: std::time::Duration::from_nanos(self.parked_nanos.load(Ordering::Relaxed)),
busy: self
.born
.elapsed()
.saturating_sub(std::time::Duration::from_nanos(
self.parked_nanos.load(Ordering::Relaxed),
)),
queue_depth: self.queue_depth.load(Ordering::Relaxed),
remote_wakes: self.remote_wakes.load(Ordering::Relaxed),
ops_in_flight: self.ops_in_flight.load(Ordering::Relaxed),
cq_overflows: self.cq_overflows.load(Ordering::Relaxed),
buffer_bytes: self.buffer_bytes.load(Ordering::Relaxed),
buffer_cap: self.buffer_cap,
buffer_exhausted: self.buffer_exhausted.load(Ordering::Relaxed),
timers: self.timers.load(Ordering::Relaxed),
handoffs: self.handoffs.load(Ordering::Relaxed),
longest_poll: std::time::Duration::from_nanos(
self.longest_poll_nanos.load(Ordering::Relaxed),
),
budget_yields: self.budget_yields.load(Ordering::Relaxed),
poll_times: self.poll_times.get().map(|buckets| buckets.read()),
#[cfg(feature = "io-stats")]
io_latencies: self
.io_times
.get()
.filter(|_| matches!(self.backend, IoBackend::IoUring | IoBackend::Iocp))
.map(|times| times.read()),
#[cfg(feature = "io-stats")]
io: self.io.get().map(|totals| totals.read()),
errors: self.errors.read(),
}
}
}
#[cfg(feature = "io-stats")]
#[derive(Default)]
struct IoTotals {
reads: AtomicU64,
writes: AtomicU64,
bytes_read: AtomicU64,
bytes_written: AtomicU64,
accepts: AtomicU64,
}
#[cfg(feature = "io-stats")]
impl IoTotals {
fn add(&self, io: &tally::Taken) {
for (total, more) in [
(&self.reads, io.reads),
(&self.writes, io.writes),
(&self.bytes_read, io.bytes_read),
(&self.bytes_written, io.bytes_written),
(&self.accepts, io.accepts),
] {
total.store(total.load(Ordering::Relaxed) + more, Ordering::Relaxed);
}
}
fn read(&self) -> crate::stats::IoCounts {
crate::stats::IoCounts {
reads: self.reads.load(Ordering::Relaxed),
writes: self.writes.load(Ordering::Relaxed),
bytes_read: self.bytes_read.load(Ordering::Relaxed),
bytes_written: self.bytes_written.load(Ordering::Relaxed),
accepts: self.accepts.load(Ordering::Relaxed),
}
}
}
pub(crate) struct Buckets([AtomicU64; crate::stats::BUCKETS]);
impl Buckets {
pub(crate) fn new() -> Self {
Self([const { AtomicU64::new(0) }; crate::stats::BUCKETS])
}
pub(crate) fn add(&self, length: std::time::Duration) {
let nanos = u64::try_from(length.as_nanos()).unwrap_or(u64::MAX);
self.0[crate::stats::bucket_of(nanos)].fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn read(&self) -> crate::stats::Histogram {
crate::stats::Histogram {
buckets: core::array::from_fn(|i| self.0[i].load(Ordering::Relaxed)),
}
}
}
#[cfg(feature = "io-stats")]
pub(crate) struct IoTimes {
reads: Buckets,
writes: Buckets,
accepts: Buckets,
connects: Buckets,
files: Buckets,
}
#[cfg(all(completion, feature = "io-stats"))]
#[derive(Clone, Copy)]
pub(crate) enum IoKind {
Read,
Write,
Accept,
Connect,
#[cfg(ring)]
File,
}
#[cfg(feature = "io-stats")]
impl IoTimes {
fn new() -> Self {
Self {
reads: Buckets::new(),
writes: Buckets::new(),
accepts: Buckets::new(),
connects: Buckets::new(),
files: Buckets::new(),
}
}
#[cfg(completion)]
pub(crate) fn add(&self, kind: IoKind, length: std::time::Duration) {
match kind {
IoKind::Read => &self.reads,
IoKind::Write => &self.writes,
IoKind::Accept => &self.accepts,
IoKind::Connect => &self.connects,
#[cfg(ring)]
IoKind::File => &self.files,
}
.add(length);
}
fn read(&self) -> crate::stats::IoLatencies {
crate::stats::IoLatencies {
reads: self.reads.read(),
writes: self.writes.read(),
accepts: self.accepts.read(),
connects: self.connects.read(),
files: self.files.read(),
}
}
}
struct Errors {
by_kind: [AtomicU64; KINDS.len()],
refused_registrations: AtomicU64,
accepts_out_of_descriptors: AtomicU64,
}
impl Errors {
const fn new() -> Self {
Self {
by_kind: [const { AtomicU64::new(0) }; KINDS.len()],
refused_registrations: AtomicU64::new(0),
accepts_out_of_descriptors: AtomicU64::new(0),
}
}
fn read(&self) -> ErrorCounts {
ErrorCounts {
by_kind: core::array::from_fn(|i| self.by_kind[i].load(Ordering::Relaxed)),
refused_registrations: self.refused_registrations.load(Ordering::Relaxed),
accepts_out_of_descriptors: self.accepts_out_of_descriptors.load(Ordering::Relaxed),
}
}
}
static OFF_SHARD: Errors = Errors::new();
static GROUPS_MISSING: AtomicU64 = AtomicU64::new(0);
#[cfg(ring)]
pub(crate) fn receive_buffers_missing() {
GROUPS_MISSING.fetch_add(1, Ordering::Relaxed);
}
fn errors_here(f: impl FnOnce(&Errors)) {
super::context::current(|shard| match shard {
Some(shard) => f(&shard.shared.counters.errors),
None => f(&OFF_SHARD),
});
}
pub(crate) fn io_error(err: &io::Error) {
if err.kind() == io::ErrorKind::WouldBlock {
return;
}
let at = kind_index(err.kind());
errors_here(|errors| {
errors.by_kind[at].fetch_add(1, Ordering::Relaxed);
});
}
pub(crate) fn accept_error(err: &io::Error) {
if out_of_descriptors(err) {
errors_here(|errors| {
errors
.accepts_out_of_descriptors
.fetch_add(1, Ordering::Relaxed);
});
}
}
pub(crate) fn out_of_descriptors(err: &io::Error) -> bool {
#[cfg(unix)]
let codes = [libc::EMFILE, libc::ENFILE];
#[cfg(windows)]
let codes = [crate::syscore::windows::WSAEMFILE];
err.raw_os_error().is_some_and(|code| codes.contains(&code))
}
fn registry() -> &'static kovan_map::HashMap<usize, Weak<Shared>> {
static SHARDS: OnceLock<kovan_map::HashMap<usize, Weak<Shared>>> = OnceLock::new();
SHARDS.get_or_init(|| kovan_map::HashMap::with_capacity(64))
}
static WENT: AtomicUsize = AtomicUsize::new(0);
pub(crate) fn register(shared: &Arc<Shared>) {
take_out_gone();
registry().insert(Arc::as_ptr(shared) as usize, Arc::downgrade(shared));
}
fn take_out_gone() {
if WENT.load(Ordering::Relaxed) == 0 || WENT.swap(0, Ordering::Acquire) == 0 {
return;
}
let gone: Vec<usize> = registry()
.iter()
.filter(|(_, shard)| shard.upgrade().is_none_or(|s| s.counters.is_gone()))
.map(|(key, _)| key)
.collect();
for key in gone {
drop(registry().remove(&key));
}
}
pub(crate) fn snapshot() -> Stats {
take_out_gone();
let mut shards: Vec<ShardStats> = registry()
.values()
.filter_map(|shared| shared.upgrade())
.filter(|shared| !shared.counters.is_gone())
.map(|shared| shared.stats())
.collect();
shards.sort_by_key(|shard| (shard.worker.is_none(), shard.runtime, shard.worker));
Stats {
shards,
blocking_threads: super::blocking::threads_alive(),
blocking_queue_depth: super::blocking::queued_everywhere(),
off_shard_errors: OFF_SHARD.read(),
receive_buffers_missing: GROUPS_MISSING.load(Ordering::Relaxed),
}
}
pub(crate) fn runtime_snapshot(
workers: impl Iterator<Item = ShardStats>,
threads: usize,
queued: usize,
) -> Stats {
Stats {
shards: workers.collect(),
blocking_threads: threads,
blocking_queue_depth: queued,
off_shard_errors: OFF_SHARD.read(),
receive_buffers_missing: GROUPS_MISSING.load(Ordering::Relaxed),
}
}
#[cold]
#[inline(never)]
pub(crate) fn budget_yield() {
super::context::current(|shard| {
if let Some(shard) = shard {
shard.shared.counters.budget_yield();
}
});
}
#[cfg(all(test, not(loom)))]
mod tests;