1use std::sync::Mutex;
8
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11
12use crate::health::HealthStatus;
13use crate::service::{ServiceType, SystemService};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct CognitiveTickConfig {
22 pub tick_interval_ms: u32,
24 pub tick_budget_ratio: f32,
26 pub calibration_ticks: u32,
28 pub adaptive_tick: bool,
30 pub adaptive_window_s: u32,
32}
33
34impl Default for CognitiveTickConfig {
35 fn default() -> Self {
36 Self {
37 tick_interval_ms: 50,
38 tick_budget_ratio: 0.3,
39 calibration_ticks: 100,
40 adaptive_tick: true,
41 adaptive_window_s: 30,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CognitiveTickStats {
53 pub tick_count: u64,
55 pub current_interval_ms: u32,
57 pub avg_compute_us: u64,
59 pub max_compute_us: u64,
61 pub drift_count: u64,
63 pub running: bool,
65}
66
67struct CognitiveTickState {
72 tick_count: u64,
73 current_interval_ms: u32,
74 running: bool,
75 drift_count: u64,
76 recent_timings_us: Vec<u64>,
77 max_compute_us: u64,
78}
79
80pub struct CognitiveTick {
89 config: CognitiveTickConfig,
90 state: Mutex<CognitiveTickState>,
91}
92
93impl CognitiveTick {
94 pub fn new(config: CognitiveTickConfig) -> Self {
96 let interval = config.tick_interval_ms;
97 Self {
98 config,
99 state: Mutex::new(CognitiveTickState {
100 tick_count: 0,
101 current_interval_ms: interval,
102 running: false,
103 drift_count: 0,
104 recent_timings_us: Vec::new(),
105 max_compute_us: 0,
106 }),
107 }
108 }
109
110 pub fn with_interval(interval_ms: u32) -> Self {
112 Self::new(CognitiveTickConfig {
113 tick_interval_ms: interval_ms,
114 ..CognitiveTickConfig::default()
115 })
116 }
117
118 pub fn stats(&self) -> CognitiveTickStats {
120 let s = self.state.lock().unwrap();
121 let avg = if s.recent_timings_us.is_empty() {
122 0
123 } else {
124 let sum: u64 = s.recent_timings_us.iter().sum();
125 sum / s.recent_timings_us.len() as u64
126 };
127 CognitiveTickStats {
128 tick_count: s.tick_count,
129 current_interval_ms: s.current_interval_ms,
130 avg_compute_us: avg,
131 max_compute_us: s.max_compute_us,
132 drift_count: s.drift_count,
133 running: s.running,
134 }
135 }
136
137 pub fn record_tick(&self, compute_us: u64) {
146 let mut s = self.state.lock().unwrap();
147
148 s.tick_count += 1;
150
151 let window_size = self.window_capacity(s.current_interval_ms);
153 s.recent_timings_us.push(compute_us);
154 if s.recent_timings_us.len() > window_size {
155 let excess = s.recent_timings_us.len() - window_size;
156 s.recent_timings_us.drain(..excess);
157 }
158
159 if compute_us > s.max_compute_us {
161 s.max_compute_us = compute_us;
162 }
163
164 let budget_us =
166 (s.current_interval_ms as f32 * 1000.0 * self.config.tick_budget_ratio) as u64;
167 if compute_us > budget_us {
168 s.drift_count += 1;
169 }
170
171 if self.config.adaptive_tick && !s.recent_timings_us.is_empty() {
173 let avg: u64 =
174 s.recent_timings_us.iter().sum::<u64>() / s.recent_timings_us.len() as u64;
175 let upper_threshold = (budget_us as f64 * 1.1) as u64;
176 let lower_threshold = (budget_us as f64 * 0.5) as u64;
177
178 if avg > upper_threshold {
179 let new_interval = (s.current_interval_ms as f64 * 1.1).round() as u32;
181 s.current_interval_ms = new_interval;
182 } else if avg < lower_threshold {
183 let new_interval = (s.current_interval_ms as f64 * 0.9).round() as u32;
185 s.current_interval_ms = new_interval.max(10);
186 }
187 }
188 }
189
190 pub fn is_running(&self) -> bool {
192 self.state.lock().unwrap().running
193 }
194
195 pub fn set_running(&self, running: bool) {
197 self.state.lock().unwrap().running = running;
198 }
199
200 pub fn tick_count(&self) -> u64 {
202 self.state.lock().unwrap().tick_count
203 }
204
205 pub fn current_interval_ms(&self) -> u32 {
207 self.state.lock().unwrap().current_interval_ms
208 }
209
210 pub fn drift_count(&self) -> u64 {
212 self.state.lock().unwrap().drift_count
213 }
214
215 pub fn reset(&self) {
217 let mut s = self.state.lock().unwrap();
218 s.tick_count = 0;
219 s.current_interval_ms = self.config.tick_interval_ms;
220 s.running = false;
221 s.drift_count = 0;
222 s.recent_timings_us.clear();
223 s.max_compute_us = 0;
224 }
225
226 fn window_capacity(&self, interval_ms: u32) -> usize {
230 if interval_ms == 0 {
231 return 1;
232 }
233 let ticks_per_window = (self.config.adaptive_window_s * 1000) / interval_ms;
234 (ticks_per_window as usize).max(1)
235 }
236}
237
238#[async_trait]
243impl SystemService for CognitiveTick {
244 fn name(&self) -> &str {
245 "ecc.cognitive_tick"
246 }
247
248 fn service_type(&self) -> ServiceType {
249 ServiceType::Core
250 }
251
252 async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
253 self.set_running(true);
254 Ok(())
255 }
256
257 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
258 self.set_running(false);
259 Ok(())
260 }
261
262 async fn health_check(&self) -> HealthStatus {
263 if self.is_running() {
264 HealthStatus::Healthy
265 } else {
266 HealthStatus::Degraded("cognitive tick not running".into())
267 }
268 }
269}
270
271#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn default_config() {
281 let cfg = CognitiveTickConfig::default();
282 assert_eq!(cfg.tick_interval_ms, 50);
283 assert!((cfg.tick_budget_ratio - 0.3).abs() < f32::EPSILON);
284 assert_eq!(cfg.calibration_ticks, 100);
285 assert!(cfg.adaptive_tick);
286 assert_eq!(cfg.adaptive_window_s, 30);
287 }
288
289 #[test]
290 fn new_with_config() {
291 let cfg = CognitiveTickConfig {
292 tick_interval_ms: 100,
293 tick_budget_ratio: 0.5,
294 calibration_ticks: 200,
295 adaptive_tick: false,
296 adaptive_window_s: 60,
297 };
298 let ct = CognitiveTick::new(cfg.clone());
299 assert_eq!(ct.current_interval_ms(), 100);
300 assert!(!ct.is_running());
301 }
302
303 #[test]
304 fn with_interval() {
305 let ct = CognitiveTick::with_interval(75);
306 assert_eq!(ct.current_interval_ms(), 75);
307 assert!(ct.config.adaptive_tick);
309 assert_eq!(ct.config.calibration_ticks, 100);
310 }
311
312 #[test]
313 fn stats_initial() {
314 let ct = CognitiveTick::new(CognitiveTickConfig::default());
315 let s = ct.stats();
316 assert_eq!(s.tick_count, 0);
317 assert_eq!(s.current_interval_ms, 50);
318 assert_eq!(s.avg_compute_us, 0);
319 assert_eq!(s.max_compute_us, 0);
320 assert_eq!(s.drift_count, 0);
321 assert!(!s.running);
322 }
323
324 #[test]
325 fn record_tick_increments_count() {
326 let ct = CognitiveTick::new(CognitiveTickConfig::default());
327 ct.record_tick(100);
328 ct.record_tick(200);
329 ct.record_tick(300);
330 assert_eq!(ct.tick_count(), 3);
331 }
332
333 #[test]
334 fn record_tick_updates_max() {
335 let ct = CognitiveTick::new(CognitiveTickConfig::default());
336 ct.record_tick(100);
337 ct.record_tick(500);
338 ct.record_tick(200);
339 assert_eq!(ct.stats().max_compute_us, 500);
340 }
341
342 #[test]
343 fn is_running_default_false() {
344 let ct = CognitiveTick::new(CognitiveTickConfig::default());
345 assert!(!ct.is_running());
346 }
347
348 #[test]
349 fn set_running() {
350 let ct = CognitiveTick::new(CognitiveTickConfig::default());
351 ct.set_running(true);
352 assert!(ct.is_running());
353 ct.set_running(false);
354 assert!(!ct.is_running());
355 }
356
357 #[test]
358 fn tick_count() {
359 let ct = CognitiveTick::new(CognitiveTickConfig::default());
360 assert_eq!(ct.tick_count(), 0);
361 ct.record_tick(10);
362 assert_eq!(ct.tick_count(), 1);
363 }
364
365 #[test]
366 fn current_interval_ms() {
367 let ct = CognitiveTick::with_interval(42);
368 assert_eq!(ct.current_interval_ms(), 42);
369 }
370
371 #[test]
372 fn drift_detection() {
373 let mut cfg = CognitiveTickConfig::default();
375 cfg.adaptive_tick = false; let ct = CognitiveTick::new(cfg);
377
378 ct.record_tick(10_000);
380 assert_eq!(ct.drift_count(), 0);
381
382 ct.record_tick(15_000);
384 assert_eq!(ct.drift_count(), 0);
385
386 ct.record_tick(16_000);
388 assert_eq!(ct.drift_count(), 1);
389
390 ct.record_tick(20_000);
392 assert_eq!(ct.drift_count(), 2);
393 }
394
395 #[test]
396 fn adaptive_increase() {
397 let cfg = CognitiveTickConfig {
400 tick_interval_ms: 50,
401 tick_budget_ratio: 0.3,
402 calibration_ticks: 100,
403 adaptive_tick: true,
404 adaptive_window_s: 30,
405 };
406 let ct = CognitiveTick::new(cfg);
407
408 for _ in 0..20 {
410 ct.record_tick(20_000);
411 }
412
413 assert!(
415 ct.current_interval_ms() > 50,
416 "expected interval > 50, got {}",
417 ct.current_interval_ms()
418 );
419 }
420
421 #[test]
422 fn adaptive_decrease() {
423 let cfg = CognitiveTickConfig {
425 tick_interval_ms: 100,
426 tick_budget_ratio: 0.3,
427 calibration_ticks: 100,
428 adaptive_tick: true,
429 adaptive_window_s: 30,
430 };
431 let ct = CognitiveTick::new(cfg);
432
433 for _ in 0..20 {
435 ct.record_tick(1_000);
436 }
437
438 assert!(
440 ct.current_interval_ms() < 100,
441 "expected interval < 100, got {}",
442 ct.current_interval_ms()
443 );
444 }
445
446 #[test]
447 fn adaptive_min_interval() {
448 let cfg = CognitiveTickConfig {
450 tick_interval_ms: 12,
451 tick_budget_ratio: 0.3,
452 calibration_ticks: 100,
453 adaptive_tick: true,
454 adaptive_window_s: 30,
455 };
456 let ct = CognitiveTick::new(cfg);
457
458 for _ in 0..200 {
460 ct.record_tick(1);
461 }
462
463 assert!(
465 ct.current_interval_ms() >= 10,
466 "expected interval >= 10, got {}",
467 ct.current_interval_ms()
468 );
469 }
470
471 #[test]
472 fn reset_clears_stats() {
473 let ct = CognitiveTick::with_interval(80);
474 ct.set_running(true);
475 ct.record_tick(5_000);
476 ct.record_tick(50_000);
477
478 assert!(ct.tick_count() > 0);
480 assert!(ct.stats().max_compute_us > 0);
481 assert!(ct.is_running());
482
483 ct.reset();
484
485 assert_eq!(ct.tick_count(), 0);
486 assert_eq!(ct.stats().max_compute_us, 0);
487 assert_eq!(ct.stats().avg_compute_us, 0);
488 assert_eq!(ct.drift_count(), 0);
489 assert!(!ct.is_running());
490 assert_eq!(ct.current_interval_ms(), 80);
492 }
493
494 #[tokio::test]
495 async fn service_name_and_type() {
496 let ct = CognitiveTick::new(CognitiveTickConfig::default());
497 assert_eq!(ct.name(), "ecc.cognitive_tick");
498 assert_eq!(ct.service_type(), ServiceType::Core);
499 }
500
501 #[tokio::test]
502 async fn service_start_stop() {
503 let ct = CognitiveTick::new(CognitiveTickConfig::default());
504 assert!(!ct.is_running());
505
506 ct.start().await.unwrap();
507 assert!(ct.is_running());
508
509 ct.stop().await.unwrap();
510 assert!(!ct.is_running());
511 }
512
513 #[tokio::test]
514 async fn health_check_reflects_running() {
515 let ct = CognitiveTick::new(CognitiveTickConfig::default());
516 assert_eq!(
517 ct.health_check().await,
518 HealthStatus::Degraded("cognitive tick not running".into())
519 );
520
521 ct.start().await.unwrap();
522 assert_eq!(ct.health_check().await, HealthStatus::Healthy);
523 }
524
525 #[test]
526 fn config_serde_roundtrip() {
527 let cfg = CognitiveTickConfig::default();
528 let json = serde_json::to_string(&cfg).unwrap();
529 let restored: CognitiveTickConfig = serde_json::from_str(&json).unwrap();
530 assert_eq!(restored.tick_interval_ms, cfg.tick_interval_ms);
531 assert!((restored.tick_budget_ratio - cfg.tick_budget_ratio).abs() < f32::EPSILON);
532 }
533
534 #[test]
535 fn stats_serde_roundtrip() {
536 let ct = CognitiveTick::new(CognitiveTickConfig::default());
537 ct.record_tick(1234);
538 let stats = ct.stats();
539 let json = serde_json::to_string(&stats).unwrap();
540 let restored: CognitiveTickStats = serde_json::from_str(&json).unwrap();
541 assert_eq!(restored.tick_count, 1);
542 assert_eq!(restored.avg_compute_us, 1234);
543 }
544}