use std::{
path::PathBuf,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
#[derive(Debug, Default)]
struct ActiveTime {
completed: Duration,
started: Option<Instant>,
}
impl ActiveTime {
fn elapsed(&self, now: Instant) -> Duration {
self.completed.saturating_add(
self.started
.map_or(Duration::ZERO, |start| now.saturating_duration_since(start)),
)
}
fn finish(&mut self, now: Instant) -> Duration {
self.completed = self.elapsed(now);
self.started = None;
self.completed
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct SessionActiveTime(Arc<Mutex<ActiveTime>>);
impl SessionActiveTime {
pub(crate) fn from_events(events: &[super::SessionEvent]) -> Self {
let millis = events
.iter()
.filter_map(active_time_millis)
.max()
.unwrap_or(0);
Self(Arc::new(Mutex::new(ActiveTime {
completed: Duration::from_millis(millis),
started: None,
})))
}
pub(crate) fn elapsed(&self, now: Instant) -> Duration {
self.0
.lock()
.unwrap_or_else(|error| error.into_inner())
.elapsed(now)
}
pub(crate) fn is_active(&self) -> bool {
self.0
.lock()
.unwrap_or_else(|error| error.into_inner())
.started
.is_some()
}
pub(crate) fn start(
&self,
session: Option<super::Session>,
cwd: PathBuf,
report_error: impl Fn(String) + Send + 'static,
) -> ActiveTimeGuard {
self.0
.lock()
.unwrap_or_else(|error| error.into_inner())
.started = Some(Instant::now());
ActiveTimeGuard {
clock: self.clone(),
session,
cwd,
report_error: Box::new(report_error),
}
}
}
pub(crate) fn active_time_millis(event: &super::SessionEvent) -> Option<u64> {
matches!(
event.kind(),
Some(super::SessionEventKind::SessionActiveTime | super::SessionEventKind::Compaction)
)
.then(|| {
event
.payload
.get("session_active_ms")
.and_then(serde_json::Value::as_u64)
})
.flatten()
}
pub(crate) struct ActiveTimeGuard {
clock: SessionActiveTime,
session: Option<super::Session>,
cwd: PathBuf,
report_error: Box<dyn Fn(String) + Send>,
}
impl Drop for ActiveTimeGuard {
fn drop(&mut self) {
let elapsed = self
.clock
.0
.lock()
.unwrap_or_else(|error| error.into_inner())
.finish(Instant::now());
let millis = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX);
if let Err(error) = super::record_session_event(
self.session.as_ref(),
&self.cwd,
super::SessionEventKind::SessionActiveTime,
serde_json::json!({"session_active_ms": millis}),
) {
(self.report_error)(format!("could not save session active time: {error}"));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn active_time_excludes_idle_and_counts_parent_interval_once() {
let start = Instant::now();
let mut time = ActiveTime::default();
assert_eq!(
time.elapsed(start + Duration::from_secs(90)),
Duration::ZERO
);
time.started = Some(start + Duration::from_secs(90));
assert_eq!(
time.finish(start + Duration::from_secs(110)),
Duration::from_secs(20)
);
assert_eq!(
time.elapsed(start + Duration::from_secs(200)),
Duration::from_secs(20)
);
time.started = Some(start + Duration::from_secs(200));
assert_eq!(
time.finish(start + Duration::from_secs(205)),
Duration::from_secs(25)
);
}
#[test]
fn active_time_worker_finish_persists_and_rotation_preserves_resume_total() {
let temp = tempfile::TempDir::new().unwrap();
let manager = super::super::SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let clock = SessionActiveTime::default();
let guard = clock.start(Some(session.clone()), temp.path().to_owned(), |error| {
panic!("{error}")
});
clock.0.lock().unwrap().started = Some(Instant::now() - Duration::from_secs(858));
drop(guard);
assert!(!clock.is_active());
let events = session.read_events().unwrap();
let resumed = SessionActiveTime::from_events(&events);
assert_eq!(resumed.elapsed(Instant::now()).as_secs(), 858);
let bytes = std::fs::read(session.path()).unwrap();
let payload =
super::super::usage::checkpoint_usage(bytes.as_slice(), bytes.len() as u64).unwrap();
let checkpoint = super::super::SessionEvent::new_kind(
super::super::SessionEventKind::Compaction,
session.id().to_string(),
temp.path().to_owned(),
payload,
);
let rotated = SessionActiveTime::from_events(&[checkpoint]);
assert_eq!(
rotated.elapsed(Instant::now()),
resumed.elapsed(Instant::now())
);
}
#[test]
fn active_time_resume_restores_total_without_restart_idle() {
let event = |kind, millis| {
super::super::SessionEvent::new_kind(
kind,
"test".into(),
PathBuf::from("."),
serde_json::json!({"session_active_ms": millis}),
)
};
let clock = SessionActiveTime::from_events(&[
event(super::super::SessionEventKind::Compaction, 858_000),
event(super::super::SessionEventKind::SessionActiveTime, 860_250),
]);
assert!(!clock.is_active());
assert_eq!(
clock.elapsed(Instant::now() + Duration::from_secs(3600)),
Duration::from_millis(860_250)
);
}
}