use std::{
fmt::Write as FmtWrite,
fs::{File, OpenOptions},
io::{BufWriter, Write},
mem,
path::Path,
sync::{
atomic::{AtomicU64, Ordering},
Mutex,
},
};
use crate::metadata::token::Token;
#[derive(Clone, Debug)]
pub enum TraceEvent {
Instruction {
method: Token,
offset: u32,
opcode: u16,
mnemonic: String,
operand: Option<String>,
stack_depth: usize,
},
MethodCall {
target: Token,
is_virtual: bool,
arg_count: usize,
call_depth: usize,
caller: Option<Token>,
caller_offset: Option<u32>,
},
MethodReturn {
method: Token,
has_return_value: bool,
call_depth: usize,
},
ExceptionThrow {
method: Token,
offset: u32,
exception_type: Option<Token>,
description: String,
},
ExceptionCatch {
method: Token,
handler_offset: u32,
catch_type: Token,
},
FinallyEnter {
method: Token,
handler_offset: u32,
},
HeapAlloc {
type_token: Token,
heap_ref: u64,
},
ArrayAlloc {
element_type: Token,
length: usize,
heap_ref: u64,
},
HookInvoke {
method: Token,
hook_name: String,
bypassed: bool,
},
RuntimeException {
method: Token,
offset: u32,
error_type: String,
description: String,
},
Branch {
method: Token,
from_offset: u32,
to_offset: u32,
conditional: bool,
},
BranchCompare {
left: String,
right: String,
op: String,
result: bool,
},
StaticFieldAccess {
field: Token,
is_load: bool,
},
ArrayStore {
method: Token,
offset: u32,
heap_ref: u64,
index: usize,
value: String,
},
ArrayLoad {
method: Token,
offset: u32,
heap_ref: u64,
index: usize,
value: String,
},
}
impl TraceEvent {
#[must_use]
pub fn to_json(&self) -> String {
self.to_json_with_context(None)
}
#[must_use]
pub fn to_json_with_context(&self, context: Option<&str>) -> String {
let context_prefix = context
.map(|c| format!(r#""context":"{}","#, escape_json(c)))
.unwrap_or_default();
match self {
TraceEvent::Instruction {
method,
offset,
opcode,
mnemonic,
operand,
stack_depth,
} => {
let operand_str = operand
.as_ref()
.map(|o| format!(r#","operand":"{}""#, escape_json(o)))
.unwrap_or_default();
format!(
r#"{{{}"type":"instruction","method":"0x{:08X}","offset":"0x{:04X}","opcode":"0x{:04X}","mnemonic":"{}","stack_depth":{}{}}}"#,
context_prefix,
method.value(),
offset,
opcode,
escape_json(mnemonic),
stack_depth,
operand_str
)
}
TraceEvent::MethodCall {
target,
is_virtual,
arg_count,
call_depth,
caller,
caller_offset,
} => {
let caller_str = caller
.map(|c| format!(r#","caller":"0x{:08X}""#, c.value()))
.unwrap_or_default();
let caller_offset_str = caller_offset
.map(|o| format!(r#","caller_offset":"0x{o:04X}""#))
.unwrap_or_default();
format!(
r#"{{{}"type":"call","target":"0x{:08X}","is_virtual":{},"arg_count":{},"call_depth":{}{}{}}}"#,
context_prefix,
target.value(),
is_virtual,
arg_count,
call_depth,
caller_str,
caller_offset_str
)
}
TraceEvent::MethodReturn {
method,
has_return_value,
call_depth,
} => {
format!(
r#"{{{}"type":"return","method":"0x{:08X}","has_return_value":{},"call_depth":{}}}"#,
context_prefix,
method.value(),
has_return_value,
call_depth
)
}
TraceEvent::ExceptionThrow {
method,
offset,
exception_type,
description,
} => {
let type_str = exception_type
.map(|t| format!(r#","exception_type":"0x{:08X}""#, t.value()))
.unwrap_or_default();
format!(
r#"{{{}"type":"throw","method":"0x{:08X}","offset":"0x{:04X}","description":"{}"{}}}"#,
context_prefix,
method.value(),
offset,
escape_json(description),
type_str
)
}
TraceEvent::ExceptionCatch {
method,
handler_offset,
catch_type,
} => {
format!(
r#"{{{}"type":"catch","method":"0x{:08X}","handler_offset":"0x{:04X}","catch_type":"0x{:08X}"}}"#,
context_prefix,
method.value(),
handler_offset,
catch_type.value()
)
}
TraceEvent::FinallyEnter {
method,
handler_offset,
} => {
format!(
r#"{{{}"type":"finally","method":"0x{:08X}","handler_offset":"0x{:04X}"}}"#,
context_prefix,
method.value(),
handler_offset
)
}
TraceEvent::HeapAlloc {
type_token,
heap_ref,
} => {
format!(
r#"{{{}"type":"heap_alloc","type_token":"0x{:08X}","heap_ref":{}}}"#,
context_prefix,
type_token.value(),
heap_ref
)
}
TraceEvent::ArrayAlloc {
element_type,
length,
heap_ref,
} => {
format!(
r#"{{{}"type":"array_alloc","element_type":"0x{:08X}","length":{},"heap_ref":{}}}"#,
context_prefix,
element_type.value(),
length,
heap_ref
)
}
TraceEvent::HookInvoke {
method,
hook_name,
bypassed,
} => {
format!(
r#"{{{}"type":"hook","method":"0x{:08X}","hook_name":"{}","bypassed":{}}}"#,
context_prefix,
method.value(),
escape_json(hook_name),
bypassed
)
}
TraceEvent::RuntimeException {
method,
offset,
error_type,
description,
} => {
format!(
r#"{{{}"type":"runtime_exception","method":"0x{:08X}","offset":"0x{:04X}","error_type":"{}","description":"{}"}}"#,
context_prefix,
method.value(),
offset,
escape_json(error_type),
escape_json(description)
)
}
TraceEvent::Branch {
method,
from_offset,
to_offset,
conditional,
} => {
format!(
r#"{{{}"type":"branch","method":"0x{:08X}","from":"0x{:04X}","to":"0x{:04X}","conditional":{}}}"#,
context_prefix,
method.value(),
from_offset,
to_offset,
conditional
)
}
TraceEvent::StaticFieldAccess { field, is_load } => {
format!(
r#"{{{}"type":"static_field","field":"0x{:08X}","is_load":{}}}"#,
context_prefix,
field.value(),
is_load
)
}
TraceEvent::BranchCompare {
left,
right,
op,
result,
} => {
format!(
r#"{{{}"type":"branch_compare","left":"{}","right":"{}","op":"{}","result":{}}}"#,
context_prefix,
escape_json(left),
escape_json(right),
escape_json(op),
result
)
}
TraceEvent::ArrayStore {
method,
offset,
heap_ref,
index,
value,
} => {
format!(
r#"{{{}"type":"array_store","method":"0x{:08X}","offset":"0x{:04X}","heap_ref":{},"index":{},"value":"{}"}}"#,
context_prefix,
method.value(),
offset,
heap_ref,
index,
escape_json(value)
)
}
TraceEvent::ArrayLoad {
method,
offset,
heap_ref,
index,
value,
} => {
format!(
r#"{{{}"type":"array_load","method":"0x{:08X}","offset":"0x{:04X}","heap_ref":{},"index":{},"value":"{}"}}"#,
context_prefix,
method.value(),
offset,
heap_ref,
index,
escape_json(value)
)
}
}
}
}
fn escape_json(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if c.is_control() => {
let _ = write!(result, "\\u{:04X}", c as u32);
}
c => result.push(c),
}
}
result
}
pub struct TraceWriter {
file: Option<Mutex<BufWriter<File>>>,
buffer: Option<Mutex<Vec<TraceEvent>>>,
max_entries: usize,
event_count: AtomicU64,
context_prefix: Option<String>,
}
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,
})
}
#[must_use]
pub fn new_memory(max_entries: usize, context: Option<String>) -> Self {
Self {
file: None,
buffer: Some(Mutex::new(Vec::with_capacity(max_entries.min(10_000)))),
max_entries,
event_count: AtomicU64::new(0),
context_prefix: context,
}
}
#[must_use]
pub fn context_prefix(&self) -> Option<&str> {
self.context_prefix.as_deref()
}
pub fn write(&self, event: TraceEvent) {
self.event_count.fetch_add(1, Ordering::Relaxed);
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.remove(0);
}
buf.push(event);
}
}
}
pub fn flush(&self) {
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<Vec<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 super::*;
#[test]
fn test_trace_event_json() {
let event = TraceEvent::Instruction {
method: Token::new(0x06000001),
offset: 0x0010,
opcode: 0x28,
mnemonic: "call".to_string(),
operand: Some("0x0A000001".to_string()),
stack_depth: 2,
};
let json = event.to_json();
assert!(json.contains("\"type\":\"instruction\""));
assert!(json.contains("\"method\":\"0x06000001\""));
assert!(json.contains("\"mnemonic\":\"call\""));
assert!(!json.contains("\"context\""));
}
#[test]
fn test_trace_event_json_with_context() {
let event = TraceEvent::MethodCall {
target: Token::new(0x06000001),
is_virtual: false,
arg_count: 2,
call_depth: 1,
caller: None,
caller_offset: None,
};
let json = event.to_json_with_context(Some("warmup"));
assert!(json.contains("\"context\":\"warmup\""));
assert!(json.contains("\"type\":\"call\""));
assert!(json.contains("\"target\":\"0x06000001\""));
let json_no_ctx = event.to_json_with_context(None);
assert!(!json_no_ctx.contains("\"context\""));
assert!(json_no_ctx.contains("\"type\":\"call\""));
}
#[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,
});
assert_eq!(writer.event_count(), 1);
assert_eq!(writer.context_prefix(), Some("test"));
let buffer = writer.take_buffer().unwrap();
assert_eq!(buffer.len(), 1);
}
#[test]
fn test_escape_json() {
assert_eq!(escape_json("hello"), "hello");
assert_eq!(escape_json("hello\"world"), "hello\\\"world");
assert_eq!(escape_json("line1\nline2"), "line1\\nline2");
}
}