Stop repainting healthy idle TUIs
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s

This commit is contained in:
DuProcess
2026-07-17 21:19:31 -04:00
parent 833ac1082f
commit 60403ba4c3
+18 -90
View File
@@ -77,7 +77,6 @@ const POST_SUBMIT_ALL_INPUT_HOLD: Duration = Duration::from_millis(120);
const STALE_TERMINAL_INPUT_AFTER: Duration = Duration::from_secs(2); const STALE_TERMINAL_INPUT_AFTER: Duration = Duration::from_secs(2);
const POST_RECONNECT_STALE_INPUT_GRACE: Duration = Duration::from_secs(5); const POST_RECONNECT_STALE_INPUT_GRACE: Duration = Duration::from_secs(5);
const FOCUS_REPAINT_COOLDOWN: Duration = Duration::from_secs(1); const FOCUS_REPAINT_COOLDOWN: Duration = Duration::from_secs(1);
const ALT_SCREEN_IDLE_REPAINT_AFTER: Duration = Duration::from_secs(15);
const LOCAL_SLEEP_REPAINT_AFTER: Duration = Duration::from_secs(5); const LOCAL_SLEEP_REPAINT_AFTER: Duration = Duration::from_secs(5);
const LOCAL_SLEEP_REPAINT_RETRY_AFTER: Duration = Duration::from_secs(1); const LOCAL_SLEEP_REPAINT_RETRY_AFTER: Duration = Duration::from_secs(1);
const LOCAL_SLEEP_REPAINT_RETRY_WINDOW: Duration = Duration::from_secs(10); const LOCAL_SLEEP_REPAINT_RETRY_WINDOW: Duration = Duration::from_secs(10);
@@ -6614,9 +6613,8 @@ async fn run_terminal(
let mut startup_input_hold_until: Option<Instant> = None; let mut startup_input_hold_until: Option<Instant> = None;
let mut startup_gate_mode = StartupGateMode::HoldControl; let mut startup_gate_mode = StartupGateMode::HoldControl;
let mut stale_terminal_input_suppress_until: Option<Instant> = None; let mut stale_terminal_input_suppress_until: Option<Instant> = None;
let mut last_terminal_frame_at = Instant::now();
let mut last_focus_repaint_at = Instant::now() - FOCUS_REPAINT_COOLDOWN; let mut last_focus_repaint_at = Instant::now() - FOCUS_REPAINT_COOLDOWN;
let mut last_idle_repaint_attempt_at = Instant::now() - ALT_SCREEN_IDLE_REPAINT_AFTER; let mut last_idle_repaint_attempt_at = Instant::now() - LOCAL_SLEEP_REPAINT_RETRY_AFTER;
let mut last_status_tick_at = Instant::now(); let mut last_status_tick_at = Instant::now();
let mut wake_repaint_retry_until: Option<Instant> = None; let mut wake_repaint_retry_until: Option<Instant> = None;
if let Some(frame) = first_frame { if let Some(frame) = first_frame {
@@ -6624,7 +6622,6 @@ async fn run_terminal(
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(&frame, &mut disconnect_status, &mut status_restore_pending); note_snapshot_rendered(&frame, &mut disconnect_status, &mut status_restore_pending);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
} }
if frame.closed { if frame.closed {
return Ok(()); return Ok(());
@@ -6694,7 +6691,6 @@ async fn run_terminal(
&mut disconnect_status, &mut disconnect_status,
&mut status_restore_pending, &mut status_restore_pending,
); );
last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
send_ack(&socket, addr, &cred, &mut send_seq).await?; send_ack(&socket, addr, &cred, &mut send_seq).await?;
if frame.closed { if frame.closed {
@@ -6779,7 +6775,6 @@ async fn run_terminal(
&mut status_restore_pending, &mut status_restore_pending,
); );
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
last_packet_at = Instant::now(); last_packet_at = Instant::now();
last_focus_repaint_at = Instant::now(); last_focus_repaint_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
@@ -6839,7 +6834,6 @@ async fn run_terminal(
&mut status_restore_pending, &mut status_restore_pending,
); );
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
last_packet_at = Instant::now(); last_packet_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
flush_pending_user_input( flush_pending_user_input(
@@ -7063,7 +7057,6 @@ async fn run_terminal(
&mut status_restore_pending, &mut status_restore_pending,
); );
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
} }
last_packet_at = Instant::now(); last_packet_at = Instant::now();
flush_pending_user_input( flush_pending_user_input(
@@ -7137,7 +7130,6 @@ async fn run_terminal(
&mut status_restore_pending, &mut status_restore_pending,
); );
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
} }
last_packet_at = Instant::now(); last_packet_at = Instant::now();
@@ -7202,7 +7194,6 @@ async fn run_terminal(
&mut status_restore_pending, &mut status_restore_pending,
); );
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
} }
last_packet_at = Instant::now(); last_packet_at = Instant::now();
@@ -7232,7 +7223,6 @@ async fn run_terminal(
predictor.clear_pending()?; predictor.clear_pending()?;
if !forward_only { if !forward_only {
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
if render_resync_needed { if render_resync_needed {
if frame.closed { if frame.closed {
@@ -7388,7 +7378,6 @@ async fn run_terminal(
&mut status_restore_pending, &mut status_restore_pending,
); );
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
} }
last_packet_at = Instant::now(); last_packet_at = Instant::now();
@@ -8008,7 +7997,6 @@ async fn run_terminal(
&mut status_restore_pending, &mut status_restore_pending,
); );
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
last_idle_repaint_attempt_at = Instant::now(); last_idle_repaint_attempt_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
repainted_this_tick = true; repainted_this_tick = true;
@@ -8058,8 +8046,6 @@ async fn run_terminal(
.await?; .await?;
let now = Instant::now(); let now = Instant::now();
if !repainted_this_tick && !render_resync_needed && !frame_renderer.has_pending() && should_repaint_idle_terminal( if !repainted_this_tick && !render_resync_needed && !frame_renderer.has_pending() && should_repaint_idle_terminal(
predictor.alternate_screen,
last_terminal_frame_at,
last_idle_repaint_attempt_at, last_idle_repaint_attempt_at,
status_tick_gap, status_tick_gap,
wake_repaint_retry_until, wake_repaint_retry_until,
@@ -8095,7 +8081,6 @@ async fn run_terminal(
&mut status_restore_pending, &mut status_restore_pending,
); );
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now();
last_packet_at = Instant::now(); last_packet_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
flush_pending_user_input( flush_pending_user_input(
@@ -8670,8 +8655,6 @@ fn strip_terminal_focus_reports(bytes: &[u8]) -> Vec<u8> {
} }
fn should_repaint_idle_terminal( fn should_repaint_idle_terminal(
alternate_screen: bool,
last_terminal_frame_at: Instant,
last_attempt_at: Instant, last_attempt_at: Instant,
status_tick_gap: Duration, status_tick_gap: Duration,
wake_repaint_retry_until: Option<Instant>, wake_repaint_retry_until: Option<Instant>,
@@ -8679,12 +8662,8 @@ fn should_repaint_idle_terminal(
) -> bool { ) -> bool {
let sleep_wake_gap = status_tick_gap >= LOCAL_SLEEP_REPAINT_AFTER; let sleep_wake_gap = status_tick_gap >= LOCAL_SLEEP_REPAINT_AFTER;
let wake_retry_active = wake_repaint_retry_until.is_some_and(|deadline| now < deadline); let wake_retry_active = wake_repaint_retry_until.is_some_and(|deadline| now < deadline);
let stale_alternate_screen = alternate_screen (sleep_wake_gap || wake_retry_active)
&& now.duration_since(last_terminal_frame_at) >= ALT_SCREEN_IDLE_REPAINT_AFTER; && now.duration_since(last_attempt_at) >= LOCAL_SLEEP_REPAINT_RETRY_AFTER
if sleep_wake_gap || wake_retry_active {
return now.duration_since(last_attempt_at) >= LOCAL_SLEEP_REPAINT_RETRY_AFTER;
}
stale_alternate_screen && now.duration_since(last_attempt_at) >= ALT_SCREEN_IDLE_REPAINT_AFTER
} }
fn arm_stale_terminal_input_suppression(suppress_until: &mut Option<Instant>) { fn arm_stale_terminal_input_suppression(suppress_until: &mut Option<Instant>) {
@@ -11206,16 +11185,16 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
ALT_SCREEN_IDLE_REPAINT_AFTER, CachedCredential, DisconnectStatus, DynamicForward, CachedCredential, DisconnectStatus, DynamicForward, FRAME_GAP_RESYNC_AFTER_MS, FrameBuffer,
FRAME_GAP_RESYNC_AFTER_MS, FrameBuffer, LOCAL_SLEEP_REPAINT_AFTER, LOCAL_SLEEP_REPAINT_AFTER, LOCAL_SLEEP_REPAINT_RETRY_AFTER,
LOCAL_SLEEP_REPAINT_RETRY_WINDOW, LocalForward, MAX_PENDING_USER_INPUT_BYTES, LOCAL_SLEEP_REPAINT_RETRY_WINDOW, LocalForward,
NativeIdentityContext, POST_RECONNECT_STALE_INPUT_GRACE, POST_SUBMIT_ALL_INPUT_HOLD, MAX_PENDING_USER_INPUT_BYTES, NativeIdentityContext, POST_RECONNECT_STALE_INPUT_GRACE,
PendingStreamControl, PendingStreamOpen, PendingWindowAdjust, PredictMode, Predictor, POST_SUBMIT_ALL_INPUT_HOLD, PendingStreamControl, PendingStreamOpen, PendingWindowAdjust,
RESTART_STATUS_SCRIPT, RemoteForward, STALE_TERMINAL_INPUT_AFTER, STARTUP_INPUT_HOLD, PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STALE_TERMINAL_INPUT_AFTER,
STDIN_QUEUE_CAPACITY, STREAM_CONTROL_RETRANSMIT_MAX_ATTEMPTS, STREAM_INITIAL_WINDOW, STARTUP_INPUT_HOLD, STDIN_QUEUE_CAPACITY, STREAM_CONTROL_RETRANSMIT_MAX_ATTEMPTS,
SshConfig, SshPathTokenContext, StartupGateMode, StatusAction, TERMINAL_CLEANUP, STREAM_INITIAL_WINDOW, SshConfig, SshPathTokenContext, StartupGateMode, StatusAction,
TERMINAL_SNAPSHOT_RESET, UpdateOptions, UpdateRole, auth_allows, cache_key, TERMINAL_CLEANUP, TERMINAL_SNAPSHOT_RESET, UpdateOptions, UpdateRole, auth_allows,
cache_server_prefix, cleanup_stream_state, clear_cached_credentials, cache_key, cache_server_prefix, cleanup_stream_state, clear_cached_credentials,
ensure_tui_safe_status_overlay, expand_ssh_path_tokens, first_resolved_addr, ensure_tui_safe_status_overlay, expand_ssh_path_tokens, first_resolved_addr,
imported_host_block, input_contains_focus_in, input_prefix_before_escape, imported_host_block, input_contains_focus_in, input_prefix_before_escape,
is_local_status_target, is_resume_response_for_client, is_local_status_target, is_resume_response_for_client,
@@ -14478,86 +14457,35 @@ mod tests {
} }
#[test] #[test]
fn idle_repaint_runs_for_stale_alternate_screen_or_sleep_gap() { fn idle_repaint_runs_only_after_sleep_or_an_armed_wake_retry() {
let now = Instant::now(); let now = Instant::now();
let stale = now - ALT_SCREEN_IDLE_REPAINT_AFTER - Duration::from_secs(1); let stale = now - LOCAL_SLEEP_REPAINT_RETRY_AFTER - Duration::from_secs(1);
let recent = now - Duration::from_secs(1);
let just_attempted = now - Duration::from_millis(500); let just_attempted = now - Duration::from_millis(500);
assert!(should_repaint_idle_terminal( assert!(!should_repaint_idle_terminal(
true,
stale,
stale, stale,
Duration::from_secs(1), Duration::from_secs(1),
None, None,
now now
)); ));
assert!(should_repaint_idle_terminal( assert!(should_repaint_idle_terminal(
false,
recent,
stale, stale,
LOCAL_SLEEP_REPAINT_AFTER + Duration::from_secs(1), LOCAL_SLEEP_REPAINT_AFTER + Duration::from_secs(1),
None, None,
now now
)); ));
assert!(should_repaint_idle_terminal( assert!(should_repaint_idle_terminal(
false, stale,
recent, Duration::from_secs(1),
recent,
LOCAL_SLEEP_REPAINT_AFTER + Duration::from_secs(1),
Some(now + LOCAL_SLEEP_REPAINT_RETRY_WINDOW), Some(now + LOCAL_SLEEP_REPAINT_RETRY_WINDOW),
now now
)); ));
assert!(!should_repaint_idle_terminal( assert!(!should_repaint_idle_terminal(
false,
recent,
just_attempted, just_attempted,
LOCAL_SLEEP_REPAINT_AFTER + Duration::from_secs(1), LOCAL_SLEEP_REPAINT_AFTER + Duration::from_secs(1),
Some(now + LOCAL_SLEEP_REPAINT_RETRY_WINDOW), Some(now + LOCAL_SLEEP_REPAINT_RETRY_WINDOW),
now now
)); ));
assert!(!should_repaint_idle_terminal( assert!(!should_repaint_idle_terminal(
false,
stale,
stale,
Duration::from_secs(1),
None,
now
));
assert!(!should_repaint_idle_terminal(
true,
recent,
stale,
Duration::from_secs(1),
None,
now
));
assert!(should_repaint_idle_terminal(
true,
stale,
recent,
LOCAL_SLEEP_REPAINT_AFTER + Duration::from_secs(1),
None,
now
));
assert!(should_repaint_idle_terminal(
false,
recent,
stale,
Duration::from_secs(1),
Some(now + LOCAL_SLEEP_REPAINT_RETRY_WINDOW),
now
));
assert!(!should_repaint_idle_terminal(
false,
recent,
just_attempted,
Duration::from_secs(1),
Some(now + LOCAL_SLEEP_REPAINT_RETRY_WINDOW),
now
));
assert!(!should_repaint_idle_terminal(
false,
recent,
stale, stale,
Duration::from_secs(1), Duration::from_secs(1),
Some(now - Duration::from_millis(1)), Some(now - Duration::from_millis(1)),