use std::collections::VecDeque;
pub const DEFAULT_TRACE_CAPACITY: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceEntry {
pub pc: u32,
pub instr: u32,
pub thumb: bool,
pub disasm: String,
pub regs: [u32; 16],
pub cpsr: u32,
pub cycles: u64,
}
#[derive(Debug, Clone)]
pub struct CpuTrace {
entries: VecDeque<TraceEntry>,
capacity: usize,
enabled: bool,
}
impl Default for CpuTrace {
fn default() -> Self {
Self::with_capacity(DEFAULT_TRACE_CAPACITY)
}
}
impl CpuTrace {
pub fn with_capacity(capacity: usize) -> Self {
let capacity = capacity.max(1);
Self {
entries: VecDeque::with_capacity(capacity),
capacity,
enabled: false,
}
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn set_capacity(&mut self, capacity: usize) {
let capacity = capacity.max(1);
self.capacity = capacity;
while self.entries.len() > capacity {
self.entries.pop_front();
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn push(&mut self, entry: TraceEntry) {
if !self.enabled {
return;
}
if self.entries.len() == self.capacity {
self.entries.pop_front();
}
self.entries.push_back(entry);
}
pub fn iter(&self) -> impl Iterator<Item = &TraceEntry> {
self.entries.iter()
}
pub fn last(&self) -> Option<&TraceEntry> {
self.entries.back()
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(pc: u32) -> TraceEntry {
TraceEntry {
pc,
instr: 0,
thumb: false,
disasm: format!("NOP @ {:08X}", pc),
regs: [0; 16],
cpsr: 0,
cycles: pc as u64,
}
}
#[test]
fn default_capacity_is_used_when_constructed_via_default() {
let trace = CpuTrace::default();
assert_eq!(trace.capacity(), DEFAULT_TRACE_CAPACITY);
assert!(trace.is_empty());
}
#[test]
fn capacity_zero_is_clamped_to_one() {
let trace = CpuTrace::with_capacity(0);
assert_eq!(trace.capacity(), 1);
}
#[test]
fn push_is_noop_when_disabled() {
let mut trace = CpuTrace::with_capacity(4);
trace.push(entry(0x1000));
assert!(trace.is_empty());
}
#[test]
fn push_records_entries_when_enabled() {
let mut trace = CpuTrace::with_capacity(4);
trace.set_enabled(true);
trace.push(entry(0x1000));
trace.push(entry(0x1004));
assert_eq!(trace.len(), 2);
assert_eq!(trace.last().unwrap().pc, 0x1004);
}
#[test]
fn ring_buffer_evicts_oldest_when_full() {
let mut trace = CpuTrace::with_capacity(3);
trace.set_enabled(true);
for pc in [1u32, 2, 3, 4, 5] {
trace.push(entry(pc));
}
let pcs: Vec<u32> = trace.iter().map(|e| e.pc).collect();
assert_eq!(pcs, vec![3, 4, 5]);
}
#[test]
fn iter_yields_oldest_to_newest() {
let mut trace = CpuTrace::with_capacity(4);
trace.set_enabled(true);
trace.push(entry(0x10));
trace.push(entry(0x20));
trace.push(entry(0x30));
let pcs: Vec<u32> = trace.iter().map(|e| e.pc).collect();
assert_eq!(pcs, vec![0x10, 0x20, 0x30]);
}
#[test]
fn clear_drops_entries_but_preserves_capacity() {
let mut trace = CpuTrace::with_capacity(8);
trace.set_enabled(true);
trace.push(entry(0x1));
trace.push(entry(0x2));
trace.clear();
assert!(trace.is_empty());
assert_eq!(trace.capacity(), 8);
}
#[test]
fn shrinking_capacity_drops_oldest_entries() {
let mut trace = CpuTrace::with_capacity(8);
trace.set_enabled(true);
for pc in 0u32..8 {
trace.push(entry(pc));
}
trace.set_capacity(3);
let pcs: Vec<u32> = trace.iter().map(|e| e.pc).collect();
assert_eq!(pcs, vec![5, 6, 7]);
}
#[test]
fn growing_capacity_does_not_disturb_entries() {
let mut trace = CpuTrace::with_capacity(2);
trace.set_enabled(true);
trace.push(entry(0x1));
trace.push(entry(0x2));
trace.set_capacity(16);
trace.push(entry(0x3));
let pcs: Vec<u32> = trace.iter().map(|e| e.pc).collect();
assert_eq!(pcs, vec![0x1, 0x2, 0x3]);
assert_eq!(trace.capacity(), 16);
}
#[test]
fn set_enabled_does_not_clear_existing_entries() {
let mut trace = CpuTrace::with_capacity(4);
trace.set_enabled(true);
trace.push(entry(0x10));
trace.set_enabled(false);
trace.push(entry(0x20));
assert_eq!(trace.len(), 1);
assert_eq!(trace.last().unwrap().pc, 0x10);
}
#[test]
fn entry_carries_register_snapshot() {
let mut regs = [0u32; 16];
regs[0] = 0xDEAD_BEEF;
regs[15] = 0x0800_0000;
let mut trace = CpuTrace::with_capacity(4);
trace.set_enabled(true);
trace.push(TraceEntry {
pc: 0x0800_0000,
instr: 0xE3A0_0001,
thumb: false,
disasm: "MOV R0, #0x1".to_string(),
regs,
cpsr: 0x6000_001F,
cycles: 42,
});
let last = trace.last().unwrap();
assert_eq!(last.regs[0], 0xDEAD_BEEF);
assert_eq!(last.regs[15], 0x0800_0000);
assert_eq!(last.cpsr, 0x6000_001F);
assert_eq!(last.cycles, 42);
assert!(!last.thumb);
}
}