use crate::gen::{BN_CREDIT, BN_DISK, BN_NETWORK, BN_SOURCE, BN_WORKERS};
pub const MIN_CHUNK: u32 = 1 << 20;
pub const MAX_CHUNK: u32 = 15 << 20;
pub const START_CHUNK: u32 = 4 << 20;
pub const MIN_BUNDLE: u32 = 256 << 10;
pub const MAX_BUNDLE: u32 = 15 << 20;
pub const START_BUNDLE: u32 = 1 << 20;
pub const START_LANES: u8 = 2;
pub const MAX_LANES: u8 = 8;
const GAIN: f64 = 1.10;
const GAIN_BELOW_BEST: f64 = 1.05;
const NEAR_BEST: f64 = 0.90;
const LANES_FIRST_CHUNK: u32 = 4 << 20;
const PREFER_LANES_UNTIL: u8 = 4;
const HOLD_TICKS: u32 = 30;
const STABLE_TICKS: u32 = 10;
const PROBE_TICKS: u32 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
Bundle,
Stream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Mixed,
StreamOnly,
BundleOnly,
}
#[derive(Debug, Clone, Default)]
pub struct Sample {
pub secs: f64,
pub bytes_acked: u64,
pub lanes: u8,
pub stalls: u32,
pub credit_starved: bool,
pub source_starved: bool,
pub receiver_bottleneck: u8,
pub small_durable: u64,
pub large_durable: u64,
pub small_left: u64,
pub large_left: u64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Decision {
pub lanes: u8,
pub chunk: u32,
pub bundle: u32,
pub bottleneck: u8,
pub mode: Mode,
pub prefer: Class,
pub sequential: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum LaneStep {
Steady,
Trying { before: f64, wait: u32 },
Hold(u32),
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Probe {
Warmup(u32),
Run { mode: Mode, left: u32 },
Done,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GovernorOptions {
pub pin_lanes: Option<u8>,
pub pin_chunk: Option<u32>,
pub lanes_first: bool,
}
impl Default for GovernorOptions {
fn default() -> Self {
Self {
pin_lanes: None,
pin_chunk: None,
lanes_first: true,
}
}
}
impl GovernorOptions {
pub fn from_env() -> Self {
let get = |k: &str| std::env::var(k).ok();
Self::from_vars(
get("AVA1_LANES").as_deref(),
get("AVA1_CHUNK").as_deref(),
get("AVA1_LANES_FIRST").as_deref(),
)
}
pub fn from_vars(
lanes: Option<&str>,
chunk_mib: Option<&str>,
lanes_first: Option<&str>,
) -> Self {
let pin_lanes = lanes
.and_then(|v| v.trim().parse::<u8>().ok())
.filter(|n| (1..=MAX_LANES).contains(n));
let pin_chunk = chunk_mib
.and_then(|v| v.trim().parse::<u32>().ok())
.filter(|m| (1..=(MAX_CHUNK >> 20)).contains(m))
.map(|m| m << 20);
Self {
pin_lanes,
pin_chunk,
lanes_first: lanes_first.map(|v| v.trim() != "0").unwrap_or(true),
}
}
}
pub struct Governor {
opts: GovernorOptions,
best_rate: f64,
lanes_capped: bool,
lanes: u8,
chunk: u32,
bundle: u32,
step: LaneStep,
stable: u32,
probe: Probe,
rates: [(f64, f64); 3], acc: (f64, f64, f64), sequential: bool,
mode: Mode,
}
impl Default for Governor {
fn default() -> Self {
Self::new()
}
}
fn idx(m: Mode) -> usize {
match m {
Mode::Mixed => 0,
Mode::StreamOnly => 1,
Mode::BundleOnly => 2,
}
}
pub fn inflight_cap(chunk: u32, lane_rate: f64) -> u64 {
((lane_rate * 2.0) as u64).max(chunk as u64)
}
const MAX_PLAUSIBLE_RATE: f64 = 1e12;
const BEST_DECAY: f64 = 0.99;
impl Governor {
pub fn new() -> Self {
Self::with_options(GovernorOptions::default())
}
pub fn with_options(opts: GovernorOptions) -> Self {
Self {
opts,
best_rate: 0.0,
lanes_capped: false,
lanes: opts.pin_lanes.unwrap_or(START_LANES),
chunk: opts.pin_chunk.unwrap_or(START_CHUNK),
bundle: START_BUNDLE,
step: LaneStep::Steady,
stable: 0,
probe: Probe::Warmup(3),
rates: [(0.0, 0.0); 3],
acc: (0.0, 0.0, 0.0),
sequential: false,
mode: Mode::Mixed,
}
}
fn decision(&self, bottleneck: u8, s: &Sample) -> Decision {
let small_rate = self.rates[0].0.max(1.0);
let large_rate = self.rates[0].1.max(1.0);
let prefer = if s.small_left as f64 / small_rate >= s.large_left as f64 / large_rate {
Class::Bundle
} else {
Class::Stream
};
let mode = if self.sequential {
if s.small_left > 0 {
Mode::BundleOnly
} else {
Mode::StreamOnly
}
} else {
self.mode
};
Decision {
lanes: self.lanes,
chunk: self.chunk,
bundle: self.bundle,
bottleneck,
mode,
prefer,
sequential: self.sequential,
}
}
pub fn tick(&mut self, s: &Sample) -> Decision {
if s.secs.is_nan() || s.secs <= 0.0 || s.secs.is_infinite() {
return self.decision(BN_NETWORK, s);
}
let rate = (s.bytes_acked as f64 / s.secs).min(MAX_PLAUSIBLE_RATE);
let lanes_now = s.lanes.max(1);
let lane_rate = rate / lanes_now as f64;
let bottleneck = if s.source_starved {
BN_SOURCE
} else if s.credit_starved {
match s.receiver_bottleneck {
BN_DISK | BN_WORKERS => s.receiver_bottleneck,
_ => BN_CREDIT,
}
} else {
BN_NETWORK
};
let reported = if bottleneck == BN_CREDIT && s.receiver_bottleneck == BN_NETWORK {
BN_NETWORK
} else {
bottleneck
};
self.best_rate = (self.best_rate * BEST_DECAY).max(rate);
let gain = if self.opts.lanes_first && rate < self.best_rate * NEAR_BEST {
GAIN_BELOW_BEST
} else {
GAIN
};
if s.stalls > 0 {
self.lanes = self.lanes.saturating_sub(1).max(1);
self.chunk = (self.chunk / 2).max(MIN_CHUNK) & !((1 << 20) - 1);
self.stable = 0;
self.lanes_capped = false;
self.step = LaneStep::Hold(HOLD_TICKS / 3);
} else {
self.stable += 1;
if self.stable >= STABLE_TICKS {
self.stable = 0;
let mut grown = (self.chunk * 2).min(MAX_CHUNK);
if self.opts.lanes_first
&& bottleneck == BN_NETWORK
&& self.lanes < PREFER_LANES_UNTIL
&& !self.lanes_capped
&& self.opts.pin_lanes.is_none()
{
grown = grown.min(LANES_FIRST_CHUNK.max(self.chunk));
}
self.chunk = grown;
}
self.step = match self.step {
LaneStep::Hold(0) => LaneStep::Steady,
LaneStep::Hold(n) => LaneStep::Hold(n - 1),
LaneStep::Trying { before, wait: 0 } => {
if rate >= before * gain {
LaneStep::Steady
} else {
self.lanes = self.lanes.saturating_sub(1).max(1);
self.lanes_capped = true; LaneStep::Hold(HOLD_TICKS)
}
}
LaneStep::Trying { before, wait } => LaneStep::Trying {
before,
wait: wait - 1,
},
LaneStep::Steady
if bottleneck == BN_NETWORK
&& self.lanes < MAX_LANES
&& self.opts.pin_lanes.is_none() =>
{
self.lanes += 1;
LaneStep::Trying {
before: rate,
wait: 1,
}
}
st => st,
};
}
if self.lanes >= MAX_LANES {
self.lanes_capped = true;
}
let half_second = ((lane_rate * 0.5) as u32) & !((1 << 20) - 1);
self.chunk = self.chunk.min(half_second.max(MIN_CHUNK));
if let Some(n) = self.opts.pin_lanes {
self.lanes = n;
}
if let Some(c) = self.opts.pin_chunk {
self.chunk = c;
}
self.bundle = ((lane_rate * 0.25) as u32).clamp(MIN_BUNDLE, MAX_BUNDLE);
self.probe = match self.probe {
Probe::Warmup(0) if s.small_left > 0 && s.large_left > 0 => {
self.acc = (0.0, 0.0, 0.0);
self.mode = Mode::Mixed;
Probe::Run {
mode: Mode::Mixed,
left: PROBE_TICKS,
}
}
Probe::Warmup(0) => Probe::Done,
Probe::Warmup(n) => Probe::Warmup(n - 1),
Probe::Run { mode, left } => {
self.acc.0 += s.small_durable as f64;
self.acc.1 += s.large_durable as f64;
self.acc.2 += s.secs;
if left > 1 {
Probe::Run {
mode,
left: left - 1,
}
} else {
self.rates[idx(mode)] = (self.acc.0 / self.acc.2, self.acc.1 / self.acc.2);
self.acc = (0.0, 0.0, 0.0);
match mode {
Mode::Mixed => {
self.mode = Mode::StreamOnly;
Probe::Run {
mode: Mode::StreamOnly,
left: PROBE_TICKS,
}
}
Mode::StreamOnly => {
self.mode = Mode::BundleOnly;
Probe::Run {
mode: Mode::BundleOnly,
left: PROBE_TICKS,
}
}
Mode::BundleOnly => {
let (ms, ml) = self.rates[0];
let (sl, ll) = (s.small_left as f64, s.large_left as f64);
let mixed = (sl / ms.max(1.0)).max(ll / ml.max(1.0));
let seq = sl / self.rates[2].0.max(1.0) + ll / self.rates[1].1.max(1.0);
self.sequential = seq < mixed * 0.9;
self.mode = Mode::Mixed;
Probe::Done
}
}
}
}
Probe::Done => Probe::Done,
};
self.decision(reported, s)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct JobSummary {
pub ticks: u32,
pub credit_starved: u32,
pub source_starved: u32,
pub receiver_bound: u32,
pub lanes_sum: u64,
pub chunk_sum: u64,
pub receiver_bottleneck: u8,
pub sequential_ticks: u32,
pub history: Vec<(u32, u8, u32)>,
stride: u32,
}
pub const HISTORY_MAX: usize = 60;
fn thin(history: &mut Vec<(u32, u8, u32)>) {
while history.len() > HISTORY_MAX {
let mut i = 0;
history.retain(|_| {
i += 1;
i % 2 == 1
});
}
}
fn bottleneck_name(b: u8) -> &'static str {
match b {
BN_NETWORK => "network",
BN_SOURCE => "source",
BN_DISK => "disk",
BN_WORKERS => "workers",
BN_CREDIT => "credit",
_ => "none",
}
}
impl JobSummary {
pub fn observe(&mut self, s: &Sample, d: &Decision) {
self.ticks += 1;
self.credit_starved += u32::from(s.credit_starved);
self.source_starved += u32::from(s.source_starved);
self.receiver_bound +=
u32::from(s.credit_starved && matches!(s.receiver_bottleneck, BN_DISK | BN_WORKERS));
self.lanes_sum += u64::from(s.lanes);
self.chunk_sum += u64::from(d.chunk);
self.receiver_bottleneck = s.receiver_bottleneck;
self.sequential_ticks += u32::from(d.sequential);
let stride = self.stride.max(1);
if (self.ticks - 1).is_multiple_of(stride) {
self.history.push((self.ticks, s.lanes, d.chunk >> 10));
if self.history.len() > HISTORY_MAX {
thin(&mut self.history);
self.stride = stride * 2;
}
}
}
pub fn merge(&mut self, o: &JobSummary) {
let base = self.ticks;
self.history
.extend(o.history.iter().map(|&(t, l, c)| (base + t, l, c)));
thin(&mut self.history);
self.ticks += o.ticks;
self.credit_starved += o.credit_starved;
self.source_starved += o.source_starved;
self.receiver_bound += o.receiver_bound;
self.lanes_sum += o.lanes_sum;
self.chunk_sum += o.chunk_sum;
self.sequential_ticks += o.sequential_ticks;
if o.ticks > 0 {
self.receiver_bottleneck = o.receiver_bottleneck;
}
self.stride = self.stride.max(o.stride);
}
pub fn line(&self) -> Option<String> {
if self.ticks == 0 {
return None;
}
let n = f64::from(self.ticks);
let pct = |c: u32| f64::from(c) * 100.0 / n;
Some(format!(
"[ava1] job bottlenecks over {} ticks: credit-starved {:.0}%, source-starved {:.0}%, receiver-bound {:.0}%; receiver reported {}; avg lanes {:.1}, avg chunk {:.1} MiB",
self.ticks,
pct(self.credit_starved),
pct(self.source_starved),
pct(self.receiver_bound),
bottleneck_name(self.receiver_bottleneck),
self.lanes_sum as f64 / n,
self.chunk_sum as f64 / n / f64::from(1u32 << 20),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn net(g: &mut Governor, per_lane: f64, cap: f64, ticks: usize) -> Decision {
let mut d = g.tick(&Sample::default());
for _ in 0..ticks {
let rate = (d.lanes as f64 * per_lane).min(cap);
d = g.tick(&Sample {
secs: 1.0,
bytes_acked: rate as u64,
lanes: d.lanes,
..Default::default()
});
}
d
}
#[test]
fn hostile_samples_cannot_panic_or_pin_the_governor() {
let mut g = Governor::new();
let hostile = [
(u64::MAX, 1e-300),
(u64::MAX, f64::MIN_POSITIVE),
(u64::MAX, 1.0),
(1, f64::INFINITY),
(1, f64::NAN),
(0, -1.0),
];
for (acked, secs) in hostile {
for stalls in [0, 3] {
let d = g.tick(&Sample {
secs,
bytes_acked: acked,
stalls,
lanes: 0,
small_left: u64::MAX,
large_left: u64::MAX,
small_durable: u64::MAX,
large_durable: u64::MAX,
receiver_bottleneck: 200,
..Sample::default()
});
assert!((1..=MAX_LANES).contains(&d.lanes));
assert!((MIN_CHUNK..=MAX_CHUNK).contains(&d.chunk), "{}", d.chunk);
assert!((MIN_BUNDLE..=MAX_BUNDLE).contains(&d.bundle));
}
}
assert!(g.best_rate.is_finite() && g.best_rate <= MAX_PLAUSIBLE_RATE);
assert_eq!(inflight_cap(4 << 20, f64::INFINITY), u64::MAX);
assert_eq!(inflight_cap(4 << 20, f64::NAN), 4 << 20);
}
#[test]
fn lanes_grow_while_throughput_rises_and_stop_at_the_ceiling() {
let mut g = Governor::new();
let d = net(&mut g, 30e6, 110e6, 40);
assert_eq!(d.lanes, 4, "3→4 gains 22 %, 4→5 gains nothing");
assert_eq!(d.bottleneck, crate::gen::BN_NETWORK);
}
#[test]
fn a_single_window_limited_lane_still_gets_company() {
let mut g = Governor::new();
assert!(net(&mut g, 52e6, 300e6, 60).lanes >= 5);
}
#[test]
fn a_stall_drops_a_lane_and_halves_the_chunk() {
let mut g = Governor::new();
let d0 = net(&mut g, 30e6, 110e6, 40);
let d = g.tick(&Sample {
secs: 1.0,
bytes_acked: 200_000_000,
lanes: d0.lanes,
stalls: 1,
..Default::default()
});
assert_eq!(d.lanes, d0.lanes - 1);
assert_eq!(d.chunk, (d0.chunk / 2).max(MIN_CHUNK) & !((1 << 20) - 1));
}
#[test]
fn the_chunk_grows_back_when_stable_and_stays_whole_groups() {
let mut g = Governor::new();
let _ = g.tick(&Sample {
secs: 1.0,
bytes_acked: 1,
lanes: 2,
stalls: 1,
..Default::default()
});
let d = net(&mut g, 60e6, 110e6, 40);
assert!(d.chunk > MIN_CHUNK);
assert_eq!(d.chunk % (1 << 20), 0);
assert!(d.chunk <= MAX_CHUNK);
}
#[test]
fn the_bottleneck_follows_who_waits() {
let mut g = Governor::new();
let s = Sample {
secs: 1.0,
bytes_acked: 10,
lanes: 2,
source_starved: true,
..Default::default()
};
assert_eq!(g.tick(&s).bottleneck, crate::gen::BN_SOURCE);
let s = Sample {
secs: 1.0,
bytes_acked: 10,
lanes: 2,
credit_starved: true,
receiver_bottleneck: crate::gen::BN_DISK,
..Default::default()
};
let d = g.tick(&s);
assert_eq!(d.bottleneck, crate::gen::BN_DISK);
assert_eq!(
d.lanes, 2,
"lanes never grow while the receiver is the limit"
);
}
#[test]
fn waiting_on_credit_while_the_console_idles_is_the_network_not_its_memory() {
let mut g = Governor::new();
let s = Sample {
secs: 1.0,
bytes_acked: 10,
lanes: 2,
credit_starved: true,
receiver_bottleneck: crate::gen::BN_NETWORK,
..Default::default()
};
assert_eq!(g.tick(&s).bottleneck, crate::gen::BN_NETWORK);
let s = Sample {
receiver_bottleneck: crate::gen::BN_NONE,
..s
};
assert_eq!(g.tick(&s).bottleneck, crate::gen::BN_CREDIT);
}
#[test]
fn the_idle_console_label_does_not_change_how_lanes_grow() {
let run = |receiver: u8| {
let mut g = Governor::new();
(0..60)
.map(|_| {
let d = g.tick(&Sample {
secs: 1.0,
bytes_acked: 50_000_000,
lanes: g.lanes,
credit_starved: true,
receiver_bottleneck: receiver,
..Default::default()
});
(d.lanes, d.chunk, d.bottleneck)
})
.collect::<Vec<_>>()
};
let idle = run(crate::gen::BN_NETWORK);
let silent = run(crate::gen::BN_NONE);
let control = |v: &[(u8, u32, u8)]| v.iter().map(|&(l, c, _)| (l, c)).collect::<Vec<_>>();
assert_eq!(control(&idle), control(&silent));
assert!(idle.iter().all(|&(_, _, b)| b == crate::gen::BN_NETWORK));
assert!(silent.iter().all(|&(_, _, b)| b == crate::gen::BN_CREDIT));
}
#[test]
fn bundles_scale_with_the_link() {
let mut g = Governor::new();
let fast = net(&mut g, 100e6, 110e6, 10).bundle;
let mut g = Governor::new();
let slow = net(&mut g, 2e6, 2e6, 10).bundle;
assert!(fast > slow);
assert!(
(MIN_BUNDLE..=MAX_BUNDLE).contains(&fast) && (MIN_BUNDLE..=MAX_BUNDLE).contains(&slow)
);
}
fn mixing(penalty: f64) -> Decision {
let mut g = Governor::new();
let mut d = g.tick(&Sample::default());
let (mut small_left, mut large_left) = (400e6, 4e9);
for _ in 0..60 {
let (sr, lr) = match d.mode {
Mode::Mixed => (20e6 * penalty, 80e6 * penalty),
Mode::StreamOnly => (0.0, 80e6),
Mode::BundleOnly => (20e6, 0.0),
};
small_left -= sr;
large_left -= lr;
d = g.tick(&Sample {
secs: 1.0,
bytes_acked: (sr + lr) as u64,
lanes: d.lanes,
small_durable: sr as u64,
large_durable: lr as u64,
small_left: small_left as u64,
large_left: large_left as u64,
..Default::default()
});
}
d
}
#[test]
fn mixing_that_slows_the_disk_switches_to_sequential() {
assert!(mixing(0.4).sequential);
assert!(!mixing(1.0).sequential);
}
#[test]
fn the_inflight_cap_is_two_seconds_of_a_lane_but_at_least_a_chunk() {
assert_eq!(inflight_cap(4 << 20, 10e6), 20_000_000);
assert_eq!(inflight_cap(4 << 20, 1e6), 4 << 20);
}
#[test]
fn a_class_running_dry_during_the_probe_still_reaches_a_decision() {
let mut g = Governor::new();
let (mut small_left, mut large_left) = (100_000_000u64, 4_000_000_000u64);
for _ in 0..25 {
let sr = small_left.min(10_000_000);
let lr = large_left.min(10_000_000);
small_left -= sr;
large_left -= lr;
g.tick(&Sample {
secs: 1.0,
bytes_acked: sr + lr,
lanes: 2,
small_durable: sr,
large_durable: lr,
small_left,
large_left,
..Default::default()
});
}
assert!(
matches!(g.probe, Probe::Done),
"the probe completes instead of hanging"
);
g.tick(&Sample {
secs: 1.0,
bytes_acked: 1,
lanes: 2,
small_left: 1,
large_left: 1,
..Default::default()
});
assert!(matches!(g.probe, Probe::Done), "the probe never runs twice");
}
fn steady(rate: f64, lanes: u8) -> Sample {
Sample {
secs: 1.0,
bytes_acked: rate as u64,
lanes,
..Default::default()
}
}
fn probing(opts: GovernorOptions, before: f64, best: f64) -> Governor {
let mut g = Governor::with_options(opts);
g.lanes = 3;
g.best_rate = best;
g.step = LaneStep::Trying { before, wait: 0 };
g
}
#[test]
fn a_small_lane_gain_is_kept_while_the_link_is_below_its_best() {
let mut g = probing(GovernorOptions::default(), 80e6, 100e6);
let d = g.tick(&steady(85.6e6, 3));
assert_eq!(d.lanes, 3, "a 7 % gain below the best rate keeps the lane");
let mut old = probing(
GovernorOptions {
lanes_first: false,
..Default::default()
},
80e6,
100e6,
);
assert_eq!(
old.tick(&steady(85.6e6, 3)).lanes,
2,
"the old bar reverts it"
);
}
#[test]
fn near_the_best_rate_the_lane_must_earn_ten_percent() {
let mut g = probing(GovernorOptions::default(), 88.8e6, 100e6);
assert_eq!(g.tick(&steady(95e6, 3)).lanes, 2);
let mut g = probing(GovernorOptions::default(), 91e6, 100e6);
assert_eq!(g.tick(&steady(100e6, 3)).lanes, 2, "9.9 % < 10 %");
let mut g = probing(GovernorOptions::default(), 90e6, 100e6);
assert_eq!(g.tick(&steady(100e6, 3)).lanes, 3, "11 % keeps it");
}
#[test]
fn the_best_rate_fades_one_percent_a_tick_and_never_below_the_current_rate() {
let mut g = Governor::new();
g.best_rate = 100e6;
g.tick(&steady(50e6, 2));
assert!((g.best_rate - 99e6).abs() < 1.0, "{}", g.best_rate);
for _ in 0..80 {
g.tick(&steady(50e6, 2));
}
assert!(
g.best_rate < 50e6 * 1.001,
"decayed to the rate: {}",
g.best_rate
);
assert!(g.best_rate >= 50e6, "never below the rate");
g.tick(&steady(120e6, 2));
assert!((g.best_rate - 120e6).abs() < 1.0);
}
fn run_stable(g: &mut Governor, lanes: u8, ticks: usize) -> Decision {
let mut d = g.tick(&Sample::default());
for _ in 0..ticks {
d = g.tick(&steady(200e6, lanes));
}
d
}
#[test]
fn the_chunk_stays_at_four_mib_while_lanes_are_few_and_the_network_limits() {
let mut g = Governor::new();
g.step = LaneStep::Hold(1_000);
let d = run_stable(&mut g, 2, 60);
assert_eq!(d.chunk, 4 << 20, "no growth past 4 MiB with 2 lanes");
}
#[test]
fn the_chunk_grows_once_lanes_stop_helping_or_reach_four() {
let mut g = Governor::new();
g.lanes = 4;
g.step = LaneStep::Hold(1_000);
assert!(run_stable(&mut g, 4, 40).chunk > 4 << 20);
let mut g = Governor::new();
g.step = LaneStep::Hold(1_000);
g.lanes_capped = true;
assert!(run_stable(&mut g, 2, 40).chunk > 4 << 20);
let mut g = Governor::new();
g.step = LaneStep::Hold(1_000);
let mut d = g.tick(&Sample::default());
for _ in 0..40 {
d = g.tick(&Sample {
credit_starved: true,
receiver_bottleneck: crate::gen::BN_DISK,
..steady(200e6, 2)
});
}
assert!(d.chunk > 4 << 20);
}
#[test]
fn with_lanes_first_off_the_chunk_grows_as_before() {
let mut g = Governor::with_options(GovernorOptions {
lanes_first: false,
..Default::default()
});
g.step = LaneStep::Hold(1_000);
assert!(run_stable(&mut g, 2, 40).chunk > 4 << 20);
}
#[test]
fn a_failed_probe_marks_lanes_as_not_helping() {
let mut g = probing(GovernorOptions::default(), 100e6, 100e6);
assert!(!g.lanes_capped);
g.tick(&steady(100e6, 3)); assert!(g.lanes_capped);
}
#[test]
fn pinned_lanes_and_chunk_are_honoured_through_everything() {
let opts = GovernorOptions {
pin_lanes: Some(4),
pin_chunk: Some(8 << 20),
lanes_first: true,
};
let mut g = Governor::with_options(opts);
let first = g.tick(&Sample::default());
assert_eq!((first.lanes, first.chunk), (4, 8 << 20));
for i in 0..80 {
let d = g.tick(&Sample {
secs: 1.0,
bytes_acked: 1_000_000,
lanes: 4,
stalls: u32::from(i % 7 == 0),
credit_starved: i % 3 == 0,
..Default::default()
});
assert_eq!((d.lanes, d.chunk), (4, 8 << 20), "tick {i}");
}
let d = net(&mut Governor::with_options(opts), 100e6, 110e6, 50);
assert_eq!((d.lanes, d.chunk), (4, 8 << 20));
}
#[test]
fn a_lane_pin_below_four_lanes_does_not_hold_the_chunk_at_four_mib() {
let mut g = Governor::with_options(GovernorOptions {
pin_lanes: Some(2),
..Default::default()
});
let d = run_stable(&mut g, 2, 60);
assert_eq!(d.lanes, 2);
assert!(d.chunk > 4 << 20, "chunk {}", d.chunk);
}
#[test]
fn a_lane_pin_alone_leaves_the_chunk_to_the_governor() {
let mut g = Governor::with_options(GovernorOptions {
pin_lanes: Some(3),
..Default::default()
});
let d = net(&mut g, 100e6, 300e6, 40);
assert_eq!(d.lanes, 3);
assert!(d.chunk >= MIN_CHUNK);
}
#[test]
fn the_env_knobs_parse_and_ignore_nonsense() {
let o = GovernorOptions::from_vars(Some("6"), Some("4"), None);
assert_eq!(
(o.pin_lanes, o.pin_chunk, o.lanes_first),
(Some(6), Some(4 << 20), true)
);
let o = GovernorOptions::from_vars(Some("0"), Some("16"), Some("0"));
assert_eq!(
(o.pin_lanes, o.pin_chunk, o.lanes_first),
(None, None, false)
);
let o = GovernorOptions::from_vars(Some("9"), Some("0"), None);
assert_eq!((o.pin_lanes, o.pin_chunk), (None, None));
let o = GovernorOptions::from_vars(Some(" 2 "), Some("15"), None);
assert_eq!((o.pin_lanes, o.pin_chunk), (Some(2), Some(15 << 20)));
let o = GovernorOptions::from_vars(Some("x"), Some(""), None);
assert_eq!(o, GovernorOptions::default());
}
#[test]
fn the_job_summary_says_where_the_time_went() {
let mut j = JobSummary::default();
assert_eq!(j.line(), None, "no ticks, no line");
let d = |chunk| Decision {
lanes: 4,
chunk,
bundle: 0,
bottleneck: crate::gen::BN_NETWORK,
mode: Mode::Mixed,
prefer: Class::Stream,
sequential: false,
};
let ticks = [
(false, false, 0, 2u8, 4u32 << 20),
(true, false, crate::gen::BN_DISK, 4, 4 << 20),
(true, false, crate::gen::BN_NONE, 4, 8 << 20),
(false, true, crate::gen::BN_DISK, 6, 8 << 20),
];
for (credit, source, rbn, lanes, chunk) in ticks {
j.observe(
&Sample {
secs: 1.0,
lanes,
credit_starved: credit,
source_starved: source,
receiver_bottleneck: rbn,
..Default::default()
},
&d(chunk),
);
}
let line = j.line().unwrap();
assert!(line.contains("over 4 ticks"), "{line}");
assert!(line.contains("credit-starved 50%"), "{line}");
assert!(line.contains("source-starved 25%"), "{line}");
assert!(
line.contains("receiver-bound 25%"),
"only the disk tick that was starved: {line}"
);
assert!(line.contains("receiver reported disk"), "{line}");
assert!(line.contains("avg lanes 4.0"), "{line}");
assert!(line.contains("avg chunk 6.0 MiB"), "{line}");
}
fn tick_of(j: &mut JobSummary, lanes: u8, chunk: u32, sequential: bool) {
j.observe(
&Sample {
secs: 1.0,
lanes,
..Default::default()
},
&Decision {
lanes,
chunk,
bundle: 0,
bottleneck: crate::gen::BN_NETWORK,
mode: Mode::Mixed,
prefer: Class::Stream,
sequential,
},
);
}
#[test]
fn the_history_is_thinned_to_a_bounded_series_and_keeps_the_first_tick() {
let mut j = JobSummary::default();
for i in 0..1000u32 {
tick_of(&mut j, 4, (4 + i % 3) << 20, false);
}
assert!(j.history.len() <= HISTORY_MAX, "{}", j.history.len());
assert!(j.history.len() > HISTORY_MAX / 4, "{}", j.history.len());
assert_eq!(j.history[0], (1, 4, 4096), "the first tick is kept");
let ticks: Vec<u32> = j.history.iter().map(|h| h.0).collect();
assert!(ticks.windows(2).all(|w| w[0] < w[1]), "ordered: {ticks:?}");
assert_eq!(j.ticks, 1000);
}
#[test]
fn sequential_ticks_record_the_slow_drive_switch() {
let mut j = JobSummary::default();
tick_of(&mut j, 2, 4 << 20, false);
tick_of(&mut j, 2, 4 << 20, true);
tick_of(&mut j, 2, 4 << 20, true);
assert_eq!(j.sequential_ticks, 2);
}
#[test]
fn merging_attempts_adds_the_counts_and_offsets_the_history() {
let mut a = JobSummary::default();
let mut b = JobSummary::default();
tick_of(&mut a, 2, 4 << 20, false);
tick_of(&mut b, 6, 8 << 20, true);
tick_of(&mut b, 6, 8 << 20, true);
a.merge(&b);
assert_eq!((a.ticks, a.sequential_ticks), (3, 2));
assert_eq!(a.lanes_sum, 2 + 6 + 6);
assert_eq!(a.history.iter().map(|h| h.0).collect::<Vec<_>>(), [1, 2, 3]);
}
}