@@ -1944,7 +1944,7 @@ pub fn run_closed_loop<F: NavigationFilter>(
19441944 let cov = filter. get_certainty ( ) ;
19451945 results. push ( NavigationResult :: from ( ( & start_time, & mean, & cov) ) ) ;
19461946 debug ! ( "Initial filter state at {}: {:?}" , start_time, mean) ;
1947- let mut last_ts = start_time;
1947+ let mut last_ts = Some ( start_time) ;
19481948
19491949 for ( i, event) in stream. events . into_iter ( ) . enumerate ( ) {
19501950 // Print detailed progress every 100 iterations or at key milestones
@@ -2034,7 +2034,7 @@ pub fn run_closed_loop<F: NavigationFilter>(
20342034 let cov = filter. get_certainty ( ) ;
20352035 results. push ( NavigationResult :: from ( ( & ts, & mean, & cov) ) ) ;
20362036 debug ! ( "Filter state at {}: {:?}" , ts, mean) ;
2037- last_ts = ts ;
2037+ last_ts = Some ( ts ) ;
20382038 }
20392039 }
20402040 debug ! ( "Closed-loop simulation complete" ) ;
@@ -2576,6 +2576,17 @@ pub fn print_sim_status<F: NavigationFilter>(filter: &F) {
25762576pub mod execution {
25772577 use super :: * ;
25782578
2579+ /// Configuration for execution timeout limits in simulations.
2580+ ///
2581+ /// This struct provides multiple timeout mechanisms to prevent runaway computations
2582+ /// and detect performance issues during simulation execution:
2583+ ///
2584+ /// - **Wall-clock ratio timeout**: Limits execution time relative to simulation duration
2585+ /// - **Absolute wall-clock timeout**: Enforces a hard limit in real-world seconds
2586+ /// - **No-progress timeout**: Detects when simulation makes no progress (possible hang)
2587+ ///
2588+ /// All timeout values <= 0 are treated as disabled. The most restrictive active timeout
2589+ /// will trigger first. These limits are enforced by [`ExecutionMonitor`] during simulation.
25792590 #[ derive( Clone , Debug , Serialize , Deserialize ) ]
25802591 pub struct ExecutionLimits {
25812592 /// Max wall-clock time as a ratio of simulated duration (<= 0 disables).
@@ -2611,6 +2622,17 @@ pub mod execution {
26112622 }
26122623 }
26132624
2625+ /// Monitors execution time and progress during simulation runs.
2626+ ///
2627+ /// This struct tracks wall-clock time and detects stalled simulations by monitoring
2628+ /// when progress was last reported. It enforces timeout limits specified in [`ExecutionLimits`].
2629+ ///
2630+ /// The monitor maintains two types of timeouts:
2631+ /// - **Wall-clock timeout**: Maximum total execution time (computed from simulation duration and limits)
2632+ /// - **No-progress timeout**: Maximum time without calling `mark_progress()`
2633+ ///
2634+ /// Use [`check`](ExecutionMonitor::check) periodically during simulation to verify execution
2635+ /// stays within limits, and call `mark_progress()` after each significant simulation step.
26142636 #[ derive( Clone , Debug ) ]
26152637 pub struct ExecutionMonitor {
26162638 start_time : Instant ,
@@ -2620,6 +2642,17 @@ pub mod execution {
26202642 }
26212643
26222644 impl ExecutionMonitor {
2645+ /// Creates a new execution monitor with the specified limits.
2646+ ///
2647+ /// # Arguments
2648+ ///
2649+ /// * `limits` - Timeout configuration including wall-clock and no-progress limits
2650+ /// * `sim_duration_s` - Expected simulation duration in seconds, used to compute
2651+ /// wall-clock timeout when `max_wall_clock_ratio` is enabled
2652+ ///
2653+ /// # Returns
2654+ ///
2655+ /// A new `ExecutionMonitor` instance initialized with the current time.
26232656 pub fn new ( limits : ExecutionLimits , sim_duration_s : f64 ) -> Self {
26242657 let max_wall_clock = compute_max_wall_clock (
26252658 sim_duration_s,
@@ -2641,6 +2674,33 @@ pub mod execution {
26412674 }
26422675 }
26432676
2677+ /// Checks if execution is within timeout limits.
2678+ ///
2679+ /// This method verifies that the simulation has not exceeded wall-clock or no-progress
2680+ /// timeout limits. It should be called periodically during simulation execution.
2681+ ///
2682+ /// # Arguments
2683+ ///
2684+ /// * `context` - A string describing the current execution context, included in error messages
2685+ ///
2686+ /// # Returns
2687+ ///
2688+ /// * `Ok(())` if execution is within limits
2689+ /// * `Err(...)` with a descriptive message if any timeout has been exceeded
2690+ ///
2691+ /// # Example
2692+ ///
2693+ /// ```no_run
2694+ /// # use strapdown_core::sim::{ExecutionMonitor, ExecutionLimits};
2695+ /// let limits = ExecutionLimits::default();
2696+ /// let mut monitor = ExecutionMonitor::new(limits, 100.0);
2697+ ///
2698+ /// // Check timeout before processing
2699+ /// monitor.check("data processing")?;
2700+ /// // ... do work ...
2701+ /// monitor.mark_progress();
2702+ /// # Ok::<(), anyhow::Error>(())
2703+ /// ```
26442704 pub fn check ( & mut self , context : & str ) -> Result < ( ) > {
26452705 let now = Instant :: now ( ) ;
26462706 if let Some ( max_wall_clock) = self . max_wall_clock
@@ -4303,6 +4363,69 @@ mod tests {
43034363 assert ! ( result. is_err( ) ) ;
43044364 }
43054365
4366+ #[ test]
4367+ fn test_execution_monitor_wall_clock_timeout ( ) {
4368+ let limits = ExecutionLimits {
4369+ max_wall_clock_ratio : 0.0 ,
4370+ max_wall_clock_s : 0.01 ,
4371+ max_no_progress_s : 0.0 ,
4372+ } ;
4373+ let mut monitor = ExecutionMonitor :: new ( limits, 1.0 ) ;
4374+
4375+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 20 ) ) ;
4376+ let result = monitor. check ( "test" ) ;
4377+ assert ! ( result. is_err( ) ) ;
4378+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "wall-clock limit" ) ) ;
4379+ }
4380+
4381+ #[ test]
4382+ fn test_execution_monitor_successful_execution_with_progress ( ) {
4383+ let limits = ExecutionLimits {
4384+ max_wall_clock_ratio : 0.0 ,
4385+ max_wall_clock_s : 1.0 ,
4386+ max_no_progress_s : 0.05 ,
4387+ } ;
4388+ let mut monitor = ExecutionMonitor :: new ( limits, 1.0 ) ;
4389+
4390+ // Simulate work with regular progress updates
4391+ for _ in 0 ..5 {
4392+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 10 ) ) ;
4393+ let result = monitor. check ( "test" ) ;
4394+ assert ! ( result. is_ok( ) ) ;
4395+ monitor. mark_progress ( ) ;
4396+ }
4397+ }
4398+
4399+ #[ test]
4400+ fn test_execution_monitor_zero_timeout_disabled ( ) {
4401+ let limits = ExecutionLimits {
4402+ max_wall_clock_ratio : 0.0 ,
4403+ max_wall_clock_s : 0.0 ,
4404+ max_no_progress_s : 0.0 ,
4405+ } ;
4406+ let mut monitor = ExecutionMonitor :: new ( limits, 1.0 ) ;
4407+
4408+ // With all timeouts disabled, sleep and check should succeed
4409+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) ;
4410+ let result = monitor. check ( "test" ) ;
4411+ assert ! ( result. is_ok( ) ) ;
4412+ }
4413+
4414+ #[ test]
4415+ fn test_execution_monitor_negative_timeout_disabled ( ) {
4416+ let limits = ExecutionLimits {
4417+ max_wall_clock_ratio : -1.0 ,
4418+ max_wall_clock_s : -1.0 ,
4419+ max_no_progress_s : -1.0 ,
4420+ } ;
4421+ let mut monitor = ExecutionMonitor :: new ( limits, 1.0 ) ;
4422+
4423+ // With negative timeouts (disabled), sleep and check should succeed
4424+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) ;
4425+ let result = monitor. check ( "test" ) ;
4426+ assert ! ( result. is_ok( ) ) ;
4427+ }
4428+
43064429 #[ test]
43074430 fn test_health_monitor_check_valid_state ( ) {
43084431 let limits = HealthLimits :: default ( ) ;
0 commit comments