use vj_abi::SimTime;
use super::instance::StateEngine;
use super::write::VcRecord;
impl StateEngine {
pub fn advance_gvt(&mut self, gvt: SimTime) {
self.vc.set_gvt_watermark(gvt);
}
pub fn drain_committed_changes(&mut self, up_to: SimTime) -> Vec<VcRecord> {
self.vc.drain_committed_changes(up_to)
}
pub fn set_change_log(&mut self, on: bool) {
self.vc.set_record_changes(on);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::instance::TemplateRegistry;
use crate::state::write::SlotLocation;
use crate::state::{
EdgeKind, FourStateValue, ListenerRole, SignalAccess, SlotClass, StateCellId,
ValueChangeSink,
};
use std::sync::{Arc, Mutex};
use vj_abi::Region;
fn fresh_engine() -> StateEngine {
StateEngine::new(TemplateRegistry::new())
}
fn inline_cell(engine: &mut StateEngine) -> StateCellId {
let word_offset = unsafe { engine.arena.alloc_inline_word() };
let loc = SlotLocation {
class: SlotClass::Inline,
word_offset,
bit_width: 8,
elem_word_count: 1,
row_stride_words: 0,
num_rows: 0,
};
engine.alloc_cell(loc)
}
fn narrow_word(val: u64, unk: u64) -> u128 {
(val as u128) | ((unk as u128) << 64)
}
fn t(units: i64) -> SimTime {
SimTime {
sim_units: units,
delta: 0,
}
}
struct SpySink {
log: Arc<Mutex<Vec<(StateCellId, SimTime)>>>,
}
impl ValueChangeSink for SpySink {
fn notify_change(
&mut self,
cell: StateCellId,
_edge: EdgeKind,
at: SimTime,
_region: Region,
) {
self.log.lock().unwrap().push((cell, at));
}
}
#[test]
fn scheduler_listener_fires_immediately_without_waiting_for_advance_gvt() {
let mut engine = fresh_engine();
let cell = inline_cell(&mut engine);
let log = Arc::new(Mutex::new(Vec::new()));
engine.subscribe(
cell,
ListenerRole::Scheduler,
Box::new(SpySink { log: log.clone() }),
);
let result = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
assert!(result.changed);
let calls = log.lock().unwrap();
assert_eq!(
calls.len(),
1,
"the Scheduler listener must fire immediately on commit_write, with no advance_gvt call anywhere in this test"
);
assert_eq!(calls[0].0, cell);
}
#[test]
fn a_disabled_change_log_records_nothing() {
let mut engine = fresh_engine();
let cell = inline_cell(&mut engine);
engine.set_change_log(false);
engine.set_current_time(t(1));
let r = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
assert!(r.changed, "the write itself still lands");
assert!(engine.drain_committed_changes(t(100)).is_empty());
engine.set_change_log(true);
let r = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(6, 0)), &mut ());
assert!(r.changed);
assert_eq!(engine.drain_committed_changes(t(100)).len(), 1);
}
#[test]
fn drain_committed_changes_excludes_records_after_the_watermark() {
let mut engine = fresh_engine();
let cell_a = inline_cell(&mut engine);
let cell_b = inline_cell(&mut engine);
engine.set_current_time(t(5));
let ra = engine.commit_write(cell_a, FourStateValue::Narrow(narrow_word(1, 0)), &mut ());
assert!(ra.changed);
engine.set_current_time(t(10));
let rb = engine.commit_write(cell_b, FourStateValue::Narrow(narrow_word(1, 0)), &mut ());
assert!(rb.changed);
engine.advance_gvt(t(7));
let drained = engine.drain_committed_changes(t(7));
assert_eq!(
drained.len(),
1,
"only cell_a's at=5 record is <= the up_to=7 watermark"
);
assert_eq!(drained[0].cell, cell_a);
assert_eq!(drained[0].at, t(5));
let drained_again = engine.drain_committed_changes(t(7));
assert!(
drained_again.is_empty(),
"a second drain with the same up_to must not re-return an already-drained record"
);
engine.advance_gvt(t(10));
let drained_b = engine.drain_committed_changes(t(10));
assert_eq!(
drained_b.len(),
1,
"cell_b's at=10 record becomes visible once up_to reaches it"
);
assert_eq!(drained_b[0].cell, cell_b);
assert_eq!(drained_b[0].at, t(10));
}
#[test]
fn advance_gvt_has_no_dependency_on_scheduler_internal_state() {
let mut engine = fresh_engine();
engine.advance_gvt(t(42));
assert_eq!(
engine.vc.gvt_watermark(),
t(42),
"advance_gvt must actually store the watermark it was given"
);
}
#[test]
fn display_style_observation_never_sees_a_pre_gvt_value_even_if_committed_later() {
let mut engine = fresh_engine();
let cell = inline_cell(&mut engine);
engine.set_current_time(t(5));
let result = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(9, 0)), &mut ());
assert!(result.changed);
match engine.read(cell) {
FourStateValue::Narrow(w) => assert_eq!(
w,
narrow_word(9, 0),
"a direct SignalAccess::read already sees the new value -- that's fine, it's not the commit-stream path"
),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
let too_early = engine.drain_committed_changes(t(3));
assert!(
too_early.is_empty(),
"drain_committed_changes must reflect nothing past its own watermark, even though read() already shows the new value"
);
}
#[test]
fn drain_committed_changes_sees_records_appended_from_a_different_real_os_thread() {
let mut engine = fresh_engine();
let cell_a = inline_cell(&mut engine);
let cell_b = inline_cell(&mut engine);
struct SendPtr(*mut StateEngine);
unsafe impl Send for SendPtr {}
let ptr1 = SendPtr(&mut engine as *mut StateEngine);
let t1 = std::thread::spawn(move || {
let ptr1 = ptr1;
let engine_ref = unsafe { &mut *ptr1.0 };
engine_ref.set_current_time(t(1));
engine_ref.commit_write(cell_a, FourStateValue::Narrow(narrow_word(1, 0)), &mut ());
});
t1.join().unwrap();
let ptr2 = SendPtr(&mut engine as *mut StateEngine);
let t2 = std::thread::spawn(move || {
let ptr2 = ptr2;
let engine_ref = unsafe { &mut *ptr2.0 };
engine_ref.set_current_time(t(2));
engine_ref.commit_write(cell_b, FourStateValue::Narrow(narrow_word(1, 0)), &mut ());
});
t2.join().unwrap();
engine.advance_gvt(t(5));
let drained = engine.drain_committed_changes(t(5));
let cells: Vec<StateCellId> = drained.iter().map(|r| r.cell).collect();
assert!(
cells.contains(&cell_a),
"T1's committed change must be visible from the draining thread"
);
assert!(
cells.contains(&cell_b),
"T2's committed change must be visible from the draining thread"
);
assert_eq!(
drained.len(),
2,
"both worker threads' buffers must be drained, not just one"
);
}
}