Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/cpu/jitv2/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11053,6 +11053,12 @@ mod tests {
let pages: [(&str, &[u32]); 5] =
[("simple", &simple), ("narrow", &narrow), ("fp", &fp), ("likely", &likely), ("blocks", &blocks)];

// The two compiles must see the same process-wide switches: other
// tests flip the entry check, interpreter fallback and the enable
// table while they run, each under its own lock.
let _table = crate::cpu::jitv2::opcode_support::table_stable();
let _fallback = crate::cpu::jitv2::analyzer::FALLBACK_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _entry = crate::cpu::jitv2::ENTRY_PREAMBLE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
for (name, words) in pages {
let mut page = [0u32; ENTRIES_PER_PAGE];
page[..words.len()].copy_from_slice(words);
Expand Down
88 changes: 72 additions & 16 deletions src/cpu/jitv2/jitv2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,16 +210,13 @@ pub const COMPILE_QUEUE_CAPACITY: usize = 2048;
/// waits seconds behind that compile's backlog, measured with jitcov on IP28).
pub const HOT_PROMOTE_ARRIVALS: u32 = 256;

/// The hot lane: a small second queue workers drain before the FIFO. One per
/// process, like the JIT itself. Requests hold raw page pointers, so
/// `CompileQueue::drain_pending` empties it too before any flush.
pub static HOT_QUEUE: std::sync::LazyLock<crossbeam_queue::ArrayQueue<CompileRequest>> =
std::sync::LazyLock::new(|| crossbeam_queue::ArrayQueue::new(256));
/// Requests the hot lane holds (see `CompileQueue::hot`).
const HOT_QUEUE_CAPACITY: usize = 256;

/// Put a copy of a request on the hot lane. A full lane just drops it: the
/// page's FIFO request is still queued, so nothing is lost but the head start.
pub fn push_hot_request(req: CompileRequest) {
let _ = HOT_QUEUE.push(req);
pub fn push_hot_request(hot: &crossbeam_queue::ArrayQueue<CompileRequest>, req: CompileRequest) {
let _ = hot.push(req);
}


Expand Down Expand Up @@ -2383,6 +2380,32 @@ impl Drop for Jitv2 {
}

impl Jitv2 {
/// Stop the CPUs that run on `jit` and its compile pool for good, before
/// the machine frees the memory and devices their requests point into.
/// A queued request holds its page's generation pointer, into the
/// machine's RAM, and the pool outlives the machine's own drop.
///
/// Not `CompileQueue::stop` under the lock: a worker leading a flush
/// takes that lock, and restarts the CPUs when the flush is done, so
/// stop both until neither comes back.
pub fn stop_for_drop(jit: &Mutex<Jitv2>, stop_cpus: impl Fn()) {
loop {
stop_cpus();
let workers = {
let mut g = jit.lock();
g.compile_queue.running.store(false, Ordering::SeqCst);
std::mem::take(&mut g.compile_queue.threads)
};
if workers.is_empty() {
break;
}
for w in workers {
let _ = w.join();
}
}
jit.lock().compile_queue.drain_pending_queue();
}

/// Allocate the full page pool up front, `capacity` slots, every one
/// starting unclaimed (`PhysicalCodePage::new(UNCLAIMED_PFN, null)`) —
/// see `Jitv2`'s own doc comment for why this is a single one-shot
Expand Down Expand Up @@ -2812,6 +2835,11 @@ pub struct CompileQueue {
/// `push` returning `Err` on full rather than blocking — preserves
/// §6.4's "drop on full, hot pages re-trigger" semantics exactly.
queue: Arc<crossbeam_queue::ArrayQueue<CompileRequest>>,
/// The hot lane: a small second queue the workers drain before `queue`,
/// for pages executed hard while they wait. One per pool, as `queue` is:
/// a request holds raw pointers into its own JIT's pages and its own
/// machine's memory, so another machine's worker must never take it.
hot: Arc<crossbeam_queue::ArrayQueue<CompileRequest>>,
running: Arc<AtomicBool>,
/// One `JoinHandle` per worker thread — empty when not running. Each
/// thread hands back its own `Codegen` on exit (see `worker_loop`'s
Expand Down Expand Up @@ -2897,6 +2925,7 @@ impl CompileQueue {
Self {
mips4: crate::cpu::jitv2::isa::mips4_enabled(),
queue: Arc::new(crossbeam_queue::ArrayQueue::new(COMPILE_QUEUE_CAPACITY)),
hot: Arc::new(crossbeam_queue::ArrayQueue::new(HOT_QUEUE_CAPACITY)),
running: Arc::new(AtomicBool::new(false)),
threads: Vec::new(),
cpu: Mutex::new(None),
Expand Down Expand Up @@ -2998,6 +3027,11 @@ impl CompileQueue {
self.queue.clone()
}

/// The hot lane, as [`Self::queue_handle`] gives the FIFO.
pub fn hot_handle(&self) -> Arc<crossbeam_queue::ArrayQueue<CompileRequest>> {
self.hot.clone()
}

/// Inject the CPU device handle — see the `cpu` field's doc comment for
/// why this is a separate setter rather than a constructor parameter.
/// Safe to call at any time (even while the worker is running); takes
Expand Down Expand Up @@ -3072,11 +3106,11 @@ impl CompileQueue {
/// after. `req.page` is still safely dereferenceable here — the pool
/// hasn't been cleared yet at either call site (see this function's own
/// doc comment above for exactly why that ordering is guaranteed).
fn drain_pending(queue: &crossbeam_queue::ArrayQueue<CompileRequest>) {
fn drain_pending(queue: &crossbeam_queue::ArrayQueue<CompileRequest>, hot: &crossbeam_queue::ArrayQueue<CompileRequest>) {
while let Some(req) = queue.pop() {
unsafe { (*req.page).clear_scheduled(); }
}
while let Some(req) = HOT_QUEUE.pop() {
while let Some(req) = hot.pop() {
unsafe { (*req.page).clear_scheduled(); }
}
}
Expand All @@ -3086,7 +3120,7 @@ impl CompileQueue {
/// `drain_pending`'s own doc comment for why this must run and when
/// it's safe to call (no worker concurrently popping).
pub fn drain_pending_queue(&mut self) {
Self::drain_pending(&self.queue);
Self::drain_pending(&self.queue, &self.hot);
}

/// Spawn the compile-thread pool with one freshly-reserved shared arena
Expand Down Expand Up @@ -3177,6 +3211,7 @@ impl CompileQueue {
for i in 0..self.thread_count {
let codegen = crate::cpu::jitv2::codegen::Codegen::new_with_shared_arena(shared.clone(), state.clone());
let queue = self.queue.clone();
let hot = self.hot.clone();
let running = self.running.clone();
let bus = bus.clone();
let cpu = self.cpu.lock().clone();
Expand All @@ -3190,7 +3225,7 @@ impl CompileQueue {
self.threads.push(
std::thread::Builder::new()
.name(format!("jitv2-compile-{i}"))
.spawn(move || Self::worker_loop(queue, running, bus, codegen, cpu, world, jitv2, function_count, stats, barrier, quiesce_in_progress, thread_count, worker_mips4))
.spawn(move || Self::worker_loop(queue, hot, running, bus, codegen, cpu, world, jitv2, function_count, stats, barrier, quiesce_in_progress, thread_count, worker_mips4))
.expect("jitv2-compile spawn"),
);
}
Expand Down Expand Up @@ -3363,6 +3398,7 @@ impl CompileQueue {
/// worker up front and simply left running/idle after they all exit).
fn worker_loop(
queue: Arc<crossbeam_queue::ArrayQueue<CompileRequest>>,
hot: Arc<crossbeam_queue::ArrayQueue<CompileRequest>>,
running: Arc<AtomicBool>,
bus: Arc<dyn BusDevice>,
mut codegen: crate::cpu::jitv2::codegen::Codegen,
Expand Down Expand Up @@ -3544,7 +3580,7 @@ impl CompileQueue {
// crash this was confirmed to cause). Safe to drain once
// here even though the queue is shared with every other
// (already-parked, not popping) worker.
Self::drain_pending(&queue);
Self::drain_pending(&queue, &hot);
unsafe { jit.lock().flush_from_jit_thread(codegen.function_count()); }
// Build ONE fresh shared arena and rebuild this (leader's
// own) Codegen on top of it — every other, parked worker
Expand Down Expand Up @@ -3673,8 +3709,8 @@ impl CompileQueue {
continue;
}
// The hot lane first: pages being executed right now that are
// still waiting (see `HOT_QUEUE`).
match HOT_QUEUE.pop().or_else(|| queue.pop()) {
// still waiting (see `CompileQueue::hot`).
match hot.pop().or_else(|| queue.pop()) {
Some(req) => {
// Always the deferred/non-forced path — never
// handle_request's forced-seal one. Forced sealing
Expand Down Expand Up @@ -5563,7 +5599,9 @@ mod tests {
// function with worker threads still live against `pages` — see
// the page-cross test's own comment for the confirmed
// use-after-free this guards.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
// Well under a second alone; with a test on every core the four
// workers can wait a long time for one.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
loop {
let published = pages.iter().filter(|p| p.is_runnable(0)).count();
Expand Down Expand Up @@ -5591,9 +5629,11 @@ mod tests {

#[test]
fn compile_queue_send_drops_when_full() {
// Declared before the queue so it outlives it: dropping the queue
// drains the requests, and each clears its page's scheduled flag.
let mut page = PhysicalCodePage::new(0, std::ptr::null());
let q = CompileQueue::new();
// Don't start the worker: nothing drains, so capacity fills exactly.
let mut page = PhysicalCodePage::new(0, std::ptr::null());
let stats = JitStats::default();
let mut accepted = 0;
for i in 0..(COMPILE_QUEUE_CAPACITY + 10) {
Expand Down Expand Up @@ -5627,6 +5667,22 @@ mod tests {
thinking a compile is still in flight for it forever");
}

#[test]
fn a_hot_request_stays_with_its_own_pool() {
// A request points into its own JIT's pages: another machine's
// pool, which may outlive that JIT, must never see it.
let mut page = PhysicalCodePage::new(0, std::ptr::null());
assert!(page.try_schedule_page());
let mut ours = CompileQueue::new();
let mut theirs = CompileQueue::new();
push_hot_request(&ours.hot_handle(), CompileRequest { page: &mut page as *mut PhysicalCodePage, compiled_for_fr1: true });
theirs.drain_pending_queue();
assert!(page.is_scheduled(), "another pool's flush discarded this pool's hot request");
assert_eq!(theirs.hot_handle().len(), 0);
ours.drain_pending_queue();
assert!(!page.is_scheduled());
}

#[test]
fn compile_queue_start_is_idempotent() {
let dev: Arc<dyn BusDevice> = Arc::new(FakeDevice(AtomicU64::new(0)));
Expand Down
15 changes: 11 additions & 4 deletions src/cpu/mips_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,10 @@ pub struct MipsExecutor<T: Tlb, C: CpuModel> {
/// every dispatch.
#[cfg(feature = "jitv2")]
pub jitv2_compile_queue_handle: std::sync::Arc<crossbeam_queue::ArrayQueue<crate::cpu::jitv2::CompileRequest>>,
/// The same pool's hot lane (`CompileQueue::hot_handle`), held for the
/// same reason.
#[cfg(feature = "jitv2")]
pub jitv2_hot_queue_handle: std::sync::Arc<crossbeam_queue::ArrayQueue<crate::cpu::jitv2::CompileRequest>>,
/// Cheap handle to `jitv2.lock().stats`, cloned once at construction —
/// same reasoning as `jitv2_compile_queue_handle`: `JitStats` is already
/// `Arc`-shared internally (see `Jitv2::stats`'s own doc comment), so
Expand Down Expand Up @@ -2346,6 +2350,8 @@ impl<T: Tlb, C: CpuModel> MipsExecutor<T, C> {
#[cfg(feature = "jitv2")]
let jitv2_compile_queue_handle = jitv2.compile_queue.queue_handle();
#[cfg(feature = "jitv2")]
let jitv2_hot_queue_handle = jitv2.compile_queue.hot_handle();
#[cfg(feature = "jitv2")]
let jitv2_stats = jitv2.stats.clone();

let mut executor = Self {
Expand Down Expand Up @@ -2425,6 +2431,8 @@ impl<T: Tlb, C: CpuModel> MipsExecutor<T, C> {
#[cfg(feature = "jitv2")]
jitv2_compile_queue_handle,
#[cfg(feature = "jitv2")]
jitv2_hot_queue_handle,
#[cfg(feature = "jitv2")]
jitv2_stats,
#[cfg(feature = "jitv2")]
pcp: std::ptr::null_mut(),
Expand Down Expand Up @@ -3066,6 +3074,7 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})",
{
let j = jit.lock();
self.jitv2_compile_queue_handle = j.compile_queue.queue_handle();
self.jitv2_hot_queue_handle = j.compile_queue.hot_handle();
self.jitv2_stats = j.stats.clone();
}
self.jitv2 = jit;
Expand Down Expand Up @@ -3726,7 +3735,7 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})",
// Already queued, and being executed hard while it
// waits: jump the queue (jitv2::HOT_QUEUE).
crate::pf_instant!(crate::perfetto::events::JIT_HOT_REQUEST, pc);
crate::cpu::jitv2::jitv2::push_hot_request(req);
crate::cpu::jitv2::jitv2::push_hot_request(&self.jitv2_hot_queue_handle, req);
}
break 'gate self.step_int();
}
Expand Down Expand Up @@ -10202,9 +10211,7 @@ impl<T: Tlb + Send + 'static, C: CpuModel + Send + 'static> MipsCpu<T, C> {


pub fn register_locks(&self) {
use crate::locks::register_lock_fn;
let ex = self.executor.clone();
register_lock_fn("cpu::executor", move || ex.is_locked());
crate::locks::register_mutex("cpu::executor", &self.executor);
}

fn try_lock_executor(&self) -> Result<parking_lot::MutexGuard<'_, MipsExecutor<T, C>>, String> {
Expand Down
12 changes: 6 additions & 6 deletions src/dev/hal2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1019,12 +1019,12 @@ impl Hal2 {
}

pub fn register_locks(self: &Arc<Self>) {
use crate::locks::register_lock_fn;
let me = self.clone(); register_lock_fn("hal2::state", move || me.state.is_locked());
let me = self.clone(); register_lock_fn("hal2::ca_state", move || me.ca_state.is_locked());
let me = self.clone(); register_lock_fn("hal2::cb_state", move || me.cb_state.is_locked());
let me = self.clone(); register_lock_fn("hal2::at_state", move || me.at_state.is_locked());
let me = self.clone(); register_lock_fn("hal2::ar_state", move || me.ar_state.is_locked());
use crate::locks::register_mutex_fn;
register_mutex_fn("hal2::state", self, |h| h.state.is_locked());
register_mutex_fn("hal2::ca_state", self, |h| h.ca_state.is_locked());
register_mutex_fn("hal2::cb_state", self, |h| h.cb_state.is_locked());
register_mutex_fn("hal2::at_state", self, |h| h.at_state.is_locked());
register_mutex_fn("hal2::ar_state", self, |h| h.ar_state.is_locked());
}
}

Expand Down
22 changes: 15 additions & 7 deletions src/dev/hpc3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1400,15 +1400,23 @@ impl Hpc3 {
self.scsi_dev1.as_ref()
}

/// Let go of the bus and of the interrupt wiring that loops back through
/// the SCSI channels (each channel holds the line that holds both), so a
/// dropped machine's memory can be freed. The machine is stopped.
pub fn release(&self) {
for chan in &self.pdma_channels {
let mut chan = chan.lock();
chan.sys_mem = None;
chan.irq = None;
}
}

pub fn register_locks(&self) {
use crate::locks::register_lock_fn;
let state = self.state.clone();
register_lock_fn("hpc3::state", move || state.is_locked());
let eeprom = self.eeprom.clone();
register_lock_fn("hpc3::eeprom", move || eeprom.is_locked());
use crate::locks::register_mutex;
register_mutex("hpc3::state", &self.state);
register_mutex("hpc3::eeprom", &self.eeprom);
for (i, chan) in self.pdma_channels.iter().enumerate() {
let chan = chan.clone();
register_lock_fn(format!("hpc3::pdma_channels[{}]", i), move || chan.is_locked());
register_mutex(format!("hpc3::pdma_channels[{}]", i), chan);
}
// Delegate to child components
self.seeq.register_locks();
Expand Down
8 changes: 3 additions & 5 deletions src/dev/ioc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,14 +677,12 @@ impl Ioc {
}

pub fn register_locks(&self) {
use crate::locks::register_lock_fn;
let s = self.state.clone();
register_lock_fn("ioc::state", move || s.is_locked());
use crate::locks::{register_mutex, register_mutex_fn};
register_mutex("ioc::state", &self.state);
// SCC (Z85c30) channels
self.scc.register_locks();
// PS/2
let ps2 = self.ps2.clone();
register_lock_fn("ps2::state", move || ps2.is_state_locked());
register_mutex_fn("ps2::state", &self.ps2, |p| p.is_state_locked());
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/dev/ioc3_eth/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,9 @@ fn a_frame_waits_for_the_negotiation_deadline_without_a_wake() {
}

fn waits_for(cond: impl Fn() -> bool) -> bool {
let end = Instant::now() + Duration::from_secs(2);
// Generous: it returns as soon as `cond` holds, and the receive thread
// can wait a long time for a core when every test is running.
let end = Instant::now() + Duration::from_secs(30);
while Instant::now() < end {
if cond() {
return true;
Expand Down
4 changes: 3 additions & 1 deletion src/dev/isp1040/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,9 @@ fn every_move_of_the_test_clock_reaches_a_waiting_selection() {
}
let _ = tx.send(());
});
match rx.recv_timeout(Duration::from_secs(5)) {
// A lost move hangs the loop for good, so the bound only has to be
// longer than 2000 handoffs take when the threads wait for a core.
match rx.recv_timeout(Duration::from_secs(60)) {
Ok(()) => {}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => panic!("a clock move was lost: the selection never ended"),
Err(e) => panic!("the clock loop failed: {e}"),
Expand Down
14 changes: 5 additions & 9 deletions src/dev/mc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,18 +563,14 @@ impl MemoryController {
pub(crate) fn vdma_stage(&self) -> &Mutex<Box<[u64]>> { &self.vdma_stage }

pub fn register_locks(&self) {
use crate::locks::register_lock_fn;
let giodma = self.giodma.clone();
register_lock_fn("mc::giodma_state", move || giodma.state.is_locked());
let state = self.state.clone();
register_lock_fn("mc::state", move || state.is_locked());
let threads = self.threads.clone();
register_lock_fn("mc::threads", move || threads.is_locked());
use crate::locks::{register_mutex, register_mutex_fn};
register_mutex_fn("mc::giodma_state", &self.giodma, |g| g.state.is_locked());
register_mutex("mc::state", &self.state);
register_mutex("mc::threads", &self.threads);
// Held across REX3 bus calls (which take REX3's own locks and can
// wait_idle), so it must be visible to the deadlock detector even
// though only the MC-DMA thread ever acquires it.
let stage = self.vdma_stage.clone();
register_lock_fn("mc::vdma_stage", move || stage.is_locked());
register_mutex("mc::vdma_stage", &self.vdma_stage);
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/dev/mgras/hq4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1890,10 +1890,11 @@ mod tests {
// Still full, still busy: nothing was consumed or lost silently.
assert_eq!(r.b.write(SH + hq4::RE_STATUS as u64, 32, 0), BUS_BUSY);
r.b.mgras().start_engines();
let mut tries = 0u32;
// A deadline, not a count of retries: the engine thread may wait a
// long time for a core when every test is running.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while r.b.write(SH + hq4::RE_STATUS as u64, 32, 0) == BUS_BUSY {
tries += 1;
assert!(tries < 10_000_000, "the store never landed after the engine drained");
assert!(std::time::Instant::now() < deadline, "the store never landed after the engine drained");
std::thread::yield_now();
}
}
Expand Down
Loading
Loading