use std::rc::Rc;
use bitflags::bitflags;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FrameKind {
Python,
Native,
Kernel,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SymbolOrigin {
Elf,
PerfMap,
KernelSymbols,
AddressOnly,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FrameFlags: u32 {
const PYTHON_RUNTIME = 1 << 0;
const HIDDEN_DEFAULT = 1 << 2;
const JIT = 1 << 3;
const TRUNCATED_STACK = 1 << 4;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocationInfo {
pub lineno: i32,
pub end_lineno: i32,
pub column: i32,
pub end_column: i32,
}
impl Default for LocationInfo {
fn default() -> Self {
const UNKNOWN: i32 = -1;
Self {
lineno: UNKNOWN,
end_lineno: UNKNOWN,
column: UNKNOWN,
end_column: UNKNOWN,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PythonFrame {
pub file_name: Rc<str>,
pub location: LocationInfo,
pub func_name: Rc<str>,
pub opcode: Option<u8>,
pub is_entry: bool,
pub basename_start: usize,
}
impl PythonFrame {
#[must_use]
pub fn new(
file_name: &str,
location: LocationInfo,
func_name: &str,
opcode: Option<u8>,
is_entry: bool,
) -> Self {
let basename_start = self::basename_start(file_name);
Self {
file_name: file_name.into(),
location,
func_name: func_name.into(),
opcode,
is_entry,
basename_start,
}
}
#[inline]
#[must_use]
pub fn basename(&self) -> &str {
&self.file_name[self.basename_start..]
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SourceLocation {
pub file: Option<Rc<str>>,
pub line: Option<u32>,
pub column: Option<u32>,
pub function_start_line: Option<u32>,
pub function_start_column: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeSymbol {
pub name: Rc<str>,
pub source: SourceLocation,
pub module: Rc<str>,
pub offset: u64,
pub inline_depth: u16,
pub is_eval_frame: bool,
pub should_ignore: bool,
}
impl NativeSymbol {
#[must_use]
pub fn new(
name: impl Into<Rc<str>>,
source: SourceLocation,
module: impl Into<Rc<str>>,
offset: u64,
is_eval_frame: bool,
should_ignore: bool,
) -> Self {
let module = module.into();
Self {
name: name.into(),
source,
module,
offset,
inline_depth: 0,
is_eval_frame,
should_ignore,
}
}
#[inline]
#[must_use]
pub fn module_basename(&self) -> &str {
&self.module[basename_start(&self.module)..]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeFrame {
pub pc: u64,
pub sp: u64,
pub symbol: Option<NativeSymbol>,
pub is_python_runtime: bool,
pub kind: FrameKind,
pub origin: SymbolOrigin,
pub flags: FrameFlags,
}
impl NativeFrame {
#[must_use]
pub fn from_address(pc: u64) -> Self {
Self {
pc,
sp: 0,
symbol: None,
is_python_runtime: false,
kind: FrameKind::Unknown,
origin: SymbolOrigin::AddressOnly,
flags: FrameFlags::empty(),
}
}
#[must_use]
pub fn truncated_stack_marker() -> Self {
Self {
pc: 0,
sp: 0,
symbol: Some(NativeSymbol::new(
"<stack truncated>",
SourceLocation::default(),
"",
0,
false,
false,
)),
is_python_runtime: false,
kind: FrameKind::Unknown,
origin: SymbolOrigin::AddressOnly,
flags: FrameFlags::TRUNCATED_STACK,
}
}
#[must_use]
pub fn func_name(&self) -> String {
self.symbol
.as_ref()
.map_or_else(|| format!("<0x{:x}>", self.pc), |s| s.name.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedFrame {
Python(PythonFrame),
Native(NativeFrame),
}
impl ResolvedFrame {
#[must_use]
pub fn func_name(&self) -> String {
match self {
Self::Python(frame) => frame.func_name.to_string(),
Self::Native(frame) => frame.func_name(),
}
}
}
#[inline]
#[must_use]
pub fn basename_start(path: &str) -> usize {
memchr::memrchr(b'/', path.as_bytes()).map_or(0, |i| i + 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_python_location_uses_documented_sentinel() {
assert_eq!(
LocationInfo::default(),
LocationInfo {
lineno: -1,
end_lineno: -1,
column: -1,
end_column: -1,
}
);
}
#[test]
fn python_frame_basename_handles_long_ascii_path() {
let path = format!("{}/leaf.py", "a".repeat(70_000));
let frame = PythonFrame::new(&path, LocationInfo::default(), "f", None, false);
assert_eq!(frame.basename_start, path.rfind('/').unwrap() + 1);
assert_eq!(frame.basename(), "leaf.py");
}
#[test]
fn python_frame_basename_handles_long_utf8_path() {
let path = format!("{}é/leaf.py", "a".repeat(65_534));
let frame = PythonFrame::new(&path, LocationInfo::default(), "f", None, false);
assert_eq!(frame.basename_start, path.rfind('/').unwrap() + 1);
assert_eq!(frame.basename(), "leaf.py");
}
#[test]
fn basename_start_reports_offsets_above_u16_max() {
let path = format!("{}/leaf.py", "a".repeat(70_000));
assert_eq!(basename_start(&path), path.rfind('/').unwrap() + 1);
}
#[test]
fn native_symbol_basename_follows_mutated_module_path() {
let mut symbol =
NativeSymbol::new("f", SourceLocation::default(), "/old/f.so", 0, false, false);
symbol.module = "new.so".into();
assert_eq!(symbol.module_basename(), "new.so");
}
}