Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/eryx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ pub use library::RuntimeLibrary;
pub use package::{ExtractedPackage, PackageFormat};
pub use sandbox::{ExecuteResult, ExecuteStats, ResourceLimits, Sandbox, SandboxBuilder, state};
pub use session::{
InProcessSession, PythonStateSnapshot, Session, SessionExecutor, SnapshotMetadata,
SnapshotSession,
InProcessSession, PythonStateSnapshot, Session, SessionExecutor, SessionStats,
SnapshotMetadata, SnapshotSession,
};
pub use trace::{OutputHandler, TraceEvent, TraceEventKind, TraceHandler};

Expand Down
84 changes: 79 additions & 5 deletions crates/eryx/src/session/in_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
///
Expand All @@ -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()
}
}
Expand All @@ -104,6 +109,7 @@ impl<'a> InProcessSession<'a> {
sandbox,
executor,
preamble_executed: false,
stats: SessionStats::new(),
})
}

Expand Down Expand Up @@ -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;
Expand All @@ -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<Instant> {
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<Duration> {
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.
Expand Down Expand Up @@ -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.
Expand All @@ -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(())
}
}
Expand Down
122 changes: 122 additions & 0 deletions crates/eryx/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Instant>,

/// 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,
Expand Down Expand Up @@ -136,4 +209,53 @@ mod tests {
// Verify SnapshotSession trait is properly defined
fn _assert_snapshot_session<T: SnapshotSession>() {}
}

#[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);
}
}