use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use regex::Regex;
use crate::plugins::diagnostics::DiagnosticsError;
use crate::plugins::diagnostics::DiagnosticsResult;
pub(crate) struct SymbolResolver {
binary_path: PathBuf,
content: String,
}
impl SymbolResolver {
pub(crate) async fn new(binary_path: PathBuf, input_path: &Path) -> DiagnosticsResult<Self> {
let content = std::fs::read_to_string(input_path).map_err(|e| {
DiagnosticsError::Internal(format!(
"Failed to read heap profile {}: {}",
input_path.to_string_lossy(),
e
))
})?;
Ok(Self {
binary_path,
content,
})
}
pub(crate) async fn enhance_heap_profile(&self, input_path: &Path) -> DiagnosticsResult<()> {
let (addresses, base_address) = self.extract_addresses_from_heap_profile()?;
if addresses.is_empty() {
return self.append_basic_symbol_section(input_path).await;
}
let symbols = self.resolve_symbols(&addresses, base_address).await?;
self.append_symbol_section(input_path, &symbols).await
}
fn extract_addresses_from_heap_profile(
&self,
) -> DiagnosticsResult<(HashSet<u64>, Option<u64>)> {
let addresses = self.extract_addresses(&self.content)?;
let base_address = self.extract_base_address(&self.content)?;
Ok((addresses, base_address))
}
fn extract_addresses(&self, content: &str) -> DiagnosticsResult<HashSet<u64>> {
let mut addresses = HashSet::new();
let hex_regex = Regex::new(r"(?m)(?:^|\s)(?:0x)?([0-9a-fA-F]+)(?:\s|$|-[0-9a-fA-F]+)")
.expect("regex must be valid");
for cap in hex_regex.captures_iter(content) {
if let Ok(addr) = u64::from_str_radix(&cap[1], 16) {
addresses.insert(addr);
}
}
Ok(addresses)
}
fn extract_base_address(&self, content: &str) -> DiagnosticsResult<Option<u64>> {
let base_regex = Regex::new(&format!(
r"(?m)^([0-9a-fA-F]+)-[0-9a-fA-F]+\s+.*\s+{}",
regex::escape(&self.binary_path.to_string_lossy())
))
.expect("regex must be valid");
if let Some(cap) = base_regex.captures(content)
&& let Ok(addr) = u64::from_str_radix(&cap[1], 16)
{
tracing::debug!(
"Parsed binary base address: 0x{:x} for binary: {}",
addr,
self.binary_path.to_string_lossy()
);
return Ok(Some(addr));
}
Ok(None)
}
async fn resolve_symbols(
&self,
addresses: &HashSet<u64>,
base_address: Option<u64>,
) -> DiagnosticsResult<HashMap<u64, String>> {
tracing::debug!(
"Creating symbol table for {} addresses from: {}",
addresses.len(),
self.binary_path.to_string_lossy()
);
if let Some(base) = base_address {
tracing::debug!("Using base address 0x{:x} for symbol resolution", base);
} else {
tracing::warn!("No base address found, symbol resolution may not work correctly");
}
let mut symbols = HashMap::new();
let loader_result = tokio::task::spawn_blocking({
let binary_path = self.binary_path.clone();
let addresses = addresses.clone();
move || {
let loader = addr2line::Loader::new(&binary_path)
.map_err(|e| format!("Failed to create addr2line loader: {}", e))?;
let mut resolved_symbols = HashMap::new();
let mut symbols_resolved = 0;
let mut symbols_failed = 0;
for &absolute_address in &addresses {
let address_for_resolution = base_address
.and_then(|base| absolute_address.checked_sub(base))
.unwrap_or(absolute_address);
match loader.find_symbol(address_for_resolution) {
Some(symbol_name) => {
let demangled_name = addr2line::demangle_auto(
std::borrow::Cow::Borrowed(symbol_name),
None, );
symbols_resolved += 1;
resolved_symbols.insert(absolute_address, demangled_name.to_string());
}
None => {
symbols_failed += 1;
resolved_symbols
.insert(absolute_address, format!("0x{:x}", absolute_address));
}
}
}
let total_addresses = addresses.len();
let success_rate = if total_addresses > 0 {
(symbols_resolved as f64 / total_addresses as f64) * 100.0
} else {
0.0
};
if symbols_resolved > 0 {
tracing::debug!(
"Symbol resolution successful for {}/{} addresses ({:.1}%)",
symbols_resolved,
total_addresses,
success_rate
);
} else {
tracing::debug!(
"No symbols resolved, all {}/{} addresses failed ({:.1}% success rate)",
symbols_failed,
total_addresses,
success_rate
);
}
Ok::<HashMap<u64, String>, String>(resolved_symbols)
}
})
.await;
match loader_result {
Ok(Ok(resolved_symbols)) => {
symbols = resolved_symbols;
}
Ok(Err(e)) => {
tracing::warn!(error = %e, "Address resolution failed, falling back to hex addresses");
for &address in addresses {
symbols.insert(address, format!("0x{:x}", address));
}
}
Err(e) => {
tracing::warn!(error = %e, "Address resolution failed, falling back to hex addresses");
for &address in addresses {
symbols.insert(address, format!("0x{:x}", address));
}
}
}
Ok(symbols)
}
async fn append_symbol_section(
&self,
input_path: &Path,
symbols: &HashMap<u64, String>,
) -> DiagnosticsResult<()> {
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
tracing::info!("Appending symbol section with {} symbols", symbols.len());
let mut file = OpenOptions::new()
.append(true)
.open(input_path)
.await
.map_err(|e| {
DiagnosticsError::Internal(format!("Failed to open file for appending: {}", e))
})?;
let mut symbol_content = String::new();
symbol_content.push_str("--- symbol\n");
symbol_content.push_str(&format!("binary={}\n", self.binary_path.to_string_lossy()));
let mut sorted_symbols: Vec<_> = symbols.iter().collect();
sorted_symbols.sort_by_key(|(addr, _)| **addr);
for (&address, symbol_name) in sorted_symbols.into_iter() {
symbol_content.push_str(&format!("0x{:x} {}\n", address, symbol_name));
}
symbol_content.push_str("---\n");
symbol_content.push_str("--- heap\n");
file.write_all(symbol_content.as_bytes())
.await
.map_err(|e| {
DiagnosticsError::Internal(format!("Failed to append symbol section: {}", e))
})?;
file.flush()
.await
.map_err(|e| DiagnosticsError::Internal(format!("Failed to flush file: {}", e)))?;
Ok(())
}
async fn append_basic_symbol_section(&self, input_path: &Path) -> DiagnosticsResult<()> {
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
let mut file = OpenOptions::new()
.append(true)
.open(input_path)
.await
.map_err(|e| {
DiagnosticsError::Internal(format!("Failed to open file for appending: {}", e))
})?;
let symbol_content = format!(
"--- symbol\nbinary={}\n---\n--- heap\n",
self.binary_path.to_string_lossy()
);
file.write_all(symbol_content.as_bytes())
.await
.map_err(|e| {
DiagnosticsError::Internal(format!("Failed to append basic symbol section: {}", e))
})?;
file.flush()
.await
.map_err(|e| DiagnosticsError::Internal(format!("Failed to flush file: {}", e)))?;
Ok(())
}
pub(crate) fn current_binary_path() -> DiagnosticsResult<PathBuf> {
std::env::current_exe().map_err(|e| {
DiagnosticsError::Internal(format!("Failed to get current binary path: {}", e))
})
}
}