use crate::{
debug_log, flush_debug_log, internal_doc,
mem_tracking::write_detailed_stack_alloc,
profiling::{clean_function_name, strip_hex_suffix_slice, Profile},
safe_alloc, ProfileError, ProfileResult,
};
use backtrace::{resolve_frame, trace};
use dashmap::{DashMap, DashSet};
use regex::Regex;
use std::{
clone::Clone, collections::HashSet, convert::AsRef, ops::Range, string::ToString, sync::Arc,
};
use thag_common::{re, static_lazy};
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
struct ProfileKey {
pub module: String,
pub function: String,
pub line_range: Range<u32>,
}
impl ProfileKey {
#[must_use]
pub const fn new(module: String, function: String, start_line: u32, end_line: u32) -> Self {
Self {
module,
function,
line_range: start_line..end_line,
}
}
#[must_use]
pub fn contains_line(&self, line: u32) -> bool {
self.line_range.contains(&line)
}
}
pub struct ProfileRegistry {
profiles: DashMap<ProfileKey, ProfileRef>,
instance_to_key: DashMap<u64, ProfileKey>,
modules: DashMap<String, ()>,
active_tasks: DashSet<usize>,
}
impl Default for ProfileRegistry {
fn default() -> Self {
Self::new()
}
}
impl ProfileRegistry {
#[must_use]
pub fn new() -> Self {
Self {
profiles: DashMap::new(),
instance_to_key: DashMap::new(),
modules: DashMap::new(),
active_tasks: DashSet::new(),
}
}
pub fn activate_task(&self, task_id: usize) {
self.active_tasks.insert(task_id); }
pub fn deactivate_task(&self, task_id: usize) {
self.active_tasks.remove(&task_id);
}
#[must_use]
pub fn get_last_active_task(&self) -> Option<usize> {
self.active_tasks.iter().map(|entry| *entry.key()).max()
}
pub fn register_profile(&self, profile_ref: &ProfileRef) -> ProfileResult<()> {
let instance_id = profile_ref.instance_id;
let profile = profile_ref
.profile()
.ok_or_else(|| ProfileError::General("No profile found for ProfileRef".to_string()))?;
let module_name = profile.file_name();
let start_line = profile.start_line();
let end_line = profile.end_line();
let key = ProfileKey::new(
module_name.to_string(),
profile.fn_name().to_string(),
start_line.unwrap_or(0),
end_line.unwrap_or(u32::MAX),
);
self.profiles.insert(key.clone(), profile_ref.clone());
self.instance_to_key.insert(instance_id, key);
self.modules.insert(module_name.to_string(), ());
Ok(())
}
pub fn deregister_profile(
&self,
instance_id: u64,
_file_name: &str, _function_name: &str, _start_line: Option<u32>, _end_line: Option<u32>, ) {
if let Some((_, key)) = self.instance_to_key.remove(&instance_id) {
self.profiles.remove(&key);
}
}
#[must_use]
#[allow(clippy::missing_panics_doc, reason = "checked start_line.is_some()")]
pub fn find_profile(&self, module: &str, function: &str, line: u32) -> Option<ProfileRef> {
let mut best_match: Option<(u32, ProfileRef)> = None;
for entry in &self.profiles {
let key = entry.key();
let profile_ref = entry.value();
if key.module == module && key.function == function && key.contains_line(line) {
let start_line = key.line_range.start;
match &best_match {
None => {
best_match = Some((start_line, profile_ref.clone()));
}
Some((best_start, _)) if start_line < *best_start => {
best_match = Some((start_line, profile_ref.clone()));
}
_ => {} }
}
}
best_match.map(|(_, profile_ref)| profile_ref)
}
#[must_use]
pub fn get_file_names(&self) -> Vec<String> {
self.modules
.iter()
.map(|entry| entry.key().clone())
.collect()
}
#[internal_doc]
#[allow(clippy::branches_sharing_code)]
pub fn record_allocation(
&self,
file_name: &str,
fn_name: &str,
line: u32,
size: usize,
) -> bool {
let profile_ref_opt = self.find_profile(file_name, fn_name, line);
if let Some(profile_ref) = profile_ref_opt {
if let Some(profile) = profile_ref.profile() {
if profile_ref.detailed_memory() {
let start_pattern: &Regex = re!("thag_profiler::mem_tracking.+Dispatcher");
let end_point = profile.fn_name();
let mut already_seen = HashSet::new();
let maybe_callstack: Option<Vec<String>> = safe_alloc! {
let capacity = 100;
let mut callstack: Vec<String> = Vec::with_capacity(capacity); let mut found_recursion = false;
let mut start = false;
let mut fin = false;
let mut i = 0;
trace(|frame| {
let mut suppress = false;
resolve_frame(frame, |symbol| {
'process_symbol: {
let Some(name) = symbol.name() else {
suppress = true;
break 'process_symbol;
};
let name = name.to_string();
if !start {
if start_pattern.is_match(&name) {
start = true;
}
suppress = true;
break 'process_symbol;
}
if name.contains(end_point) {
fin = true;
suppress = true;
break 'process_symbol;
}
let mut name = strip_hex_suffix_slice(&name);
let name = clean_function_name(&mut name);
if already_seen.contains(&name) {
suppress = true;
break 'process_symbol;
}
already_seen.insert(name.clone());
if suppress { break 'process_symbol; }
if i > 0 && name.contains("record_allocation") {
found_recursion = true;
break 'process_symbol;
}
callstack.push(name);
i += 1;
if i >= capacity {
safe_alloc! {
println!("frames={callstack:#?}");
};
panic!("Max limit of {capacity} frames exceeded");
}
}
});
!found_recursion && !fin
});
if found_recursion {
None } else {
Some(callstack)
}
};
let Some(callstack) = maybe_callstack else {
return false;
};
let detailed_stack = profile
.path()
.iter()
.cloned()
.chain(profile.section_name())
.chain(callstack.iter().rev().cloned())
.collect();
write_detailed_stack_alloc(size, false, &detailed_stack);
} else {
let _ = profile.record_allocation(size);
}
return true;
}
debug_log!(
"Profile reference contains an invalid profile pointer for {file_name}::{fn_name}"
);
} else {
debug_log!("No matching profile found for {file_name}::{fn_name} at line {line}");
}
false
}
}
#[internal_doc]
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ProfileRef {
name: String,
detailed_memory: bool,
instance_id: u64,
profile: Option<Arc<Profile>>,
dropping: bool,
}
impl ProfileRef {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn detailed_memory(&self) -> bool {
self.detailed_memory
}
#[must_use]
pub const fn instance_id(&self) -> u64 {
self.instance_id
}
#[must_use]
pub fn profile(&self) -> Option<&Profile> {
self.profile.as_ref().map(AsRef::as_ref)
}
}
type AllocationInfo = (Vec<String>, usize);
type AddressAllocMap = DashMap<usize, AllocationInfo>;
static_lazy! {
ProfileReg: ProfileRegistry = ProfileRegistry::new()
}
static_lazy! {
DetailedAddressRegistry: AddressAllocMap = DashMap::new()
}
static NEXT_PROFILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
#[must_use]
pub fn get_next_profile_id() -> u64 {
NEXT_PROFILE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
}
#[internal_doc]
pub fn register_profile(profile: &Profile) {
safe_alloc! {
flush_debug_log();
let profile_arc = Arc::new(profile.clone());
let instance_id = profile.instance_id();
let profile_ref = ProfileRef {
name: profile
.section_name()
.unwrap_or_else(|| profile.registered_name().to_string()),
detailed_memory: profile.detailed_memory(),
instance_id,
profile: Some(profile_arc),
dropping: false,
};
ProfileReg::get()
.register_profile(&profile_ref)
.expect("Error registering profile");
};
}
pub fn deregister_profile(profile: &Profile) {
static DEREGISTERING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if DEREGISTERING
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_ok()
{
let instance_id = profile.instance_id();
let file_name = safe_alloc!(profile.file_name().to_string());
let fn_name = safe_alloc!(profile.fn_name().to_string());
let start_line = profile.start_line();
let end_line = profile.end_line();
safe_alloc! {
{
ProfileReg::get().deregister_profile(
instance_id,
&file_name,
&fn_name,
start_line,
end_line,
);
}
};
DEREGISTERING.store(false, std::sync::atomic::Ordering::SeqCst);
} else {
debug_log!("Already deregistering a profile, skipping to avoid recursion");
}
}
#[internal_doc]
#[must_use]
pub fn find_profile(file_name: &str, fn_name: &str, line: u32) -> Option<ProfileRef> {
safe_alloc! {
ProfileReg::get().find_profile(file_name, fn_name, line)
}
}