denet 0.6.0

a simple process monitor
Documentation
//! eBPF-specific metrics structures

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// eBPF profiling metrics
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EbpfMetrics {
    /// Syscall frequency counts
    #[serde(skip_serializing_if = "Option::is_none")]
    pub syscalls: Option<SyscallMetrics>,

    /// Off-CPU profiling data
    #[serde(skip_serializing_if = "Option::is_none")]
    pub offcpu: Option<OffCpuMetrics>,

    /// Error message if eBPF collection failed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

impl EbpfMetrics {
    /// Create metrics with an error message
    pub fn error(message: &str) -> Self {
        Self {
            syscalls: None,
            offcpu: None,
            error: Some(message.to_string()),
        }
    }

    /// Create metrics with syscall data
    pub fn with_syscalls(syscalls: SyscallMetrics) -> Self {
        Self {
            syscalls: Some(syscalls),
            offcpu: None,
            error: None,
        }
    }

    /// Create metrics with off-CPU profiling data
    pub fn with_offcpu(offcpu: OffCpuMetrics) -> Self {
        Self {
            syscalls: None,
            offcpu: Some(offcpu),
            error: None,
        }
    }

    /// Create metrics with both syscalls and off-CPU data
    pub fn with_all(syscalls: SyscallMetrics, offcpu: OffCpuMetrics) -> Self {
        Self {
            syscalls: Some(syscalls),
            offcpu: Some(offcpu),
            error: None,
        }
    }

    /// Check if there's an error
    pub fn has_error(&self) -> bool {
        self.error.is_some()
    }
}

/// System call frequency metrics with enhanced analysis
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SyscallMetrics {
    /// Total number of syscalls
    pub total: u64,

    /// Syscalls by category
    pub by_category: HashMap<String, u64>,

    /// Top 10 most frequent individual syscalls
    pub top_syscalls: Vec<SyscallCount>,

    /// Enhanced syscall analysis for bottleneck diagnosis
    #[serde(skip_serializing_if = "Option::is_none")]
    pub analysis: Option<SyscallAnalysis>,
}

/// Raw syscall intensity metrics derived from observed syscall counts.
/// Classification is left to the caller.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyscallAnalysis {
    /// Syscalls per second rate
    pub syscall_rate_per_sec: f64,

    /// I/O intensity (0.0 to 1.0)
    pub io_intensity: f64,

    /// Memory management intensity (0.0 to 1.0)
    pub memory_intensity: f64,

    /// CPU-related syscall intensity (0.0 to 1.0)
    pub cpu_intensity: f64,

    /// Network activity intensity (0.0 to 1.0)
    pub network_intensity: f64,
}

/// Individual syscall count
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyscallCount {
    /// Syscall name
    pub name: String,
    /// Number of times called
    pub count: u64,
}

/// Syscall categories for grouping
pub const SYSCALL_CATEGORIES: &[(u64, &str)] = &[
    // File I/O
    (0, "read"),     // SYS_read
    (1, "write"),    // SYS_write
    (2, "open"),     // SYS_open
    (3, "close"),    // SYS_close
    (8, "lseek"),    // SYS_lseek
    (257, "openat"), // SYS_openat
    // Memory management
    (9, "mmap"),          // SYS_mmap
    (11, "munmap"),       // SYS_munmap
    (12, "brk"),          // SYS_brk
    (13, "rt_sigaction"), // SYS_rt_sigaction
    // Process/thread management
    (56, "clone"),  // SYS_clone
    (57, "fork"),   // SYS_fork
    (58, "vfork"),  // SYS_vfork
    (59, "execve"), // SYS_execve
    (60, "exit"),   // SYS_exit
    (61, "wait4"),  // SYS_wait4
    // Network
    (41, "socket"),   // SYS_socket
    (42, "connect"),  // SYS_connect
    (43, "accept"),   // SYS_accept
    (44, "sendto"),   // SYS_sendto
    (45, "recvfrom"), // SYS_recvfrom
    // Time/scheduling
    (35, "nanosleep"),      // SYS_nanosleep
    (96, "gettimeofday"),   // SYS_gettimeofday
    (201, "time"),          // SYS_time
    (228, "clock_gettime"), // SYS_clock_gettime
];

