use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EbpfMetrics {
#[serde(skip_serializing_if = "Option::is_none")]
pub syscalls: Option<SyscallMetrics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offcpu: Option<OffCpuMetrics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl EbpfMetrics {
pub fn error(message: &str) -> Self {
Self {
syscalls: None,
offcpu: None,
error: Some(message.to_string()),
}
}
pub fn with_syscalls(syscalls: SyscallMetrics) -> Self {
Self {
syscalls: Some(syscalls),
offcpu: None,
error: None,
}
}
pub fn with_offcpu(offcpu: OffCpuMetrics) -> Self {
Self {
syscalls: None,
offcpu: Some(offcpu),
error: None,
}
}
pub fn with_all(syscalls: SyscallMetrics, offcpu: OffCpuMetrics) -> Self {
Self {
syscalls: Some(syscalls),
offcpu: Some(offcpu),
error: None,
}
}
pub fn has_error(&self) -> bool {
self.error.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SyscallMetrics {
pub total: u64,
pub by_category: HashMap<String, u64>,
pub top_syscalls: Vec<SyscallCount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub analysis: Option<SyscallAnalysis>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyscallAnalysis {
pub syscall_rate_per_sec: f64,
pub io_intensity: f64,
pub memory_intensity: f64,
pub cpu_intensity: f64,
pub network_intensity: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyscallCount {
pub name: String,
pub count: u64,
}
pub const SYSCALL_CATEGORIES: &[(u64, &str)] = &[
(0, "read"), (1, "write"), (2, "open"), (3, "close"), (8, "lseek"), (257, "openat"), (9, "mmap"), (11, "munmap"), (12, "brk"), (13, "rt_sigaction"), (56, "clone"), (57, "fork"), (58, "vfork"), (59, "execve"), (60, "exit"), (61, "wait4"), (41, "socket"), (42, "connect"), (43, "accept"), (44, "sendto"), (45, "recvfrom"), (35, "nanosleep"), (96, "gettimeofday"), (201, "time"), (228, "clock_gettime"), ];
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))
}
pub fn categorize_syscall(syscall_nr: u64) -> String {
match syscall_nr {
0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 32 | 33 | 40 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 132 | 133 | 187 | 188 | 189 | 190 | 217 | 257 | 258 | 259 | 260 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 285 | 286 | 294 | 295 | 296 | 303 | 304 | 306 => "file_io".to_string(),
9 | 10 | 11 | 12 | 25 | 26 | 27 | 28 | 158 | 213 | 214 | 215 | 216 | 218 | 237 | 238 | 239 | 273 | 274 | 275 | 276 | 318 | 319 => "memory".to_string(),
56 | 57 | 58 | 59 | 60 | 61 | 231 | 247 | 322 | 435 => "process".to_string(),
13 | 14 | 15 | 34 | 62 | 127 | 128 | 129 | 130 | 131 | 200 | 234 | 282 | 297 => "signal".to_string(),
22 | 29 | 30 | 31 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 202 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 283 | 284 | 293 => "ipc".to_string(),
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55
| 288 | 299 | 307 => "network".to_string(),
23 | 24 | 35 | 96 | 97 | 98 | 201 | 203 | 204 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 232 | 233 | 235 | 249 | 277 | 278 | 279 | 280 => "time".to_string(),
91 | 92 | 93 | 94 | 95 | 105 | 106 | 117 | 119 | 120 | 122 | 123 | 124 | 125 | 126 | 137 | 138 | 139 | 140 | 141 | 142 | 157 | 161 | 162 | 163 | 164 | 165 | 166 | 281 => "security".to_string(),
63 | 99 | 100 | 101 | 102 | 103 | 153 | 154 | 155 | 156 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 => "system".to_string(),
_ => "other".to_string(),
}
}
use super::offcpu_profiler::{ProcessedOffCpuEvent, StackFrame};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AggregatedStacks {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub user_stack: Vec<StackFrame>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub kernel_stack: Vec<StackFrame>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OffCpuMetrics {
pub total_time_ns: u64,
pub total_events: u64,
pub avg_time_ns: u64,
pub max_time_ns: u64,
pub min_time_ns: u64,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub thread_stats: HashMap<String, ThreadOffCpuStats>,
pub top_blocking_threads: Vec<ThreadOffCpuInfo>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub bottlenecks: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub stack_traces: Vec<ProcessedOffCpuEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stacks: Option<AggregatedStacks>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ThreadOffCpuStats {
pub tid: u32,
pub total_time_ns: u64,
pub count: u64,
pub avg_time_ns: u64,
pub max_time_ns: u64,
pub min_time_ns: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreadOffCpuInfo {
pub tid: u32,
pub pid: u32,
#[serde(rename = "time_ms")]
pub total_time_ms: f64,
#[serde(serialize_with = "serialize_percentage_2dp")]
pub percentage: f64,
}
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)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OffCpuAnalysis {
pub bottleneck_type: OffCpuBottleneckType,
pub io_wait_percentage: f64,
pub lock_contention_percentage: f64,
pub sleep_percentage: f64,
pub optimization_hints: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum OffCpuBottleneckType {
IoBlocked,
LockContention,
Sleep,
Mixed,
Unknown,
}
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,
}
}
#[cfg(test)]
mod tests {
use super::*;
const READ: u64 = 0;
const WRITE: u64 = 1;
const CLOSE: u64 = 3;
const FCNTL: u64 = 72;
const FLOCK: u64 = 73;
const FSYNC: u64 = 74;
const GETDENTS: u64 = 78;
const CHDIR: u64 = 80;
const OPENAT: u64 = 257;
const MMAP: u64 = 9;
const MUNMAP: u64 = 11;
const BRK: u64 = 12;
const CLONE: u64 = 56;
const FORK: u64 = 57;
const EXECVE: u64 = 59;
const EXIT: u64 = 60;
const EXIT_GROUP: u64 = 231;
const CLONE3: u64 = 435;
const SOCKET: u64 = 41;
const CONNECT: u64 = 42;
const SENDTO: u64 = 44;
const RECVFROM: u64 = 45;
const SENDMSG: u64 = 46;
const RECVMSG: u64 = 47;
const ACCEPT4: u64 = 288;
const SENDMMSG: u64 = 307;
const PIPE: u64 = 22;
const PIPE2: u64 = 293;
const FUTEX: u64 = 202;
const SHMGET: u64 = 29;
const SEMOP: u64 = 65;
const MSGSND: u64 = 69;
const RT_SIGACTION: u64 = 13;
const RT_SIGPROCMASK: u64 = 14;
const KILL: u64 = 62;
const TKILL: u64 = 200;
const TGKILL: u64 = 234;
const NANOSLEEP: u64 = 35;
const CLOCK_GETTIME: u64 = 228;
const CLOCK_NANOSLEEP: u64 = 230;
const SCHED_SETAFFINITY: u64 = 203;
const UNAME: u64 = 63;
fn cat(nr: u64) -> String {
categorize_syscall(nr)
}
#[test]
fn file_io_basic_rw() {
assert_eq!(cat(READ), "file_io");
assert_eq!(cat(WRITE), "file_io");
assert_eq!(cat(CLOSE), "file_io");
assert_eq!(cat(OPENAT), "file_io");
}
#[test]
fn regression_file_io_not_ipc() {
assert_eq!(cat(FCNTL), "file_io");
assert_eq!(cat(FLOCK), "file_io");
assert_eq!(cat(FSYNC), "file_io");
assert_eq!(cat(GETDENTS), "file_io");
assert_eq!(cat(CHDIR), "file_io");
}
#[test]
fn memory_basic() {
assert_eq!(cat(MMAP), "memory");
assert_eq!(cat(MUNMAP), "memory");
assert_eq!(cat(BRK), "memory");
}
#[test]
fn process_lifecycle() {
assert_eq!(cat(CLONE), "process");
assert_eq!(cat(FORK), "process");
assert_eq!(cat(EXECVE), "process");
assert_eq!(cat(EXIT), "process");
assert_eq!(cat(EXIT_GROUP), "process");
assert_eq!(cat(CLONE3), "process");
}
#[test]
fn network_full_socket_family() {
for nr in [
SOCKET, CONNECT, SENDTO, RECVFROM, SENDMSG, RECVMSG, ACCEPT4, SENDMMSG,
] {
assert_eq!(cat(nr), "network", "syscall {} should be network", nr);
}
}
#[test]
fn regression_futex_not_network() {
assert_eq!(cat(FUTEX), "ipc");
assert_ne!(cat(SCHED_SETAFFINITY), "network");
}
#[test]
fn ipc_primitives() {
assert_eq!(cat(PIPE), "ipc");
assert_eq!(cat(PIPE2), "ipc");
assert_eq!(cat(FUTEX), "ipc");
assert_eq!(cat(SHMGET), "ipc");
assert_eq!(cat(SEMOP), "ipc");
assert_eq!(cat(MSGSND), "ipc");
}
#[test]
fn signal_family() {
assert_eq!(cat(RT_SIGACTION), "signal");
assert_eq!(cat(RT_SIGPROCMASK), "signal");
assert_eq!(cat(KILL), "signal");
assert_eq!(cat(TKILL), "signal");
assert_eq!(cat(TGKILL), "signal");
}
#[test]
fn time_family() {
assert_eq!(cat(NANOSLEEP), "time");
assert_eq!(cat(CLOCK_GETTIME), "time");
assert_eq!(cat(CLOCK_NANOSLEEP), "time");
}
#[test]
fn system_uname() {
assert_eq!(cat(UNAME), "system");
}
#[test]
fn unknown_falls_back_to_other() {
assert_eq!(cat(9999), "other");
}
#[test]
fn categories_are_disjoint() {
let samples = [
(READ, "file_io"),
(MMAP, "memory"),
(CLONE, "process"),
(SOCKET, "network"),
(FUTEX, "ipc"),
(KILL, "signal"),
(NANOSLEEP, "time"),
(UNAME, "system"),
];
for (nr, expected) in samples {
assert_eq!(cat(nr), expected, "{} should map to {}", nr, expected);
}
}
}