swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! Dumpable Stats - TickDumper 連携
//!
//! SwarmStats を Dumpable として公開し、デバッグ出力に含める。

use std::sync::{Arc, RwLock};

use crate::debug::Dumpable;

use super::swarm::SwarmStats;

/// SwarmStats を Dumpable として公開
///
/// TickDumper に登録することで、Tick 単位のデバッグ出力に統計情報を含められる。
pub struct DumpableStats {
    stats: Arc<RwLock<SwarmStats>>,
}

impl DumpableStats {
    pub fn new(stats: Arc<RwLock<SwarmStats>>) -> Self {
        Self { stats }
    }
}

impl Dumpable for DumpableStats {
    fn name(&self) -> &'static str {
        "swarm_stats"
    }

    fn snapshot(&self, _tick: u64) -> Option<serde_json::Value> {
        let stats = self.stats.read().ok()?;
        let global = stats.global();

        let action_stats: serde_json::Map<String, serde_json::Value> = stats
            .all_action_stats()
            .map(|(action, s)| {
                (
                    action.clone(),
                    serde_json::json!({
                        "visits": s.visits,
                        "successes": s.successes,
                        "failures": s.failures,
                        "success_rate": s.success_rate(),
                        "avg_duration_ms": s.avg_duration().as_millis(),
                    }),
                )
            })
            .collect();

        Some(serde_json::json!({
            "global": {
                "total_visits": global.total_visits,
                "total_successes": global.total_successes,
                "total_failures": global.total_failures,
                "success_rate": global.success_rate(),
            },
            "actions": action_stats,
        }))
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use super::*;
    use crate::events::{ActionEventBuilder, ActionEventResult};
    use crate::types::WorkerId;

    fn make_event(tick: u64, action: &str, success: bool) -> crate::events::ActionEvent {
        let result = if success {
            ActionEventResult::success()
        } else {
            ActionEventResult::failure("error")
        };

        ActionEventBuilder::new(tick, WorkerId(0), action)
            .result(result)
            .duration(Duration::from_millis(50))
            .build()
    }

    #[test]
    fn test_dumpable_stats() {
        let stats = Arc::new(RwLock::new(SwarmStats::new()));

        {
            let mut s = stats.write().unwrap();
            s.record(&make_event(1, "CheckStatus", true));
            s.record(&make_event(2, "ReadLogs", true));
            s.record(&make_event(3, "CheckStatus", false));
        }

        let dumpable = DumpableStats::new(Arc::clone(&stats));
        let snapshot = dumpable.snapshot(0).unwrap();

        assert_eq!(snapshot["global"]["total_visits"], 3);
        assert_eq!(snapshot["global"]["total_successes"], 2);
        assert!(snapshot["actions"]["CheckStatus"].is_object());
    }
}