#[derive(Clone, Debug, PartialEq)]
pub enum TraceEventKind {
RuleEvalStart { rule_id: u64 },
RuleEvalEnd { rule_id: u64, matched: bool },
FactLookup { predicate: String, found: bool },
TensorRead { tensor_id: u64, size_bytes: u64 },
TensorWrite { tensor_id: u64, size_bytes: u64 },
InferenceStart { goal: String },
InferenceEnd { goal: String, success: bool },
}
#[derive(Clone, Debug, PartialEq)]
pub struct TraceEvent {
pub event_id: u64,
pub tick: u64,
pub kind: TraceEventKind,
pub session_id: u64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TraceSummary {
pub session_id: u64,
pub event_count: usize,
pub rule_evaluations: usize,
pub successful_rules: usize,
pub tensor_reads: u64,
pub tensor_writes: u64,
}
impl TraceSummary {
pub fn rule_match_rate(&self) -> f64 {
if self.rule_evaluations == 0 {
0.0
} else {
self.successful_rules as f64 / self.rule_evaluations as f64
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TracerConfig {
pub max_events: usize,
pub enabled: bool,
}
impl Default for TracerConfig {
fn default() -> Self {
Self {
max_events: 10_000,
enabled: true,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TracerStats {
pub total_events: usize,
pub total_sessions: usize,
pub dropped_events: u64,
}
pub struct TensorExecutionTracer {
pub events: Vec<TraceEvent>,
pub next_event_id: u64,
pub config: TracerConfig,
dropped_events: u64,
}
impl TensorExecutionTracer {
pub fn new(config: TracerConfig) -> Self {
Self {
events: Vec::new(),
next_event_id: 0,
config,
dropped_events: 0,
}
}
pub fn record(&mut self, session_id: u64, tick: u64, kind: TraceEventKind) {
if !self.config.enabled {
return;
}
if self.events.len() >= self.config.max_events {
self.events.remove(0);
self.dropped_events += 1;
}
self.events.push(TraceEvent {
event_id: self.next_event_id,
tick,
kind,
session_id,
});
self.next_event_id += 1;
}
pub fn events_for_session(&self, session_id: u64) -> Vec<&TraceEvent> {
self.events
.iter()
.filter(|e| e.session_id == session_id)
.collect()
}
pub fn summarize_session(&self, session_id: u64) -> TraceSummary {
let mut summary = TraceSummary {
session_id,
event_count: 0,
rule_evaluations: 0,
successful_rules: 0,
tensor_reads: 0,
tensor_writes: 0,
};
for event in self.events.iter().filter(|e| e.session_id == session_id) {
summary.event_count += 1;
match &event.kind {
TraceEventKind::RuleEvalStart { .. } => {
summary.rule_evaluations += 1;
}
TraceEventKind::RuleEvalEnd { matched, .. } if *matched => {
summary.successful_rules += 1;
}
TraceEventKind::RuleEvalEnd { .. } => {}
TraceEventKind::TensorRead { size_bytes, .. } => {
summary.tensor_reads += size_bytes;
}
TraceEventKind::TensorWrite { size_bytes, .. } => {
summary.tensor_writes += size_bytes;
}
_ => {}
}
}
summary
}
pub fn clear_session(&mut self, session_id: u64) {
self.events.retain(|e| e.session_id != session_id);
}
pub fn all_sessions(&self) -> Vec<u64> {
let mut ids: Vec<u64> = self.events.iter().map(|e| e.session_id).collect();
ids.sort_unstable();
ids.dedup();
ids
}
pub fn stats(&self) -> TracerStats {
let total_sessions = {
let mut ids: Vec<u64> = self.events.iter().map(|e| e.session_id).collect();
ids.sort_unstable();
ids.dedup();
ids.len()
};
TracerStats {
total_events: self.events.len(),
total_sessions,
dropped_events: self.dropped_events,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_tracer() -> TensorExecutionTracer {
TensorExecutionTracer::new(TracerConfig::default())
}
fn rule_start(rule_id: u64) -> TraceEventKind {
TraceEventKind::RuleEvalStart { rule_id }
}
fn rule_end(rule_id: u64, matched: bool) -> TraceEventKind {
TraceEventKind::RuleEvalEnd { rule_id, matched }
}
fn fact_lookup(predicate: &str, found: bool) -> TraceEventKind {
TraceEventKind::FactLookup {
predicate: predicate.to_string(),
found,
}
}
fn tensor_read(tensor_id: u64, size_bytes: u64) -> TraceEventKind {
TraceEventKind::TensorRead {
tensor_id,
size_bytes,
}
}
fn tensor_write(tensor_id: u64, size_bytes: u64) -> TraceEventKind {
TraceEventKind::TensorWrite {
tensor_id,
size_bytes,
}
}
fn inf_start(goal: &str) -> TraceEventKind {
TraceEventKind::InferenceStart {
goal: goal.to_string(),
}
}
fn inf_end(goal: &str, success: bool) -> TraceEventKind {
TraceEventKind::InferenceEnd {
goal: goal.to_string(),
success,
}
}
#[test]
fn test_new_starts_empty() {
let tracer = default_tracer();
assert!(tracer.events.is_empty());
assert_eq!(tracer.next_event_id, 0);
}
#[test]
fn test_record_adds_event() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(42));
assert_eq!(tracer.events.len(), 1);
assert_eq!(tracer.events[0].session_id, 1);
assert_eq!(tracer.events[0].tick, 0);
assert_eq!(tracer.events[0].kind, rule_start(42));
}
#[test]
fn test_record_noop_when_disabled() {
let mut tracer = TensorExecutionTracer::new(TracerConfig {
max_events: 100,
enabled: false,
});
tracer.record(1, 0, rule_start(1));
tracer.record(1, 1, rule_end(1, true));
assert!(tracer.events.is_empty());
assert_eq!(tracer.next_event_id, 0);
}
#[test]
fn test_record_evicts_oldest_at_max_events() {
let mut tracer = TensorExecutionTracer::new(TracerConfig {
max_events: 3,
enabled: true,
});
tracer.record(1, 0, rule_start(1));
tracer.record(1, 1, rule_start(2));
tracer.record(1, 2, rule_start(3));
tracer.record(1, 3, rule_start(4));
assert_eq!(tracer.events.len(), 3);
assert_eq!(tracer.events[0].event_id, 1); assert_eq!(tracer.events[2].event_id, 3); }
#[test]
fn test_record_increments_event_id() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.record(1, 1, rule_start(2));
assert_eq!(tracer.next_event_id, 2);
assert_eq!(tracer.events[0].event_id, 0);
assert_eq!(tracer.events[1].event_id, 1);
}
#[test]
fn test_events_for_session_order() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.record(2, 1, rule_start(99));
tracer.record(1, 2, rule_end(1, true));
let events = tracer.events_for_session(1);
assert_eq!(events.len(), 2);
assert_eq!(events[0].tick, 0);
assert_eq!(events[1].tick, 2);
}
#[test]
fn test_events_for_session_unknown() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
assert!(tracer.events_for_session(999).is_empty());
}
#[test]
fn test_summarize_event_count() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.record(1, 1, rule_end(1, false));
tracer.record(1, 2, fact_lookup("parent", true));
tracer.record(2, 3, rule_start(2)); let summary = tracer.summarize_session(1);
assert_eq!(summary.event_count, 3);
}
#[test]
fn test_summarize_rule_evaluations() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.record(1, 1, rule_start(2));
tracer.record(1, 2, rule_end(1, true));
let summary = tracer.summarize_session(1);
assert_eq!(summary.rule_evaluations, 2);
}
#[test]
fn test_summarize_successful_rules() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_end(1, true));
tracer.record(1, 1, rule_end(2, false));
tracer.record(1, 2, rule_end(3, true));
let summary = tracer.summarize_session(1);
assert_eq!(summary.successful_rules, 2);
}
#[test]
fn test_summarize_tensor_reads() {
let mut tracer = default_tracer();
tracer.record(1, 0, tensor_read(10, 1024));
tracer.record(1, 1, tensor_read(11, 2048));
let summary = tracer.summarize_session(1);
assert_eq!(summary.tensor_reads, 3072);
}
#[test]
fn test_summarize_tensor_writes() {
let mut tracer = default_tracer();
tracer.record(1, 0, tensor_write(10, 512));
tracer.record(1, 1, tensor_write(11, 256));
let summary = tracer.summarize_session(1);
assert_eq!(summary.tensor_writes, 768);
}
#[test]
fn test_rule_match_rate() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.record(1, 1, rule_start(2));
tracer.record(1, 2, rule_end(1, true));
tracer.record(1, 3, rule_end(2, false));
let summary = tracer.summarize_session(1);
assert!((summary.rule_match_rate() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_rule_match_rate_zero_when_no_evals() {
let tracer = default_tracer();
let summary = tracer.summarize_session(99);
assert_eq!(summary.rule_match_rate(), 0.0);
}
#[test]
fn test_clear_session() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.record(2, 1, rule_start(2));
tracer.record(1, 2, rule_end(1, true));
tracer.clear_session(1);
assert_eq!(tracer.events.len(), 1);
assert_eq!(tracer.events[0].session_id, 2);
}
#[test]
fn test_all_sessions_unique_sorted() {
let mut tracer = default_tracer();
tracer.record(3, 0, rule_start(1));
tracer.record(1, 1, rule_start(2));
tracer.record(3, 2, rule_end(1, false));
tracer.record(2, 3, fact_lookup("foo", true));
let sessions = tracer.all_sessions();
assert_eq!(sessions, vec![1, 2, 3]);
}
#[test]
fn test_stats_total_events() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.record(2, 1, rule_start(2));
assert_eq!(tracer.stats().total_events, 2);
}
#[test]
fn test_stats_total_sessions() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.record(2, 1, rule_start(2));
tracer.record(1, 2, rule_end(1, true));
assert_eq!(tracer.stats().total_sessions, 2);
}
#[test]
fn test_stats_dropped_events() {
let mut tracer = TensorExecutionTracer::new(TracerConfig {
max_events: 2,
enabled: true,
});
tracer.record(1, 0, rule_start(1));
tracer.record(1, 1, rule_start(2));
assert_eq!(tracer.stats().dropped_events, 0);
tracer.record(1, 2, rule_start(3)); assert_eq!(tracer.stats().dropped_events, 1);
tracer.record(1, 3, rule_start(4)); assert_eq!(tracer.stats().dropped_events, 2);
}
#[test]
fn test_multiple_sessions_interleaved() {
let mut tracer = default_tracer();
tracer.record(1, 0, inf_start("goal_a"));
tracer.record(2, 1, inf_start("goal_b"));
tracer.record(1, 2, tensor_read(5, 256));
tracer.record(2, 3, tensor_write(6, 512));
tracer.record(1, 4, inf_end("goal_a", true));
tracer.record(2, 5, inf_end("goal_b", false));
let s1 = tracer.summarize_session(1);
assert_eq!(s1.event_count, 3);
assert_eq!(s1.tensor_reads, 256);
let s2 = tracer.summarize_session(2);
assert_eq!(s2.event_count, 3);
assert_eq!(s2.tensor_writes, 512);
}
#[test]
fn test_fact_lookup_in_event_count() {
let mut tracer = default_tracer();
tracer.record(1, 0, fact_lookup("edge", true));
tracer.record(1, 1, fact_lookup("edge", false));
tracer.record(1, 2, fact_lookup("node", true));
let summary = tracer.summarize_session(1);
assert_eq!(summary.event_count, 3);
assert_eq!(summary.rule_evaluations, 0);
assert_eq!(summary.tensor_reads, 0);
assert_eq!(summary.tensor_writes, 0);
}
#[test]
fn test_config_disabled_suppresses_all_variants() {
let mut tracer = TensorExecutionTracer::new(TracerConfig {
max_events: 1000,
enabled: false,
});
tracer.record(1, 0, rule_start(1));
tracer.record(1, 1, rule_end(1, true));
tracer.record(1, 2, fact_lookup("p", false));
tracer.record(1, 3, tensor_read(7, 128));
tracer.record(1, 4, tensor_write(8, 64));
tracer.record(1, 5, inf_start("g"));
tracer.record(1, 6, inf_end("g", true));
assert!(tracer.events.is_empty());
assert_eq!(tracer.next_event_id, 0);
let stats = tracer.stats();
assert_eq!(stats.total_events, 0);
assert_eq!(stats.dropped_events, 0);
}
#[test]
fn test_summarize_unknown_session_zeros() {
let tracer = default_tracer();
let summary = tracer.summarize_session(42);
assert_eq!(summary.event_count, 0);
assert_eq!(summary.rule_evaluations, 0);
assert_eq!(summary.successful_rules, 0);
assert_eq!(summary.tensor_reads, 0);
assert_eq!(summary.tensor_writes, 0);
}
#[test]
fn test_all_sessions_empty() {
let tracer = default_tracer();
assert!(tracer.all_sessions().is_empty());
}
#[test]
fn test_event_ids_across_sessions() {
let mut tracer = default_tracer();
tracer.record(10, 0, rule_start(1));
tracer.record(20, 1, rule_start(2));
tracer.record(10, 2, rule_end(1, true));
assert_eq!(tracer.events[0].event_id, 0);
assert_eq!(tracer.events[1].event_id, 1);
assert_eq!(tracer.events[2].event_id, 2);
}
#[test]
fn test_clear_then_rerecord() {
let mut tracer = default_tracer();
tracer.record(1, 0, rule_start(1));
tracer.clear_session(1);
assert!(tracer.events.is_empty());
tracer.record(1, 1, rule_start(2));
assert_eq!(tracer.events.len(), 1);
assert_eq!(tracer.summarize_session(1).event_count, 1);
}
}