use std::fmt::Write as _;
use std::path::Path;
use crate::live::probe::types::LostEvents;
const MAX_TRACE_LINE_BYTES: usize = 4096;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParsedRegisters {
pub rdi: u64,
pub rsi: u64,
pub rdx: u64,
pub rcx: u64,
pub r8: u64,
pub r9: u64,
pub rax: u64,
pub rsp: u64,
pub rbp: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParsedTraceEvent {
pub probe_index: usize,
pub tid: u32,
pub tgid: Option<u32>,
pub timestamp_ns: Option<u64>,
pub hit_ip: u64,
pub return_addr: Option<u64>,
pub regs: Option<ParsedRegisters>,
}
#[must_use]
pub fn format_uprobe_definition(
group: &str,
event_name: &str,
exec_path: &str,
file_offset: u64,
ra_fetch: Option<&str>,
want_regs: bool,
) -> String {
let mut def = format!("p:{group}/{event_name} {exec_path}:{file_offset:#x}");
if let Some(ra_expr) = ra_fetch {
let _ = write!(def, " ra={ra_expr}");
}
if want_regs {
def.push_str(
" rdi=%di:x64 rsi=%si:x64 rdx=%dx:x64 rcx=%cx:x64 r8=%r8:x64 r9=%r9:x64 rax=%ax:x64 rsp=%sp:x64 rbp=%bp:x64",
);
}
def.push('\n');
def
}
#[derive(Default)]
pub struct StreamingTraceLineBuffer {
partial: Vec<u8>,
discarding_overlong: bool,
}
impl StreamingTraceLineBuffer {
pub fn push_chunk(&mut self, chunk: &[u8], mut on_line: impl FnMut(&str)) {
for &byte in chunk {
if byte == b'\n' {
if !self.discarding_overlong && !self.partial.is_empty() {
emit_non_comment(&self.partial, &mut on_line);
}
self.partial.clear();
self.discarding_overlong = false;
} else if !self.discarding_overlong {
if self.partial.len() < MAX_TRACE_LINE_BYTES {
self.partial.push(byte);
} else {
self.partial.clear();
self.discarding_overlong = true;
}
}
}
}
pub fn finish(&mut self, mut on_line: impl FnMut(&str)) {
if !self.discarding_overlong && !self.partial.is_empty() {
emit_non_comment(&self.partial, &mut on_line);
}
self.partial.clear();
self.discarding_overlong = false;
}
}
fn emit_non_comment(raw: &[u8], on_line: &mut impl FnMut(&str)) {
let line = String::from_utf8_lossy(raw);
let trimmed = line.trim_end_matches('\r');
if !trimmed.trim_start().is_empty() && !trimmed.trim_start().starts_with('#') {
on_line(trimmed);
}
}
#[must_use]
pub fn parse_uprobe_trace_line(line: &str, event_prefix: &str) -> Option<ParsedTraceEvent> {
let marker = format!(": {event_prefix}");
let marker_pos = line.find(&marker)?;
let header = &line[..marker_pos];
let after_prefix = &line[marker_pos + marker.len()..];
let (idx_str, after_event) = after_prefix.split_once(": (")?;
let probe_index = idx_str.parse::<usize>().ok()?;
let (ip_str, args_tail) = after_event.split_once(')')?;
let hit_ip = parse_hex_or_dec_u64(ip_str.trim())?;
let (thread_id, group_id) = parse_header_tid_tgid(header)?;
let timestamp_ns = header.split_whitespace().last().and_then(parse_trace_timestamp_ns);
let mut return_addr = None;
let mut rdi = None;
let mut rsi = None;
let mut rdx = None;
let mut rcx = None;
let mut r8 = None;
let mut r9 = None;
let mut rax = None;
let mut rsp = None;
let mut rbp = None;
for token in args_tail.split_whitespace() {
let Some((key, val_str)) = token.split_once('=') else { continue };
let Some(val) = parse_hex_or_dec_u64(val_str) else { continue };
match key {
"ra" => return_addr = Some(val),
"rdi" => rdi = Some(val),
"rsi" => rsi = Some(val),
"rdx" => rdx = Some(val),
"rcx" => rcx = Some(val),
"r8" => r8 = Some(val),
"r9" => r9 = Some(val),
"rax" => rax = Some(val),
"rsp" => rsp = Some(val),
"rbp" => rbp = Some(val),
_ => {}
}
}
let regs = match (rdi, rsi, rdx, rcx, r8, r9, rax, rsp) {
(Some(rdi), Some(rsi), Some(rdx), Some(rcx), Some(r8), Some(r9), Some(rax), Some(rsp)) => {
Some(ParsedRegisters { rdi, rsi, rdx, rcx, r8, r9, rax, rsp, rbp })
}
_ => None,
};
Some(ParsedTraceEvent { probe_index, tid: thread_id, tgid: group_id, timestamp_ns, hit_ip, return_addr, regs })
}
fn parse_trace_timestamp_ns(token: &str) -> Option<u64> {
if let Some((sec_str, frac_str)) = token.split_once('.') {
let secs = sec_str.parse::<u64>().ok()?;
if frac_str.is_empty() || !frac_str.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let mut frac_ns = 0_u64;
for &byte in frac_str.as_bytes().iter().take(9) {
frac_ns = frac_ns.checked_mul(10)?.checked_add(u64::from(byte - b'0'))?;
}
let digits = frac_str.len().min(9);
for _ in digits..9 {
frac_ns = frac_ns.checked_mul(10)?;
}
secs.checked_mul(1_000_000_000)?.checked_add(frac_ns)
} else {
token.parse::<u64>().ok()
}
}
fn parse_header_tid_tgid(header: &str) -> Option<(u32, Option<u32>)> {
let cpu_bracket_pos = find_last_cpu_bracket(header)?;
let left_of_cpu = header[..cpu_bracket_pos].trim_end();
let (task_field, group_id) = if let Some(stripped_close) = left_of_cpu.strip_suffix(')')
&& let Some((before_paren, inside_paren)) = stripped_close.rsplit_once('(')
&& before_paren.ends_with(char::is_whitespace)
&& inside_paren.bytes().all(|b| b == b' ' || b == b'-' || b.is_ascii_digit())
{
let parsed_group = inside_paren.trim().parse::<u32>().ok();
(before_paren.trim_end(), parsed_group)
} else {
(left_of_cpu, None)
};
let (_comm, tid_str) = task_field.rsplit_once('-')?;
let thread_id = tid_str.trim().parse::<u32>().ok()?;
Some((thread_id, group_id))
}
fn find_last_cpu_bracket(header: &str) -> Option<usize> {
for (idx, _) in header.rmatch_indices(" [") {
let rest = &header[idx + 2..];
if let Some((digits, _after)) = rest.split_once(']')
&& !digits.is_empty()
&& digits.bytes().all(|b| b.is_ascii_digit())
{
return Some(idx);
}
}
None
}
fn parse_hex_or_dec_u64(text: &str) -> Option<u64> {
text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")).map_or_else(
|| text.parse::<u64>().or_else(|_| u64::from_str_radix(text, 16)).ok(),
|hex| u64::from_str_radix(hex, 16).ok(),
)
}
#[must_use]
pub fn parse_per_cpu_stats_dir(per_cpu_dir: &Path) -> LostEvents {
let entries = match std::fs::read_dir(per_cpu_dir) {
Ok(dir) => dir,
Err(err) => {
return LostEvents::Unknown { reason: format!("failed to read {}: {err}", per_cpu_dir.display()) };
}
};
let mut cpu_files = 0_usize;
let mut overrun = 0_u64;
let mut commit_overrun = 0_u64;
let mut dropped_events = 0_u64;
for entry_res in entries {
let entry = match entry_res {
Ok(e) => e,
Err(err) => return LostEvents::Unknown { reason: format!("per_cpu dir entry error: {err}") },
};
let name = entry.file_name();
let name_str = name.to_string_lossy();
if !name_str.starts_with("cpu") || !name_str[3..].bytes().all(|b| b.is_ascii_digit()) {
continue;
}
let stats_path = entry.path().join("stats");
let text = match std::fs::read_to_string(&stats_path) {
Ok(s) => s,
Err(err) => {
return LostEvents::Unknown { reason: format!("failed to read {}: {err}", stats_path.display()) };
}
};
cpu_files += 1;
let (c_overrun, c_commit, c_dropped) = match parse_single_cpu_stats_file(&stats_path, &text) {
Ok(vals) => vals,
Err(reason) => return LostEvents::Unknown { reason },
};
overrun = overrun.saturating_add(c_overrun);
commit_overrun = commit_overrun.saturating_add(c_commit);
dropped_events = dropped_events.saturating_add(c_dropped);
}
if cpu_files == 0 {
return LostEvents::Unknown { reason: format!("no cpu*/stats files found in {}", per_cpu_dir.display()) };
}
let total = overrun.saturating_add(commit_overrun).saturating_add(dropped_events);
LostEvents::Known { overrun, commit_overrun, dropped_events, perf_lost_records: 0, total }
}
fn parse_single_cpu_stats_file(stats_path: &Path, text: &str) -> Result<(u64, u64, u64), String> {
let mut seen_overrun: Option<u64> = None;
let mut seen_commit: Option<u64> = None;
let mut seen_dropped: Option<u64> = None;
for line in text.lines() {
let trimmed = line.trim();
if let Some(val) = trimmed.strip_prefix("overrun:") {
if seen_overrun.is_some() {
return Err(format!("duplicate overrun in {}", stats_path.display()));
}
let n = val.trim().parse::<u64>().map_err(|_| format!("malformed overrun in {}", stats_path.display()))?;
seen_overrun = Some(n);
} else if let Some(val) = trimmed.strip_prefix("commit overrun:") {
if seen_commit.is_some() {
return Err(format!("duplicate commit overrun in {}", stats_path.display()));
}
let n = val
.trim()
.parse::<u64>()
.map_err(|_| format!("malformed commit overrun in {}", stats_path.display()))?;
seen_commit = Some(n);
} else if let Some(val) = trimmed.strip_prefix("dropped events:") {
if seen_dropped.is_some() {
return Err(format!("duplicate dropped events in {}", stats_path.display()));
}
let n = val
.trim()
.parse::<u64>()
.map_err(|_| format!("malformed dropped events in {}", stats_path.display()))?;
seen_dropped = Some(n);
}
}
let Some(overrun) = seen_overrun else {
return Err(format!("missing overrun counter in {}", stats_path.display()));
};
let Some(commit_overrun) = seen_commit else {
return Err(format!("missing commit overrun counter in {}", stats_path.display()));
};
let Some(dropped_events) = seen_dropped else {
return Err(format!("missing dropped events counter in {}", stats_path.display()));
};
Ok((overrun, commit_overrun, dropped_events))
}