use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::{LazyLock, Mutex, OnceLock};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::dev_logging::warn;
struct BackendHandle {
session_id: String,
session_dir: PathBuf,
stop_path: PathBuf,
profile_path: PathBuf,
done_path: PathBuf,
}
#[derive(Debug, Clone)]
pub(crate) struct SessionInfo {
pub(crate) session_id: String,
pub(crate) session_dir: PathBuf,
}
static HANDLE: OnceLock<Mutex<Option<BackendHandle>>> = OnceLock::new();
static BACKEND_BIN: LazyLock<PathBuf> = LazyLock::new(|| {
std::env::var("HOTPATH_SAMPLY_WRAPPER_BIN")
.map(PathBuf::from)
.unwrap_or_else(|_| {
PathBuf::from(format!("hotpath-samply{}", std::env::consts::EXE_SUFFIX))
})
});
pub(crate) fn start() {
let pid = std::process::id();
let backend_bin = &*BACKEND_BIN;
let session_id = match session_id() {
Some(id) => id,
None => {
warn!("failed to generate CPU profiling session id");
return;
}
};
let session_dir = PathBuf::from("/tmp/hotpath").join(&session_id);
if let Err(e) = fs::create_dir_all(&session_dir) {
warn!(
"failed to create CPU profiling session dir {}: {}",
session_dir.display(),
e
);
return;
}
let stop_path = session_dir.join("stop-profiling");
let profile_path = session_dir.join("hp.json.gz");
let done_path = session_dir.join("done");
let _child = match Command::new(backend_bin)
.arg("--detach")
.arg(pid.to_string())
.arg(&session_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => child,
Err(e) => {
warn!(
"failed to spawn backend process via {}: {}",
backend_bin.display(),
e
);
return;
}
};
let handle = BackendHandle {
session_id,
session_dir,
stop_path,
profile_path,
done_path,
};
let slot = HANDLE.get_or_init(|| Mutex::new(None));
if let Ok(mut guard) = slot.lock() {
*guard = Some(handle);
}
}
pub(crate) fn current_session() -> Option<SessionInfo> {
let slot = HANDLE.get()?;
let guard = slot.lock().ok()?;
guard.as_ref().map(|h| SessionInfo {
session_id: h.session_id.clone(),
session_dir: h.session_dir.clone(),
})
}
pub(crate) fn stop() -> Result<PathBuf, String> {
let handle = HANDLE
.get()
.and_then(|m| m.lock().ok().and_then(|mut g| g.take()))
.ok_or_else(|| "samply worker not started".to_string())?;
if let Err(e) = fs::write(&handle.stop_path, b"") {
return Err(format!(
"failed to create stop signal {}: {e}",
handle.stop_path.display()
));
}
let t0 = Instant::now();
let deadline = t0 + Duration::from_secs(15);
loop {
if handle.done_path.exists() {
let body = fs::read_to_string(&handle.done_path).unwrap_or_default();
let trimmed = body.trim();
if trimmed.is_empty() {
return Ok(handle.profile_path);
}
return Err(trimmed.to_string());
}
if Instant::now() >= deadline {
return Err(format!(
"timed out waiting for samply worker (session {})",
handle.session_dir.display()
));
}
thread::sleep(Duration::from_millis(100));
}
}
fn session_id() -> Option<String> {
let elapsed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock is before UNIX_EPOCH");
Some(format!("{}-{}", std::process::id(), elapsed.as_nanos()))
}