use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, RwLock};
use crate::sched::ProcessId;
use crate::sched::sensitivity::SensitivityTable;
use crate::state::{EdgeKind, StateCellId};
pub struct QueuedGate {
flags: RwLock<Vec<AtomicBool>>,
sparse: Mutex<HashMap<ProcessId, bool>>,
}
const DENSE_LIMIT: ProcessId = 1 << 24;
impl QueuedGate {
pub fn new() -> Self {
Self {
flags: RwLock::new(Vec::new()),
sparse: Mutex::new(HashMap::new()),
}
}
fn with_flag<R>(&self, proc: ProcessId, f: impl FnOnce(&AtomicBool) -> R) -> R {
if proc >= DENSE_LIMIT {
let mut sparse = self.sparse.lock().expect("QueuedGate mutex poisoned");
let flag = AtomicBool::new(*sparse.get(&proc).unwrap_or(&false));
let r = f(&flag);
sparse.insert(proc, flag.into_inner());
return r;
}
let i = proc as usize;
{
let flags = self.flags.read().expect("QueuedGate lock poisoned");
if let Some(flag) = flags.get(i) {
return f(flag);
}
}
let mut flags = self.flags.write().expect("QueuedGate lock poisoned");
if flags.len() <= i {
flags.resize_with(i + 1, || AtomicBool::new(false));
}
f(&flags[i])
}
pub fn mark_queued_if_not_already(&self, proc: ProcessId) -> bool {
self.with_flag(proc, |flag| !flag.swap(true, Ordering::AcqRel))
}
pub fn reset_on_dequeue(&self, proc: ProcessId) {
self.with_flag(proc, |flag| flag.store(false, Ordering::Release));
}
}
impl Default for QueuedGate {
fn default() -> Self {
Self::new()
}
}
pub fn sensitivity_match_to_enqueue(
table: &SensitivityTable,
gate: &QueuedGate,
cell: StateCellId,
edge: EdgeKind,
) -> Vec<ProcessId> {
table
.matches(cell, edge)
.map(|(proc, _, _, _)| proc)
.filter(|&proc| gate.mark_queued_if_not_already(proc))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sched::StaticSensitivity;
const PROC_MAIN: ProcessId = 1;
#[test]
fn process_sensitive_to_two_signals_changing_same_pass_queues_once() {
let a = StateCellId(1);
let b = StateCellId(2);
let mut table = SensitivityTable::new();
table.install_static(
&StaticSensitivity {
edges: vec![(a, None)],
},
PROC_MAIN,
);
table.install_static(
&StaticSensitivity {
edges: vec![(b, None)],
},
PROC_MAIN,
);
let gate = QueuedGate::new();
let first = sensitivity_match_to_enqueue(&table, &gate, a, EdgeKind::None);
assert_eq!(
first,
vec![PROC_MAIN],
"the first match this pass must be the one that enqueues the process"
);
let second = sensitivity_match_to_enqueue(&table, &gate, b, EdgeKind::None);
assert!(
second.is_empty(),
"a second signal changing in the same pass must not re-enqueue an already-queued process"
);
}
#[test]
fn reset_on_dequeue_happens_at_dequeue_not_enqueue() {
let gate = QueuedGate::new();
assert!(
gate.mark_queued_if_not_already(PROC_MAIN),
"first mark must succeed (transitions unqueued -> queued)"
);
assert!(
!gate.mark_queued_if_not_already(PROC_MAIN),
"a repeat mark before any dequeue-reset must report already-queued"
);
gate.reset_on_dequeue(PROC_MAIN);
assert!(
gate.mark_queued_if_not_already(PROC_MAIN),
"after reset_on_dequeue, the process must be markable as queued again"
);
}
#[test]
fn retrigger_arriving_in_the_dequeue_to_reregister_gap_is_not_swallowed() {
let gate = QueuedGate::new();
assert!(gate.mark_queued_if_not_already(PROC_MAIN));
gate.reset_on_dequeue(PROC_MAIN);
assert!(
gate.mark_queued_if_not_already(PROC_MAIN),
"a retrigger landing in the dequeue-to-re-registration gap must \
still be able to re-queue the process, not be silently eaten"
);
}
#[test]
fn dedup_does_not_falsely_eat_a_second_legitimate_process() {
let cell = StateCellId(5);
const PROC_B: ProcessId = 2;
let mut table = SensitivityTable::new();
table.install_static(
&StaticSensitivity {
edges: vec![(cell, None)],
},
PROC_MAIN,
);
table.install_static(
&StaticSensitivity {
edges: vec![(cell, None)],
},
PROC_B,
);
let gate = QueuedGate::new();
let mut hits = sensitivity_match_to_enqueue(&table, &gate, cell, EdgeKind::None);
hits.sort_unstable();
assert_eq!(
hits,
vec![PROC_MAIN, PROC_B],
"both processes sensitive to the same cell must be enqueued on its change"
);
}
}