From 5060ca519c126b677328cdeb238f4623f5d0cb31 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Wed, 14 Jan 2026 22:51:51 +0000 Subject: [PATCH 1/2] feat(session): add activity/idle tracking to InProcessSession Add SessionStats struct and tracking methods to InProcessSession for monitoring session activity and resource usage: - SessionStats struct with created_at, last_activity, execution_count, total_execution_time, total_callback_invocations, and peak_memory_bytes - last_activity() - returns the time of last execution completion - idle_duration() - returns duration since last execution - execution_count() - returns total number of executions (now u64) - total_execution_time() - returns cumulative execution time - stats() - returns complete SessionStats snapshot - reset_stats() - clears statistics without affecting session state Statistics behavior: - Stats survive clear_state() (only clears Python state) - Stats are fully reset on reset() (new WASM instance) - reset_stats() preserves original creation time - reset_full() updates creation time Includes unit tests for SessionStats default values, reset behavior, and reset_full timestamp handling. Closes: eryx-cvr --- crates/eryx/src/lib.rs | 1 + crates/eryx/src/session/in_process.rs | 84 ++++++++++++++++-- crates/eryx/src/session/mod.rs | 122 ++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 5 deletions(-) diff --git a/crates/eryx/src/lib.rs b/crates/eryx/src/lib.rs index 52b59f42..b6cbacc8 100644 --- a/crates/eryx/src/lib.rs +++ b/crates/eryx/src/lib.rs @@ -78,6 +78,7 @@ pub use library::RuntimeLibrary; pub use package::{ExtractedPackage, PackageFormat}; pub use sandbox::{ExecuteResult, ExecuteStats, ResourceLimits, Sandbox, SandboxBuilder, state}; pub use session::{ + SessionStats, InProcessSession, PythonStateSnapshot, Session, SessionExecutor, SnapshotMetadata, SnapshotSession, }; diff --git a/crates/eryx/src/session/in_process.rs b/crates/eryx/src/session/in_process.rs index 328f5852..74423c33 100644 --- a/crates/eryx/src/session/in_process.rs +++ b/crates/eryx/src/session/in_process.rs @@ -43,7 +43,7 @@ //! ``` use std::sync::Arc; -use std::time::Instant; +use std::time::{Duration, Instant}; use async_trait::async_trait; use tokio::sync::mpsc; @@ -54,8 +54,8 @@ use crate::error::Error; use crate::sandbox::{ExecuteResult, ExecuteStats, Sandbox}; use crate::wasm::{CallbackRequest, TraceRequest}; -use super::Session; use super::executor::{PythonStateSnapshot, SessionExecutor}; +use super::{Session, SessionStats}; /// An in-process session that keeps the WASM instance alive between executions. /// @@ -72,13 +72,18 @@ pub struct InProcessSession<'a> { /// Whether the preamble has been executed. preamble_executed: bool, + + /// Session activity and execution statistics. + stats: SessionStats, } impl std::fmt::Debug for InProcessSession<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InProcessSession") - .field("execution_count", &self.executor.execution_count()) + .field("execution_count", &self.stats.execution_count) .field("preamble_executed", &self.preamble_executed) + .field("last_activity", &self.stats.last_activity) + .field("total_execution_time", &self.stats.total_execution_time) .finish_non_exhaustive() } } @@ -104,6 +109,7 @@ impl<'a> InProcessSession<'a> { sandbox, executor, preamble_executed: false, + stats: SessionStats::new(), }) } @@ -163,8 +169,19 @@ impl<'a> InProcessSession<'a> { let duration = start.elapsed(); + // Update session statistics + self.stats.execution_count += 1; + self.stats.total_execution_time += duration; + self.stats.total_callback_invocations += u64::from(callback_invocations); + self.stats.last_activity = Some(Instant::now()); + match execution_result { Ok(output) => { + // Update peak memory if this execution used more + if output.peak_memory_bytes > self.stats.peak_memory_bytes { + self.stats.peak_memory_bytes = output.peak_memory_bytes; + } + // Stream output if handler is configured if let Some(handler) = self.sandbox.output_handler() { handler.on_output(&output.stdout).await; @@ -186,8 +203,59 @@ impl<'a> InProcessSession<'a> { /// Get the number of executions performed in this session. #[must_use] - pub fn execution_count(&self) -> u32 { - self.executor.execution_count() + pub fn execution_count(&self) -> u64 { + self.stats.execution_count + } + + /// Get the time of the last execution completion. + /// + /// Returns `None` if no executions have been performed yet. + #[must_use] + pub fn last_activity(&self) -> Option { + self.stats.last_activity + } + + /// Get the duration since the last execution completed. + /// + /// Returns `None` if no executions have been performed yet. + /// + /// # Example + /// + /// ```rust,ignore + /// session.execute("x = 1").await?; + /// std::thread::sleep(Duration::from_secs(1)); + /// let idle = session.idle_duration().unwrap(); + /// assert!(idle >= Duration::from_secs(1)); + /// ``` + #[must_use] + pub fn idle_duration(&self) -> Option { + self.stats.last_activity.map(|last| last.elapsed()) + } + + /// Get the total execution time across all runs in this session. + #[must_use] + pub fn total_execution_time(&self) -> Duration { + self.stats.total_execution_time + } + + /// Get the complete session statistics. + /// + /// This includes creation time, last activity, execution count, + /// total execution time, callback invocations, and peak memory usage. + #[must_use] + pub fn stats(&self) -> SessionStats { + self.stats.clone() + } + + /// Reset the session statistics without affecting session state. + /// + /// This clears all statistics (execution count, total time, etc.) but + /// preserves the original session creation time and all Python state. + /// + /// Use this if you want to measure statistics for a specific workload + /// without resetting the Python environment. + pub fn reset_stats(&mut self) { + self.stats.reset(); } /// Capture a snapshot of the current Python session state. @@ -217,6 +285,9 @@ impl<'a> InProcessSession<'a> { /// This is lighter-weight than `reset()` because it doesn't recreate /// the WASM instance - it just clears the Python-level state. /// + /// Note: Statistics are preserved across `clear_state()`. Use `reset_stats()` + /// to clear statistics, or `reset()` to clear both state and statistics. + /// /// # Errors /// /// Returns an error if the clear fails. @@ -240,6 +311,9 @@ impl Session for InProcessSession<'_> { // Reset preamble flag so it runs again on next execute self.preamble_executed = false; + // Full reset of stats (including creation time) + self.stats.reset_full(); + Ok(()) } } diff --git a/crates/eryx/src/session/mod.rs b/crates/eryx/src/session/mod.rs index ef0aec6c..8e0257fe 100644 --- a/crates/eryx/src/session/mod.rs +++ b/crates/eryx/src/session/mod.rs @@ -44,6 +44,8 @@ pub mod executor; pub mod in_process; +use std::time::{Duration, Instant}; + use async_trait::async_trait; use crate::error::Error; @@ -52,6 +54,77 @@ use crate::sandbox::ExecuteResult; pub use executor::{PythonStateSnapshot, SessionExecutor, SnapshotMetadata}; pub use in_process::InProcessSession; +/// Statistics about a session's activity and resource usage. +/// +/// This struct tracks various metrics about a session's lifetime, including +/// when it was created, when it was last active, and aggregate execution statistics. +/// +/// # Example +/// +/// ```rust,ignore +/// let mut session = InProcessSession::new(&sandbox).await?; +/// session.execute("x = 1").await?; +/// session.execute("y = 2").await?; +/// +/// let stats = session.stats(); +/// println!("Executions: {}", stats.execution_count); +/// println!("Total time: {:?}", stats.total_execution_time); +/// println!("Idle for: {:?}", session.idle_duration()); +/// ``` +#[derive(Debug, Clone)] +pub struct SessionStats { + /// When the session was created. + pub created_at: Instant, + + /// When the last execution completed (None if never executed). + pub last_activity: Option, + + /// Total number of executions performed. + pub execution_count: u64, + + /// Total time spent executing code across all runs. + pub total_execution_time: Duration, + + /// Total number of callback invocations across all executions. + pub total_callback_invocations: u64, + + /// Peak memory usage observed across all executions (in bytes). + pub peak_memory_bytes: u64, +} + +impl Default for SessionStats { + fn default() -> Self { + Self { + created_at: Instant::now(), + last_activity: None, + execution_count: 0, + total_execution_time: Duration::ZERO, + total_callback_invocations: 0, + peak_memory_bytes: 0, + } + } +} + +impl SessionStats { + /// Create a new SessionStats with the current time as creation time. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Reset all statistics to their default values, preserving the original creation time. + pub fn reset(&mut self) { + let created_at = self.created_at; + *self = Self::default(); + self.created_at = created_at; + } + + /// Reset all statistics to their default values and update the creation time. + pub fn reset_full(&mut self) { + *self = Self::default(); + } +} + /// Common trait for all session implementations. /// /// A session maintains persistent state across multiple `execute()` calls, @@ -136,4 +209,53 @@ mod tests { // Verify SnapshotSession trait is properly defined fn _assert_snapshot_session() {} } + + #[test] + fn test_session_stats_default() { + let stats = SessionStats::default(); + assert_eq!(stats.execution_count, 0); + assert_eq!(stats.total_execution_time, Duration::ZERO); + assert_eq!(stats.total_callback_invocations, 0); + assert_eq!(stats.peak_memory_bytes, 0); + assert!(stats.last_activity.is_none()); + } + + #[test] + fn test_session_stats_reset() { + let mut stats = SessionStats::default(); + let original_created_at = stats.created_at; + + // Modify stats + stats.execution_count = 10; + stats.total_execution_time = Duration::from_secs(5); + stats.total_callback_invocations = 20; + stats.peak_memory_bytes = 1024; + stats.last_activity = Some(Instant::now()); + + // Reset preserving created_at + stats.reset(); + + assert_eq!(stats.created_at, original_created_at); + assert_eq!(stats.execution_count, 0); + assert_eq!(stats.total_execution_time, Duration::ZERO); + assert_eq!(stats.total_callback_invocations, 0); + assert_eq!(stats.peak_memory_bytes, 0); + assert!(stats.last_activity.is_none()); + } + + #[test] + fn test_session_stats_reset_full() { + let mut stats = SessionStats::default(); + let original_created_at = stats.created_at; + + // Small delay to ensure new created_at differs + std::thread::sleep(Duration::from_millis(1)); + + // Full reset (updates created_at) + stats.reset_full(); + + // created_at should be updated (newer) + assert!(stats.created_at >= original_created_at); + assert_eq!(stats.execution_count, 0); + } } From af32eaf3918024197293f97756ab23cd113854db Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Fri, 16 Jan 2026 22:22:35 +0000 Subject: [PATCH 2/2] style: fix rustfmt formatting in lib.rs exports --- crates/eryx/src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/eryx/src/lib.rs b/crates/eryx/src/lib.rs index b6cbacc8..cb4c0385 100644 --- a/crates/eryx/src/lib.rs +++ b/crates/eryx/src/lib.rs @@ -78,9 +78,8 @@ pub use library::RuntimeLibrary; pub use package::{ExtractedPackage, PackageFormat}; pub use sandbox::{ExecuteResult, ExecuteStats, ResourceLimits, Sandbox, SandboxBuilder, state}; pub use session::{ - SessionStats, - InProcessSession, PythonStateSnapshot, Session, SessionExecutor, SnapshotMetadata, - SnapshotSession, + InProcessSession, PythonStateSnapshot, Session, SessionExecutor, SessionStats, + SnapshotMetadata, SnapshotSession, }; pub use trace::{OutputHandler, TraceEvent, TraceEventKind, TraceHandler};