orbit/component/
state_tracking.rs

1//! State change detection and tracking for Orbit components
2//!
3//! This module provides efficient state change detection, dirty checking,
4//! and state diff computation for optimized component updates.
5
6use std::collections::HashMap;
7use std::time::{Duration, Instant};
8
9use crate::component::{ComponentError, ComponentId};
10
11/// Represents a snapshot of component state at a point in time
12#[derive(Debug, Clone)]
13pub struct StateSnapshot {
14    /// Timestamp when this snapshot was taken
15    pub timestamp: Instant,
16    /// Hash of the state for quick comparison
17    pub state_hash: u64,
18    /// Detailed state fields for diff computation
19    pub fields: HashMap<String, StateValue>,
20}
21
22/// Represents different types of state values that can be tracked
23#[derive(Debug, Clone, PartialEq)]
24pub enum StateValue {
25    String(String),
26    Integer(i64),
27    Float(f64),
28    Boolean(bool),
29    Array(Vec<StateValue>),
30    Object(HashMap<String, StateValue>),
31    Null,
32}
33
34/// Represents a specific change to component state
35#[derive(Debug, Clone)]
36pub struct StateChange {
37    /// The field that changed
38    pub field_name: String,
39    /// Previous value
40    pub old_value: Option<StateValue>,
41    /// New value
42    pub new_value: StateValue,
43    /// When the change occurred
44    pub timestamp: Instant,
45    /// Priority of this change for batching
46    pub priority: ChangePriority,
47}
48
49/// Priority levels for state changes
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
51pub enum ChangePriority {
52    Low,
53    Normal,
54    High,
55    Critical,
56}
57
58/// Collection of state changes for batching
59#[derive(Debug, Clone)]
60pub struct StateChanges {
61    /// List of individual changes
62    pub changes: Vec<StateChange>,
63    /// When the batch was created
64    pub batch_timestamp: Instant,
65    /// Whether this batch requires immediate processing
66    pub immediate: bool,
67}
68
69/// Tracks state changes for a component with dirty checking optimization
70pub struct StateTracker {
71    /// ID of the component being tracked
72    component_id: ComponentId,
73    /// Previous state snapshot for comparison
74    previous_state: Option<StateSnapshot>,
75    /// Current state snapshot
76    current_state: Option<StateSnapshot>,
77    /// Batched changes pending processing
78    change_batch: Vec<StateChange>,
79    /// Dirty flags for performance optimization
80    dirty_fields: HashMap<String, bool>,
81    /// Configuration for change detection
82    config: StateTrackingConfig,
83}
84
85/// Configuration options for state tracking
86#[derive(Debug, Clone)]
87pub struct StateTrackingConfig {
88    /// Maximum time to batch changes before forcing flush
89    pub max_batch_time: Duration,
90    /// Maximum number of changes to batch
91    pub max_batch_size: usize,
92    /// Whether to use deep comparison for objects/arrays
93    pub deep_comparison: bool,
94    /// Minimum time between state snapshots
95    pub snapshot_throttle: Duration,
96}
97
98impl Default for StateTrackingConfig {
99    fn default() -> Self {
100        Self {
101            max_batch_time: Duration::from_millis(16), // ~60fps
102            max_batch_size: 50,
103            deep_comparison: true,
104            snapshot_throttle: Duration::from_millis(1),
105        }
106    }
107}
108
109impl StateSnapshot {
110    /// Create a new state snapshot
111    pub fn new(fields: HashMap<String, StateValue>) -> Self {
112        let state_hash = Self::compute_hash(&fields);
113        Self {
114            timestamp: Instant::now(),
115            state_hash,
116            fields,
117        }
118    }
119
120    /// Compute a hash of the state for quick comparison
121    fn compute_hash(fields: &HashMap<String, StateValue>) -> u64 {
122        use std::collections::hash_map::DefaultHasher;
123        use std::hash::{Hash, Hasher};
124
125        let mut hasher = DefaultHasher::new();
126
127        // Sort keys for consistent hashing
128        let mut sorted_keys: Vec<_> = fields.keys().collect();
129        sorted_keys.sort();
130
131        for key in sorted_keys {
132            key.hash(&mut hasher);
133            if let Some(value) = fields.get(key) {
134                value.hash(&mut hasher);
135            }
136        }
137
138        hasher.finish()
139    }
140
141    /// Compare with another snapshot to detect changes
142    pub fn diff(&self, other: &StateSnapshot) -> Vec<StateChange> {
143        let mut changes = Vec::new();
144        let now = Instant::now();
145
146        // Check for modified fields
147        for (field_name, new_value) in &other.fields {
148            let old_value = self.fields.get(field_name);
149
150            if old_value.map(|v| v != new_value).unwrap_or(true) {
151                changes.push(StateChange {
152                    field_name: field_name.clone(),
153                    old_value: old_value.cloned(),
154                    new_value: new_value.clone(),
155                    timestamp: now,
156                    priority: ChangePriority::Normal,
157                });
158            }
159        }
160
161        // Check for removed fields
162        for (field_name, old_value) in &self.fields {
163            if !other.fields.contains_key(field_name) {
164                changes.push(StateChange {
165                    field_name: field_name.clone(),
166                    old_value: Some(old_value.clone()),
167                    new_value: StateValue::Null,
168                    timestamp: now,
169                    priority: ChangePriority::Normal,
170                });
171            }
172        }
173
174        changes
175    }
176}
177
178impl std::hash::Hash for StateValue {
179    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
180        match self {
181            StateValue::String(s) => {
182                0u8.hash(state);
183                s.hash(state);
184            }
185            StateValue::Integer(i) => {
186                1u8.hash(state);
187                i.hash(state);
188            }
189            StateValue::Float(f) => {
190                2u8.hash(state);
191                f.to_bits().hash(state);
192            }
193            StateValue::Boolean(b) => {
194                3u8.hash(state);
195                b.hash(state);
196            }
197            StateValue::Array(arr) => {
198                4u8.hash(state);
199                arr.hash(state);
200            }
201            StateValue::Object(obj) => {
202                5u8.hash(state);
203                let mut sorted_keys: Vec<_> = obj.keys().collect();
204                sorted_keys.sort();
205                for key in sorted_keys {
206                    key.hash(state);
207                    if let Some(value) = obj.get(key) {
208                        value.hash(state);
209                    }
210                }
211            }
212            StateValue::Null => {
213                6u8.hash(state);
214            }
215        }
216    }
217}
218
219impl StateTracker {
220    /// Create a new state tracker for a component
221    pub fn new(component_id: ComponentId, config: StateTrackingConfig) -> Self {
222        Self {
223            component_id,
224            previous_state: None,
225            current_state: None,
226            change_batch: Vec::new(),
227            dirty_fields: HashMap::new(),
228            config,
229        }
230    }
231
232    /// Create a state tracker with default configuration
233    pub fn new_default(component_id: ComponentId) -> Self {
234        Self::new(component_id, StateTrackingConfig::default())
235    }
236    /// Update the current state and detect changes
237    pub fn update_state(
238        &mut self,
239        new_fields: HashMap<String, StateValue>,
240    ) -> Result<Option<StateChanges>, ComponentError> {
241        let new_snapshot = StateSnapshot::new(new_fields);
242
243        // Check if enough time has passed for a new snapshot
244        if let Some(ref current) = self.current_state {
245            if new_snapshot.timestamp.duration_since(current.timestamp)
246                < self.config.snapshot_throttle
247            {
248                return Ok(None);
249            }
250        }
251
252        // Detect changes if we have a previous state
253        let changes = if let Some(ref previous) = self.current_state {
254            previous.diff(&new_snapshot)
255        } else {
256            // First state - all fields are new
257            new_snapshot
258                .fields
259                .iter()
260                .map(|(field_name, value)| StateChange {
261                    field_name: field_name.clone(),
262                    old_value: None,
263                    new_value: value.clone(),
264                    timestamp: new_snapshot.timestamp,
265                    priority: ChangePriority::Normal,
266                })
267                .collect()
268        };
269
270        // Update state snapshots
271        self.previous_state = self.current_state.take();
272        self.current_state = Some(new_snapshot); // Add changes to batch
273        for change in changes {
274            self.dirty_fields.insert(change.field_name.clone(), true);
275            self.change_batch.push(change);
276        }
277
278        // Check if we should flush the batch
279        if self.should_flush_batch() {
280            Ok(Some(self.flush_batch()))
281        } else {
282            Ok(None)
283        }
284    }
285
286    /// Check if a specific field is dirty
287    pub fn is_field_dirty(&self, field_name: &str) -> bool {
288        self.dirty_fields.get(field_name).copied().unwrap_or(false)
289    }
290
291    /// Mark a field as clean
292    pub fn mark_field_clean(&mut self, field_name: &str) {
293        self.dirty_fields.insert(field_name.to_string(), false);
294    }
295
296    /// Get all dirty fields
297    pub fn get_dirty_fields(&self) -> Vec<String> {
298        self.dirty_fields
299            .iter()
300            .filter_map(
301                |(field, is_dirty)| {
302                    if *is_dirty {
303                        Some(field.clone())
304                    } else {
305                        None
306                    }
307                },
308            )
309            .collect()
310    }
311
312    /// Check if any fields are dirty
313    pub fn has_dirty_fields(&self) -> bool {
314        self.dirty_fields.values().any(|&dirty| dirty)
315    }
316    /// Force flush of current batch
317    pub fn flush_batch(&mut self) -> StateChanges {
318        StateChanges {
319            changes: std::mem::take(&mut self.change_batch),
320            batch_timestamp: Instant::now(),
321            immediate: false,
322        }
323
324        // Note: Don't clear dirty flags here - they should be cleared explicitly
325        // via mark_field_clean to allow fine-grained control
326    }
327
328    /// Check if batch should be flushed based on configuration
329    fn should_flush_batch(&self) -> bool {
330        if self.change_batch.is_empty() {
331            return false;
332        }
333
334        // Check batch size limit
335        if self.change_batch.len() >= self.config.max_batch_size {
336            return true;
337        }
338
339        // Check time limit
340        if let Some(oldest_change) = self.change_batch.first() {
341            if oldest_change.timestamp.elapsed() >= self.config.max_batch_time {
342                return true;
343            }
344        }
345
346        // Check for critical priority changes
347        self.change_batch
348            .iter()
349            .any(|change| change.priority == ChangePriority::Critical)
350    }
351
352    /// Get the component ID being tracked
353    pub fn component_id(&self) -> ComponentId {
354        self.component_id
355    }
356
357    /// Get current state snapshot
358    pub fn current_snapshot(&self) -> Option<&StateSnapshot> {
359        self.current_state.as_ref()
360    }
361
362    /// Get previous state snapshot
363    pub fn previous_snapshot(&self) -> Option<&StateSnapshot> {
364        self.previous_state.as_ref()
365    }
366
367    /// Clear all tracking data
368    pub fn clear(&mut self) {
369        self.previous_state = None;
370        self.current_state = None;
371        self.change_batch.clear();
372        self.dirty_fields.clear();
373    }
374}
375
376impl StateChanges {
377    /// Create a new state changes batch
378    pub fn new(changes: Vec<StateChange>, immediate: bool) -> Self {
379        Self {
380            changes,
381            batch_timestamp: Instant::now(),
382            immediate,
383        }
384    }
385
386    /// Check if this batch is empty
387    pub fn is_empty(&self) -> bool {
388        self.changes.is_empty()
389    }
390
391    /// Get the number of changes in this batch
392    pub fn len(&self) -> usize {
393        self.changes.len()
394    }
395
396    /// Get changes affecting a specific field
397    pub fn changes_for_field(&self, field_name: &str) -> Vec<&StateChange> {
398        self.changes
399            .iter()
400            .filter(|change| change.field_name == field_name)
401            .collect()
402    }
403
404    /// Check if this batch contains critical changes
405    pub fn has_critical_changes(&self) -> bool {
406        self.changes
407            .iter()
408            .any(|change| change.priority == ChangePriority::Critical)
409    }
410
411    /// Sort changes by priority
412    pub fn sort_by_priority(&mut self) {
413        self.changes.sort_by(|a, b| b.priority.cmp(&a.priority));
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_state_snapshot_creation() {
423        let mut fields = HashMap::new();
424        fields.insert("count".to_string(), StateValue::Integer(42));
425        fields.insert("name".to_string(), StateValue::String("test".to_string()));
426
427        let snapshot = StateSnapshot::new(fields);
428
429        assert_eq!(snapshot.fields.len(), 2);
430        assert_eq!(snapshot.fields.get("count"), Some(&StateValue::Integer(42)));
431        assert_eq!(
432            snapshot.fields.get("name"),
433            Some(&StateValue::String("test".to_string()))
434        );
435    }
436
437    #[test]
438    fn test_state_diff_detection() {
439        let mut fields1 = HashMap::new();
440        fields1.insert("count".to_string(), StateValue::Integer(1));
441        let snapshot1 = StateSnapshot::new(fields1);
442
443        let mut fields2 = HashMap::new();
444        fields2.insert("count".to_string(), StateValue::Integer(2));
445        let snapshot2 = StateSnapshot::new(fields2);
446
447        let changes = snapshot1.diff(&snapshot2);
448
449        assert_eq!(changes.len(), 1);
450        assert_eq!(changes[0].field_name, "count");
451        assert_eq!(changes[0].old_value, Some(StateValue::Integer(1)));
452        assert_eq!(changes[0].new_value, StateValue::Integer(2));
453    }
454    #[test]
455    fn test_state_tracker_dirty_fields() {
456        let component_id = ComponentId::new();
457        let mut tracker = StateTracker::new(
458            component_id,
459            StateTrackingConfig {
460                max_batch_size: 1, // Force immediate flush for testing
461                ..Default::default()
462            },
463        );
464
465        let mut fields = HashMap::new();
466        fields.insert("count".to_string(), StateValue::Integer(1));
467
468        let changes = tracker.update_state(fields).unwrap();
469
470        assert!(tracker.is_field_dirty("count"));
471        assert!(changes.is_some());
472
473        tracker.mark_field_clean("count");
474        assert!(!tracker.is_field_dirty("count"));
475    }
476    #[test]
477    fn test_batch_flushing() {
478        let component_id = ComponentId::new();
479        let mut tracker = StateTracker::new(
480            component_id,
481            StateTrackingConfig {
482                max_batch_size: 2,                          // Small batch size for testing
483                snapshot_throttle: Duration::from_nanos(1), // Very small throttle for testing
484                ..Default::default()
485            },
486        );
487
488        // Add first change
489        let mut fields1 = HashMap::new();
490        fields1.insert("count".to_string(), StateValue::Integer(1));
491        let changes1 = tracker.update_state(fields1).unwrap();
492        assert!(changes1.is_none()); // Should not flush yet
493
494        // Add second change - should trigger flush
495        let mut fields2 = HashMap::new();
496        fields2.insert("count".to_string(), StateValue::Integer(2));
497        let changes2 = tracker.update_state(fields2).unwrap();
498        assert!(changes2.is_some()); // Should flush now
499
500        let changes = changes2.unwrap();
501        assert_eq!(changes.changes.len(), 2);
502    }
503}