tramex_tools/interface/association/mod.rs
1//! Association module for parent/child trace relationships
2//!
3//! This module provides the logic for associating traces that are logically related
4//! (e.g., NAS messages encapsulated in RRC messages).
5
6mod matcher;
7mod relation;
8mod rules;
9
10pub use matcher::TraceMatcher;
11pub use relation::{AssociationStatus, TraceRelation};
12pub use rules::{AssociationRule, AssociationRules, NasToRrcRule, SearchDirection};
13
14use crate::data::Trace;
15
16/// Default lookback window for cross-batch association computation
17pub const ASSOCIATION_LOOKBACK_WINDOW: usize = 10;
18
19/// Compute associations for traces starting from a given index.
20/// This is a standalone function that can be used by any code that has a `&mut [Trace]`.
21///
22/// # Arguments
23/// * `events` - Mutable slice of traces to compute associations for
24/// * `rules` - The association rules to use for matching
25/// * `start_index` - Index to start computing from (useful for incremental batch processing)
26///
27/// # Note
28/// When processing new batches, pass `start_index = max(0, new_batch_start - LOOKBACK_WINDOW)`
29/// to handle cases where parent/child relationships span across batches.
30pub fn compute_associations(events: &mut [Trace], rules: &AssociationRules, start_index: usize) {
31 let trace_count = events.len();
32
33 for index in start_index..trace_count {
34 // Check if we're in the lookback window for recomputation
35 let in_lookback = index < start_index + ASSOCIATION_LOOKBACK_WINDOW;
36
37 // Reset relation if we're recomputing in lookback window
38 if in_lookback &&
39 let Some(trace) = events.get_mut(index) &&
40 // Only reset if it was NotFound (might find match in new batch)
41 matches!(trace.relation.parent, AssociationStatus::NotFound)
42 {
43 trace.relation.parent = AssociationStatus::NotComputed;
44 }
45
46 // Get the trace's layer
47 let layer = match events.get(index) {
48 Some(t) => t.layer.clone(),
49 None => continue,
50 };
51
52 // Find applicable rules for this layer
53 let applicable_rules = rules.rules_for_layer(&layer);
54
55 if applicable_rules.is_empty() {
56 // No rules for this layer - mark as not applicable
57 if let Some(trace) = events.get_mut(index) {
58 trace.relation.set_parent_not_applicable();
59 }
60 continue;
61 }
62
63 // Try each applicable rule
64 let mut found = false;
65 for rule in applicable_rules {
66 let status = TraceMatcher::find_relative(index, events, rule, &layer, &rule.target_layer());
67
68 // If found, set parent/child based on rule's direction
69 if let AssociationStatus::Found(target_indices) = status {
70 for &target_idx in &target_indices {
71 if rule.source_is_child() {
72 // Source is child, target is parent (e.g., NAS→RRC)
73 if let Some(trace) = events.get_mut(index) {
74 trace.relation.add_parent(target_idx);
75 }
76 if let Some(target_trace) = events.get_mut(target_idx) {
77 target_trace.relation.add_child(index);
78 }
79 } else {
80 // Source is parent, target is child (e.g., NGAP→NAS)
81 if let Some(trace) = events.get_mut(index) {
82 trace.relation.add_child(target_idx);
83 }
84 if let Some(target_trace) = events.get_mut(target_idx) {
85 target_trace.relation.add_parent(index);
86 }
87 }
88 }
89 found = true;
90 // Don't break - continue to run other rules for additional associations
91 }
92 }
93
94 // Mark as not found if no rules matched
95 if !found
96 && let Some(trace) = events.get_mut(index)
97 && !trace.relation.parent.is_computed()
98 {
99 trace.relation.parent = AssociationStatus::NotFound;
100 }
101 }
102}