From fc4e1bbf6b0fa2ec37445a5b72356514c7c6be6a Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sat, 30 May 2026 23:58:15 +0300 Subject: perf: reduce sidebar git polling overhead refs #353 --- src/server/client_transport.rs | 197 ++++++++++++++++++++++++++++------------- src/server/clients.rs | 17 +++- src/server/headless.rs | 153 +++++++++++++++++++++++++++++++- 3 files changed, 301 insertions(+), 66 deletions(-) (limited to 'src/server') diff --git a/src/server/client_transport.rs b/src/server/client_transport.rs index 36334fda..931f1100 100644 --- a/src/server/client_transport.rs +++ b/src/server/client_transport.rs @@ -14,8 +14,8 @@ use tokio::sync::mpsc; use tracing::{debug, warn}; use crate::protocol::{ - self, AttachScrollDirection, AttachScrollSource, ClientKeybindings, ClientMessage, - RenderEncoding, ServerMessage, MAX_CLIPBOARD_IMAGE_PAYLOAD, MAX_FRAME_SIZE, + self, AttachScrollDirection, AttachScrollSource, ClientKeybindings, ClientLaunchMode, + ClientMessage, RenderEncoding, ServerMessage, MAX_CLIPBOARD_IMAGE_PAYLOAD, MAX_FRAME_SIZE, MAX_GRAPHICS_FRAME_SIZE, PROTOCOL_VERSION, }; @@ -57,6 +57,7 @@ pub(crate) enum ServerEvent { cell_height_px: u32, render_encoding: RenderEncoding, keybindings: Option>, + direct_attach_requested: bool, writer: ClientWriter, }, /// A client sent an input message. @@ -159,68 +160,77 @@ pub(crate) fn handle_client_handshake( } }; - let (client_cols, client_rows, cell_width_px, cell_height_px, render_encoding, keybindings) = - match hello { - ClientMessage::Hello { - version, - cols, - rows, + let ( + client_cols, + client_rows, + cell_width_px, + cell_height_px, + render_encoding, + keybindings, + direct_attach_requested, + ) = match hello { + ClientMessage::Hello { + version, + cols, + rows, + cell_width_px, + cell_height_px, + requested_encoding, + keybindings, + launch_mode, + } => { + // Version check. + match protocol::check_client_version(version) { + protocol::VersionCheck::Compatible => {} + protocol::VersionCheck::Incompatible(reason) => { + // Send rejection Welcome. + let welcome = ServerMessage::Welcome { + version: PROTOCOL_VERSION, + encoding: RenderEncoding::SemanticFrame, + error: Some(reason), + }; + let _ = protocol::write_message(&mut stream, &welcome); + return Ok(()); + } + } + + let keybindings = match parse_client_keybindings(keybindings) { + Ok(keybindings) => keybindings, + Err(error) => { + let welcome = ServerMessage::Welcome { + version: PROTOCOL_VERSION, + encoding: RenderEncoding::SemanticFrame, + error: Some(error), + }; + let _ = protocol::write_message(&mut stream, &welcome); + return Ok(()); + } + }; + + // Clamp size. + let (clamped_cols, clamped_rows) = clamp_terminal_size(cols, rows); + ( + clamped_cols, + clamped_rows, cell_width_px, cell_height_px, requested_encoding, keybindings, - } => { - // Version check. - match protocol::check_client_version(version) { - protocol::VersionCheck::Compatible => {} - protocol::VersionCheck::Incompatible(reason) => { - // Send rejection Welcome. - let welcome = ServerMessage::Welcome { - version: PROTOCOL_VERSION, - encoding: RenderEncoding::SemanticFrame, - error: Some(reason), - }; - let _ = protocol::write_message(&mut stream, &welcome); - return Ok(()); - } - } - - let keybindings = match parse_client_keybindings(keybindings) { - Ok(keybindings) => keybindings, - Err(error) => { - let welcome = ServerMessage::Welcome { - version: PROTOCOL_VERSION, - encoding: RenderEncoding::SemanticFrame, - error: Some(error), - }; - let _ = protocol::write_message(&mut stream, &welcome); - return Ok(()); - } - }; - - // Clamp size. - let (clamped_cols, clamped_rows) = clamp_terminal_size(cols, rows); - ( - clamped_cols, - clamped_rows, - cell_width_px, - cell_height_px, - requested_encoding, - keybindings, - ) - } - _ => { - // First message must be Hello. - debug!(client_id, "first message was not Hello, closing"); - let welcome = ServerMessage::Welcome { - version: PROTOCOL_VERSION, - encoding: RenderEncoding::SemanticFrame, - error: Some("expected Hello as first message".to_owned()), - }; - let _ = protocol::write_message(&mut stream, &welcome); - return Ok(()); - } - }; + launch_mode == ClientLaunchMode::TerminalAttach, + ) + } + _ => { + // First message must be Hello. + debug!(client_id, "first message was not Hello, closing"); + let welcome = ServerMessage::Welcome { + version: PROTOCOL_VERSION, + encoding: RenderEncoding::SemanticFrame, + error: Some("expected Hello as first message".to_owned()), + }; + let _ = protocol::write_message(&mut stream, &welcome); + return Ok(()); + } + }; // Send Welcome. let welcome = ServerMessage::Welcome { @@ -250,6 +260,7 @@ pub(crate) fn handle_client_handshake( cell_height_px, render_encoding, keybindings, + direct_attach_requested, writer, }); @@ -576,6 +587,7 @@ new_tab = "ctrl+notakey" cell_height_px: 16, requested_encoding: RenderEncoding::TerminalAnsi, keybindings: ClientKeybindings::Server, + launch_mode: ClientLaunchMode::App, }, ) .expect("write hello"); @@ -607,6 +619,7 @@ new_tab = "ctrl+notakey" cell_height_px, render_encoding, keybindings, + direct_attach_requested, writer, } => { assert_eq!(client_id, 42); @@ -614,6 +627,70 @@ new_tab = "ctrl+notakey" assert_eq!((cell_width_px, cell_height_px), (8, 16)); assert_eq!(render_encoding, RenderEncoding::TerminalAnsi); assert!(keybindings.is_none()); + assert!(!direct_attach_requested); + drop(writer); + } + other => panic!("expected ClientConnected, got {other:?}"), + } + + drop(client_stream); + should_quit.store(true, Ordering::Release); + handle + .join() + .expect("handshake thread join") + .expect("handshake thread result"); + } + + #[test] + fn handshake_marks_terminal_attach_launch_mode() { + let (mut client_stream, server_stream) = UnixStream::pair().expect("socket pair"); + let (server_event_tx, mut server_event_rx) = mpsc::channel(4); + let should_quit = Arc::new(AtomicBool::new(false)); + let handshake_quit = should_quit.clone(); + let handle = std::thread::spawn(move || { + handle_client_handshake(server_stream, 42, &server_event_tx, &handshake_quit) + }); + + protocol::write_message( + &mut client_stream, + &ClientMessage::Hello { + version: PROTOCOL_VERSION, + cols: 100, + rows: 30, + cell_width_px: 8, + cell_height_px: 16, + requested_encoding: RenderEncoding::TerminalAnsi, + keybindings: ClientKeybindings::Server, + launch_mode: ClientLaunchMode::TerminalAttach, + }, + ) + .expect("write hello"); + + let welcome: ServerMessage = + protocol::read_message(&mut client_stream, MAX_FRAME_SIZE).expect("read welcome"); + match welcome { + ServerMessage::Welcome { + version, + encoding, + error, + } => { + assert_eq!(version, PROTOCOL_VERSION); + assert_eq!(encoding, RenderEncoding::TerminalAnsi); + assert_eq!(error, None); + } + other => panic!("expected Welcome, got {other:?}"), + } + + match server_event_rx + .blocking_recv() + .expect("client connected event") + { + ServerEvent::ClientConnected { + direct_attach_requested, + writer, + .. + } => { + assert!(direct_attach_requested); drop(writer); } other => panic!("expected ClientConnected, got {other:?}"), diff --git a/src/server/clients.rs b/src/server/clients.rs index 026dd378..611d87eb 100644 --- a/src/server/clients.rs +++ b/src/server/clients.rs @@ -23,6 +23,8 @@ pub(crate) type RenderTarget = ( pub(crate) struct ClientConnection { /// Whether this connection is the full app client or a direct terminal attach. pub(crate) mode: ClientConnectionMode, + /// True after the handshake for clients that will switch into direct terminal attach mode. + pub(crate) pending_terminal_attach: bool, /// Client-local app keybindings. None means use the server's keybindings. pub(crate) keybindings: Option>, /// The client's terminal size after clamping. @@ -73,6 +75,7 @@ impl ClientConnection { outer_terminal_focus, last_activity, render_encoding, + false, writer, ) } @@ -86,10 +89,12 @@ impl ClientConnection { outer_terminal_focus: Option, last_activity: u64, render_encoding: RenderEncoding, + pending_terminal_attach: bool, writer: Option, ) -> Self { Self { mode, + pending_terminal_attach, keybindings, terminal_size, cell_size, @@ -112,6 +117,10 @@ impl ClientConnection { self.graphics_surface_reset_pending = true; } + pub(crate) fn is_full_app_client(&self) -> bool { + matches!(self.mode, ClientConnectionMode::App) && !self.pending_terminal_attach + } + pub(crate) fn request_semantic_redraw_after_input(&mut self) { self.render_state.reset_semantic_input_baseline(); } @@ -168,7 +177,7 @@ pub(crate) fn events_include_interaction(events: &[crate::raw_input::RawInputEve pub(crate) fn latest_app_client(clients: &HashMap) -> Option { clients .iter() - .filter(|(_, client)| matches!(client.mode, ClientConnectionMode::App)) + .filter(|(_, client)| client.is_full_app_client()) .max_by_key(|(_, client)| client.last_activity) .map(|(&client_id, _)| client_id) } @@ -194,7 +203,11 @@ pub(crate) fn render_targets( ) -> Vec { let mut targets: Vec = clients .iter() - .filter(|(_, client)| client.writer.is_some()) + .filter(|(_, client)| { + client.writer.is_some() + && (client.is_full_app_client() + || matches!(client.mode, ClientConnectionMode::TerminalAttach { .. })) + }) .map(|(&client_id, client)| { ( client_id, diff --git a/src/server/headless.rs b/src/server/headless.rs index f59201e0..3d273b1c 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -423,7 +423,11 @@ impl HeadlessServer { // 8. Wait for next event. let next_deadline = self .app - .next_headless_loop_deadline(now, needs_render) + .next_headless_loop_deadline_with_git_refresh( + now, + needs_render, + self.has_app_client(), + ) .map(|deadline| deadline.min(now + CLIENT_ACCEPT_POLL_INTERVAL)) .or(Some(now + CLIENT_ACCEPT_POLL_INTERVAL)); let event = { @@ -901,6 +905,17 @@ impl HeadlessServer { changed } + fn app_client_count(&self) -> usize { + self.clients + .values() + .filter(|client| client.is_full_app_client() && client.writer.is_some()) + .count() + } + + fn has_app_client(&self) -> bool { + self.app_client_count() > 0 + } + fn remove_client(&mut self, client_id: u64) -> bool { let was_foreground = self.foreground_client_id == Some(client_id); self.send_client_graphics_cleanup(client_id); @@ -1619,6 +1634,7 @@ impl HeadlessServer { client.mode = ClientConnectionMode::TerminalAttach { terminal_id: terminal_id.clone(), }; + client.pending_terminal_attach = false; client.render_state.reset_baseline(); client.last_activity = stamp; let was_foreground = self.foreground_client_id == Some(client_id); @@ -1655,6 +1671,7 @@ impl HeadlessServer { keybindings, writer, render_encoding, + direct_attach_requested, } => { if self.handoff_in_progress { if let Ok(message) = @@ -1669,6 +1686,7 @@ impl HeadlessServer { } return false; } + let first_app_client = !direct_attach_requested && self.app_client_count() == 0; info!( client_id, cols, @@ -1693,10 +1711,16 @@ impl HeadlessServer { None, last_activity, render_encoding, + direct_attach_requested, Some(writer), ), ); - self.foreground_client_id = Some(client_id); + if !direct_attach_requested { + self.foreground_client_id = Some(client_id); + } + if first_app_client { + self.app.mark_git_status_refresh_due(Instant::now()); + } self.sync_foreground_client_state(); self.resize_shared_runtime_to_effective_size(); self.nudge_handoff_panes_on_first_client_attach(); @@ -2190,7 +2214,7 @@ impl HeadlessServer { let mut broken_clients: Vec = Vec::new(); for (&client_id, client) in &mut self.clients { - if !matches!(client.mode, ClientConnectionMode::App) { + if !client.is_full_app_client() { continue; } if client.host_mouse_capture_active == Some(enabled) { @@ -2484,7 +2508,9 @@ impl HeadlessServer { changed |= self.app.clear_due_selection_highlight(now); - self.app.start_git_status_refresh_if_due(now); + if self.has_app_client() { + self.app.start_git_status_refresh_if_due(now); + } if self .app @@ -3011,6 +3037,7 @@ new_tab = "prefix+t" cell_height_px: 0, render_encoding: RenderEncoding::SemanticFrame, keybindings: Some(Box::new(local_keybindings)), + direct_attach_requested: false, writer: writer_a, })); assert_eq!( @@ -3034,6 +3061,7 @@ new_tab = "prefix+t" cell_height_px: 0, render_encoding: RenderEncoding::SemanticFrame, keybindings: None, + direct_attach_requested: false, writer: writer_b, })); assert_eq!( @@ -3073,6 +3101,7 @@ new_tab = "prefix+t" cell_height_px: 0, render_encoding: RenderEncoding::SemanticFrame, keybindings: Some(Box::new(local_keybindings)), + direct_attach_requested: false, writer: writer_a, })); assert_eq!(server.app.state.config_diagnostic, without_keybindings); @@ -3085,6 +3114,7 @@ new_tab = "prefix+t" cell_height_px: 0, render_encoding: RenderEncoding::SemanticFrame, keybindings: None, + direct_attach_requested: false, writer: writer_b, })); assert_eq!( @@ -3127,6 +3157,7 @@ next_tab = "" cell_height_px: 0, render_encoding: RenderEncoding::SemanticFrame, keybindings: Some(Box::new(local_keybindings)), + direct_attach_requested: false, writer, })); server.app.state.mode = crate::app::Mode::Settings; @@ -3200,6 +3231,7 @@ next_tab = "" cell_height_px: 0, render_encoding: RenderEncoding::SemanticFrame, keybindings: Some(Box::new(local_config.live_keybinds().unwrap())), + direct_attach_requested: false, writer: writer_a, })); server.app.state.mode = crate::app::Mode::Settings; @@ -3219,6 +3251,7 @@ next_tab = "" cell_height_px: 0, render_encoding: RenderEncoding::SemanticFrame, keybindings: None, + direct_attach_requested: false, writer: writer_b, })); assert_eq!( @@ -3251,6 +3284,7 @@ next_tab = "" cell_height_px: 0, render_encoding: RenderEncoding::TerminalAnsi, keybindings: None, + direct_attach_requested: true, writer, })); assert!(server.clients.contains_key(&7)); @@ -3270,6 +3304,115 @@ next_tab = "" ); } + fn app_client_marks_git_refresh_due_on_first_attach(render_encoding: RenderEncoding) { + let mut server = test_headless_server(); + server + .app + .state + .workspaces + .push(crate::workspace::Workspace::test_new("test")); + let future = Instant::now() + Duration::from_secs(60); + server.app.last_git_remote_status_refresh = future; + let (writer, _control_rx, _render_rx) = test_client_writer(); + + assert!(server.handle_server_event(ServerEvent::ClientConnected { + client_id: 7, + cols: 80, + rows: 24, + cell_width_px: 0, + cell_height_px: 0, + render_encoding, + keybindings: None, + direct_attach_requested: false, + writer, + })); + + assert!(server.has_app_client()); + assert!(server + .app + .git_refresh_deadline() + .is_some_and(|deadline| deadline <= Instant::now())); + } + + #[test] + fn terminal_ansi_app_client_enables_headless_git_refresh() { + app_client_marks_git_refresh_due_on_first_attach(RenderEncoding::TerminalAnsi); + } + + #[test] + fn pending_terminal_attach_client_does_not_enable_headless_git_refresh() { + let mut server = test_headless_server(); + server + .app + .state + .workspaces + .push(crate::workspace::Workspace::test_new("test")); + let (writer, _control_rx, _render_rx) = test_client_writer(); + + assert!(server.handle_server_event(ServerEvent::ClientConnected { + client_id: 7, + cols: 80, + rows: 24, + cell_width_px: 0, + cell_height_px: 0, + render_encoding: RenderEncoding::TerminalAnsi, + keybindings: None, + direct_attach_requested: true, + writer, + })); + + assert!(!server.has_app_client()); + assert_eq!( + server.app.next_headless_loop_deadline_with_git_refresh( + Instant::now(), + false, + server.has_app_client() + ), + None + ); + } + + #[test] + fn writerless_app_client_does_not_enable_headless_git_refresh() { + let mut server = test_headless_server(); + server + .app + .state + .workspaces + .push(crate::workspace::Workspace::test_new("test")); + let (writer, _control_rx, _render_rx) = test_client_writer(); + + assert!(server.handle_server_event(ServerEvent::ClientConnected { + client_id: 7, + cols: 80, + rows: 24, + cell_width_px: 0, + cell_height_px: 0, + render_encoding: RenderEncoding::SemanticFrame, + keybindings: None, + direct_attach_requested: false, + writer, + })); + assert!(server.has_app_client()); + + server.clients.get_mut(&7).expect("client").writer = None; + + assert!(!server.has_app_client()); + assert_eq!( + server.app.next_headless_loop_deadline_with_git_refresh( + Instant::now(), + false, + server.has_app_client() + ), + None + ); + } + + #[test] + fn semantic_app_client_marks_git_refresh_due_on_first_attach() { + app_client_marks_git_refresh_due_on_first_attach(RenderEncoding::SemanticFrame); + } + #[test] fn terminal_attach_client_exits_when_attached_pane_dies() { let mut server = test_headless_server(); @@ -3292,6 +3435,7 @@ next_tab = "" cell_height_px: 0, render_encoding: RenderEncoding::TerminalAnsi, keybindings: None, + direct_attach_requested: true, writer, })); assert!( @@ -4334,6 +4478,7 @@ next_tab = "" cell_height_px: 0, render_encoding: RenderEncoding::TerminalAnsi, keybindings: None, + direct_attach_requested: true, writer, })); assert!( -- cgit v1.2.3