use std::fmt::Write as FmtWrite;
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,
stack_values: Option<Vec<String>>,
},
MethodCall {
target: Token,
is_virtual: bool,
arg_count: usize,
call_depth: usize,
caller: Option<Token>,
caller_offset: Option<u32>,
call_id: u64,
},
MethodReturn {
method: Token,
has_return_value: bool,
call_depth: usize,
call_id: u64,
},
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,
return_value: Option<String>,
},
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,
},
StaticFieldValue {
method: Token,
offset: u32,
field: Token,
is_load: bool,
value: String,
},
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,
stack_values,
} => {
let operand_str = operand
.as_ref()
.map(|o| format!(r#","operand":"{}""#, escape_json(o)))
.unwrap_or_default();
let stack_str = stack_values
.as_ref()
.map(|vals| {
let items: Vec<String> = vals
.iter()
.map(|v| format!(r#""{}""#, escape_json(v)))
.collect();
format!(r#","stack":[{}]"#, items.join(","))
})
.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,
stack_str
)
}
TraceEvent::MethodCall {
target,
is_virtual,
arg_count,
call_depth,
caller,
caller_offset,
call_id,
} => {
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":{},"call_id":{}{}{}}}"#,
context_prefix,
target.value(),
is_virtual,
arg_count,
call_depth,
call_id,
caller_str,
caller_offset_str
)
}
TraceEvent::MethodReturn {
method,
has_return_value,
call_depth,
call_id,
} => {
format!(
r#"{{{}"type":"return","method":"0x{:08X}","has_return_value":{},"call_depth":{},"call_id":{}}}"#,
context_prefix,
method.value(),
has_return_value,
call_depth,
call_id
)
}
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,
return_value,
} => {
let rv_str = return_value
.as_ref()
.map(|v| format!(r#","return_value":"{}""#, escape_json(v)))
.unwrap_or_default();
format!(
r#"{{{}"type":"hook","method":"0x{:08X}","hook_name":"{}","bypassed":{}{}}}"#,
context_prefix,
method.value(),
escape_json(hook_name),
bypassed,
rv_str
)
}
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::StaticFieldValue {
method,
offset,
field,
is_load,
value,
} => {
format!(
r#"{{{}"type":"static_field_value","method":"0x{:08X}","offset":"0x{:04X}","field":"0x{:08X}","is_load":{},"value":"{}"}}"#,
context_prefix,
method.value(),
offset,
field.value(),
is_load,
escape_json(value)
)
}
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
}
#[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,
stack_values: None,
};
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,
call_id: 1,
};
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_escape_json() {
assert_eq!(escape_json("hello"), "hello");
assert_eq!(escape_json("hello\"world"), "hello\\\"world");
assert_eq!(escape_json("line1\nline2"), "line1\\nline2");
}
#[test]
fn test_static_field_value_json() {
let event = TraceEvent::StaticFieldValue {
method: Token::new(0x06000001),
offset: 0x0010,
field: Token::new(0x04000001),
is_load: true,
value: "I32(42)".to_string(),
};
let json = event.to_json();
assert!(json.contains("\"type\":\"static_field_value\""));
assert!(json.contains("\"field\":\"0x04000001\""));
assert!(json.contains("\"value\":\"I32(42)\""));
}
}