use vj_abi::SimHandle;
use super::{ProcessId, ProcessTable, ResumeDriver, SchedulerOps};
#[derive(Clone, Copy, Debug)]
pub struct FinalProcess {
pub proc: ProcessId,
pub registration_order: u64,
}
pub fn run_finals(
finals: &mut [FinalProcess],
table: &ProcessTable,
state: SimHandle,
ops: &dyn SchedulerOps,
driver: &ResumeDriver,
) {
finals.sort_by_key(|f| f.registration_order);
for f in finals.iter() {
super::fire_thread_event(
ops,
super::ThreadEvent::Start {
pid: f.proc,
parent: None,
spawn: None,
},
);
driver
.start(table, f.proc, state, ops)
.expect("registered final process must be startable");
super::fire_thread_event(
ops,
super::ThreadEvent::End {
pid: f.proc,
killed: false,
},
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sched::{
CompiledProcess, PendingActivation, RegionQueues, ResumeArg, SchedTime, Suspend,
form_ready_set,
};
use crate::state::{FourStateValue, StateCellId};
use std::sync::{Arc, Mutex};
use vj_abi::Region;
struct NoopOps;
impl SchedulerOps for NoopOps {
fn now(&self) -> SchedTime {
SchedTime { ticks: 0 }
}
fn current_region(&self) -> Region {
Region::Active
}
fn stage_nba(&self, _cell: StateCellId, _value: FourStateValue, _source: ProcessId) {}
fn commit_blocking_write(&self, _cell: StateCellId, _value: FourStateValue) {}
fn request_finish(&self, _code: i32) {}
fn defer_io(&self, _at: SchedTime, _region: Region, _cb: super::super::DeferredIoCallback) {
}
fn register_region_callback(
&self,
_region: Region,
_at: SchedTime,
_cb: Box<dyn super::super::RegionCallback>,
) {
}
}
fn null_state() -> SimHandle {
SimHandle(std::ptr::null_mut())
}
struct LoggingProcess {
tag: u64,
log: Arc<Mutex<Vec<(u64, bool)>>>,
}
impl CompiledProcess for LoggingProcess {
fn resume(
&mut self,
_state: SimHandle,
arg: ResumeArg,
_ops: &dyn SchedulerOps,
) -> Suspend {
self.log
.lock()
.unwrap()
.push((self.tag, matches!(arg, ResumeArg::Start)));
Suspend::Done
}
}
fn act(
id: u64,
proc: ProcessId,
ticks: u64,
reg_order: u64,
arg: ResumeArg,
) -> PendingActivation {
PendingActivation {
id,
proc,
arg,
reconv_iter: 0,
logical_order: (SchedTime { ticks }, Region::Active, reg_order),
}
}
#[test]
fn finals_do_not_run_during_the_timestep_loop() {
let final_log = Arc::new(Mutex::new(Vec::new()));
let loop_log = Arc::new(Mutex::new(Vec::new()));
let mut table = ProcessTable::new();
let driver = ResumeDriver;
let ops = NoopOps;
let mut finals = Vec::new();
for tag in 0..3u64 {
let proc = table.register(Box::new(LoggingProcess {
tag,
log: Arc::clone(&final_log),
}));
finals.push(FinalProcess {
proc,
registration_order: tag,
});
}
let loop_proc_a = table.register(Box::new(LoggingProcess {
tag: 100,
log: Arc::clone(&loop_log),
}));
let loop_proc_b = table.register(Box::new(LoggingProcess {
tag: 101,
log: Arc::clone(&loop_log),
}));
let mut queues = RegionQueues::new();
queues
.push(Region::Active, act(0, loop_proc_a, 0, 0, ResumeArg::Start))
.unwrap();
queues
.push(Region::Active, act(1, loop_proc_b, 0, 1, ResumeArg::Start))
.unwrap();
let ready = form_ready_set(&mut queues, Region::Active).unwrap();
assert_eq!(ready.len(), 2);
for a in &ready {
driver.start(&table, a.proc, null_state(), &ops).unwrap();
}
assert!(
final_log.lock().unwrap().is_empty(),
"final processes must not have been touched by cycle 1 of the \
ordinary timestep loop"
);
queues
.push(
Region::Active,
act(2, loop_proc_a, 5, 0, ResumeArg::TimeElapsed),
)
.unwrap();
queues
.push(
Region::Active,
act(3, loop_proc_b, 5, 1, ResumeArg::TimeElapsed),
)
.unwrap();
let ready = form_ready_set(&mut queues, Region::Active).unwrap();
assert_eq!(ready.len(), 2);
for a in &ready {
driver
.resume_for(&table, a.proc, ResumeArg::TimeElapsed, null_state(), &ops)
.unwrap();
}
assert!(
final_log.lock().unwrap().is_empty(),
"final processes must still be untouched after 2 full \
Active-region form_ready_set/dispatch cycles that never \
mentioned them"
);
assert_eq!(
loop_log.lock().unwrap().len(),
4,
"sanity: the 2 unrelated processes really were dispatched twice each"
);
run_finals(&mut finals, &table, null_state(), &ops, &driver);
let final_log = final_log.lock().unwrap();
assert_eq!(
final_log.len(),
3,
"run_finals must start every registered final process exactly once"
);
assert!(
final_log.iter().all(|(_, is_start)| *is_start),
"every final process must be reached via resume(Start), not some \
other ResumeArg"
);
}
#[test]
fn finals_run_in_stable_registration_order_across_repeated_runs() {
fn run_once(orders_in_input_order: [u64; 4]) -> Vec<u64> {
let log = Arc::new(Mutex::new(Vec::new()));
let mut table = ProcessTable::new();
let driver = ResumeDriver;
let ops = NoopOps;
let mut finals = Vec::new();
for ®istration_order in orders_in_input_order.iter() {
let proc = table.register(Box::new(LoggingProcess {
tag: registration_order,
log: Arc::clone(&log),
}));
finals.push(FinalProcess {
proc,
registration_order,
});
}
run_finals(&mut finals, &table, null_state(), &ops, &driver);
log.lock().unwrap().iter().map(|(tag, _)| *tag).collect()
}
let run1 = run_once([3, 1, 0, 2]);
assert_eq!(
run1,
vec![0, 1, 2, 3],
"run_finals must dispatch strictly by ascending registration_order"
);
let run2 = run_once([2, 0, 3, 1]);
assert_eq!(
run2,
vec![0, 1, 2, 3],
"a second run from a differently-shuffled input must produce the \
identical dispatch order (determinism across runs)"
);
}
#[test]
fn finals_across_different_module_instances_still_use_one_global_order() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut table = ProcessTable::new();
let driver = ResumeDriver;
let ops = NoopOps;
let instance_a = [5u64, 2u64];
let instance_b = [4u64, 1u64];
let interleaved: Vec<u64> =
vec![instance_a[0], instance_b[0], instance_a[1], instance_b[1]];
let mut finals = Vec::new();
for ®istration_order in &interleaved {
let proc = table.register(Box::new(LoggingProcess {
tag: registration_order,
log: Arc::clone(&log),
}));
finals.push(FinalProcess {
proc,
registration_order,
});
}
run_finals(&mut finals, &table, null_state(), &ops, &driver);
let order: Vec<u64> = log.lock().unwrap().iter().map(|(tag, _)| *tag).collect();
assert_eq!(
order,
vec![1, 2, 4, 5],
"the merged run order must be purely numeric by registration_order \
-- no per-instance grouping may leak in"
);
}
}