/// Get syscall name by number
pub fn syscall_name(syscall_nr: u64) -> String {
    SYSCALL_CATEGORIES
        .iter()
        .find(|(nr, _)| *nr == syscall_nr)
        .map(|(_, name)| name.to_string())
        .unwrap_or_else(|| format!("syscall_{}", syscall_nr))
}

/// Categorize syscalls into functional groups
///
/// Categorizes Linux syscalls based on their primary functionality:
/// - `file_io`: File and I/O operations
/// - `memory`: Memory allocation and management
/// - `process`: Process and thread management
/// - `network`: Network-related operations
/// - `time`: Time and scheduling operations
/// - `ipc`: Inter-process communication
/// - `security`: Permission and security operations
/// - `signal`: Signal handling
/// - `system`: System configuration and information
/// - `other`: Uncategorized syscalls
pub fn categorize_syscall(syscall_nr: u64) -> String {
    match syscall_nr {
        // File I/O operations
        0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 40 | 82 | 83 | 84
        | 85 | 86 | 87 | 88 | 89 | 90 | 132 | 133 | 187 | 188 | 189 | 190 | 257 | 258 | 259
        | 260 | 263 | 264 | 265 | 285 | 286 | 293 | 294 | 295 | 296 | 304 | 305 | 306 | 307 => {
            "file_io".to_string()
        }

        // Memory management
        9 | 10 | 11 | 12 | 15 | 25 | 26 | 27 | 28 | 158 | 159 | 160 | 213 | 214 | 215 | 216
        | 217 | 218 | 226 | 273 | 274 | 275 | 276 | 317 | 318 | 319 => "memory".to_string(),

        // Process/thread management
        56 | 57 | 58 | 59 | 60 | 61 | 62 | 224 | 231 | 232 | 233 | 234 | 235 | 236 | 246 | 266
        | 267 | 268 | 269 | 270 | 271 | 272 => "process".to_string(),

        // Network operations
        41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 198 | 199
        | 200 | 202 | 203 | 288 | 289 | 290 | 291 | 292 | 326 | 327 | 328 | 329 | 330 | 331
        | 332 => "network".to_string(),

        // Time and scheduling operations
        23 | 24 | 35 | 96 | 97 | 98 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 201 | 228
        | 229 | 230 | 249 | 252 | 277 | 278 | 279 | 280 => "time".to_string(),

        // Inter-process communication
        63..=81 => "ipc".to_string(),

        // Security, permissions, capabilities
        91 | 92 | 95 | 123 | 124 | 125 | 126 | 137 | 138 | 139 | 140 | 141 | 142 | 157 | 163
        | 164 | 165 | 166 | 281 | 282 | 283 | 284 => "security".to_string(),

        // Signal handling
        13 | 14 => "signal".to_string(),

        // System configuration and information
        99 | 100 | 101 | 102 | 103 | 153 | 154 | 155 | 156 | 168 | 169 | 170 | 171 | 172 | 173
        | 174 | 175 => "system".to_string(),

        // Unknown
        _ => "other".to_string(),
    }
}

use super::offcpu_profiler::{ProcessedOffCpuEvent, StackFrame};

/// Aggregated stack trace information for display
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AggregatedStacks {
    /// Aggregated user-space stack traces
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub user_stack: Vec<StackFrame>,

    /// Aggregated kernel-space stack traces
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub kernel_stack: Vec<StackFrame>,
}

/// Off-CPU profiling metrics
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OffCpuMetrics {
    /// Total time spent off-CPU (nanoseconds)
    pub total_time_ns: u64,

    /// Number of off-CPU events
    pub total_events: u64,

    /// Average time spent off-CPU (nanoseconds)
    pub avg_time_ns: u64,

    /// Maximum time spent off-CPU (nanoseconds)
    pub max_time_ns: u64,

    /// Minimum time spent off-CPU (nanoseconds)
    pub min_time_ns: u64,

    /// Thread-specific off-CPU statistics
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub thread_stats: HashMap<String, ThreadOffCpuStats>,

    /// Top threads ranked by total off-CPU time, descending (max 10 entries).
    ///
    /// Each entry is a summary across all off-CPU events for that thread since
    /// profiling started. `percentage` is the thread's share of the total
    /// off-CPU time accumulated across all monitored threads — not a share of
    /// wall-clock time.
    ///
    /// This is derived directly from the raw per-thread event counters in
    /// `thread_stats`, not from a separate analysis step.
    pub top_blocking_threads: Vec<ThreadOffCpuInfo>,

    /// Analysis of off-CPU bottlenecks
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub bottlenecks: Vec<String>,

    /// Symbolicated stack traces (very verbose, for debugging/export)
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub stack_traces: Vec<ProcessedOffCpuEvent>,

    /// Aggregated stack information (for display)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stacks: Option<AggregatedStacks>,
}

