use super::{Collector, MetricValue, Metrics};
use crate::visualize::app::SyscallCategory;
use anyhow::Result;
use std::collections::HashMap;
use std::sync::mpsc::{Receiver, TryRecvError};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct SyscallEvent {
pub name: String,
pub duration_us: u64,
pub result: i64,
pub pid: i32,
pub source_file: Option<String>,
pub source_line: Option<u32>,
}
impl SyscallEvent {
pub fn new(name: &str, duration_us: u64) -> Self {
Self {
name: name.to_string(),
duration_us,
result: 0,
pid: 0,
source_file: None,
source_line: None,
}
}
pub fn with_result(mut self, result: i64) -> Self {
self.result = result;
self
}
pub fn with_pid(mut self, pid: i32) -> Self {
self.pid = pid;
self
}
pub fn with_source(mut self, file: &str, line: u32) -> Self {
self.source_file = Some(file.to_string());
self.source_line = Some(line);
self
}
pub fn is_error(&self) -> bool {
self.result < 0
}
pub fn category(&self) -> SyscallCategory {
SyscallCategory::from_name(&self.name)
}
}
#[derive(Debug, Clone, Default)]
pub struct SyscallStats {
pub count: u64,
pub errors: u64,
pub duration_sum: u64,
pub duration_min: u64,
pub duration_max: u64,
pub last_update: Option<Instant>,
}
impl SyscallStats {
pub fn update(&mut self, event: &SyscallEvent) {
self.count += 1;
if event.is_error() {
self.errors += 1;
}
self.duration_sum += event.duration_us;
self.duration_min = self.duration_min.min(event.duration_us);
self.duration_max = self.duration_max.max(event.duration_us);
self.last_update = Some(Instant::now());
}
pub fn avg_duration(&self) -> f64 {
if self.count == 0 {
0.0
} else {
self.duration_sum as f64 / self.count as f64
}
}
pub fn rate(&self, window: Duration) -> f64 {
if window.as_secs_f64() == 0.0 {
0.0
} else {
self.count as f64 / window.as_secs_f64()
}
}
pub fn reset(&mut self) {
self.count = 0;
self.errors = 0;
self.duration_sum = 0;
self.duration_min = u64::MAX;
self.duration_max = 0;
}
}
pub struct SyscallCollector {
rx: Option<Receiver<SyscallEvent>>,
stats: HashMap<String, SyscallStats>,
category_stats: HashMap<SyscallCategory, SyscallStats>,
window_start: Instant,
_interval: Duration,
total_count: u64,
total_errors: u64,
available: bool,
}
impl SyscallCollector {
pub fn new(rx: Receiver<SyscallEvent>) -> Self {
Self {
rx: Some(rx),
stats: HashMap::new(),
category_stats: HashMap::new(),
window_start: Instant::now(),
_interval: Duration::from_secs(1),
total_count: 0,
total_errors: 0,
available: true,
}
}
pub fn mock() -> Self {
Self {
rx: None,
stats: HashMap::new(),
category_stats: HashMap::new(),
window_start: Instant::now(),
_interval: Duration::from_secs(1),
total_count: 0,
total_errors: 0,
available: true,
}
}
pub fn inject(&mut self, event: SyscallEvent) {
self.process_event(&event);
}
fn process_event(&mut self, event: &SyscallEvent) {
self.stats.entry(event.name.clone()).or_default().update(event);
self.category_stats.entry(event.category()).or_default().update(event);
self.total_count += 1;
if event.is_error() {
self.total_errors += 1;
}
}
fn drain_events(&mut self) {
let events: Vec<SyscallEvent> = if let Some(ref rx) = self.rx {
let mut events = Vec::new();
loop {
match rx.try_recv() {
Ok(event) => events.push(event),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
self.available = false;
break;
}
}
}
events
} else {
Vec::new()
};
for event in events {
self.process_event(&event);
}
}
pub fn get_stats(&self, name: &str) -> Option<&SyscallStats> {
self.stats.get(name)
}
pub fn get_category_stats(&self, category: &SyscallCategory) -> Option<&SyscallStats> {
self.category_stats.get(category)
}
pub fn top_syscalls(&self, limit: usize) -> Vec<(&str, u64)> {
let mut syscalls: Vec<_> =
self.stats.iter().map(|(name, stats)| (name.as_str(), stats.count)).collect();
syscalls.sort_by(|a, b| b.1.cmp(&a.1));
syscalls.truncate(limit);
syscalls
}
pub fn total_rate(&self) -> f64 {
let elapsed = self.window_start.elapsed();
if elapsed.as_secs_f64() == 0.0 {
0.0
} else {
self.total_count as f64 / elapsed.as_secs_f64()
}
}
}
impl Collector for SyscallCollector {
fn collect(&mut self) -> Result<Metrics> {
self.drain_events();
let elapsed = self.window_start.elapsed();
let mut values = HashMap::new();
values.insert("syscall.total.rate".to_string(), MetricValue::Rate(self.total_rate()));
values.insert("syscall.total.count".to_string(), MetricValue::Counter(self.total_count));
values.insert("syscall.total.errors".to_string(), MetricValue::Counter(self.total_errors));
for (category, stats) in &self.category_stats {
let key = format!("syscall.category.{}.rate", category.name());
values.insert(key, MetricValue::Rate(stats.rate(elapsed)));
}
for (name, stats) in &self.stats {
let key = format!("syscall.{}.rate", name);
values.insert(key, MetricValue::Rate(stats.rate(elapsed)));
let key = format!("syscall.{}.avg_duration", name);
values.insert(key, MetricValue::Gauge(stats.avg_duration()));
}
Ok(Metrics::new(values))
}
fn is_available(&self) -> bool {
self.available
}
fn name(&self) -> &'static str {
"syscall"
}
fn reset(&mut self) {
self.stats.clear();
self.category_stats.clear();
self.window_start = Instant::now();
self.total_count = 0;
self.total_errors = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_syscall_event_new() {
let event = SyscallEvent::new("read", 100);
assert_eq!(event.name, "read");
assert_eq!(event.duration_us, 100);
assert_eq!(event.result, 0);
assert!(!event.is_error());
}
#[test]
fn test_syscall_event_with_error() {
let event = SyscallEvent::new("open", 50).with_result(-1);
assert!(event.is_error());
}
#[test]
fn test_syscall_event_with_source() {
let event = SyscallEvent::new("write", 200).with_source("main.c", 42);
assert_eq!(event.source_file, Some("main.c".to_string()));
assert_eq!(event.source_line, Some(42));
}
#[test]
fn test_syscall_event_category() {
assert_eq!(SyscallEvent::new("read", 0).category(), SyscallCategory::File);
assert_eq!(SyscallEvent::new("socket", 0).category(), SyscallCategory::Network);
assert_eq!(SyscallEvent::new("mmap", 0).category(), SyscallCategory::Memory);
assert_eq!(SyscallEvent::new("fork", 0).category(), SyscallCategory::Process);
}
#[test]
fn test_syscall_stats_update() {
let mut stats = SyscallStats::default();
stats.duration_min = u64::MAX;
let event1 = SyscallEvent::new("read", 100);
let event2 = SyscallEvent::new("read", 200).with_result(-1);
stats.update(&event1);
assert_eq!(stats.count, 1);
assert_eq!(stats.errors, 0);
assert_eq!(stats.duration_sum, 100);
stats.update(&event2);
assert_eq!(stats.count, 2);
assert_eq!(stats.errors, 1);
assert_eq!(stats.duration_sum, 300);
assert!((stats.avg_duration() - 150.0).abs() < f64::EPSILON);
}
#[test]
fn test_syscall_stats_reset() {
let mut stats = SyscallStats::default();
stats.count = 100;
stats.errors = 10;
stats.reset();
assert_eq!(stats.count, 0);
assert_eq!(stats.errors, 0);
}
#[test]
fn test_syscall_collector_mock() {
let mut collector = SyscallCollector::mock();
assert!(collector.is_available());
assert_eq!(collector.name(), "syscall");
collector.inject(SyscallEvent::new("read", 100));
collector.inject(SyscallEvent::new("write", 200));
collector.inject(SyscallEvent::new("read", 150));
let stats = collector.get_stats("read").unwrap();
assert_eq!(stats.count, 2);
let top = collector.top_syscalls(10);
assert_eq!(top[0].0, "read");
assert_eq!(top[0].1, 2);
}
#[test]
fn test_syscall_collector_collect() {
let mut collector = SyscallCollector::mock();
collector.inject(SyscallEvent::new("read", 100));
let metrics = collector.collect().unwrap();
assert!(metrics.values.contains_key("syscall.total.count"));
}
#[test]
fn test_syscall_collector_category_stats() {
let mut collector = SyscallCollector::mock();
collector.inject(SyscallEvent::new("read", 100));
collector.inject(SyscallEvent::new("open", 50));
collector.inject(SyscallEvent::new("socket", 200));
let file_stats = collector.get_category_stats(&SyscallCategory::File).unwrap();
assert_eq!(file_stats.count, 2);
let net_stats = collector.get_category_stats(&SyscallCategory::Network).unwrap();
assert_eq!(net_stats.count, 1);
}
#[test]
fn test_syscall_collector_reset() {
let mut collector = SyscallCollector::mock();
collector.inject(SyscallEvent::new("read", 100));
assert_eq!(collector.total_count, 1);
collector.reset();
assert_eq!(collector.total_count, 0);
assert!(collector.stats.is_empty());
}
}