use super::path::DecisionPath;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DecisionTrace<P: DecisionPath> {
pub timestamp_ns: u64,
pub sequence: u64,
pub input_hash: u64,
pub path: P,
pub output: f32,
pub latency_ns: u64,
}
impl<P: DecisionPath> DecisionTrace<P> {
pub fn new(
timestamp_ns: u64,
sequence: u64,
input_hash: u64,
path: P,
output: f32,
latency_ns: u64,
) -> Self {
Self { timestamp_ns, sequence, input_hash, path, output, latency_ns }
}
pub fn confidence(&self) -> f32 {
self.path.confidence()
}
pub fn explain(&self) -> String {
let mut explanation = format!(
"Trace #{} @ {}ns (latency: {}ns)\n",
self.sequence, self.timestamp_ns, self.latency_ns
);
explanation.push_str(&format!("Input hash: 0x{:016x}\n", self.input_hash));
explanation.push_str(&format!("Output: {:.4}\n", self.output));
explanation.push_str("---\n");
explanation.push_str(&self.path.explain());
explanation
}
pub fn feature_contributions(&self) -> &[f32] {
self.path.feature_contributions()
}
pub fn to_bytes(&self) -> Vec<u8> {
let path_bytes = self.path.to_bytes();
let mut bytes = Vec::with_capacity(36 + path_bytes.len());
bytes.extend_from_slice(&self.timestamp_ns.to_le_bytes());
bytes.extend_from_slice(&self.sequence.to_le_bytes());
bytes.extend_from_slice(&self.input_hash.to_le_bytes());
bytes.extend_from_slice(&self.output.to_le_bytes());
let latency_us = (self.latency_ns / 1000) as u32;
bytes.extend_from_slice(&latency_us.to_le_bytes());
bytes.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
bytes.extend_from_slice(&path_bytes);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, super::path::PathError>
where
P: DecisionPath,
{
if bytes.len() < 36 {
return Err(super::path::PathError::InsufficientData {
expected: 36,
actual: bytes.len(),
});
}
let timestamp_ns = u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]);
let sequence = u64::from_le_bytes([
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
]);
let input_hash = u64::from_le_bytes([
bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
]);
let output = f32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]);
let latency_us = u32::from_le_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]);
let latency_ns = u64::from(latency_us) * 1000;
let path_length = u32::from_le_bytes([bytes[32], bytes[33], bytes[34], bytes[35]]) as usize;
if bytes.len() < 36 + path_length {
return Err(super::path::PathError::InsufficientData {
expected: 36 + path_length,
actual: bytes.len(),
});
}
let path = P::from_bytes(&bytes[36..36 + path_length])?;
Ok(Self { timestamp_ns, sequence, input_hash, path, output, latency_ns })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::inference::path::LinearPath;
#[test]
fn test_decision_trace_new() {
let path = LinearPath::new(vec![0.5, -0.3], 0.1, 0.5, 0.87);
let trace = DecisionTrace::new(1000, 42, 0xdeadbeef, path, 0.87, 500);
assert_eq!(trace.timestamp_ns, 1000);
assert_eq!(trace.sequence, 42);
assert_eq!(trace.input_hash, 0xdeadbeef);
assert_eq!(trace.output, 0.87);
assert_eq!(trace.latency_ns, 500);
}
#[test]
fn test_decision_trace_confidence() {
let path = LinearPath::new(vec![0.5], 0.0, 0.0, 0.0).with_probability(0.9);
let trace = DecisionTrace::new(0, 0, 0, path, 0.0, 0);
assert!((trace.confidence() - 0.9).abs() < 1e-6);
}
#[test]
fn test_decision_trace_explain() {
let path = LinearPath::new(vec![0.5, -0.3], 0.1, 0.5, 0.87);
let trace = DecisionTrace::new(1000, 42, 0xdeadbeef, path, 0.87, 500);
let explanation = trace.explain();
assert!(explanation.contains("Trace #42"));
assert!(explanation.contains("0x00000000deadbeef"));
assert!(explanation.contains("Output: 0.87"));
}
#[test]
fn test_decision_trace_serialization_roundtrip() {
let path = LinearPath::new(vec![0.5, -0.3, 0.2], 0.1, 0.5, 0.87).with_probability(0.87);
let trace = DecisionTrace::new(1_000_000_000, 42, 0xdeadbeef12345678, path, 0.87, 500_000);
let bytes = trace.to_bytes();
let restored: DecisionTrace<LinearPath> =
DecisionTrace::from_bytes(&bytes).expect("Failed to deserialize");
assert_eq!(trace.timestamp_ns, restored.timestamp_ns);
assert_eq!(trace.sequence, restored.sequence);
assert_eq!(trace.input_hash, restored.input_hash);
assert!((trace.output - restored.output).abs() < 1e-6);
assert!((trace.latency_ns as i64 - restored.latency_ns as i64).abs() < 1000);
}
#[test]
fn test_decision_trace_feature_contributions() {
let path = LinearPath::new(vec![0.5, -0.3, 0.2], 0.0, 0.0, 0.0);
let trace = DecisionTrace::new(0, 0, 0, path, 0.0, 0);
let contributions = trace.feature_contributions();
assert_eq!(contributions.len(), 3);
assert!((contributions[0] - 0.5).abs() < 1e-6);
}
#[test]
fn test_decision_trace_insufficient_data() {
let result: Result<DecisionTrace<LinearPath>, _> = DecisionTrace::from_bytes(&[0; 10]);
assert!(result.is_err());
}
}