use std::ops::Deref;
use std::sync::Arc;
use crate::auto_export::{AutoExportConfig, MemScopeConfig};
use crate::capture::backends::global_tracking::GlobalTracker;
use crate::core::error::MemScopeResult;
use crate::lifecycle;
use crate::periodic_flusher::PeriodicFlusher;
pub struct MemScopeGuard {
tracker: Arc<GlobalTracker>,
flusher: Option<PeriodicFlusher>,
}
impl MemScopeGuard {
#[must_use]
pub fn tracker(&self) -> &Arc<GlobalTracker> {
&self.tracker
}
#[must_use]
pub fn export_now(&self) -> bool {
lifecycle::trigger_export_now()
}
pub fn snapshot_json(&self) -> MemScopeResult<String> {
lifecycle::snapshot_json()
}
}
impl Deref for MemScopeGuard {
type Target = GlobalTracker;
fn deref(&self) -> &Self::Target {
&self.tracker
}
}
impl Drop for MemScopeGuard {
fn drop(&mut self) {
if let Some(flusher) = self.flusher.take() {
drop(flusher);
}
let _ = lifecycle::export_for_reason(lifecycle::ExportReason::Drop);
}
}
pub fn start() -> MemScopeResult<MemScopeGuard> {
start_with(MemScopeConfig::default())
}
pub fn start_with(config: MemScopeConfig) -> MemScopeResult<MemScopeGuard> {
crate::init_logging()?;
crate::init_global_tracking_with_config(config.tracker.clone())?;
let tracker = crate::global_tracker()?;
lifecycle::install(config.auto_export.clone(), tracker.clone())?;
let flusher = spawn_flusher(&config.auto_export);
Ok(MemScopeGuard { tracker, flusher })
}
fn spawn_flusher(cfg: &AutoExportConfig) -> Option<PeriodicFlusher> {
let interval = cfg.flush_interval?;
Some(PeriodicFlusher::new(interval, || {
let _ = lifecycle::trigger_export_now();
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auto_export::{AutoExportConfig, MemScopeConfig};
use crate::track;
use parking_lot::Mutex;
use serial_test::serial;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
fn reset_globals() {
crate::capture::backends::global_tracking::reset_global_tracking();
lifecycle::reset_for_test();
}
fn config_with_tempdir() -> (TempDir, MemScopeConfig) {
let dir = TempDir::new().expect("tempdir creation must succeed in tests");
let cfg = MemScopeConfig::default()
.with_auto_export(AutoExportConfig::default().with_output_path(dir.path()));
(dir, cfg)
}
#[test]
#[serial]
fn start_returns_guard_with_tracker() {
reset_globals();
let (_dir, cfg) = config_with_tempdir();
let guard = start_with(cfg).expect("start_with with default config must succeed");
let guard_tracker: &Arc<GlobalTracker> = guard.tracker();
let singleton: Arc<GlobalTracker> = crate::global_tracker()
.expect("global_tracker() must succeed immediately after start_with()");
assert!(
Arc::ptr_eq(guard_tracker, &singleton),
"guard.tracker() must reference the same Arc<GlobalTracker> as the global singleton"
);
drop(guard);
}
#[test]
#[serial]
fn drop_guard_writes_html_report() {
reset_globals();
let dir = TempDir::new().expect("tempdir creation must succeed in tests");
let cfg = MemScopeConfig::default()
.with_auto_export(AutoExportConfig::default().with_output_path(dir.path()));
let guard = start_with(cfg).expect("start_with with tempdir output must succeed");
drop(guard);
let html = dir.path().join("dashboard_unified_dashboard.html");
assert!(
html.exists(),
"HTML dashboard must exist at {html:?} after dropping the guard"
);
let content = std::fs::read_to_string(&html)
.expect("HTML dashboard file must be readable after export");
assert!(
!content.is_empty(),
"HTML dashboard content must not be empty"
);
}
#[test]
#[serial]
fn start_with_flusher_runs_periodic_export() {
reset_globals();
let dir = TempDir::new().expect("tempdir creation must succeed in tests");
let cfg = MemScopeConfig::default().with_auto_export(
AutoExportConfig::default()
.with_output_path(dir.path())
.with_flush_interval(Duration::from_millis(50)),
);
let guard =
start_with(cfg).expect("start_with with flush_interval must succeed and spawn flusher");
thread::sleep(Duration::from_millis(200));
let html = dir.path().join("dashboard_unified_dashboard.html");
assert!(
html.exists(),
"periodic flusher must have written the HTML dashboard within 200ms"
);
drop(guard);
}
#[test]
#[serial]
fn export_now_returns_true_and_produces_report() {
reset_globals();
let dir = TempDir::new().expect("tempdir creation must succeed in tests");
let cfg = MemScopeConfig::default()
.with_auto_export(AutoExportConfig::default().with_output_path(dir.path()));
let guard = start_with(cfg).expect("start_with must succeed");
let did = guard.export_now();
assert!(
did,
"export_now() must return true when tracker and cfg are installed"
);
let html = dir.path().join("dashboard_unified_dashboard.html");
assert!(
html.exists(),
"HTML dashboard must exist at {html:?} after export_now()"
);
drop(guard);
}
#[test]
#[serial]
fn snapshot_json_returns_non_empty_string() {
reset_globals();
let (_dir, cfg) = config_with_tempdir();
let guard = start_with(cfg).expect("start_with must succeed");
let v: Vec<u64> = vec![1, 2, 3];
guard.track(&v);
let json = guard
.snapshot_json()
.expect("snapshot_json() must succeed when the tracker is installed");
assert!(
!json.is_empty(),
"snapshot_json() must return a non-empty JSON string"
);
assert!(
json.contains("allocation_count"),
"snapshot JSON must contain the allocation_count field, got: {json}"
);
drop(guard);
}
#[test]
#[serial]
fn deref_to_tracker_allows_track() {
reset_globals();
let (_dir, cfg) = config_with_tempdir();
let guard = start_with(cfg).expect("start_with must succeed");
let data: Vec<u64> = vec![1, 2, 3];
guard.track(&data);
let stats = guard.get_stats();
assert!(
stats.total_allocations > 0,
"tracker must have recorded at least one allocation after track() via Deref"
);
drop(guard);
}
#[test]
#[serial]
fn start_twice_second_returns_err() {
reset_globals();
let (_dir1, cfg1) = config_with_tempdir();
let guard1 =
start_with(cfg1).expect("first start_with must succeed with a clean global state");
let (_dir2, cfg2) = config_with_tempdir();
let result2 = start_with(cfg2);
let err = match result2 {
Err(e) => e,
Ok(_) => {
panic!("second start_with must fail because global tracking is already initialized")
}
};
let err_msg = format!("{err}");
assert!(
err_msg.contains("already initialized"),
"error must mention 'already initialized', got: {err_msg}"
);
drop(guard1);
}
#[test]
#[serial]
fn drop_after_reset_global_tracking_still_succeeds() {
reset_globals();
let dir = TempDir::new().expect("tempdir creation must succeed in tests");
let cfg = MemScopeConfig::default()
.with_auto_export(AutoExportConfig::default().with_output_path(dir.path()));
let guard = start_with(cfg).expect("start_with must succeed");
crate::capture::backends::global_tracking::reset_global_tracking();
drop(guard);
let html = dir.path().join("dashboard_unified_dashboard.html");
assert!(
html.exists(),
"HTML must be written even after reset_global_tracking because TRACKER_HANDLE retains the Arc"
);
}
#[test]
#[serial]
fn fifty_threads_start_track_drop_no_deadlock() {
const THREAD_COUNT: usize = 50;
let lock = Arc::new(Mutex::new(()));
let dir = Arc::new(TempDir::new().expect("tempdir creation must succeed in tests"));
let mut handles = Vec::with_capacity(THREAD_COUNT);
for _ in 0..THREAD_COUNT {
let lock = Arc::clone(&lock);
let dir = Arc::clone(&dir);
handles.push(thread::spawn(move || {
let _guard_lock = lock.lock();
reset_globals();
let cfg = MemScopeConfig::default()
.with_auto_export(AutoExportConfig::default().with_output_path(dir.path()));
let guard = start_with(cfg).expect("start_with must succeed in each worker thread");
let data: Vec<u64> = vec![42; 8];
guard.track(&data);
drop(guard);
}));
}
let mut completed = 0usize;
for handle in handles {
handle
.join()
.expect("worker thread must not panic during start+track+drop");
completed += 1;
}
assert_eq!(
completed, THREAD_COUNT,
"all 50 worker threads must complete without deadlock or panic"
);
}
#[test]
#[serial]
fn end_to_end_start_track_drop_html_contains_marker() {
reset_globals();
let dir = TempDir::new().expect("tempdir creation must succeed in tests");
let cfg = MemScopeConfig::default()
.with_auto_export(AutoExportConfig::default().with_output_path(dir.path()));
let guard = start_with(cfg).expect("start_with must succeed for end-to-end flow");
let vec_data: Vec<u64> = vec![1, 2, 3, 4, 5];
track!(guard, vec_data);
drop(guard);
let html = dir.path().join("dashboard_unified_dashboard.html");
assert!(
html.exists(),
"HTML dashboard must exist after end-to-end start+track+drop"
);
let content = std::fs::read_to_string(&html)
.expect("HTML dashboard file must be readable after end-to-end export");
assert!(
!content.is_empty(),
"HTML dashboard content must not be empty after end-to-end flow"
);
assert!(
content.contains("memscope"),
"HTML dashboard must contain the 'memscope' marker from the template"
);
}
}