use std::time::{Duration, Instant};
use agent_core::core::memstat;
const IDLE_TICK: Duration = Duration::from_secs(30);
fn env_is(name: &str, value: &str) -> bool {
std::env::var(name).is_ok_and(|v| v == value)
}
pub fn allocator_tuning_disabled() -> bool {
env_is("SYNAPS_CLIENT_MALLOC", "off") || env_is("SYNAPS_CLIENT_ALLOC", "default")
}
pub fn tune_allocator() {
if allocator_tuning_disabled() {
memstat::ladder("alloc", &"skipped=1");
return;
}
let thp = if env_is("SYNAPS_CLIENT_THP", "1") {
Err("kept".to_string())
} else {
memstat::disable_thp()
};
let bg = memstat::set_background_threads(false);
let decay = memstat::set_decay_ms(0, 0);
let tcache = if env_is("SYNAPS_CLIENT_TCACHE", "0") {
Some(memstat::set_thread_tcache(false))
} else {
None
};
memstat::ladder(
"alloc",
&format_args!(
"thp_off={} bg_off={} decay0={} tcache_off={} bg_now={:?}",
res(&thp),
res(&bg),
res(&decay),
tcache.as_ref().map(res).unwrap_or("n/a"),
memstat::background_threads_enabled()
),
);
}
fn res(r: &memstat::MallctlResult) -> &str {
match r {
Ok(()) => "ok",
Err(e) => e.as_str(),
}
}
pub fn purge_arenas(stage: &'static str) {
memstat::purge_arenas();
memstat::ladder(stage, &"");
}
pub fn purge_delay() -> Option<Duration> {
if env_is("SYNAPS_MEMPROF_PURGE", "1") {
return Some(Duration::ZERO);
}
let secs = std::env::var("SYNAPS_CLIENT_PURGE_SECS")
.or_else(|_| std::env::var("SYNAPS_CLIENT_PURGE_IDLE_SECS"))
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(10);
(secs > 0).then(|| Duration::from_secs(secs))
}
fn syntect_idle() -> Option<Duration> {
let secs = std::env::var("SYNAPS_TUI_SYNTECT_IDLE_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(120);
(secs > 0).then(|| Duration::from_secs(secs))
}
pub struct IdlePurge {
purge: bool,
delay: Option<Duration>,
idle_since: Option<Instant>,
next: Option<Instant>,
fired_since_idle: bool,
}
impl IdlePurge {
pub fn new(socket: bool) -> Self {
Self {
purge: socket,
delay: purge_delay(),
idle_since: None,
next: None,
fired_since_idle: false,
}
}
pub fn observe(&mut self, busy: bool) {
if busy {
self.idle_since = None;
self.next = None;
self.fired_since_idle = false;
} else if self.idle_since.is_none() {
let now = Instant::now();
self.idle_since = Some(now);
self.next = self.delay.map(|d| now + d);
}
}
pub async fn wait(&self) {
match self.next {
Some(at) => tokio::time::sleep_until(tokio::time::Instant::from_std(at)).await,
None => std::future::pending().await,
}
}
pub fn fire(&mut self) {
let first = !self.fired_since_idle;
self.fired_since_idle = true;
self.next = Some(Instant::now() + IDLE_TICK);
if self.purge {
if first {
let n = self.delay.map(|d| d.as_secs()).unwrap_or(0);
memstat::ladder("idle+N", &format_args!("n={n}"));
purge_arenas("purged");
} else {
memstat::purge_arenas();
}
}
if let (Some(window), Some(since)) = (syntect_idle(), self.idle_since) {
if since.elapsed() >= window {
super::highlight::evict_if_idle(window);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tune_allocator_spawns_nothing_and_purge_returns() {
tune_allocator();
purge_arenas("test");
if !allocator_tuning_disabled() {
assert_ne!(memstat::background_threads_enabled(), Some(true));
}
}
#[test]
fn idle_purge_arms_on_idle_edge_only() {
let mut p = IdlePurge {
purge: true,
delay: Some(Duration::from_secs(10)),
idle_since: None,
next: None,
fired_since_idle: false,
};
assert!(p.next.is_none());
p.observe(false);
assert!(p.next.is_some());
p.observe(true);
assert!(p.next.is_none());
p.observe(false);
p.fire();
assert!(p.fired_since_idle);
assert!(p.next.unwrap() > Instant::now() + Duration::from_secs(20));
}
#[test]
fn disabled_delay_never_schedules() {
let mut p = IdlePurge {
purge: true,
delay: None,
idle_since: None,
next: None,
fired_since_idle: false,
};
p.observe(false);
assert!(p.next.is_none());
}
}