use alloc::vec::Vec;
use crate::core::error::Result;
use crate::core::sched::ExitFlag;
use crate::ir::{Block, Fault, Interp, IrHost, Opcode, Outcome, RegSlot};
use crate::jit::cache::{BlockCache, BlockId, CacheStats};
use crate::jit::tlb::{Epoch, PAGE_MASK, PAGE_SIZE};
#[derive(Debug)]
pub struct Translation {
pub block: Block,
pub page: u64,
pub insns: usize,
}
pub trait Frontend {
fn epoch(&mut self) -> Epoch;
fn key(&mut self) -> u64;
fn pc_slot(&self) -> RegSlot;
fn translate(&mut self, pc: u64) -> Result<Translation>;
}
pub trait StoreLog {
fn drain_dirty(&mut self, sink: &mut dyn FnMut(u64));
}
#[derive(Debug, Clone, Default)]
pub struct DirtyPages {
pages: Vec<u64>,
}
impl DirtyPages {
#[must_use]
pub fn new() -> DirtyPages {
DirtyPages::default()
}
#[inline]
pub fn note(&mut self, phys: u64, len: u64) {
if len == 0 {
return;
}
let first = phys & !PAGE_MASK;
let last = phys.saturating_add(len - 1) & !PAGE_MASK;
let mut page = first;
loop {
if self.pages.last() != Some(&page) {
self.pages.push(page);
}
if page >= last {
break;
}
page = page.saturating_add(PAGE_SIZE);
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.pages.is_empty()
}
}
impl StoreLog for DirtyPages {
fn drain_dirty(&mut self, sink: &mut dyn FnMut(u64)) {
for page in self.pages.drain(..) {
sink(page);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Stop {
Budget,
Exit,
Fault(Fault),
Unsupported {
op: Opcode,
at: usize,
},
Untranslatable {
pc: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Run {
pub pc: u64,
pub blocks: usize,
pub insns: usize,
pub stop: Stop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DispatchStats {
pub blocks: u64,
pub chained: u64,
pub looked_up: u64,
pub translated: u64,
pub smc: u64,
pub resyncs: u64,
}
#[derive(Debug)]
pub struct Dispatcher {
cache: BlockCache,
interp: Interp,
exit: Option<ExitFlag>,
stats: DispatchStats,
}
impl Dispatcher {
#[must_use]
pub fn new() -> Dispatcher {
Dispatcher::with_cache(BlockCache::new())
}
#[must_use]
pub fn with_cache(cache: BlockCache) -> Dispatcher {
Dispatcher {
cache,
interp: Interp::new(),
exit: None,
stats: DispatchStats::default(),
}
}
#[must_use]
pub fn with_exit_flag(mut self, flag: ExitFlag) -> Dispatcher {
self.exit = Some(flag);
self
}
#[inline]
#[must_use]
pub fn cache(&self) -> &BlockCache {
&self.cache
}
#[inline]
pub fn cache_mut(&mut self) -> &mut BlockCache {
&mut self.cache
}
#[inline]
#[must_use]
pub fn stats(&self) -> DispatchStats {
self.stats
}
#[inline]
#[must_use]
pub fn cache_stats(&self) -> CacheStats {
self.cache.stats()
}
pub fn run<F, H>(
&mut self,
front: &mut F,
host: &mut H,
mut pc: u64,
budget: usize,
) -> Result<Run>
where
F: Frontend + ?Sized,
H: IrHost + StoreLog + ?Sized,
{
if self.cache.sync(front.epoch()) {
self.stats.resyncs += 1;
}
let pc_slot = front.pc_slot();
let mut from: Option<BlockId> = None;
let mut blocks = 0usize;
let mut insns = 0usize;
let stop = loop {
if blocks >= budget {
break Stop::Budget;
}
if self.exit.as_ref().is_some_and(ExitFlag::raised) {
break Stop::Exit;
}
let key = front.key();
let (id, chained) = match from.and_then(|f| self.cache.follow(f, pc, key)) {
Some(id) => (id, true),
None => match self.cache.lookup(pc, key) {
Some(id) => {
self.stats.looked_up += 1;
(id, false)
}
None => {
let t = front.translate(pc)?;
self.stats.translated += 1;
if t.insns == 0 {
break Stop::Untranslatable { pc };
}
(self.cache.insert(pc, key, t.page, t.insns, t.block), false)
}
},
};
if chained {
self.stats.chained += 1;
} else if let Some(f) = from {
self.cache.link(f, pc, id);
}
let block = self
.cache
.block(id)
.expect("a block just found or just inserted is resident");
let outcome = self.interp.run(block, host)?;
self.stats.blocks += 1;
blocks += 1;
insns += (self.interp.boundaries().saturating_sub(1)) as usize;
let cache = &mut self.cache;
let mut hit = 0usize;
host.drain_dirty(&mut |page| hit += cache.note_write(page, 1));
self.stats.smc += hit as u64;
let survived = self.cache.block(id).is_some();
match outcome {
Outcome::Exit => pc = host.read_slot(pc_slot) as u64,
Outcome::Goto { pc: next } | Outcome::Lookup { pc: next } => pc = next,
Outcome::Fault(f) => break Stop::Fault(f),
Outcome::Unsupported { op, at } => break Stop::Unsupported { op, at },
}
from = survived.then_some(id);
};
Ok(Run {
pc,
blocks,
insns,
stop,
})
}
}
impl Default for Dispatcher {
fn default() -> Dispatcher {
Dispatcher::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::error::BusError;
use crate::core::space::MemResult;
use crate::ir::{BlockBuilder, Const, InsnStart, MemOp, Type};
use alloc::vec;
const PC: RegSlot = RegSlot(0);
fn straight(pc: u64, next: u64) -> Block {
let mut b = BlockBuilder::new(pc, 0);
b.insn_start(InsnStart {
pc,
next_pc: next,
ticks: 0,
live: Vec::new(),
});
b.charge(1);
let t = b.imm(Type::I64, Const::Int(u128::from(next)));
b.insn_start(InsnStart {
pc: next,
next_pc: next,
ticks: 1,
live: vec![(PC, t)],
});
b.exit_tb();
b.finish()
}
struct Chain {
step: u64,
limit: u64,
epoch: Epoch,
key: u64,
translated: Vec<u64>,
}
impl Frontend for Chain {
fn epoch(&mut self) -> Epoch {
self.epoch
}
fn key(&mut self) -> u64 {
self.key
}
fn pc_slot(&self) -> RegSlot {
PC
}
fn translate(&mut self, pc: u64) -> Result<Translation> {
self.translated.push(pc);
let next = if pc + self.step >= self.limit {
0x1000
} else {
pc + self.step
};
Ok(Translation {
block: straight(pc, next),
page: pc & !PAGE_MASK,
insns: 1,
})
}
}
#[derive(Default)]
struct Host {
slots: [u64; 4],
ticks: u64,
dirty: DirtyPages,
}
impl IrHost for Host {
fn read_slot(&mut self, slot: RegSlot) -> u128 {
u128::from(self.slots[slot.0 as usize])
}
fn write_slot(&mut self, slot: RegSlot, value: u128) {
self.slots[slot.0 as usize] = value as u64;
}
fn load(&mut self, _mem: &MemOp, _addr: u64) -> MemResult<u64> {
Err(BusError::Unassigned)
}
fn store(&mut self, mem: &MemOp, addr: u64, _value: u64) -> MemResult {
self.dirty.note(addr, mem.size.bytes());
Ok(())
}
fn charge(&mut self, ticks: u64) {
self.ticks += ticks;
}
fn insn_start(&mut self, _mark: &InsnStart) {}
}
impl StoreLog for Host {
fn drain_dirty(&mut self, sink: &mut dyn FnMut(u64)) {
self.dirty.drain_dirty(sink);
}
}
fn chain(step: u64, limit: u64) -> Chain {
Chain {
step,
limit,
epoch: Epoch::default(),
key: 0,
translated: Vec::new(),
}
}
#[test]
fn a_loop_is_translated_once_and_then_chained() {
let mut d = Dispatcher::with_cache(BlockCache::with_capacity(64));
let mut f = chain(4, 0x1010);
let mut h = Host::default();
let run = d.run(&mut f, &mut h, 0x1000, 400).expect("runs");
assert_eq!(run.blocks, 400);
assert_eq!(run.insns, 400);
assert_eq!(run.stop, Stop::Budget);
assert_eq!(f.translated.len(), 4);
assert_eq!(d.stats().translated, 4);
assert!(
d.stats().chained >= 390,
"chained {} of {}",
d.stats().chained,
run.blocks
);
assert_eq!(d.cache_stats().stale_links, 0);
d.cache().check().expect("consistent");
}
#[test]
fn every_tick_is_charged_whether_the_block_was_cached_or_not() {
let mut d = Dispatcher::new();
let mut f = chain(4, 0x1010);
let mut h = Host::default();
let run = d.run(&mut f, &mut h, 0x1000, 97).expect("runs");
assert_eq!(h.ticks, run.blocks as u64);
assert!(d.stats().chained > 0, "and chaining really happened");
}
fn trace(pc: u64, insns: u64, leave_at: Option<u64>, after: u64) -> Block {
let mut b = BlockBuilder::new(pc, 0);
let skip = b.imm(Type::I1, Const::Int(0));
let mut ticks = 0u64;
for i in 0..insns {
b.insn_start(InsnStart {
pc: pc + i * 4,
next_pc: pc + (i + 1) * 4,
ticks,
live: Vec::new(),
});
b.charge(1);
ticks += 1;
if leave_at == Some(i) {
let over = b.emit_raw(
Opcode::BRCOND,
Type::I64,
None,
None,
&[skip],
None,
None,
0,
);
let t = b.imm(Type::I64, Const::Int(u128::from(after)));
b.insn_start(InsnStart {
pc: after,
next_pc: after,
ticks,
live: vec![(PC, t)],
});
b.exit_tb();
b.patch_aux(over, b.next_index() as u32);
}
}
let t = b.imm(Type::I64, Const::Int(u128::from(after)));
b.insn_start(InsnStart {
pc: after,
next_pc: after,
ticks,
live: vec![(PC, t)],
});
b.exit_tb();
b.finish()
}
struct Traces {
insns: u64,
leave_at: Option<u64>,
epoch: Epoch,
}
impl Frontend for Traces {
fn epoch(&mut self) -> Epoch {
self.epoch
}
fn key(&mut self) -> u64 {
0
}
fn pc_slot(&self) -> RegSlot {
PC
}
fn translate(&mut self, pc: u64) -> Result<Translation> {
Ok(Translation {
block: trace(pc, self.insns, self.leave_at, pc),
page: pc & !PAGE_MASK,
insns: self.insns as usize,
})
}
}
#[test]
fn a_side_exit_retires_fewer_instructions_than_the_trace_covers() {
let mut d = Dispatcher::new();
let mut f = Traces {
insns: 16,
leave_at: Some(4),
epoch: Epoch::default(),
};
let mut h = Host::default();
let run = d.run(&mut f, &mut h, 0x1000, 10).expect("runs");
assert_eq!(run.blocks, 10);
assert_eq!(
run.insns, 50,
"five guest instructions a block, not sixteen"
);
assert_eq!(h.ticks, 50);
}
#[test]
fn a_trace_that_runs_to_its_end_retires_everything_it_covers() {
let mut d = Dispatcher::new();
let mut f = Traces {
insns: 16,
leave_at: None,
epoch: Epoch::default(),
};
let mut h = Host::default();
let run = d.run(&mut f, &mut h, 0x1000, 10).expect("runs");
assert_eq!(run.insns, 160);
assert_eq!(h.ticks, 160);
}
#[test]
fn a_raised_exit_flag_stops_within_one_block_however_long_the_block_is() {
let flag = ExitFlag::default();
let mut d = Dispatcher::new().with_exit_flag(flag.clone());
let mut f = Traces {
insns: 64,
leave_at: None,
epoch: Epoch::default(),
};
let mut h = Host::default();
d.run(&mut f, &mut h, 0x1000, 1).expect("runs");
flag.raise();
let run = d.run(&mut f, &mut h, 0x1000, 100).expect("runs");
assert_eq!(run.stop, Stop::Exit);
assert_eq!(run.blocks, 0, "no block starts once the flag is up");
}
#[test]
fn a_raised_exit_flag_stops_at_a_block_boundary() {
let flag = ExitFlag::default();
let mut d = Dispatcher::new().with_exit_flag(flag.clone());
let mut f = chain(4, 0x1010);
let mut h = Host::default();
assert_eq!(
d.run(&mut f, &mut h, 0x1000, 10).expect("runs").stop,
Stop::Budget
);
flag.raise();
let run = d.run(&mut f, &mut h, 0x1000, 10).expect("runs");
assert_eq!(run.stop, Stop::Exit);
assert_eq!(run.blocks, 0, "no block starts once the flag is up");
}
#[test]
fn an_epoch_change_between_runs_resynchronises_the_cache() {
let mut d = Dispatcher::new();
let mut f = chain(4, 0x1010);
let mut h = Host::default();
d.run(&mut f, &mut h, 0x1000, 20).expect("runs");
assert_eq!(d.stats().translated, 4);
f.epoch.topology += 1;
d.run(&mut f, &mut h, 0x1000, 20).expect("runs");
assert_eq!(d.stats().resyncs, 1);
assert_eq!(d.stats().translated, 8, "every block was lifted again");
}
#[test]
fn a_key_change_is_a_different_translation_at_the_same_pc() {
let mut d = Dispatcher::new();
let mut f = chain(4, 0x1010);
let mut h = Host::default();
d.run(&mut f, &mut h, 0x1000, 20).expect("runs");
f.key = 1;
d.run(&mut f, &mut h, 0x1000, 20).expect("runs");
assert_eq!(d.stats().translated, 8);
assert_eq!(d.cache_stats().stale_links, 0);
d.cache().check().expect("consistent");
}
}