use crate::net::Socket;
use crate::udp::{RecvMeta, Transmit};
use std::fmt;
use std::io;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd"))]
mod readiness;
#[cfg(target_os = "linux")]
mod epoll;
#[cfg(all(target_os = "linux", feature = "io-uring"))]
pub mod uring;
#[cfg(windows)]
mod iocp;
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
mod kqueue;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Backend {
Auto,
Epoll,
IoUring,
Iocp,
Kqueue,
}
impl Backend {
pub fn resolve(self) -> Backend {
if self != Backend::Auto {
return self;
}
match env_bytes("SIMOOM_BACKEND").as_deref() {
Some(b"epoll") => return Backend::Epoll,
Some(b"io_uring") | Some(b"uring") => return Backend::IoUring,
Some(b"iocp") => return Backend::Iocp,
Some(b"kqueue") => return Backend::Kqueue,
_ => {}
}
if cfg!(windows) {
Backend::Iocp
} else if cfg!(target_os = "linux") {
Backend::Epoll
} else {
Backend::Kqueue
}
}
pub fn name(self) -> &'static str {
match self {
Backend::Auto => "auto",
Backend::Epoll => "epoll",
Backend::IoUring => "io_uring",
Backend::Iocp => "iocp",
Backend::Kqueue => "kqueue",
}
}
}
#[derive(Clone, Debug)]
pub struct DriverConfig {
pub backend: Backend,
pub events: usize,
pub recv_budget: usize,
pub uring_entries: u32,
pub uring_buffers: u16,
pub uring_fixed_files: bool,
pub uring_max_files: u32,
pub uring_simulate_failure: Option<i32>,
pub iocp_eager_recv: bool,
pub accepts_outstanding: usize,
pub udp_recvs_outstanding: usize,
pub busy_poll: Duration,
}
impl Default for DriverConfig {
fn default() -> Self {
DriverConfig {
backend: Backend::Auto,
events: 1024,
recv_budget: 16,
uring_entries: 1024,
uring_buffers: 1024,
uring_fixed_files: true,
uring_max_files: 16384,
uring_simulate_failure: None,
iocp_eager_recv: false,
accepts_outstanding: 32,
udp_recvs_outstanding: 4,
busy_poll: Duration::from_micros(30),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Id(pub(crate) u64);
impl Id {
pub(crate) fn new(idx: u32, gen: u32) -> Id {
Id(idx as u64 | (gen as u64) << 32)
}
pub(crate) fn idx(self) -> u32 {
self.0 as u32
}
pub(crate) fn gen(self) -> u32 {
(self.0 >> 32) as u32
}
pub fn raw(self) -> u64 {
self.0
}
}
impl fmt::Debug for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Id({}#{})", self.idx(), self.gen())
}
}
#[derive(Debug)]
pub enum OutBuf {
Owned(Vec<u8>),
Static(&'static [u8]),
}
impl OutBuf {
#[inline]
pub fn as_slice(&self) -> &[u8] {
match self {
OutBuf::Owned(v) => v,
OutBuf::Static(s) => s,
}
}
#[inline]
pub fn len(&self) -> usize {
self.as_slice().len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug)]
pub enum Event {
Accepted { key: u64, socket: Socket, peer: Option<SocketAddr> },
Data { key: u64, buf: Vec<u8> },
Eof { key: u64 },
Error { key: u64, error: io::Error },
Sent { key: u64, buf: OutBuf },
Datagram { key: u64, buf: Vec<u8>, meta: RecvMeta },
Closed { key: u64 },
Wake,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DriverStats {
pub waits: u64,
pub recv_calls: u64,
pub send_calls: u64,
pub accept_calls: u64,
pub other_calls: u64,
pub sqes: u64,
pub os_events: u64,
pub sleeps: u64,
pub spin_polls: u64,
}
impl DriverStats {
pub fn syscalls(&self) -> u64 {
self.waits + self.recv_calls + self.send_calls + self.accept_calls + self.other_calls
}
}
pub(crate) const EVENTS_START: usize = 64;
pub(crate) const MAX_SKIPS: u8 = 3;
const PROBE_EVERY: u8 = 64;
pub(crate) struct Spin {
max: u64,
gap: u64,
probe: u8,
pub(crate) worked: bool,
}
#[inline(never)]
pub(crate) fn env_bytes(name: &str) -> Option<Vec<u8>> {
std::env::var_os(name).map(|v| v.into_encoded_bytes())
}
pub(crate) fn env_u64(name: &str) -> Option<u64> {
let v = env_bytes(name)?;
if v.is_empty() || v.len() > 19 || !v.iter().all(u8::is_ascii_digit) {
return None;
}
Some(v.iter().fold(0, |n, &d| n * 10 + u64::from(d - b'0')))
}
impl Spin {
pub(crate) fn new(cfg: &DriverConfig) -> Spin {
let max = env_u64("SIMOOM_BUSY_POLL_US").map_or(cfg.busy_poll.as_nanos().min(u64::MAX as u128) as u64, |us| us.saturating_mul(1000));
Spin { max, gap: max, probe: 0, worked: false }
}
fn spins(&self) -> bool {
self.worked && self.max > 0 && self.gap <= self.max / 4
}
fn record(&mut self, ns: u64) {
let ns = ns.min(4 * self.max);
self.gap = self.gap - self.gap / 4 + ns / 4;
}
#[inline(never)]
pub(crate) fn wait(&mut self, timeout: Option<Duration>, syscall_polls: bool, stats: &mut DriverStats, w: &WakerInner, poll: &mut dyn FnMut(Option<Duration>, &mut DriverStats) -> io::Result<bool>) -> io::Result<()> {
if timeout == Some(Duration::ZERO) {
return poll(timeout, stats).map(drop);
}
let mut spin = self.spins();
if !spin && self.worked && self.max > 0 {
self.probe += 1;
if self.probe >= PROBE_EVERY {
self.probe = 0;
spin = true;
}
}
let mut spun = 0;
if spin {
let limit = timeout.map_or(u64::MAX, |t| t.as_nanos().min(u64::MAX as u128) as u64);
let window = self.max.min(limit);
let t0 = crate::time::now_ns();
let mut last = 0;
loop {
let now = crate::time::now_ns() - t0;
if w.posted.load(Ordering::Relaxed) {
return Ok(());
}
if !syscall_polls || now - last >= 1_000 || last == 0 {
last = now.max(1);
stats.spin_polls += 1;
if poll(Some(Duration::ZERO), stats)? {
self.record(crate::time::now_ns() - t0);
return Ok(());
}
}
if now >= window {
spun = now;
break;
}
core::hint::spin_loop();
}
if window == self.max {
self.record(u64::MAX); }
}
let rest = timeout.map(|t| t.saturating_sub(Duration::from_nanos(spun)));
w.parked.store(true, Ordering::SeqCst);
if w.posted.load(Ordering::SeqCst) {
w.parked.store(false, Ordering::Relaxed);
return poll(Some(Duration::ZERO), stats).map(drop);
}
stats.sleeps += 1;
let r = poll(rest, stats);
w.parked.store(false, Ordering::Relaxed);
r.map(drop)
}
}
#[derive(Clone)]
pub struct Waker {
pub(crate) inner: Arc<WakerInner>,
}
pub(crate) struct WakerInner {
pub(crate) pending: AtomicBool,
pub(crate) signal: Box<dyn Fn() + Send + Sync>,
pub(crate) posted: AtomicBool,
pub(crate) parked: AtomicBool,
}
impl Waker {
pub(crate) fn new(signal: Box<dyn Fn() + Send + Sync>) -> Waker {
Waker { inner: Arc::new(WakerInner { pending: AtomicBool::new(false), signal, posted: AtomicBool::new(false), parked: AtomicBool::new(false) }) }
}
pub fn wake(&self) {
if !self.inner.pending.swap(true, Ordering::AcqRel) {
(self.inner.signal)();
}
}
pub fn post(&self) {
let i = &*self.inner;
i.posted.store(true, Ordering::SeqCst);
if i.parked.load(Ordering::SeqCst) {
self.wake();
}
}
pub(crate) fn take_posted(&self) -> bool {
self.inner.posted.load(Ordering::Relaxed) && self.inner.posted.swap(false, Ordering::AcqRel)
}
pub(crate) fn clear(&self) {
self.inner.pending.store(false, Ordering::Release);
}
}
impl fmt::Debug for Waker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Waker").field("pending", &self.inner.pending.load(Ordering::Relaxed)).finish()
}
}
pub(crate) struct Slab<T> {
slots: Vec<(u32, Option<T>)>,
free: Vec<u32>,
len: usize,
}
impl<T> Slab<T> {
pub(crate) fn new() -> Slab<T> {
Slab { slots: Vec::new(), free: Vec::new(), len: 0 }
}
pub(crate) fn insert(&mut self, v: T) -> Id {
self.len += 1;
if let Some(i) = self.free.pop() {
let s = &mut self.slots[i as usize];
s.1 = Some(v);
Id::new(i, s.0)
} else {
self.slots.push((0, Some(v)));
Id::new(self.slots.len() as u32 - 1, 0)
}
}
pub(crate) fn get(&self, id: Id) -> Option<&T> {
match self.slots.get(id.idx() as usize) {
Some((g, Some(v))) if *g == id.gen() => Some(v),
_ => None,
}
}
pub(crate) fn get_mut(&mut self, id: Id) -> Option<&mut T> {
match self.slots.get_mut(id.idx() as usize) {
Some((g, Some(v))) if *g == id.gen() => Some(v),
_ => None,
}
}
#[cfg_attr(not(all(target_os = "linux", feature = "io-uring")), allow(dead_code))]
pub(crate) fn id_at(&self, idx: u32) -> Option<Id> {
match self.slots.get(idx as usize) {
Some((g, Some(_))) => Some(Id::new(idx, *g)),
_ => None,
}
}
pub(crate) fn remove(&mut self, id: Id) -> Option<T> {
let s = self.slots.get_mut(id.idx() as usize)?;
if s.0 != id.gen() || s.1.is_none() {
return None;
}
s.0 = s.0.wrapping_add(1);
self.free.push(id.idx());
self.len -= 1;
s.1.take()
}
pub(crate) fn len(&self) -> usize {
self.len
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn ids(&self) -> Vec<Id> {
self.slots.iter().enumerate().filter(|(_, s)| s.1.is_some()).map(|(i, s)| Id::new(i as u32, s.0)).collect()
}
}
pub(crate) const LARGE_BATCH: usize = 8;
#[derive(Default)]
pub(crate) struct LargeBufs {
pub(crate) size: usize,
free: Vec<Vec<u8>>,
}
impl LargeBufs {
#[inline(never)]
pub(crate) fn take(&mut self) -> Vec<u8> {
self.free.pop().unwrap_or_else(|| Vec::with_capacity(self.size))
}
#[inline]
pub(crate) fn keep(&mut self, buf: Vec<u8>) -> Option<Vec<u8>> {
if !cfg!(feature = "udp") || self.size == 0 || buf.capacity() < self.size {
return Some(buf);
}
self.store(buf);
None
}
#[inline(never)]
fn store(&mut self, mut buf: Vec<u8>) {
if self.free.len() < 2 * LARGE_BATCH {
buf.clear();
self.free.push(buf);
}
}
}
enum Imp {
#[cfg(target_os = "linux")]
Epoll(readiness::Driver<epoll::Poller>),
#[cfg(all(target_os = "linux", feature = "io-uring"))]
Uring(Box<uring::Driver>),
#[cfg(windows)]
Iocp(iocp::Driver),
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
Kqueue(readiness::Driver<kqueue::Poller>),
}
macro_rules! dispatch {
($self:expr, $d:ident => $e:expr) => {
match &mut $self.imp {
#[cfg(target_os = "linux")]
Imp::Epoll($d) => $e,
#[cfg(all(target_os = "linux", feature = "io-uring"))]
Imp::Uring($d) => $e,
#[cfg(windows)]
Imp::Iocp($d) => $e,
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
Imp::Kqueue($d) => $e,
}
};
}
macro_rules! dispatch_ref {
($self:expr, $d:ident => $e:expr) => {
match &$self.imp {
#[cfg(target_os = "linux")]
Imp::Epoll($d) => $e,
#[cfg(all(target_os = "linux", feature = "io-uring"))]
Imp::Uring($d) => $e,
#[cfg(windows)]
Imp::Iocp($d) => $e,
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
Imp::Kqueue($d) => $e,
}
};
}
pub struct Driver {
imp: Imp,
kind: Backend,
fallback: Option<String>,
_not_send: core::marker::PhantomData<*const ()>,
}
impl fmt::Debug for Driver {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Driver").field("backend", &self.kind).field("fallback", &self.fallback).finish()
}
}
impl Driver {
pub fn new(cfg: &DriverConfig) -> io::Result<Driver> {
let want = cfg.backend.resolve();
#[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
let mut fallback = None;
#[cfg(target_os = "linux")]
if want == Backend::IoUring {
#[cfg(feature = "io-uring")]
match uring::Driver::new(cfg) {
Ok(d) => {
let fallback = d.degraded();
return Ok(Driver { imp: Imp::Uring(d), kind: Backend::IoUring, fallback, _not_send: core::marker::PhantomData });
}
Err(e) => fallback = Some(uring_unavailable(&e)),
}
#[cfg(not(feature = "io-uring"))]
{
fallback = Some("io_uring unavailable (built without the `io-uring` feature); using epoll".into());
}
}
let (imp, kind) = match want {
#[cfg(target_os = "linux")]
Backend::Epoll | Backend::IoUring => (Imp::Epoll(readiness::Driver::new(cfg)?), Backend::Epoll),
#[cfg(windows)]
Backend::Iocp => (Imp::Iocp(iocp::Driver::new(cfg)?), Backend::Iocp),
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
Backend::Kqueue => (Imp::Kqueue(readiness::Driver::new(cfg)?), Backend::Kqueue),
other => return Err(unsupported_backend(other)),
};
Ok(Driver { imp, kind, fallback, _not_send: core::marker::PhantomData })
}
pub fn backend(&self) -> Backend {
self.kind
}
pub fn fallback_reason(&self) -> Option<&str> {
self.fallback.as_deref()
}
pub fn waker(&self) -> Waker {
dispatch_ref!(self, d => d.waker())
}
pub fn listen(&mut self, socket: Socket, key: u64) -> io::Result<Id> {
dispatch!(self, d => d.listen(socket, key))
}
pub fn stream(&mut self, socket: Socket, key: u64) -> io::Result<Id> {
dispatch!(self, d => d.stream(socket, key))
}
pub fn udp(&mut self, socket: Socket, key: u64) -> io::Result<Id> {
dispatch!(self, d => d.udp(socket, key))
}
pub fn udp_large(&mut self, socket: Socket, key: u64, buf_size: usize) -> io::Result<Id> {
dispatch!(self, d => d.udp_large(socket, key, buf_size))
}
pub fn recv(&mut self, id: Id, on: bool) {
dispatch!(self, d => d.recv(id, on))
}
pub fn send(&mut self, id: Id, buf: OutBuf) {
dispatch!(self, d => d.send(id, buf))
}
pub fn send_to(&mut self, id: Id, msgs: &[Transmit<'_>]) -> io::Result<usize> {
dispatch!(self, d => d.send_to(id, msgs))
}
pub fn close(&mut self, id: Id) {
dispatch!(self, d => d.close(id))
}
pub fn socket(&self, id: Id) -> Option<&Socket> {
dispatch_ref!(self, d => d.socket(id))
}
pub fn provide(&mut self, buf: Vec<u8>) {
dispatch!(self, d => d.provide(buf))
}
pub fn buffers_wanted(&self) -> usize {
dispatch_ref!(self, d => d.buffers_wanted())
}
pub fn set_buffer_target(&mut self, n: usize) {
dispatch!(self, d => d.set_buffer_target(n))
}
pub fn flush_now(&mut self, id: Id) {
match &mut self.imp {
#[cfg(target_os = "linux")]
Imp::Epoll(d) => d.flush_now(id),
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
Imp::Kqueue(d) => d.flush_now(id),
#[allow(unreachable_patterns)]
_ => {
let _ = id;
}
}
}
pub fn trim_buffers(&mut self, keep: usize) {
dispatch!(self, d => d.trim_buffers(keep))
}
pub fn turn(&mut self, timeout: Option<Duration>, out: &mut Vec<Event>) -> io::Result<()> {
dispatch!(self, d => d.turn(timeout, out))?;
if dispatch_ref!(self, d => d.waker.take_posted()) {
out.push(Event::Wake);
}
Ok(())
}
pub fn stats(&self) -> DriverStats {
dispatch_ref!(self, d => d.stats())
}
pub fn len(&self) -> usize {
dispatch_ref!(self, d => d.len())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(all(target_os = "linux", feature = "io-uring"))]
#[cold]
#[inline(never)]
fn uring_unavailable(e: &io::Error) -> String {
format!("io_uring unavailable ({e}); using epoll")
}
#[cold]
#[inline(never)]
fn unsupported_backend(b: Backend) -> io::Error {
io::Error::new(io::ErrorKind::Unsupported, format!("the {} back end is not available on this OS", b.name()))
}
#[allow(dead_code)]
pub(crate) fn timeout_ms(t: Option<Duration>) -> i32 {
match t {
None => -1,
Some(d) => {
let ms = d.as_nanos().div_ceil(1_000_000);
ms.min(i32::MAX as u128) as i32
}
}
}
#[cfg(test)]
mod tests;