entrenar/monitor/inference/
trace.rs1use super::path::DecisionPath;
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct DecisionTrace<P: DecisionPath> {
13 pub timestamp_ns: u64,
15 pub sequence: u64,
17 pub input_hash: u64,
19 pub path: P,
21 pub output: f32,
23 pub latency_ns: u64,
25}
26
27impl<P: DecisionPath> DecisionTrace<P> {
28 pub fn new(
30 timestamp_ns: u64,
31 sequence: u64,
32 input_hash: u64,
33 path: P,
34 output: f32,
35 latency_ns: u64,
36 ) -> Self {
37 Self { timestamp_ns, sequence, input_hash, path, output, latency_ns }
38 }
39
40 pub fn confidence(&self) -> f32 {
42 self.path.confidence()
43 }
44
45 pub fn explain(&self) -> String {
47 let mut explanation = format!(
48 "Trace #{} @ {}ns (latency: {}ns)\n",
49 self.sequence, self.timestamp_ns, self.latency_ns
50 );
51 explanation.push_str(&format!("Input hash: 0x{:016x}\n", self.input_hash));
52 explanation.push_str(&format!("Output: {:.4}\n", self.output));
53 explanation.push_str("---\n");
54 explanation.push_str(&self.path.explain());
55 explanation
56 }
57
58 pub fn feature_contributions(&self) -> &[f32] {
60 self.path.feature_contributions()
61 }
62
63 pub fn to_bytes(&self) -> Vec<u8> {
74 let path_bytes = self.path.to_bytes();
75
76 let mut bytes = Vec::with_capacity(36 + path_bytes.len());
77
78 bytes.extend_from_slice(&self.timestamp_ns.to_le_bytes());
79 bytes.extend_from_slice(&self.sequence.to_le_bytes());
80 bytes.extend_from_slice(&self.input_hash.to_le_bytes());
81 bytes.extend_from_slice(&self.output.to_le_bytes());
82
83 let latency_us = (self.latency_ns / 1000) as u32;
85 bytes.extend_from_slice(&latency_us.to_le_bytes());
86
87 bytes.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
88 bytes.extend_from_slice(&path_bytes);
89
90 bytes
91 }
92
93 pub fn from_bytes(bytes: &[u8]) -> Result<Self, super::path::PathError>
95 where
96 P: DecisionPath,
97 {
98 if bytes.len() < 36 {
99 return Err(super::path::PathError::InsufficientData {
100 expected: 36,
101 actual: bytes.len(),
102 });
103 }
104
105 let timestamp_ns = u64::from_le_bytes([
106 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
107 ]);
108
109 let sequence = u64::from_le_bytes([
110 bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
111 ]);
112
113 let input_hash = u64::from_le_bytes([
114 bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
115 ]);
116
117 let output = f32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]);
118
119 let latency_us = u32::from_le_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]);
120 let latency_ns = u64::from(latency_us) * 1000;
121
122 let path_length = u32::from_le_bytes([bytes[32], bytes[33], bytes[34], bytes[35]]) as usize;
123
124 if bytes.len() < 36 + path_length {
125 return Err(super::path::PathError::InsufficientData {
126 expected: 36 + path_length,
127 actual: bytes.len(),
128 });
129 }
130
131 let path = P::from_bytes(&bytes[36..36 + path_length])?;
132
133 Ok(Self { timestamp_ns, sequence, input_hash, path, output, latency_ns })
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::monitor::inference::path::LinearPath;
141
142 #[test]
143 fn test_decision_trace_new() {
144 let path = LinearPath::new(vec![0.5, -0.3], 0.1, 0.5, 0.87);
145 let trace = DecisionTrace::new(1000, 42, 0xdeadbeef, path, 0.87, 500);
146
147 assert_eq!(trace.timestamp_ns, 1000);
148 assert_eq!(trace.sequence, 42);
149 assert_eq!(trace.input_hash, 0xdeadbeef);
150 assert_eq!(trace.output, 0.87);
151 assert_eq!(trace.latency_ns, 500);
152 }
153
154 #[test]
155 fn test_decision_trace_confidence() {
156 let path = LinearPath::new(vec![0.5], 0.0, 0.0, 0.0).with_probability(0.9);
157 let trace = DecisionTrace::new(0, 0, 0, path, 0.0, 0);
158 assert!((trace.confidence() - 0.9).abs() < 1e-6);
159 }
160
161 #[test]
162 fn test_decision_trace_explain() {
163 let path = LinearPath::new(vec![0.5, -0.3], 0.1, 0.5, 0.87);
164 let trace = DecisionTrace::new(1000, 42, 0xdeadbeef, path, 0.87, 500);
165
166 let explanation = trace.explain();
167 assert!(explanation.contains("Trace #42"));
168 assert!(explanation.contains("0x00000000deadbeef"));
169 assert!(explanation.contains("Output: 0.87"));
170 }
171
172 #[test]
173 fn test_decision_trace_serialization_roundtrip() {
174 let path = LinearPath::new(vec![0.5, -0.3, 0.2], 0.1, 0.5, 0.87).with_probability(0.87);
175 let trace = DecisionTrace::new(1_000_000_000, 42, 0xdeadbeef12345678, path, 0.87, 500_000);
176
177 let bytes = trace.to_bytes();
178 let restored: DecisionTrace<LinearPath> =
179 DecisionTrace::from_bytes(&bytes).expect("Failed to deserialize");
180
181 assert_eq!(trace.timestamp_ns, restored.timestamp_ns);
182 assert_eq!(trace.sequence, restored.sequence);
183 assert_eq!(trace.input_hash, restored.input_hash);
184 assert!((trace.output - restored.output).abs() < 1e-6);
185 assert!((trace.latency_ns as i64 - restored.latency_ns as i64).abs() < 1000);
187 }
188
189 #[test]
190 fn test_decision_trace_feature_contributions() {
191 let path = LinearPath::new(vec![0.5, -0.3, 0.2], 0.0, 0.0, 0.0);
192 let trace = DecisionTrace::new(0, 0, 0, path, 0.0, 0);
193
194 let contributions = trace.feature_contributions();
195 assert_eq!(contributions.len(), 3);
196 assert!((contributions[0] - 0.5).abs() < 1e-6);
197 }
198
199 #[test]
200 fn test_decision_trace_insufficient_data() {
201 let result: Result<DecisionTrace<LinearPath>, _> = DecisionTrace::from_bytes(&[0; 10]);
202 assert!(result.is_err());
203 }
204}