use std::{
collections::VecDeque,
fs::{File, OpenOptions},
io::{BufWriter, Write},
mem,
path::Path,
sync::{
atomic::{AtomicU64, Ordering},
Mutex,
},
};
use crate::emulation::tracer::{event::TraceEvent, listener::TraceListener};
pub struct TraceWriter {
file: Option<Mutex<BufWriter<File>>>,
buffer: Option<Mutex<VecDeque<TraceEvent>>>,
max_entries: usize,
event_count: AtomicU64,
context_prefix: Option<String>,
listeners: Vec<Box<dyn TraceListener>>,
}
impl TraceWriter {
pub fn new_file<P: AsRef<Path>>(path: P, context: Option<String>) -> std::io::Result<Self> {
let file = OpenOptions::new().create(true).append(true).open(path)?;
Ok(Self {
file: Some(Mutex::new(BufWriter::new(file))),
buffer: None,
max_entries: 0,
event_count: AtomicU64::new(0),
context_prefix: context,
listeners: Vec::new(),
})
}
#[must_use]
pub fn new_memory(max_entries: usize, context: Option<String>) -> Self {
Self {
file: None,
buffer: Some(Mutex::new(VecDeque::with_capacity(max_entries.min(10_000)))),
max_entries,
event_count: AtomicU64::new(0),
context_prefix: context,
listeners: Vec::new(),
}
}
#[must_use]
pub fn context_prefix(&self) -> Option<&str> {
self.context_prefix.as_deref()
}
pub fn add_listener(&mut self, listener: Box<dyn TraceListener>) {
self.listeners.push(listener);
}
pub fn write(&self, event: TraceEvent) {
self.event_count.fetch_add(1, Ordering::Relaxed);
for listener in &self.listeners {
listener.on_event(&event);
}
if let Some(ref file) = self.file {
if let Ok(mut writer) = file.lock() {
let json = event.to_json_with_context(self.context_prefix.as_deref());
let _ = writeln!(writer, "{json}");
}
} else if let Some(ref buffer) = self.buffer {
if let Ok(mut buf) = buffer.lock() {
if self.max_entries > 0 && buf.len() >= self.max_entries {
buf.pop_front();
}
buf.push_back(event);
}
}
}
pub fn flush(&self) {
for listener in &self.listeners {
listener.on_flush();
}
if let Some(ref file) = self.file {
if let Ok(mut writer) = file.lock() {
let _ = writer.flush();
}
}
}
#[must_use]
pub fn event_count(&self) -> u64 {
self.event_count.load(Ordering::Relaxed)
}
pub fn take_buffer(&self) -> Option<VecDeque<TraceEvent>> {
self.buffer
.as_ref()
.and_then(|buf| buf.lock().ok().map(|mut b| mem::take(&mut *b)))
}
}
impl std::fmt::Debug for TraceWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TraceWriter")
.field("is_file_based", &self.file.is_some())
.field("max_entries", &self.max_entries)
.field("event_count", &self.event_count())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use crate::{
emulation::tracer::{event::TraceEvent, listener::TraceListener, writer::TraceWriter},
metadata::token::Token,
};
#[test]
fn test_trace_writer_memory() {
let writer = TraceWriter::new_memory(100, Some("test".to_string()));
writer.write(TraceEvent::MethodCall {
target: Token::new(0x06000001),
is_virtual: false,
arg_count: 2,
call_depth: 1,
caller: None,
caller_offset: None,
call_id: 1,
});
assert_eq!(writer.event_count(), 1);
assert_eq!(writer.context_prefix(), Some("test"));
let buffer = writer.take_buffer().unwrap();
assert_eq!(buffer.len(), 1);
}
struct EventCounter {
count: std::sync::atomic::AtomicUsize,
}
impl EventCounter {
fn new() -> Self {
Self {
count: std::sync::atomic::AtomicUsize::new(0),
}
}
#[allow(dead_code)]
fn get(&self) -> usize {
self.count.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl TraceListener for EventCounter {
fn on_event(&self, _event: &TraceEvent) {
self.count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
#[test]
fn test_listener_receives_events() {
let mut writer = TraceWriter::new_memory(100, None);
writer.add_listener(Box::new(EventCounter::new()));
writer.write(TraceEvent::MethodCall {
target: Token::new(0x06000001),
is_virtual: false,
arg_count: 0,
call_depth: 1,
caller: None,
caller_offset: None,
call_id: 1,
});
writer.write(TraceEvent::MethodReturn {
method: Token::new(0x06000001),
has_return_value: false,
call_depth: 0,
call_id: 1,
});
assert_eq!(writer.event_count(), 2);
assert_eq!(writer.take_buffer().unwrap().len(), 2);
}
#[test]
fn test_listener_coexists_with_buffer() {
let mut writer = TraceWriter::new_memory(100, None);
writer.add_listener(Box::new(EventCounter::new()));
writer.write(TraceEvent::Instruction {
method: Token::new(0x06000001),
offset: 0,
opcode: 0,
mnemonic: "nop".to_string(),
operand: None,
stack_depth: 0,
stack_values: None,
});
let buf = writer.take_buffer().unwrap();
assert_eq!(buf.len(), 1);
assert_eq!(writer.event_count(), 1);
}
#[test]
fn test_multiple_listeners() {
let mut writer = TraceWriter::new_memory(100, None);
writer.add_listener(Box::new(EventCounter::new()));
writer.add_listener(Box::new(EventCounter::new()));
writer.write(TraceEvent::Instruction {
method: Token::new(0x06000001),
offset: 0,
opcode: 0,
mnemonic: "nop".to_string(),
operand: None,
stack_depth: 0,
stack_values: None,
});
assert_eq!(writer.event_count(), 1);
}
}