Skip to main content

feagi_npu_plasticity/
memory_stats_cache.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Global memory area statistics cache
5//!
6//! Architecture: Event-driven stats updates (NOT query-based)
7//! - Updated by PlasticityService when neurons created/deleted
8//! - Read by health check API (O(1) cache read, no queries)
9//! - Thread-safe via Arc<RwLock>
10
11use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::Arc;
15
16#[cfg(feature = "std")]
17use feagi_state_manager::StateManager;
18
19/// Statistics for a single memory cortical area
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct MemoryAreaStats {
22    /// Current number of active neurons in this memory area
23    pub neuron_count: usize,
24
25    /// Total neurons created since area was initialized
26    pub created_total: usize,
27
28    /// Total neurons deleted/expired since area was initialized
29    pub deleted_total: usize,
30
31    /// Last update timestamp (milliseconds since epoch)
32    pub last_updated: u64,
33}
34
35impl Default for MemoryAreaStats {
36    fn default() -> Self {
37        Self {
38            neuron_count: 0,
39            created_total: 0,
40            deleted_total: 0,
41            last_updated: std::time::SystemTime::now()
42                .duration_since(std::time::UNIX_EPOCH)
43                .unwrap()
44                .as_millis() as u64,
45        }
46    }
47}
48
49/// Global memory statistics cache
50/// Key: cortical_area_name (e.g., "mem_00")
51/// Value: MemoryAreaStats
52pub type MemoryStatsCache = Arc<RwLock<HashMap<String, MemoryAreaStats>>>;
53
54/// Create a new empty memory stats cache
55pub fn create_memory_stats_cache() -> MemoryStatsCache {
56    Arc::new(RwLock::new(HashMap::new()))
57}
58
59/// Update stats when a neuron is created
60pub fn on_neuron_created(cache: &MemoryStatsCache, area_name: &str) {
61    let mut stats = cache.write();
62    let area_stats = stats.entry(area_name.to_string()).or_default();
63    area_stats.neuron_count += 1;
64    area_stats.created_total += 1;
65    area_stats.last_updated = std::time::SystemTime::now()
66        .duration_since(std::time::UNIX_EPOCH)
67        .unwrap()
68        .as_millis() as u64;
69
70    #[cfg(feature = "std")]
71    if let Some(state_manager) = StateManager::instance().try_read() {
72        // @cursor:critical-path - Keep per-area neuron count synced for BV reads.
73        state_manager.add_cortical_area_neuron_count(area_name, 1);
74        state_manager.get_core_state().add_neuron_count(1);
75        state_manager.get_core_state().add_memory_neuron_count(1);
76    }
77}
78
79/// Update stats when a neuron is deleted/expired
80pub fn on_neuron_deleted(cache: &MemoryStatsCache, area_name: &str) {
81    let mut stats = cache.write();
82    if let Some(area_stats) = stats.get_mut(area_name) {
83        area_stats.neuron_count = area_stats.neuron_count.saturating_sub(1);
84        area_stats.deleted_total += 1;
85        area_stats.last_updated = std::time::SystemTime::now()
86            .duration_since(std::time::UNIX_EPOCH)
87            .unwrap()
88            .as_millis() as u64;
89    }
90
91    #[cfg(feature = "std")]
92    if let Some(state_manager) = StateManager::instance().try_read() {
93        // @cursor:critical-path - Keep per-area neuron count synced for BV reads.
94        state_manager.subtract_cortical_area_neuron_count(area_name, 1);
95        state_manager.get_core_state().subtract_neuron_count(1);
96        state_manager
97            .get_core_state()
98            .subtract_memory_neuron_count(1);
99    }
100}
101
102/// Initialize stats for a new memory area
103pub fn init_memory_area(cache: &MemoryStatsCache, area_name: &str) {
104    let mut stats = cache.write();
105    stats.entry(area_name.to_string()).or_default();
106
107    #[cfg(feature = "std")]
108    if let Some(state_manager) = StateManager::instance().try_read() {
109        // @cursor:critical-path - Memory areas start with zero neurons/synapses.
110        state_manager.init_cortical_area_stats(area_name);
111    }
112}
113
114/// Remove stats for a deleted memory area
115pub fn remove_memory_area(cache: &MemoryStatsCache, area_name: &str) {
116    let mut stats = cache.write();
117    stats.remove(area_name);
118}
119
120/// Get a snapshot of all memory area stats (for health check)
121pub fn get_stats_snapshot(cache: &MemoryStatsCache) -> HashMap<String, MemoryAreaStats> {
122    cache.read().clone()
123}
124
125/// Get stats for a specific memory area
126pub fn get_area_stats(cache: &MemoryStatsCache, area_name: &str) -> Option<MemoryAreaStats> {
127    cache.read().get(area_name).cloned()
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use std::sync::Mutex;
134
135    static CORE_STATE_LOCK: Mutex<()> = Mutex::new(());
136
137    #[test]
138    fn test_memory_stats_creation() {
139        let _lock = CORE_STATE_LOCK.lock().unwrap();
140        let cache = create_memory_stats_cache();
141
142        on_neuron_created(&cache, "mem_00");
143        on_neuron_created(&cache, "mem_00");
144        on_neuron_created(&cache, "mem_01");
145
146        let snapshot = get_stats_snapshot(&cache);
147        assert_eq!(snapshot.len(), 2);
148        assert_eq!(snapshot.get("mem_00").unwrap().neuron_count, 2);
149        assert_eq!(snapshot.get("mem_01").unwrap().neuron_count, 1);
150    }
151
152    #[test]
153    fn test_memory_stats_deletion() {
154        let _lock = CORE_STATE_LOCK.lock().unwrap();
155        let cache = create_memory_stats_cache();
156
157        on_neuron_created(&cache, "mem_00");
158        on_neuron_created(&cache, "mem_00");
159        on_neuron_deleted(&cache, "mem_00");
160
161        let stats = get_area_stats(&cache, "mem_00").unwrap();
162        assert_eq!(stats.neuron_count, 1);
163        assert_eq!(stats.created_total, 2);
164        assert_eq!(stats.deleted_total, 1);
165    }
166
167    #[test]
168    fn test_memory_area_removal() {
169        let _lock = CORE_STATE_LOCK.lock().unwrap();
170        let cache = create_memory_stats_cache();
171
172        on_neuron_created(&cache, "mem_00");
173        remove_memory_area(&cache, "mem_00");
174
175        assert!(get_area_stats(&cache, "mem_00").is_none());
176    }
177
178    #[cfg(feature = "std")]
179    #[test]
180    fn test_memory_stats_updates_core_state_counts() {
181        let _lock = CORE_STATE_LOCK.lock().unwrap();
182        let cache = create_memory_stats_cache();
183        let state_manager = StateManager::instance();
184        let state_manager = state_manager.read();
185        let core_state = state_manager.get_core_state();
186        let start_total = core_state.get_neuron_count();
187        let start_memory = core_state.get_memory_neuron_count();
188
189        on_neuron_created(&cache, "mem_00");
190        assert_eq!(core_state.get_neuron_count(), start_total + 1);
191        assert_eq!(core_state.get_memory_neuron_count(), start_memory + 1);
192
193        on_neuron_deleted(&cache, "mem_00");
194        assert_eq!(core_state.get_neuron_count(), start_total);
195        assert_eq!(core_state.get_memory_neuron_count(), start_memory);
196    }
197}