/// Thread-specific off-CPU statistics
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ThreadOffCpuStats {
    /// Thread ID
    pub tid: u32,

    /// Total time spent off-CPU (nanoseconds)
    pub total_time_ns: u64,

    /// Number of off-CPU events
    pub count: u64,

    /// Average time spent off-CPU (nanoseconds)
    pub avg_time_ns: u64,

    /// Maximum time spent off-CPU (nanoseconds)
    pub max_time_ns: u64,

    /// Minimum time spent off-CPU (nanoseconds)
    pub min_time_ns: u64,
}

/// One entry in `top_blocking_threads`: a per-thread off-CPU summary.
///
/// Appears in JSON as:
/// ```json
/// { "pid": 1234, "tid": 1235, "time_ms": 450.2, "percentage": 33.33 }
/// ```
///
/// `time_ms` is the cumulative off-CPU time for this thread since monitoring
/// started. `percentage` is its share of the combined off-CPU time across all
/// monitored threads (not of wall-clock time).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreadOffCpuInfo {
    /// Thread ID
    pub tid: u32,

    /// Process ID (TGID)
    pub pid: u32,

    /// Total time spent off-CPU in milliseconds
    #[serde(rename = "time_ms")]
    pub total_time_ms: f64,

    /// This thread's share of total off-CPU time across all monitored threads
    #[serde(serialize_with = "serialize_percentage_2dp")]
    pub percentage: f64,
}

/// Serialize a f64 percentage value with 2 decimal places
fn serialize_percentage_2dp<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    let rounded = (value * 100.0).round() / 100.0;
    serializer.serialize_f64(rounded)
}

/// Analysis of off-CPU patterns
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OffCpuAnalysis {
    /// Classification of what's causing the most off-CPU time
    pub bottleneck_type: OffCpuBottleneckType,

    /// Percentage of time spent in I/O-related waits
    pub io_wait_percentage: f64,

    /// Percentage of time spent in lock contention
    pub lock_contention_percentage: f64,

    /// Percentage of time spent in sleep/idle
    pub sleep_percentage: f64,

    /// Optimization suggestions
    pub optimization_hints: Vec<String>,
}

/// Classification of off-CPU bottlenecks
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum OffCpuBottleneckType {
    /// Blocking I/O operations
    IoBlocked,

    /// Lock contention
    LockContention,

    /// Voluntary sleep/yield
    Sleep,

    /// Various mixed causes
    Mixed,

    /// Unknown cause
    Unknown,
}

/// Compute raw syscall intensity ratios and rate from observed metrics.
pub fn generate_syscall_analysis(
    metrics: &SyscallMetrics,
    elapsed_seconds: f64,
) -> SyscallAnalysis {
    let total = metrics.total as f64;

    if total < 1.0 || elapsed_seconds < 0.1 {
        return SyscallAnalysis {
            syscall_rate_per_sec: 0.0,
            io_intensity: 0.0,
            memory_intensity: 0.0,
            cpu_intensity: 0.0,
            network_intensity: 0.0,
        };
    }

    SyscallAnalysis {
        syscall_rate_per_sec: total / elapsed_seconds,
        io_intensity: *metrics.by_category.get("file_io").unwrap_or(&0) as f64 / total,
        memory_intensity: *metrics.by_category.get("memory").unwrap_or(&0) as f64 / total,
        cpu_intensity: *metrics.by_category.get("process").unwrap_or(&0) as f64 / total,
        network_intensity: *metrics.by_category.get("network").unwrap_or(&0) as f64 / total,
    }
}