Skip to main content

ringkernel_procint/analytics/
dfg_metrics.rs

1//! DFG metrics calculation.
2//!
3//! Computes graph metrics from Directly-Follows Graphs.
4
5use crate::models::{DFGGraph, GpuDFGEdge, GpuDFGNode};
6
7/// DFG metrics calculator.
8#[derive(Debug, Default)]
9pub struct DFGMetricsCalculator {
10    /// Last computed metrics.
11    pub metrics: DFGMetrics,
12}
13
14impl DFGMetricsCalculator {
15    /// Create a new calculator.
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    /// Calculate metrics from a DFG.
21    pub fn calculate(&mut self, dfg: &DFGGraph) -> &DFGMetrics {
22        let nodes = dfg.nodes();
23        let edges = dfg.edges();
24
25        self.metrics = DFGMetrics {
26            node_count: nodes.len(),
27            edge_count: edges.len(),
28            total_events: nodes.iter().map(|n| n.event_count as u64).sum(),
29            avg_events_per_node: self.avg_events_per_node(nodes),
30            density: self.graph_density(nodes.len(), edges.len()),
31            avg_degree: self.avg_degree(nodes, edges),
32            max_in_degree: self.max_in_degree(nodes),
33            max_out_degree: self.max_out_degree(nodes),
34            avg_duration_ms: self.avg_duration(nodes),
35            total_cost: 0.0, // Cost tracking moved to separate analytics
36            bottleneck_score: self.bottleneck_score(nodes),
37            parallelism_factor: self.parallelism_factor(edges),
38        };
39
40        &self.metrics
41    }
42
43    fn avg_events_per_node(&self, nodes: &[GpuDFGNode]) -> f32 {
44        if nodes.is_empty() {
45            return 0.0;
46        }
47        let total: u64 = nodes.iter().map(|n| n.event_count as u64).sum();
48        total as f32 / nodes.len() as f32
49    }
50
51    fn graph_density(&self, node_count: usize, edge_count: usize) -> f32 {
52        if node_count <= 1 {
53            return 0.0;
54        }
55        let max_edges = node_count * (node_count - 1);
56        edge_count as f32 / max_edges as f32
57    }
58
59    fn avg_degree(&self, nodes: &[GpuDFGNode], edges: &[GpuDFGEdge]) -> f32 {
60        if nodes.is_empty() {
61            return 0.0;
62        }
63        // Total degree = 2 * edges (each edge contributes to in and out degree)
64        2.0 * edges.len() as f32 / nodes.len() as f32
65    }
66
67    fn max_in_degree(&self, nodes: &[GpuDFGNode]) -> u32 {
68        nodes
69            .iter()
70            .map(|n| n.incoming_count as u32)
71            .max()
72            .unwrap_or(0)
73    }
74
75    fn max_out_degree(&self, nodes: &[GpuDFGNode]) -> u32 {
76        nodes
77            .iter()
78            .map(|n| n.outgoing_count as u32)
79            .max()
80            .unwrap_or(0)
81    }
82
83    fn avg_duration(&self, nodes: &[GpuDFGNode]) -> f32 {
84        if nodes.is_empty() {
85            return 0.0;
86        }
87        let total: f32 = nodes.iter().map(|n| n.avg_duration_ms).sum();
88        total / nodes.len() as f32
89    }
90
91    fn bottleneck_score(&self, nodes: &[GpuDFGNode]) -> f32 {
92        // Score based on variance in throughput rates (using degree counts)
93        if nodes.is_empty() {
94            return 0.0;
95        }
96
97        let rates: Vec<f32> = nodes
98            .iter()
99            .filter(|n| n.event_count > 0 && n.incoming_count > 0)
100            .map(|n| n.outgoing_count as f32 / n.incoming_count.max(1) as f32)
101            .collect();
102
103        if rates.is_empty() {
104            return 0.0;
105        }
106
107        let mean: f32 = rates.iter().sum::<f32>() / rates.len() as f32;
108        let variance: f32 =
109            rates.iter().map(|r| (r - mean).powi(2)).sum::<f32>() / rates.len() as f32;
110
111        // Normalize variance to 0-1 score
112        (variance / (variance + 1.0)).min(1.0)
113    }
114
115    fn parallelism_factor(&self, edges: &[GpuDFGEdge]) -> f32 {
116        // Estimate parallelism from split/join patterns
117        if edges.is_empty() {
118            return 1.0;
119        }
120
121        // Count activities with multiple outgoing edges
122        let mut out_counts: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
123        for edge in edges {
124            *out_counts.entry(edge.source_activity).or_insert(0) += 1;
125        }
126
127        let split_count = out_counts.values().filter(|&&c| c > 1).count();
128        1.0 + split_count as f32 * 0.5
129    }
130
131    /// Get current metrics.
132    pub fn metrics(&self) -> &DFGMetrics {
133        &self.metrics
134    }
135}
136
137/// Computed DFG metrics.
138#[derive(Debug, Clone, Default)]
139pub struct DFGMetrics {
140    /// Number of nodes (activities).
141    pub node_count: usize,
142    /// Number of edges (transitions).
143    pub edge_count: usize,
144    /// Total events processed.
145    pub total_events: u64,
146    /// Average events per node.
147    pub avg_events_per_node: f32,
148    /// Graph density (edges / max_edges).
149    pub density: f32,
150    /// Average node degree.
151    pub avg_degree: f32,
152    /// Maximum in-degree.
153    pub max_in_degree: u32,
154    /// Maximum out-degree.
155    pub max_out_degree: u32,
156    /// Average activity duration (ms).
157    pub avg_duration_ms: f32,
158    /// Total cost across all activities.
159    pub total_cost: f64,
160    /// Bottleneck score (0-1).
161    pub bottleneck_score: f32,
162    /// Parallelism factor.
163    pub parallelism_factor: f32,
164}
165
166impl DFGMetrics {
167    /// Get complexity score (combination of density and degree).
168    pub fn complexity_score(&self) -> f32 {
169        self.density * 0.4 + (self.avg_degree / 10.0).min(1.0) * 0.3 + self.bottleneck_score * 0.3
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_metrics_calculation() {
179        let mut dfg = DFGGraph::default();
180
181        // Add some test data
182        dfg.update_node(1, 100, 5000.0, 100.0);
183        dfg.update_node(2, 80, 3000.0, 80.0);
184        dfg.update_edge(1, 2, 50, 2000.0);
185
186        let mut calc = DFGMetricsCalculator::new();
187        let metrics = calc.calculate(&dfg);
188
189        assert!(metrics.node_count > 0);
190        assert!(metrics.total_events > 0);
191    }
192}