#![warn(missing_docs)]
#![allow(unused_variables)]
#![cfg_attr(
feature = "document-features",
cfg_attr(doc, doc = ::document_features::document_features!())
)]
mod errors;
mod logging;
pub mod profiling;
#[cfg_attr(not(feature = "internal_docs"), doc(hidden))]
#[cfg(feature = "full_profiling")]
pub mod mem_tracking;
#[cfg_attr(not(feature = "internal_docs"), doc(hidden))]
#[cfg(feature = "full_profiling")]
pub mod mem_attribution;
#[cfg(feature = "demo")]
pub mod visualization;
use std::{fmt::Display, fs, path::Path};
#[cfg(feature = "time_profiling")]
use std::sync::OnceLock;
pub use {
errors::{ProfileError, ProfileResult},
profiling::{
Profile, ProfileConfiguration, ProfileType, disable_profiling, extract_filename_timestamp,
is_profiling_enabled,
},
thag_common::{
V, init_verbosity, lazy_static_var, re, set_global_verbosity, set_verbosity, static_lazy,
warn_once,
},
thag_proc_macros::{fn_name, internal_doc},
};
#[internal_doc]
pub use profiling::{
clear_profile_config_cache, get_config_profile_type, get_global_profile_type,
is_detailed_memory, parse_env_profile_config, strip_hex_suffix_slice,
};
#[internal_doc]
pub use logging::{DebugLogger, flush_debug_log, get_debug_log_path};
#[internal_doc]
pub mod __private {
pub use thag_proc_macros::safe_alloc_private;
}
#[cfg(feature = "full_profiling")]
pub use {
mem_attribution::{ProfileRef, find_profile, register_profile},
mem_tracking::{
Allocator, Dispatcher, TaskGuard, TaskMemoryContext, TrackingAllocator, create_memory_task,
current_allocator, get_last_active_task, record_allocation,
},
};
#[cfg(feature = "full_profiling")]
#[internal_doc]
pub use profiling::extract_path;
#[cfg(feature = "demo")]
pub use {
thag_proc_macros::timing,
visualization::{AnalysisType, prompted_analysis},
};
#[cfg(feature = "full_profiling")]
pub use mem_tracking::{
compare_exchange_using_system, get_using_system, reset_allocator_state, set_using_system,
};
pub use thag_proc_macros::{enable_profiling, end, profile, profiled};
#[cfg(feature = "time_profiling")]
pub use profiling::PROFILING_MUTEX;
#[cfg(feature = "time_profiling")]
static PROFILER: OnceLock<Profiler> = OnceLock::new();
#[cfg(feature = "time_profiling")]
#[derive(Debug)]
struct Profiler {
base_location: &'static str,
}
#[cfg(feature = "time_profiling")]
impl Profiler {
const fn new(base_location: &'static str) -> Self {
Self { base_location }
}
}
#[cfg(feature = "time_profiling")]
#[internal_doc]
pub fn get_base_location() -> Option<&'static str> {
PROFILER.get().map(|profiler| profiler.base_location)
}
#[cfg(feature = "time_profiling")]
static PROFILEE: OnceLock<Profilee> = OnceLock::new();
#[cfg(feature = "time_profiling")]
#[derive(Debug)]
struct Profilee {
root_module: &'static str,
}
#[cfg(feature = "time_profiling")]
impl Profilee {
const fn new(root_module: &'static str) -> Self {
Self { root_module }
}
}
#[cfg(feature = "time_profiling")]
#[internal_doc]
pub fn get_root_module() -> Option<&'static str> {
PROFILEE.get().map(|profilee| profilee.root_module)
}
#[internal_doc]
#[must_use]
#[cfg(not(target_os = "windows"))]
pub fn file_stem_from_path_str(file_name: &str) -> String {
let fname_start = file_name.rfind('/').map_or_else(|| 0, |pos| pos + 1);
let fname_dot = file_name.rfind('.').unwrap_or(file_name.len());
file_name[fname_start..fname_dot].to_string()
}
#[internal_doc]
#[must_use]
#[cfg(target_os = "windows")]
pub fn file_stem_from_path_str(file_name: &str) -> String {
let fname_start = file_name
.rfind("\\\\")
.map(|pos| pos + 2)
.or_else(|| file_name.rfind('\\').map(|pos| pos + 1))
.unwrap_or(0);
let fname_dot = file_name.rfind('.').unwrap_or_else(|| file_name.len());
file_name[fname_start..fname_dot].to_string()
}
#[internal_doc]
#[cfg(not(feature = "full_profiling"))]
#[must_use]
pub fn file_stem_from_path(path: &Path) -> String {
path.file_stem().unwrap().to_string_lossy().to_string()
}
#[internal_doc]
#[cfg(feature = "full_profiling")]
#[must_use]
pub fn file_stem_from_path(path: &Path) -> String {
safe_alloc!(path.file_stem().unwrap().to_string_lossy().to_string())
}
pub fn enhance_svg_accessibility(svg_path: &str) -> ProfileResult<()> {
let content = fs::read_to_string(svg_path)?;
let enhanced = content.replace(
"opacity:0.1",
"opacity:0.5", );
fs::write(svg_path, enhanced)?;
Ok(())
}
#[cfg(feature = "time_profiling")]
pub const PROFILING_FEATURE_ENABLED: bool = true;
#[cfg(not(feature = "time_profiling"))]
pub const PROFILING_FEATURE_ENABLED: bool = false;
#[cfg(feature = "full_profiling")]
#[macro_export]
macro_rules! safe_alloc {
($($tt:tt)*) => {
{
use $crate::mem_tracking::{compare_exchange_using_system, set_using_system};
$crate::__private::safe_alloc_private!($($tt)*)
}
};
}
#[cfg(not(feature = "full_profiling"))]
#[macro_export]
macro_rules! safe_alloc {
($($tt:tt)*) => {
{ $($tt)* }
};
}
#[internal_doc]
pub fn thousands<T: Display>(n: T) -> String {
n.to_string()
.as_bytes()
.rchunks(3)
.rev()
.map(std::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.join(",")
}
#[internal_doc]
#[fn_name]
pub fn init_profiling(root_module: &'static str, profile_type: Option<ProfileType>) {
#[cfg(feature = "full_profiling")]
safe_alloc! {
if PROFILEE.get().is_none() {
PROFILEE.set(Profilee::new(root_module)).unwrap();
} else if PROFILEE.get().unwrap().root_module != root_module {
eprintln!(
"Warning: PROFILEE already set with root_module={}, not changing to {root_module}",
PROFILEE.get().unwrap().root_module,
);
}
set_base_location(file!(), fn_name, line!());
profiling::enable_profiling(true, profile_type)
.expect("Failed to enable profiling");
let global_profile_type = get_global_profile_type();
if global_profile_type == ProfileType::Time {
} else {
mem_tracking::initialize_memory_profiling();
}
};
#[cfg(all(feature = "time_profiling", not(feature = "full_profiling")))]
{
if PROFILEE.get().is_none() {
PROFILEE.set(Profilee::new(root_module)).unwrap();
} else if PROFILEE.get().unwrap().root_module != root_module {
eprintln!(
"Warning: PROFILEE already set with root_module={}, not changing to {}",
PROFILEE.get().unwrap().root_module,
root_module
);
}
set_base_location(file!(), fn_name, line!());
profiling::enable_profiling(true, profile_type).expect("Failed to enable profiling");
eprintln!("Exiting init_profiling");
}
#[cfg(feature = "time_profiling")]
{
unsafe extern "C" fn finalize_atexit() {
crate::finalize_profiling();
}
unsafe extern "C" {
fn atexit(func: unsafe extern "C" fn()) -> i32;
}
unsafe {
atexit(finalize_atexit);
}
}
#[cfg(not(feature = "time_profiling"))]
{}
}
#[cfg(feature = "time_profiling")]
fn set_base_location(file_name: &'static str, fn_name: &str, _line_no: u32) {
let base_loc = format!("{file_name}::{fn_name}");
if PROFILER.get().is_none() {
let base_location = Box::leak(base_loc.into_boxed_str());
PROFILER.set(Profiler::new(base_location)).unwrap();
} else if PROFILER.get().unwrap().base_location != base_loc {
eprintln!(
"Warning: PROFILER already set with base_location={}, not changing to {}",
PROFILER.get().unwrap().base_location,
base_loc
);
}
}
#[internal_doc]
#[allow(clippy::missing_const_for_fn)]
pub fn finalize_profiling() {
#[cfg(feature = "time_profiling")]
if profiling::PROFILING_FINALIZED.swap(true, std::sync::atomic::Ordering::SeqCst) {
return;
}
#[cfg(all(feature = "time_profiling", not(feature = "full_profiling")))]
{
flush_debug_log();
disable_profiling();
let global_profile_type = get_global_profile_type();
if matches!(global_profile_type, ProfileType::Time | ProfileType::Both) {
if let Err(e) = profiling::process_time_profile() {
debug_log!("Error converting time profile to exclusive time: {e:?}");
}
}
flush_debug_log();
std::thread::sleep(std::time::Duration::from_millis(10));
}
#[cfg(feature = "full_profiling")]
safe_alloc! {
disable_profiling();
let global_profile_type = get_global_profile_type();
if matches!(global_profile_type, ProfileType::Time | ProfileType::Both) {
if let Err(e) = profiling::process_time_profile() {
debug_log!("Error converting time profile to exclusive time: {e:?}");
}
}
if matches!(global_profile_type, ProfileType::Memory | ProfileType::Both) {
mem_tracking::finalize_memory_profiling();
}
std::thread::sleep(std::time::Duration::from_millis(10));
};
#[cfg(not(feature = "time_profiling"))]
{}
}
#[cfg(test)]
mod feature_tests {
use crate::profiling::is_profiling_enabled;
#[test]
fn test_profiling_feature_flag_behavior() {
#[cfg(feature = "time_profiling")]
{
assert!(
!is_profiling_enabled(),
"With profiling feature enabled but disabled at runtime, is_profiling_enabled() should return false in test mode"
);
crate::profiling::force_set_profiling_state(true);
assert!(
is_profiling_enabled(),
"After enabling profiling, is_profiling_enabled() should return true"
);
crate::profiling::force_set_profiling_state(false);
}
#[cfg(not(feature = "time_profiling"))]
{
assert!(
!is_profiling_enabled(),
"Without profiling feature, is_profiling_enabled() should always return false"
);
}
}
}
#[cfg(test)]
mod config_tests {
#[cfg(feature = "time_profiling")]
use super::*;
#[cfg(feature = "time_profiling")]
use std::env;
#[test]
#[cfg(feature = "time_profiling")]
fn test_profile_config_picks_up_env_changes() {
let _guard = PROFILING_MUTEX.lock();
let original = env::var("THAG_PROFILER").ok();
let orig_global_profile_type = profiling::get_global_profile_type();
eprintln!("orig_global_profile_type={orig_global_profile_type:?}");
unsafe { env::set_var("THAG_PROFILER", "time,.,none,false") };
profiling::clear_profile_config_cache();
assert_eq!(
profiling::get_config_profile_type(),
profiling::ProfileType::Time
);
assert_eq!(get_config_profile_type(), profiling::ProfileType::Time);
unsafe { env::set_var("THAG_PROFILER", "both,.,none,false") };
profiling::clear_profile_config_cache();
assert_eq!(get_config_profile_type(), profiling::ProfileType::Both);
if let Some(val) = original {
unsafe { env::set_var("THAG_PROFILER", val) };
} else {
unsafe { env::remove_var("THAG_PROFILER") };
}
profiling::clear_profile_config_cache();
}
}
#[cfg(test)]
mod lib_tests {
use super::*;
#[test]
fn test_file_stem_functions() {
#[cfg(not(target_os = "windows"))]
{
let file_name = "path/to/my_file.rs";
assert_eq!(file_stem_from_path_str(file_name), "my_file");
}
#[cfg(target_os = "windows")]
{
let win_file = "C:\\\\Users\\\\donforbes\\\\Documents\\\\GitHub\\\\thag_rs\\\\demo\\\\document_pipeline_profile_minimal.rs";
assert_eq!(
file_stem_from_path_str(win_file),
"document_pipeline_profile_minimal"
);
let win_file_single = "C:\\Users\\Documents\\test_file.rs";
assert_eq!(file_stem_from_path_str(win_file_single), "test_file");
let simple_file = "simple.rs";
assert_eq!(file_stem_from_path_str(simple_file), "simple");
}
let simple_file = "simple.rs";
assert_eq!(file_stem_from_path_str(simple_file), "simple");
#[cfg(not(target_os = "windows"))]
{
let multi_ext = "test.data.rs";
assert_eq!(file_stem_from_path_str(multi_ext), "test.data");
}
#[cfg(target_os = "windows")]
{
let multi_ext = "C:\\\\temp\\\\test.data.rs";
assert_eq!(file_stem_from_path_str(multi_ext), "test.data");
}
let bare_stem = "bare";
assert_eq!(file_stem_from_path_str(bare_stem), "bare");
}
#[test]
fn test_thousands_formatter() {
assert_eq!(thousands(0), "0");
assert_eq!(thousands(42), "42");
assert_eq!(thousands(1000), "1,000");
assert_eq!(thousands(1234), "1,234");
assert_eq!(thousands(1_234_567), "1,234,567");
assert_eq!(thousands(1_234_567_890u32), "1,234,567,890");
assert_eq!(thousands(123_456_789_012_345u64), "123,456,789,012,345");
assert_eq!(thousands(1), "1");
assert_eq!(thousands(12), "12");
assert_eq!(thousands(123), "123");
assert_eq!(thousands("1234567"), "1,234,567");
}
#[test]
fn test_lazy_static_var_macro() {
let vec_ref = lazy_static_var!(Vec<i32>, vec![1, 2, 3, 4]);
assert_eq!(vec_ref.len(), 4);
assert_eq!(vec_ref[0], 1);
let bool_val = lazy_static_var!(bool, deref, true);
assert!(bool_val);
let map_ref = lazy_static_var!(std::collections::HashMap<&str, i32>, {
let mut map = std::collections::HashMap::new();
map.insert("one", 1);
map.insert("two", 2);
map
});
assert_eq!(map_ref.len(), 2);
assert_eq!(map_ref.get("one"), Some(&1));
}
#[test]
fn test_regex_macro() {
let re = re!(r"\d+");
assert!(re.is_match("123"));
assert!(!re.is_match("abc"));
let cap_re = re!(r"(\w+):(\d+)");
let caps = cap_re.captures("name:42").unwrap();
assert_eq!(caps.get(1).unwrap().as_str(), "name");
assert_eq!(caps.get(2).unwrap().as_str(), "42");
let special_re = re!(r"\s+");
assert!(special_re.is_match(" \t\n"));
assert!(!special_re.is_match("abc"));
}
#[test]
fn test_static_lazy_macro() {
static_lazy! {
TestLazy: Vec<i32> = vec![1, 2, 3]
}
let test_lazy = TestLazy::get();
assert_eq!(test_lazy.len(), 3);
assert_eq!(test_lazy[0], 1);
static_lazy! {
TestOptional: Option<String> = Some("test".to_string())
}
let test_opt = TestOptional::get();
assert!(test_opt.is_some());
assert_eq!(test_opt.unwrap(), "test");
TestLazy::init();
TestOptional::init();
}
#[cfg(feature = "time_profiling")]
#[test]
fn test_profiler_and_profilee() {
let test_module = "test_module";
let leaked_module = Box::leak(test_module.to_string().into_boxed_str());
let profilee = Profilee::new(leaked_module);
assert_eq!(profilee.root_module, leaked_module);
set_base_location(file!(), "test_function", line!());
let location = get_base_location();
eprintln!("location={location:#?}");
assert!(location.is_some());
let loc_str = location.unwrap();
assert!(loc_str.contains(file!()));
if PROFILEE.get().is_none() {
let _ = PROFILEE.set(profilee);
}
let root = get_root_module();
assert!(root.is_some());
assert_eq!(root.unwrap(), leaked_module);
}
#[cfg(feature = "time_profiling")]
#[test]
fn test_init_and_finalize() {
let _guard = PROFILING_MUTEX.lock();
disable_profiling();
let config = ProfileConfiguration::try_from(vec!["time", "", "none"].as_slice()).unwrap();
init_profiling("test_module", config.profile_type());
assert!(is_profiling_enabled());
finalize_profiling();
assert!(!is_profiling_enabled());
}
#[test]
#[allow(clippy::unnecessary_literal_unwrap)]
fn test_error_types() {
let error = ProfileError::General("test error".to_string());
let error_string = error.to_string();
assert!(error_string.contains("test error"));
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let profile_error = ProfileError::from(io_error);
assert!(matches!(profile_error, ProfileError::Io(_)));
let result: ProfileResult<i32> = Ok(42);
assert_eq!(result.unwrap(), 42);
let err_result: ProfileResult<()> = Err(ProfileError::General("test".into()));
assert!(err_result.is_err());
}
#[test]
fn test_profile_type_enum() {
let time = ProfileType::Time;
let memory = ProfileType::Memory;
let both = ProfileType::Both;
let none = ProfileType::None;
assert_eq!(time.to_string(), "time");
assert_eq!(memory.to_string(), "memory");
assert_eq!(both.to_string(), "both");
assert_eq!(none.to_string(), "none");
assert_eq!("time".parse::<ProfileType>().unwrap(), ProfileType::Time);
assert_eq!(
"memory".parse::<ProfileType>().unwrap(),
ProfileType::Memory
);
assert_eq!("both".parse::<ProfileType>().unwrap(), ProfileType::Both);
let invalid = "invalid".parse::<ProfileType>();
assert!(invalid.is_err());
}
#[cfg(feature = "full_profiling")]
#[test]
fn test_mem_tracking_integration() {
let current = mem_tracking::current_allocator();
assert!(matches!(current, Allocator::Tracking) || matches!(current, Allocator::System));
let result = safe_alloc!(42);
assert_eq!(result, 42);
let task = create_memory_task();
assert!(task.id() > 0);
let guard = TaskGuard::new(task.id());
drop(guard); }
}