@@ -24,7 +24,8 @@ use crate::state::{ProbeEvent, ProbeOutcome, Session};
2424use crate :: trace:: receiver:: SessionMap ;
2525use crate :: tui:: theme:: Theme ;
2626use crate :: tui:: views:: {
27- HelpView , HopDetailView , MainView , SettingsState , SettingsView , TargetListView ,
27+ HelpView , HopDetailView , MainView , SettingsState , SettingsView , TargetInfo , TargetListView ,
28+ extract_target_infos,
2829} ;
2930
3031/// State for animated replay playback
@@ -99,6 +100,10 @@ pub struct UiState {
99100 pub update_available : Option < String > ,
100101 /// Replay animation state (None = live mode or static replay)
101102 pub replay_state : Option < ReplayState > ,
103+ /// Cached target list info (populated when overlay is open, refreshed every 30 ticks)
104+ pub target_list_cache : Option < Vec < TargetInfo > > ,
105+ /// Tick counter for target list cache refresh
106+ pub target_list_tick : u32 ,
102107}
103108
104109impl UiState {
@@ -310,22 +315,23 @@ where
310315 // Capture target before acquiring locks to prevent race condition
311316 let target_ip = targets[ ui_state. selected_target ] ;
312317
313- // Apply all events up to current adjusted time
314- while replay. current_index < replay. events . len ( ) {
315- let event = & replay. events [ replay. current_index ] ;
316- if event. offset_ms <= adjusted_elapsed {
317- let sessions_read = sessions. read ( ) ;
318- if let Some ( session_lock) = sessions_read. get ( & target_ip) {
319- let mut session = session_lock. write ( ) ;
320- apply_replay_event (
321- & mut session,
322- & replay. events [ replay. current_index ] . clone ( ) ,
323- ) ;
318+ // Find all events up to current adjusted time
319+ let start = replay. current_index ;
320+ let mut end = start;
321+ while end < replay. events . len ( ) && replay. events [ end] . offset_ms <= adjusted_elapsed {
322+ end += 1 ;
323+ }
324+
325+ // Apply all events in a single lock acquisition (no per-event lock churn)
326+ if end > start {
327+ let sessions_read = sessions. read ( ) ;
328+ if let Some ( session_lock) = sessions_read. get ( & target_ip) {
329+ let mut session = session_lock. write ( ) ;
330+ for event in & replay. events [ start..end] {
331+ apply_replay_event ( & mut session, event) ;
324332 }
325- replay. current_index += 1 ;
326- } else {
327- break ; // Wait for this event's time
328333 }
334+ replay. current_index = end;
329335 }
330336
331337 // Check if replay is complete
@@ -344,24 +350,41 @@ where
344350 // Get cache status for settings modal
345351 let cache_status = ix_lookup. as_ref ( ) . map ( |ix| ix. get_cache_status ( ) ) ;
346352
347- // Draw
348- terminal. draw ( |f| {
353+ // Snapshot session data BEFORE draw so no locks are held during rendering.
354+ // Uses snapshot_for_render() to skip cloning the events vec (unbounded, not used in render).
355+ let session_snapshot = {
349356 let sessions_read = sessions. read ( ) ;
350- if let Some ( state) = sessions_read. get ( & current_target) {
351- let session = state. read ( ) ;
357+ sessions_read
358+ . get ( & current_target)
359+ . map ( |state| state. read ( ) . snapshot_for_render ( ) )
360+ } ;
361+
362+ // Refresh target list cache (~every 500ms while overlay is open)
363+ if ui_state. show_target_list {
364+ ui_state. target_list_tick += 1 ;
365+ if ui_state. target_list_cache . is_none ( ) || ui_state. target_list_tick . is_multiple_of ( 30 )
366+ {
367+ ui_state. target_list_cache = Some ( extract_target_infos ( & sessions, & targets) ) ;
368+ }
369+ } else {
370+ ui_state. target_list_cache = None ;
371+ ui_state. target_list_tick = 0 ;
372+ }
373+
374+ // Draw (no locks held — all data is pre-extracted snapshots)
375+ if let Some ( ref session) = session_snapshot {
376+ terminal. draw ( |f| {
352377 draw_ui (
353378 f,
354- & session,
379+ session,
355380 ui_state,
356381 & theme,
357382 num_targets,
358- & sessions,
359- & targets,
360383 cache_status. clone ( ) ,
361384 ix_enabled,
362385 ) ;
363- }
364- } ) ? ;
386+ } ) ? ;
387+ }
365388
366389 // Handle input with timeout
367390 if event:: poll ( tick_rate) ?
@@ -685,6 +708,9 @@ where
685708 if num_targets > 1 {
686709 ui_state. target_list_index = ui_state. selected_target ;
687710 ui_state. show_target_list = true ;
711+ ui_state. target_list_cache =
712+ Some ( extract_target_infos ( & sessions, & targets) ) ;
713+ ui_state. target_list_tick = 0 ;
688714 }
689715 }
690716 KeyCode :: Char ( 'u' ) => {
@@ -782,8 +808,6 @@ fn draw_ui(
782808 ui_state : & UiState ,
783809 theme : & Theme ,
784810 num_targets : usize ,
785- sessions : & SessionMap ,
786- targets : & [ IpAddr ] ,
787811 cache_status : Option < crate :: lookup:: ix:: CacheStatus > ,
788812 ix_enabled : bool ,
789813) {
@@ -897,9 +921,11 @@ fn draw_ui(
897921 }
898922 }
899923
900- if ui_state. show_target_list {
924+ if ui_state. show_target_list
925+ && let Some ( ref infos) = ui_state. target_list_cache
926+ {
901927 f. render_widget (
902- TargetListView :: new ( theme, sessions , targets , ui_state. target_list_index ) ,
928+ TargetListView :: new ( theme, infos , ui_state. target_list_index ) ,
903929 area,
904930 ) ;
905931 }
0 commit comments