use super::{quietly, run_planned, splitmix, Outcome};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::future::Future;
pub const DEFAULT_FAIR_AFTER: u64 = 20_000;
pub const DEFAULT_PCT_STEPS: u64 = 100;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Strategy {
Random,
Pct {
depth: u32,
steps: Option<u64>,
fair_after: Option<u64>,
},
Dfs {
bound: u32,
depth: Option<u64>,
fair_after: Option<u64>,
},
}
impl Strategy {
pub fn parse(text: &str) -> Result<Self, String> {
let text = text.trim();
let (name, args) = text.split_once(':').unwrap_or((text, ""));
let mut options = BTreeMap::new();
for arg in args.split(',').map(str::trim).filter(|a| !a.is_empty()) {
let (key, value) = arg
.split_once('=')
.ok_or_else(|| format!("strategy {text:?}: expected key=value, got {arg:?}"))?;
let value: u64 = value
.trim()
.parse()
.map_err(|e| format!("strategy {text:?}: {key}: {e}"))?;
options.insert(key.trim().to_string(), value);
}
let mut take = |keys: &[&str]| keys.iter().find_map(|k| options.remove(*k));
let strategy = match name {
"random" => Strategy::Random,
"pct" => Strategy::Pct {
depth: take(&["d", "depth"]).unwrap_or(3).max(1) as u32,
steps: take(&["k", "steps"]),
fair_after: take(&["fair", "fair_after"]),
},
"dfs" => Strategy::Dfs {
bound: take(&["bound", "b", "delays"]).unwrap_or(2) as u32,
depth: take(&["depth"]),
fair_after: take(&["fair", "fair_after"]),
},
_ => return Err(format!("unknown strategy {name:?} (random, pct, dfs)")),
};
match options.keys().next() {
Some(key) => Err(format!("strategy {text:?}: unknown option {key:?}")),
None => Ok(strategy),
}
}
}
impl fmt::Display for Strategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Strategy::Random => f.write_str("random"),
Strategy::Pct {
depth,
steps,
fair_after,
} => {
write!(f, "pct:d={depth}")?;
if let Some(k) = steps {
write!(f, ",k={k}")?;
}
if let Some(fair) = fair_after {
write!(f, ",fair={fair}")?;
}
Ok(())
}
Strategy::Dfs {
bound,
depth,
fair_after,
} => {
write!(f, "dfs:bound={bound}")?;
if let Some(depth) = depth {
write!(f, ",depth={depth}")?;
}
if let Some(fair) = fair_after {
write!(f, ",fair={fair}")?;
}
Ok(())
}
}
}
}
impl Strategy {
pub fn fair_after(&self) -> u64 {
match self {
Strategy::Pct {
fair_after: Some(fair),
..
}
| Strategy::Dfs {
fair_after: Some(fair),
..
} => *fair,
_ => default_fair_after(),
}
}
fn resolved(&self, estimate: u64) -> Strategy {
let fair_after = Some(self.fair_after());
match *self {
Strategy::Random => Strategy::Random,
Strategy::Pct { depth, .. } => Strategy::Pct {
depth,
steps: Some(estimate),
fair_after,
},
Strategy::Dfs { bound, depth, .. } => Strategy::Dfs {
bound,
depth,
fair_after,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Choice {
pub ready: u32,
pub delays: u32,
pub choice: bool,
pub set: u64,
}
fn set_hash(ready: &[u64]) -> u64 {
let mut ids = ready.to_vec();
ids.sort_unstable();
ids.iter().fold(0xcbf2_9ce4_8422_2325, |h, id| {
(h ^ id).wrapping_mul(0x0100_0000_01b3)
})
}
#[derive(Debug)]
enum Kind {
Pct {
change_points: Vec<u64>,
},
Delays {
prefix: Vec<u32>,
depth: u64,
},
}
#[derive(Debug)]
pub(super) struct Plan {
kind: Kind,
rng: u64,
priority: BTreeMap<u64, i64>,
last_polled: BTreeMap<u64, u64>,
step: u64,
fair_after: u64,
demotions: i64,
choices: Vec<Choice>,
}
fn default_fair_after() -> u64 {
std::env::var("SPAWNED_SIM_FAIR_AFTER")
.ok()
.and_then(|v| v.trim().parse().ok())
.unwrap_or(DEFAULT_FAIR_AFTER)
}
impl Plan {
fn new(kind: Kind, rng: u64, fair_after: u64) -> Self {
Self {
kind,
rng,
priority: BTreeMap::new(),
last_polled: BTreeMap::new(),
step: 0,
fair_after,
demotions: 0,
choices: Vec::new(),
}
}
fn pct(seed: u64, depth: u32, steps: u64, fair_after: u64) -> Self {
let mut rng = seed ^ 0x5043_545f_5043_545f;
let mut points = Vec::new();
let wanted = (depth.saturating_sub(1) as u64).min(steps.max(1));
while (points.len() as u64) < wanted {
let point = splitmix(&mut rng) % steps.max(1);
if !points.contains(&point) {
points.push(point);
}
}
Self::new(
Kind::Pct {
change_points: points,
},
rng,
fair_after,
)
}
fn delays(prefix: Vec<u32>, depth: u64, fair_after: u64) -> Self {
Self::new(Kind::Delays { prefix, depth }, 0, fair_after)
}
fn order(&self, ready: &[u64]) -> Vec<usize> {
let mut order: Vec<usize> = (0..ready.len()).collect();
order.sort_by_key(|&i| (std::cmp::Reverse(self.priority[&ready[i]]), ready[i]));
order
}
pub(super) fn pick(&mut self, ready: &[u64]) -> usize {
let step = self.step;
self.step += 1;
let mut fresh: Vec<u64> = ready
.iter()
.copied()
.filter(|id| !self.priority.contains_key(id))
.collect();
fresh.sort_unstable();
for id in fresh {
let priority = match &self.kind {
Kind::Pct { change_points } => {
change_points.len() as i64 + 1 + (splitmix(&mut self.rng) >> 33) as i64
}
Kind::Delays { .. } => 0,
};
self.priority.insert(id, priority);
}
let set = set_hash(ready);
let (index, delays, choice) = if step >= self.fair_after {
let index = (0..ready.len())
.min_by_key(|&i| (self.last_polled.get(&ready[i]).copied(), ready[i]))
.unwrap_or(0);
(index, 0, false)
} else {
match &self.kind {
Kind::Pct { change_points } => {
let index = self.order(ready)[0];
let mut delays = 0;
if let Some(i) = change_points.iter().position(|p| *p == step) {
self.priority.insert(ready[index], i as i64 + 1);
delays = 1;
}
(index, delays, true)
}
Kind::Delays { prefix, depth } => {
let choice = step < *depth;
let wanted = if choice {
prefix.get(step as usize).copied().unwrap_or(0)
} else {
0
};
let wanted = wanted.min(ready.len() as u32 - 1);
let mut order = self.order(ready);
for _ in 0..wanted {
self.demotions += 1;
self.priority.insert(ready[order[0]], -self.demotions);
order = self.order(ready);
}
(order[0], wanted, choice)
}
}
};
self.choices.push(Choice {
ready: ready.len() as u32,
delays,
choice,
set,
});
self.last_polled.insert(ready[index], step);
index
}
}
#[derive(Debug, Clone)]
pub struct Found {
pub run: usize,
pub seed: u64,
pub strategy: Strategy,
pub delays: Option<Vec<u32>>,
pub outcome: Outcome,
}
#[derive(Debug, Clone)]
pub struct Report {
pub strategy: Strategy,
pub runs: usize,
pub failures: usize,
pub schedules: usize,
pub max_polls: u64,
pub exhausted: bool,
pub truncated: bool,
pub divergences: usize,
pub found: Option<Found>,
}
impl Report {
pub fn complete(&self) -> bool {
self.exhausted && !self.truncated && self.divergences == 0
}
}
pub fn explore<F, Fut>(strategy: &Strategy, first_seed: u64, runs: usize, body: &F) -> Report
where
F: Fn(u64) -> Fut,
Fut: Future<Output = ()>,
{
run_strategy(strategy, first_seed, runs, true, body)
}
pub fn sweep<F, Fut>(strategy: &Strategy, first_seed: u64, runs: usize, body: &F) -> Report
where
F: Fn(u64) -> Fut,
Fut: Future<Output = ()>,
{
run_strategy(strategy, first_seed, runs, false, body)
}
fn run_strategy<F, Fut>(
strategy: &Strategy,
first_seed: u64,
runs: usize,
stop: bool,
body: &F,
) -> Report
where
F: Fn(u64) -> Fut,
Fut: Future<Output = ()>,
{
let mut report = Report {
strategy: strategy.clone(),
runs: 0,
failures: 0,
schedules: 0,
max_polls: 0,
exhausted: false,
truncated: false,
divergences: 0,
found: None,
};
let mut hashes = BTreeSet::new();
let mut estimate = match strategy {
Strategy::Pct { steps, .. } => steps.unwrap_or(DEFAULT_PCT_STEPS),
_ => 0,
};
let mut prefix: Vec<u32> = Vec::new();
let mut parent: Vec<Choice> = Vec::new();
while report.runs < runs {
let seed = match strategy {
Strategy::Dfs { .. } => first_seed,
_ => first_seed.wrapping_add(report.runs as u64),
};
let fair_after = strategy.fair_after();
let replayed = strategy.resolved(estimate);
let plan = match strategy {
Strategy::Random => None,
Strategy::Pct { depth, .. } => Some(Plan::pct(seed, *depth, estimate, fair_after)),
Strategy::Dfs { depth, .. } => Some(Plan::delays(
prefix.clone(),
depth.unwrap_or(u64::MAX),
fair_after,
)),
};
let (outcome, plan) = quietly(|| run_planned(seed, None, plan, body));
report.runs += 1;
hashes.insert(outcome.stats.schedule_hash);
report.schedules = hashes.len();
report.max_polls = report.max_polls.max(outcome.stats.steps);
let choices = plan.map(|p| p.choices).unwrap_or_default();
report.truncated |= choices.iter().any(|c| !c.choice && c.ready > 1);
let diverged = choices.len() < prefix.len()
|| prefix.iter().enumerate().any(|(i, &delays)| {
let (c, p) = (choices[i], parent[i]);
c.delays != delays || c.ready != p.ready || c.set != p.set || c.choice != p.choice
});
if diverged {
report.divergences += 1;
}
if outcome.failure.is_some() {
report.failures += 1;
if report.found.is_none() {
report.found = Some(Found {
run: report.runs,
seed,
strategy: replayed,
delays: matches!(strategy, Strategy::Dfs { .. })
.then(|| choices.iter().map(|c| c.delays).collect()),
outcome,
});
}
if stop {
break;
}
}
match strategy {
Strategy::Random => {}
Strategy::Pct { steps, .. } => {
if steps.is_none() {
estimate = report.max_polls.max(1);
}
}
Strategy::Dfs { .. } if diverged => break,
Strategy::Dfs { bound, .. } => match next_prefix(&choices, *bound) {
Some(next) => {
prefix = next;
parent = choices;
}
None => {
report.exhausted = true;
break;
}
},
}
}
report
}
fn next_prefix(choices: &[Choice], bound: u32) -> Option<Vec<u32>> {
let mut used: Vec<u32> = Vec::with_capacity(choices.len() + 1);
used.push(0);
for c in choices {
used.push(used.last().unwrap() + c.delays);
}
(0..choices.len()).rev().find_map(|i| {
let c = choices[i];
(c.choice && c.delays + 1 < c.ready && used[i] + c.delays < bound).then(|| {
let mut next: Vec<u32> = choices[..i].iter().map(|c| c.delays).collect();
next.push(c.delays + 1);
next
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strategies_parse_and_print() {
for (text, printed) in [
("random", "random"),
("pct", "pct:d=3"),
("pct:d=2,k=50", "pct:d=2,k=50"),
("pct:d=2,k=50,fair=9", "pct:d=2,k=50,fair=9"),
("dfs", "dfs:bound=2"),
("dfs:bound=3,depth=40", "dfs:bound=3,depth=40"),
("dfs:bound=1,fair=1", "dfs:bound=1,fair=1"),
] {
let strategy = Strategy::parse(text).unwrap();
assert_eq!(strategy.to_string(), printed);
assert_eq!(Strategy::parse(printed).unwrap(), strategy);
}
assert!(Strategy::parse("pct:x=1").is_err());
assert!(Strategy::parse("bfs").is_err());
}
fn permutations(n: usize) -> Vec<Vec<usize>> {
if n == 0 {
return vec![Vec::new()];
}
let mut out = Vec::new();
for rest in permutations(n - 1) {
for at in 0..=rest.len() {
let mut p = rest.clone();
p.insert(at, n - 1);
out.push(p);
}
}
out
}
fn tuples(len: usize, k: u64) -> Vec<Vec<u64>> {
if len == 0 {
return vec![Vec::new()];
}
let mut out = Vec::new();
for rest in tuples(len - 1, k) {
for v in 0..k {
if !rest.contains(&v) {
let mut t = rest.clone();
t.push(v);
out.push(t);
}
}
}
out
}
const TASKS: usize = 3;
const STEPS: usize = 3;
type Needs = [[&'static [(usize, usize)]; STEPS]; TASKS];
const FREE: Needs = [[&[]; STEPS]; TASKS];
const BLOCKING: Needs = [
[&[], &[], &[(2, 2)]],
[&[], &[(0, 2)], &[]],
[&[(1, 1)], &[], &[]],
];
fn enabled(needs: &Needs, done: &[usize; TASKS]) -> Vec<usize> {
(0..TASKS)
.filter(|&t| done[t] < STEPS && needs[t][done[t]].iter().all(|&(o, n)| done[o] >= n))
.collect()
}
fn paper_schedule(needs: &Needs, initial: &[i64], change_points: &[u64]) -> String {
let mut priority = initial.to_vec();
let mut done = [0; TASKS];
let mut out = String::new();
let mut step = 0u64;
loop {
let ready = enabled(needs, &done);
let Some(&t) = ready.iter().max_by_key(|&&t| priority[t]) else {
break;
};
done[t] += 1;
out.push((b'A' + t as u8) as char);
if let Some(i) = change_points.iter().position(|&k| k == step) {
priority[t] = i as i64 + 1;
}
step += 1;
}
out
}
fn plan_schedule(needs: &Needs, initial: &[i64], change_points: &[u64]) -> String {
let mut plan = Plan::new(
Kind::Pct {
change_points: change_points.to_vec(),
},
0,
u64::MAX,
);
for (t, &p) in initial.iter().enumerate() {
plan.priority.insert(t as u64 + 1, p);
}
let mut done = [0; TASKS];
let mut out = String::new();
loop {
let ready: Vec<u64> = enabled(needs, &done)
.into_iter()
.map(|t| t as u64 + 1)
.collect();
if ready.is_empty() {
break;
}
let t = ready[plan.pick(&ready)] as usize - 1;
done[t] += 1;
out.push((b'A' + t as u8) as char);
}
out
}
fn reachable(needs: &Needs) -> (BTreeSet<String>, BTreeSet<String>) {
let depth = 3;
let k = (TASKS * STEPS) as u64;
let (mut paper, mut ours) = (BTreeSet::new(), BTreeSet::new());
for order in permutations(TASKS) {
let initial: Vec<i64> = order.iter().map(|&r| depth + r as i64).collect();
for points in tuples(depth as usize - 1, k) {
let expected = paper_schedule(needs, &initial, &points);
let got = plan_schedule(needs, &initial, &points);
assert_eq!(
got, expected,
"priorities {initial:?}, change points {points:?}"
);
paper.insert(expected);
ours.insert(got);
}
}
(paper, ours)
}
#[test]
fn pct_matches_the_papers_algorithm() {
let (paper, ours) = reachable(&FREE);
assert_eq!(ours, paper);
assert!(ours.contains("AAABBCBCC"), "{} schedules", ours.len());
assert_eq!(ours.len(), 126);
let (paper, ours) = reachable(&BLOCKING);
assert_eq!(ours, paper);
assert_eq!(ours.len(), 55);
let unsorted = (0..200u64).any(|seed| match Plan::pct(seed, 3, 9, u64::MAX).kind {
Kind::Pct { change_points } => change_points[0] > change_points[1],
Kind::Delays { .. } => unreachable!(),
});
assert!(unsorted, "PCT change points are always drawn in time order");
}
#[test]
fn pct_priorities_do_not_depend_on_ready_order() {
for seed in 0..32 {
let mut a = Plan::pct(seed, 3, 9, u64::MAX);
let mut b = Plan::pct(seed, 3, 9, u64::MAX);
let forward = [1, 2, 3, 4];
let backward = [4, 3, 2, 1];
assert_eq!(
forward[a.pick(&forward)],
backward[b.pick(&backward)],
"seed {seed}"
);
assert_eq!(a.priority, b.priority, "seed {seed}");
}
}
#[test]
fn dfs_enumerates_each_bounded_schedule_once() {
let ready = [2, 3, 1];
let mut prefix = Vec::new();
let mut seen = BTreeSet::new();
loop {
let choices: Vec<Choice> = ready
.iter()
.enumerate()
.map(|(i, &r)| Choice {
ready: r,
delays: prefix.get(i).copied().unwrap_or(0),
choice: true,
set: i as u64,
})
.collect();
let schedule: Vec<u32> = choices.iter().map(|c| c.delays).collect();
assert!(schedule.iter().sum::<u32>() <= 2);
assert!(seen.insert(schedule), "visited twice");
match next_prefix(&choices, 2) {
Some(next) => prefix = next,
None => break,
}
}
assert_eq!(seen.len(), 5);
}
}