use simoom_sys::cputime::{thread_times, ThreadTimes};
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
const R: Ordering = Ordering::Relaxed;
const SUSPECT_CALLS: u32 = 16;
const BLOCKED_FACTOR: u64 = 8;
const MAX_RING: usize = 256;
const MAX_BACK_AFTER: u32 = 1 << 20;
#[derive(Clone, Copy, Debug)]
pub(crate) struct Policy {
pub(crate) budget_ns: u64,
pub(crate) stalls: u32,
pub(crate) window_ns: u64,
pub(crate) recover: u32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Sample {
pub(crate) cpu_ns: u64,
pub(crate) voluntary: Option<u64>,
}
impl Sample {
fn between(a: ThreadTimes, b: ThreadTimes) -> Sample {
let voluntary = match (a.voluntary_switches, b.voluntary_switches) {
(Some(x), Some(y)) => Some(y.saturating_sub(x)),
_ => None,
};
Sample { cpu_ns: b.cpu_ns.saturating_sub(a.cpu_ns), voluntary }
}
}
pub(crate) type Mark = Option<ThreadTimes>;
pub(crate) fn mark() -> Mark {
thread_times()
}
pub(crate) fn since(a: Mark) -> Option<Sample> {
match (a, thread_times()) {
(Some(a), Some(b)) => Some(Sample::between(a, b)),
_ => None,
}
}
pub(crate) fn measured<T>(f: impl FnOnce() -> T) -> (T, Option<Sample>) {
let a = mark();
let r = f();
(r, since(a))
}
pub(crate) fn is_stall(wall_ns: u64, s: Sample, budget_ns: u64) -> bool {
if s.cpu_ns > budget_ns {
return true;
}
if wall_ns <= budget_ns {
return false;
}
match s.voluntary {
Some(v) => v > 0,
None => wall_ns.saturating_sub(s.cpu_ns) > budget_ns.saturating_mul(BLOCKED_FACTOR),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Event {
None,
Stall,
Offloaded,
}
pub(crate) struct Watch {
offload: AtomicBool,
suspect: AtomicU32,
ring: Mutex<VecDeque<(u64, u64)>>,
fast: AtomicU32,
back_after: AtomicU32,
budget_ns: AtomicU64,
recover: AtomicU32,
pub(crate) logged: AtomicBool,
group: Option<Arc<AtomicU32>>,
}
impl Watch {
pub(crate) fn new(group: Option<Arc<AtomicU32>>) -> Watch {
Watch {
offload: AtomicBool::new(false),
suspect: AtomicU32::new(0),
ring: Mutex::new(VecDeque::new()),
fast: AtomicU32::new(0),
back_after: AtomicU32::new(0),
budget_ns: AtomicU64::new(0),
recover: AtomicU32::new(0),
logged: AtomicBool::new(false),
group,
}
}
fn group_add(&self, up: bool) {
if let Some(g) = &self.group {
if up {
g.fetch_add(1, R);
} else {
g.fetch_sub(1, R);
}
}
}
#[inline]
pub(crate) fn offloaded(&self) -> bool {
self.offload.load(R)
}
#[inline]
pub(crate) fn suspect(&self) -> bool {
self.suspect.load(R) != 0
}
pub(crate) fn wall_overrun(&self) -> bool {
if self.offloaded() {
return false;
}
let was = self.suspect.swap(SUSPECT_CALLS, R);
if was == 0 {
self.group_add(true);
}
was == 0
}
fn end_suspicion(&self) {
if self.suspect.swap(0, R) != 0 {
self.group_add(false);
}
}
pub(crate) fn measured(&self, wall_ns: u64, s: Sample, p: &Policy, stalls: u32, now: u64) -> Event {
if !is_stall(wall_ns, s, p.budget_ns) {
let mut x = self.suspect.load(R);
while x != 0 {
match self.suspect.compare_exchange_weak(x, x - 1, R, R) {
Ok(_) => {
if x == 1 {
self.group_add(false);
}
break;
}
Err(y) => x = y,
}
}
return Event::None;
}
if self.suspect.swap(SUSPECT_CALLS, R) == 0 {
self.group_add(true);
}
if stalls == 0 {
return Event::Stall;
}
let k = (stalls as usize).min(MAX_RING);
let full = {
let mut ring = self.ring.lock().unwrap_or_else(|e| e.into_inner());
ring.push_back((now, wall_ns));
while ring.len() > k {
ring.pop_front();
}
let held: u64 = ring.iter().skip(1).map(|&(_, w)| w).sum();
ring.len() == k && now.saturating_sub(ring[0].0).saturating_sub(held) <= p.window_ns
};
if !full {
return Event::Stall;
}
self.budget_ns.store(p.budget_ns, R);
self.recover.store(p.recover, R);
if self.back_after.load(R) == 0 {
self.back_after.store(p.recover, R);
}
self.fast.store(0, R);
let moved = !self.offload.swap(true, R);
if moved {
self.group_add(true);
}
self.end_suspicion();
if moved {
Event::Offloaded
} else {
Event::Stall
}
}
pub(crate) fn offloaded_call(&self, wall_ns: u64, s: Option<Sample>) -> bool {
let budget = self.budget_ns.load(R);
let fast = match s {
Some(s) => !is_stall(wall_ns, s, budget),
None => wall_ns <= budget,
};
if !fast {
self.fast.store(0, R);
return false;
}
let need = self.back_after.load(R);
if self.recover.load(R) == 0 || need == 0 {
return false;
}
if self.fast.fetch_add(1, R) + 1 < need {
return false;
}
self.fast.store(0, R);
if !self.offload.swap(false, R) {
return false;
}
self.back_after.store(need.saturating_mul(2).min(MAX_BACK_AFTER), R);
self.ring.lock().unwrap_or_else(|e| e.into_inner()).clear();
self.group_add(false);
true
}
}
impl std::fmt::Debug for Watch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Watch").field("offload", &self.offloaded()).field("suspect", &self.suspect.load(R)).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
const P: Policy = Policy { budget_ns: 1_000_000, stalls: 3, window_ns: 1_000_000_000, recover: 4 };
const BUSY: Sample = Sample { cpu_ns: 5_000_000, voluntary: Some(0) };
const IDLE: Sample = Sample { cpu_ns: 10_000, voluntary: Some(0) };
#[test]
fn preemption_is_not_a_stall_but_cpu_and_blocking_are() {
assert!(!is_stall(9_000_000, IDLE, P.budget_ns));
assert!(is_stall(9_000_000, BUSY, P.budget_ns));
assert!(is_stall(2_000_000, Sample { cpu_ns: 10_000, voluntary: Some(1) }, P.budget_ns), "blocked");
assert!(!is_stall(500_000, Sample { cpu_ns: 10_000, voluntary: Some(1) }, P.budget_ns), "short block");
assert!(!is_stall(5_000_000, Sample { cpu_ns: 10_000, voluntary: None }, P.budget_ns));
assert!(is_stall(20_000_000, Sample { cpu_ns: 10_000, voluntary: None }, P.budget_ns));
}
#[test]
fn stalls_must_fall_in_the_window_and_the_route_comes_back() {
let g = Arc::new(AtomicU32::new(0));
let w = Watch::new(Some(g.clone()));
assert!(w.wall_overrun());
assert_eq!(g.load(R), 1);
for i in 0..10u64 {
assert_eq!(w.measured(5_000_000, BUSY, &P, 3, i * 2_000_000_000), Event::Stall);
}
assert!(!w.offloaded());
let t = 100_000_000_000;
assert_eq!(w.measured(5_000_000, BUSY, &P, 3, t), Event::Stall);
assert_eq!(w.measured(5_000_000, BUSY, &P, 3, t + 1), Event::Stall);
assert_eq!(w.measured(5_000_000, BUSY, &P, 3, t + 2), Event::Offloaded);
assert!(w.offloaded() && !w.suspect());
assert_eq!(g.load(R), 1, "offloaded, no longer suspect");
for _ in 0..3 {
assert!(!w.offloaded_call(100_000, Some(IDLE)));
}
assert!(!w.offloaded_call(5_000_000, Some(BUSY)));
for _ in 0..3 {
assert!(!w.offloaded_call(100_000, Some(IDLE)));
}
assert!(w.offloaded_call(100_000, Some(IDLE)));
assert!(!w.offloaded());
assert_eq!(g.load(R), 0);
w.wall_overrun();
for i in 0..3 {
w.measured(5_000_000, BUSY, &P, 3, t + 10 + i);
}
assert!(w.offloaded());
for _ in 0..7 {
assert!(!w.offloaded_call(100_000, Some(IDLE)));
}
assert!(w.offloaded_call(100_000, Some(IDLE)));
}
#[test]
fn long_blocking_calls_move_the_route() {
let w = Watch::new(None);
w.wall_overrun();
let blocked = Sample { cpu_ns: 10_000, voluntary: Some(1) };
let call = 400_000_000; let t = 10_000_000_000u64;
assert_eq!(w.measured(call, blocked, &P, 3, t), Event::Stall);
assert_eq!(w.measured(call, blocked, &P, 3, t + call + 1_000_000), Event::Stall);
assert_eq!(w.measured(call, blocked, &P, 3, t + 2 * (call + 1_000_000)), Event::Offloaded);
let w = Watch::new(None);
w.wall_overrun();
for i in 0..6u64 {
assert_eq!(w.measured(call, blocked, &P, 3, t + i * (call + 2_000_000_000)), Event::Stall);
}
assert!(!w.offloaded());
}
#[test]
fn suspicion_lapses_after_fast_measured_calls() {
let g = Arc::new(AtomicU32::new(0));
let w = Watch::new(Some(g.clone()));
w.wall_overrun();
for _ in 0..SUSPECT_CALLS {
assert!(w.suspect());
assert_eq!(w.measured(9_000_000, IDLE, &P, 3, 0), Event::None);
}
assert!(!w.suspect());
assert_eq!(g.load(R), 0);
}
}