use std::path::PathBuf;
use std::time::Duration;
fn process_start_millis() -> u128 {
static START_MS: std::sync::OnceLock<u128> = std::sync::OnceLock::new();
*START_MS.get_or_init(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
})
}
fn default_dump_path() -> PathBuf {
let dir = std::env::var_os("CAPTRACK_DUMP_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("target/captrack-pgo"));
let stem = std::env::current_exe()
.ok()
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
.unwrap_or_else(|| "unknown".to_string());
dir.join(format!(
"profile-{stem}-{}-{}.json",
std::process::id(),
process_start_millis()
))
}
fn autodump_enabled() -> bool {
match std::env::var("CAPTRACK_AUTODUMP") {
Ok(v) => !matches!(
v.trim().to_ascii_lowercase().as_str(),
"0" | "off" | "false" | "no"
),
Err(_) => true,
}
}
fn autodump_interval_ms() -> u64 {
std::env::var("CAPTRACK_AUTODUMP_INTERVAL_MS")
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
.unwrap_or(500)
}
#[ctor::dtor]
fn captrack_autodump_on_exit() {
if !autodump_enabled() {
return;
}
let path = default_dump_path();
let _ = crate::dump::dump_capacity_stats(&path);
}
#[ctor::ctor]
fn captrack_autodump_spawn_ticker() {
if !autodump_enabled() {
return;
}
let interval_ms = autodump_interval_ms();
if interval_ms == 0 {
return;
}
let path = default_dump_path();
std::thread::Builder::new()
.name("captrack-autodump".into())
.spawn(move || {
let interval = Duration::from_millis(interval_ms);
loop {
std::thread::sleep(interval);
let _ = crate::dump::dump_capacity_stats(&path);
}
})
.ok();
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn interval_defaults_to_500ms_when_unset() {
let _guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("CAPTRACK_AUTODUMP_INTERVAL_MS");
std::env::remove_var("CAPTRACK_AUTODUMP_INTERVAL_MS");
assert_eq!(autodump_interval_ms(), 500);
if let Some(v) = prev {
std::env::set_var("CAPTRACK_AUTODUMP_INTERVAL_MS", v);
}
}
#[test]
fn interval_respects_env_override() {
let _guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("CAPTRACK_AUTODUMP_INTERVAL_MS");
std::env::set_var("CAPTRACK_AUTODUMP_INTERVAL_MS", "123");
assert_eq!(autodump_interval_ms(), 123);
match prev {
Some(v) => std::env::set_var("CAPTRACK_AUTODUMP_INTERVAL_MS", v),
None => std::env::remove_var("CAPTRACK_AUTODUMP_INTERVAL_MS"),
}
}
#[test]
fn interval_falls_back_to_default_on_unparseable_value() {
let _guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("CAPTRACK_AUTODUMP_INTERVAL_MS");
std::env::set_var("CAPTRACK_AUTODUMP_INTERVAL_MS", "not-a-number");
assert_eq!(autodump_interval_ms(), 500);
match prev {
Some(v) => std::env::set_var("CAPTRACK_AUTODUMP_INTERVAL_MS", v),
None => std::env::remove_var("CAPTRACK_AUTODUMP_INTERVAL_MS"),
}
}
#[test]
fn enabled_by_default_when_unset() {
let _guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("CAPTRACK_AUTODUMP");
std::env::remove_var("CAPTRACK_AUTODUMP");
assert!(autodump_enabled());
if let Some(v) = prev {
std::env::set_var("CAPTRACK_AUTODUMP", v);
}
}
#[test]
fn disabled_by_recognized_falsy_values() {
let _guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("CAPTRACK_AUTODUMP");
for falsy in ["0", "off", "false", "no", "OFF", "False"] {
std::env::set_var("CAPTRACK_AUTODUMP", falsy);
assert!(!autodump_enabled(), "expected {falsy} to disable autodump");
}
match prev {
Some(v) => std::env::set_var("CAPTRACK_AUTODUMP", v),
None => std::env::remove_var("CAPTRACK_AUTODUMP"),
}
}
#[test]
fn dump_path_respects_captrack_dump_dir_override() {
let _guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("CAPTRACK_DUMP_DIR");
std::env::set_var("CAPTRACK_DUMP_DIR", "some/custom/dir");
let path = default_dump_path();
assert!(path.starts_with("some/custom/dir"));
assert!(path
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("profile-"));
match prev {
Some(v) => std::env::set_var("CAPTRACK_DUMP_DIR", v),
None => std::env::remove_var("CAPTRACK_DUMP_DIR"),
}
}
#[test]
fn dump_path_is_qualified_by_pid_and_start_time() {
let _guard = ENV_LOCK.lock().unwrap();
let path = default_dump_path();
let file_name = path.file_name().unwrap().to_string_lossy().into_owned();
let expected_suffix = format!("-{}-{}.json", std::process::id(), process_start_millis());
assert!(
file_name.ends_with(&expected_suffix),
"dump path must end with -<pid>-<start_ms>.json, got: {file_name}"
);
assert_eq!(
path,
default_dump_path(),
"default_dump_path must return the same path on every call"
);
}
#[test]
fn dump_output_is_atomic_and_leaves_no_tmp_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("profile-test.json");
crate::dump::dump_capacity_stats(&path).unwrap();
assert!(
path.is_file(),
"dump file must exist after a successful write"
);
let tmp_path = {
let mut name = path.file_name().unwrap().to_os_string();
name.push(format!(".{}.tmp", std::process::id()));
path.with_file_name(name)
};
assert!(
!tmp_path.exists(),
"no .tmp file must remain after a successful write"
);
}
#[test]
fn dump_concurrent_writers_never_corrupt_the_destination() {
let dir = tempfile::tempdir().unwrap();
let path = std::sync::Arc::new(dir.path().join("profile-concurrent.json"));
let handles: Vec<_> = (0..8)
.map(|_| {
let path = std::sync::Arc::clone(&path);
std::thread::spawn(move || {
for _ in 0..20 {
crate::dump::dump_capacity_stats(path.as_path()).unwrap();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let text = std::fs::read_to_string(path.as_path()).unwrap();
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&text);
assert!(
parsed.is_ok(),
"destination must always be exactly one valid JSON document, got parse error on: {text}"
);
}
#[test]
fn dump_output_contains_total_observed_field() {
use crate::tvec;
let v: crate::TrackedVec<i32> = tvec!("autodump-test-total-observed", 4);
drop(v);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("profile-total-observed.json");
crate::dump::dump_capacity_stats(&path).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
assert!(
text.contains("total_observed"),
"dump JSON must include total_observed; got: {text}"
);
}
#[test]
fn dump_does_not_fail_on_empty_registry() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("profile-empty.json");
let result = crate::dump::dump_capacity_stats(&path);
assert!(result.is_ok());
assert!(path.is_file());
let text = std::fs::read_to_string(&path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&text).unwrap();
assert!(parsed.get("stats").is_some());
}
}