1#[derive(Clone, Debug, PartialEq)]
6pub enum TraceEventKind {
7 RuleEvalStart { rule_id: u64 },
9 RuleEvalEnd { rule_id: u64, matched: bool },
11 FactLookup { predicate: String, found: bool },
13 TensorRead { tensor_id: u64, size_bytes: u64 },
15 TensorWrite { tensor_id: u64, size_bytes: u64 },
17 InferenceStart { goal: String },
19 InferenceEnd { goal: String, success: bool },
21}
22
23#[derive(Clone, Debug, PartialEq)]
25pub struct TraceEvent {
26 pub event_id: u64,
28 pub tick: u64,
30 pub kind: TraceEventKind,
32 pub session_id: u64,
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub struct TraceSummary {
39 pub session_id: u64,
41 pub event_count: usize,
43 pub rule_evaluations: usize,
45 pub successful_rules: usize,
47 pub tensor_reads: u64,
49 pub tensor_writes: u64,
51}
52
53impl TraceSummary {
54 pub fn rule_match_rate(&self) -> f64 {
58 if self.rule_evaluations == 0 {
59 0.0
60 } else {
61 self.successful_rules as f64 / self.rule_evaluations as f64
62 }
63 }
64}
65
66#[derive(Clone, Debug, PartialEq)]
68pub struct TracerConfig {
69 pub max_events: usize,
72 pub enabled: bool,
74}
75
76impl Default for TracerConfig {
77 fn default() -> Self {
78 Self {
79 max_events: 10_000,
80 enabled: true,
81 }
82 }
83}
84
85#[derive(Clone, Debug, PartialEq)]
87pub struct TracerStats {
88 pub total_events: usize,
90 pub total_sessions: usize,
92 pub dropped_events: u64,
94}
95
96pub struct TensorExecutionTracer {
102 pub events: Vec<TraceEvent>,
104 pub next_event_id: u64,
106 pub config: TracerConfig,
108 dropped_events: u64,
110}
111
112impl TensorExecutionTracer {
113 pub fn new(config: TracerConfig) -> Self {
115 Self {
116 events: Vec::new(),
117 next_event_id: 0,
118 config,
119 dropped_events: 0,
120 }
121 }
122
123 pub fn record(&mut self, session_id: u64, tick: u64, kind: TraceEventKind) {
129 if !self.config.enabled {
130 return;
131 }
132
133 if self.events.len() >= self.config.max_events {
134 self.events.remove(0);
135 self.dropped_events += 1;
136 }
137
138 self.events.push(TraceEvent {
139 event_id: self.next_event_id,
140 tick,
141 kind,
142 session_id,
143 });
144 self.next_event_id += 1;
145 }
146
147 pub fn events_for_session(&self, session_id: u64) -> Vec<&TraceEvent> {
149 self.events
150 .iter()
151 .filter(|e| e.session_id == session_id)
152 .collect()
153 }
154
155 pub fn summarize_session(&self, session_id: u64) -> TraceSummary {
157 let mut summary = TraceSummary {
158 session_id,
159 event_count: 0,
160 rule_evaluations: 0,
161 successful_rules: 0,
162 tensor_reads: 0,
163 tensor_writes: 0,
164 };
165
166 for event in self.events.iter().filter(|e| e.session_id == session_id) {
167 summary.event_count += 1;
168 match &event.kind {
169 TraceEventKind::RuleEvalStart { .. } => {
170 summary.rule_evaluations += 1;
171 }
172 TraceEventKind::RuleEvalEnd { matched, .. } if *matched => {
173 summary.successful_rules += 1;
174 }
175 TraceEventKind::RuleEvalEnd { .. } => {}
176 TraceEventKind::TensorRead { size_bytes, .. } => {
177 summary.tensor_reads += size_bytes;
178 }
179 TraceEventKind::TensorWrite { size_bytes, .. } => {
180 summary.tensor_writes += size_bytes;
181 }
182 _ => {}
183 }
184 }
185
186 summary
187 }
188
189 pub fn clear_session(&mut self, session_id: u64) {
191 self.events.retain(|e| e.session_id != session_id);
192 }
193
194 pub fn all_sessions(&self) -> Vec<u64> {
197 let mut ids: Vec<u64> = self.events.iter().map(|e| e.session_id).collect();
198 ids.sort_unstable();
199 ids.dedup();
200 ids
201 }
202
203 pub fn stats(&self) -> TracerStats {
205 let total_sessions = {
206 let mut ids: Vec<u64> = self.events.iter().map(|e| e.session_id).collect();
207 ids.sort_unstable();
208 ids.dedup();
209 ids.len()
210 };
211
212 TracerStats {
213 total_events: self.events.len(),
214 total_sessions,
215 dropped_events: self.dropped_events,
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 fn default_tracer() -> TensorExecutionTracer {
227 TensorExecutionTracer::new(TracerConfig::default())
228 }
229
230 fn rule_start(rule_id: u64) -> TraceEventKind {
231 TraceEventKind::RuleEvalStart { rule_id }
232 }
233
234 fn rule_end(rule_id: u64, matched: bool) -> TraceEventKind {
235 TraceEventKind::RuleEvalEnd { rule_id, matched }
236 }
237
238 fn fact_lookup(predicate: &str, found: bool) -> TraceEventKind {
239 TraceEventKind::FactLookup {
240 predicate: predicate.to_string(),
241 found,
242 }
243 }
244
245 fn tensor_read(tensor_id: u64, size_bytes: u64) -> TraceEventKind {
246 TraceEventKind::TensorRead {
247 tensor_id,
248 size_bytes,
249 }
250 }
251
252 fn tensor_write(tensor_id: u64, size_bytes: u64) -> TraceEventKind {
253 TraceEventKind::TensorWrite {
254 tensor_id,
255 size_bytes,
256 }
257 }
258
259 fn inf_start(goal: &str) -> TraceEventKind {
260 TraceEventKind::InferenceStart {
261 goal: goal.to_string(),
262 }
263 }
264
265 fn inf_end(goal: &str, success: bool) -> TraceEventKind {
266 TraceEventKind::InferenceEnd {
267 goal: goal.to_string(),
268 success,
269 }
270 }
271
272 #[test]
275 fn test_new_starts_empty() {
276 let tracer = default_tracer();
277 assert!(tracer.events.is_empty());
278 assert_eq!(tracer.next_event_id, 0);
279 }
280
281 #[test]
284 fn test_record_adds_event() {
285 let mut tracer = default_tracer();
286 tracer.record(1, 0, rule_start(42));
287 assert_eq!(tracer.events.len(), 1);
288 assert_eq!(tracer.events[0].session_id, 1);
289 assert_eq!(tracer.events[0].tick, 0);
290 assert_eq!(tracer.events[0].kind, rule_start(42));
291 }
292
293 #[test]
296 fn test_record_noop_when_disabled() {
297 let mut tracer = TensorExecutionTracer::new(TracerConfig {
298 max_events: 100,
299 enabled: false,
300 });
301 tracer.record(1, 0, rule_start(1));
302 tracer.record(1, 1, rule_end(1, true));
303 assert!(tracer.events.is_empty());
304 assert_eq!(tracer.next_event_id, 0);
305 }
306
307 #[test]
310 fn test_record_evicts_oldest_at_max_events() {
311 let mut tracer = TensorExecutionTracer::new(TracerConfig {
312 max_events: 3,
313 enabled: true,
314 });
315 tracer.record(1, 0, rule_start(1));
316 tracer.record(1, 1, rule_start(2));
317 tracer.record(1, 2, rule_start(3));
318 tracer.record(1, 3, rule_start(4));
320 assert_eq!(tracer.events.len(), 3);
321 assert_eq!(tracer.events[0].event_id, 1); assert_eq!(tracer.events[2].event_id, 3); }
324
325 #[test]
328 fn test_record_increments_event_id() {
329 let mut tracer = default_tracer();
330 tracer.record(1, 0, rule_start(1));
331 tracer.record(1, 1, rule_start(2));
332 assert_eq!(tracer.next_event_id, 2);
333 assert_eq!(tracer.events[0].event_id, 0);
334 assert_eq!(tracer.events[1].event_id, 1);
335 }
336
337 #[test]
340 fn test_events_for_session_order() {
341 let mut tracer = default_tracer();
342 tracer.record(1, 0, rule_start(1));
343 tracer.record(2, 1, rule_start(99));
344 tracer.record(1, 2, rule_end(1, true));
345 let events = tracer.events_for_session(1);
346 assert_eq!(events.len(), 2);
347 assert_eq!(events[0].tick, 0);
348 assert_eq!(events[1].tick, 2);
349 }
350
351 #[test]
354 fn test_events_for_session_unknown() {
355 let mut tracer = default_tracer();
356 tracer.record(1, 0, rule_start(1));
357 assert!(tracer.events_for_session(999).is_empty());
358 }
359
360 #[test]
363 fn test_summarize_event_count() {
364 let mut tracer = default_tracer();
365 tracer.record(1, 0, rule_start(1));
366 tracer.record(1, 1, rule_end(1, false));
367 tracer.record(1, 2, fact_lookup("parent", true));
368 tracer.record(2, 3, rule_start(2)); let summary = tracer.summarize_session(1);
370 assert_eq!(summary.event_count, 3);
371 }
372
373 #[test]
376 fn test_summarize_rule_evaluations() {
377 let mut tracer = default_tracer();
378 tracer.record(1, 0, rule_start(1));
379 tracer.record(1, 1, rule_start(2));
380 tracer.record(1, 2, rule_end(1, true));
381 let summary = tracer.summarize_session(1);
382 assert_eq!(summary.rule_evaluations, 2);
383 }
384
385 #[test]
388 fn test_summarize_successful_rules() {
389 let mut tracer = default_tracer();
390 tracer.record(1, 0, rule_end(1, true));
391 tracer.record(1, 1, rule_end(2, false));
392 tracer.record(1, 2, rule_end(3, true));
393 let summary = tracer.summarize_session(1);
394 assert_eq!(summary.successful_rules, 2);
395 }
396
397 #[test]
400 fn test_summarize_tensor_reads() {
401 let mut tracer = default_tracer();
402 tracer.record(1, 0, tensor_read(10, 1024));
403 tracer.record(1, 1, tensor_read(11, 2048));
404 let summary = tracer.summarize_session(1);
405 assert_eq!(summary.tensor_reads, 3072);
406 }
407
408 #[test]
411 fn test_summarize_tensor_writes() {
412 let mut tracer = default_tracer();
413 tracer.record(1, 0, tensor_write(10, 512));
414 tracer.record(1, 1, tensor_write(11, 256));
415 let summary = tracer.summarize_session(1);
416 assert_eq!(summary.tensor_writes, 768);
417 }
418
419 #[test]
422 fn test_rule_match_rate() {
423 let mut tracer = default_tracer();
424 tracer.record(1, 0, rule_start(1));
425 tracer.record(1, 1, rule_start(2));
426 tracer.record(1, 2, rule_end(1, true));
427 tracer.record(1, 3, rule_end(2, false));
428 let summary = tracer.summarize_session(1);
429 assert!((summary.rule_match_rate() - 0.5).abs() < f64::EPSILON);
431 }
432
433 #[test]
436 fn test_rule_match_rate_zero_when_no_evals() {
437 let tracer = default_tracer();
438 let summary = tracer.summarize_session(99);
439 assert_eq!(summary.rule_match_rate(), 0.0);
440 }
441
442 #[test]
445 fn test_clear_session() {
446 let mut tracer = default_tracer();
447 tracer.record(1, 0, rule_start(1));
448 tracer.record(2, 1, rule_start(2));
449 tracer.record(1, 2, rule_end(1, true));
450 tracer.clear_session(1);
451 assert_eq!(tracer.events.len(), 1);
452 assert_eq!(tracer.events[0].session_id, 2);
453 }
454
455 #[test]
458 fn test_all_sessions_unique_sorted() {
459 let mut tracer = default_tracer();
460 tracer.record(3, 0, rule_start(1));
461 tracer.record(1, 1, rule_start(2));
462 tracer.record(3, 2, rule_end(1, false));
463 tracer.record(2, 3, fact_lookup("foo", true));
464 let sessions = tracer.all_sessions();
465 assert_eq!(sessions, vec![1, 2, 3]);
466 }
467
468 #[test]
471 fn test_stats_total_events() {
472 let mut tracer = default_tracer();
473 tracer.record(1, 0, rule_start(1));
474 tracer.record(2, 1, rule_start(2));
475 assert_eq!(tracer.stats().total_events, 2);
476 }
477
478 #[test]
481 fn test_stats_total_sessions() {
482 let mut tracer = default_tracer();
483 tracer.record(1, 0, rule_start(1));
484 tracer.record(2, 1, rule_start(2));
485 tracer.record(1, 2, rule_end(1, true));
486 assert_eq!(tracer.stats().total_sessions, 2);
487 }
488
489 #[test]
492 fn test_stats_dropped_events() {
493 let mut tracer = TensorExecutionTracer::new(TracerConfig {
494 max_events: 2,
495 enabled: true,
496 });
497 tracer.record(1, 0, rule_start(1));
498 tracer.record(1, 1, rule_start(2));
499 assert_eq!(tracer.stats().dropped_events, 0);
500 tracer.record(1, 2, rule_start(3)); assert_eq!(tracer.stats().dropped_events, 1);
502 tracer.record(1, 3, rule_start(4)); assert_eq!(tracer.stats().dropped_events, 2);
504 }
505
506 #[test]
509 fn test_multiple_sessions_interleaved() {
510 let mut tracer = default_tracer();
511 tracer.record(1, 0, inf_start("goal_a"));
512 tracer.record(2, 1, inf_start("goal_b"));
513 tracer.record(1, 2, tensor_read(5, 256));
514 tracer.record(2, 3, tensor_write(6, 512));
515 tracer.record(1, 4, inf_end("goal_a", true));
516 tracer.record(2, 5, inf_end("goal_b", false));
517
518 let s1 = tracer.summarize_session(1);
519 assert_eq!(s1.event_count, 3);
520 assert_eq!(s1.tensor_reads, 256);
521
522 let s2 = tracer.summarize_session(2);
523 assert_eq!(s2.event_count, 3);
524 assert_eq!(s2.tensor_writes, 512);
525 }
526
527 #[test]
530 fn test_fact_lookup_in_event_count() {
531 let mut tracer = default_tracer();
532 tracer.record(1, 0, fact_lookup("edge", true));
533 tracer.record(1, 1, fact_lookup("edge", false));
534 tracer.record(1, 2, fact_lookup("node", true));
535 let summary = tracer.summarize_session(1);
536 assert_eq!(summary.event_count, 3);
537 assert_eq!(summary.rule_evaluations, 0);
539 assert_eq!(summary.tensor_reads, 0);
540 assert_eq!(summary.tensor_writes, 0);
541 }
542
543 #[test]
546 fn test_config_disabled_suppresses_all_variants() {
547 let mut tracer = TensorExecutionTracer::new(TracerConfig {
548 max_events: 1000,
549 enabled: false,
550 });
551 tracer.record(1, 0, rule_start(1));
552 tracer.record(1, 1, rule_end(1, true));
553 tracer.record(1, 2, fact_lookup("p", false));
554 tracer.record(1, 3, tensor_read(7, 128));
555 tracer.record(1, 4, tensor_write(8, 64));
556 tracer.record(1, 5, inf_start("g"));
557 tracer.record(1, 6, inf_end("g", true));
558 assert!(tracer.events.is_empty());
559 assert_eq!(tracer.next_event_id, 0);
560 let stats = tracer.stats();
561 assert_eq!(stats.total_events, 0);
562 assert_eq!(stats.dropped_events, 0);
563 }
564
565 #[test]
568 fn test_summarize_unknown_session_zeros() {
569 let tracer = default_tracer();
570 let summary = tracer.summarize_session(42);
571 assert_eq!(summary.event_count, 0);
572 assert_eq!(summary.rule_evaluations, 0);
573 assert_eq!(summary.successful_rules, 0);
574 assert_eq!(summary.tensor_reads, 0);
575 assert_eq!(summary.tensor_writes, 0);
576 }
577
578 #[test]
581 fn test_all_sessions_empty() {
582 let tracer = default_tracer();
583 assert!(tracer.all_sessions().is_empty());
584 }
585
586 #[test]
589 fn test_event_ids_across_sessions() {
590 let mut tracer = default_tracer();
591 tracer.record(10, 0, rule_start(1));
592 tracer.record(20, 1, rule_start(2));
593 tracer.record(10, 2, rule_end(1, true));
594 assert_eq!(tracer.events[0].event_id, 0);
595 assert_eq!(tracer.events[1].event_id, 1);
596 assert_eq!(tracer.events[2].event_id, 2);
597 }
598
599 #[test]
602 fn test_clear_then_rerecord() {
603 let mut tracer = default_tracer();
604 tracer.record(1, 0, rule_start(1));
605 tracer.clear_session(1);
606 assert!(tracer.events.is_empty());
607 tracer.record(1, 1, rule_start(2));
608 assert_eq!(tracer.events.len(), 1);
609 assert_eq!(tracer.summarize_session(1).event_count, 1);
610 }
611}