aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-28 23:29:11 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-28 23:29:11 +0300
commite27a4ea427c9ed6248c0ee80570e94ed7b1928bd (patch)
tree0cae21a2b3939293be129aaaecb23641f68bab54 /src/server
parente16d7d8c07a20f5ee0b4111808680bbcfd7df9ac (diff)
perf: skip rendering hidden pty updates
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless.rs352
-rw-r--r--src/server/notifications.rs2
2 files changed, 337 insertions, 17 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 7444f10d..899dbf9b 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -14,7 +14,7 @@
//! - Handles stale socket cleanup, explicit server stop, minimum terminal size,
//! and pane spawn failure during restore
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
@@ -145,6 +145,42 @@ impl RenderImpact {
}
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum PtyRenderState {
+ Clean,
+ Hidden,
+ Visible,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct RetainedRenderInput {
+ needs_full_render: bool,
+ needs_graphics_render: bool,
+ pty: PtyRenderState,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum RetainedRenderPlan {
+ Full,
+ Graphics,
+ Pty,
+ HiddenPty,
+}
+
+fn retained_render_plan(input: RetainedRenderInput) -> RetainedRenderPlan {
+ if input.needs_full_render {
+ RetainedRenderPlan::Full
+ } else if input.needs_graphics_render && input.pty != PtyRenderState::Visible {
+ RetainedRenderPlan::Graphics
+ } else {
+ match input.pty {
+ PtyRenderState::Visible => RetainedRenderPlan::Pty,
+ PtyRenderState::Hidden => RetainedRenderPlan::HiddenPty,
+ PtyRenderState::Clean => RetainedRenderPlan::Full,
+ }
+ }
+}
+
fn record_render_impact(source: &'static str, impact: RenderImpact) {
let event = match (source, impact) {
("api_requests", RenderImpact::Graphics) => "graphics_render_cause.api_requests",
@@ -511,10 +547,10 @@ impl HeadlessServer {
continue;
}
- // 1. Check render_dirty flag from PTY reader tasks.
- if self.app.render_dirty.load(Ordering::Acquire) {
+ // 1. Check the coalesced render signal from PTY readers and generic runtime work.
+ if self.app.render_dirty.is_pending() {
needs_render = true;
- crate::render_prof::event("render.request.pty_dirty");
+ crate::render_prof::event("render.request.signal");
}
let terminal_title_changed = self.app.sync_terminal_titles();
if terminal_title_changed && self.app.terminal_title_sidebar_configured() {
@@ -618,28 +654,54 @@ impl HeadlessServer {
// 7. Render virtually and stream frames.
if needs_render && self.app.can_render_now(now) {
crate::render_prof::event("render.attempt");
- let pty_dirty = self.app.render_dirty.swap(false, Ordering::AcqRel);
+ let render_request = self.app.render_dirty.take();
+ let pty_dirty = !render_request.pty_sources.is_empty();
if pty_dirty {
crate::render_prof::event("render.attempt.pty_dirty");
+ crate::render_prof::counter(
+ "render.attempt.pty_sources",
+ render_request.pty_sources.len() as u64,
+ );
+ }
+ if render_request.generic {
+ needs_full_render = true;
+ crate::render_prof::event("full_render_cause.generic_dirty");
}
if needs_full_render {
crate::render_prof::event("retained_gate.needs_full_render");
} else if !pty_dirty {
crate::render_prof::event("retained_gate.not_pty_dirty");
}
- let mut deferred_graphics = false;
- let rendered_retained = if needs_graphics_render && !needs_full_render && !pty_dirty
+ let pty = if !pty_dirty {
+ PtyRenderState::Clean
+ } else if self.pty_sources_visible_to_any_render_target(&render_request.pty_sources)
{
- match self.render_retained_graphics_update_and_stream() {
- RetainedGraphicsOutcome::Sent => true,
- RetainedGraphicsOutcome::Deferred => {
- deferred_graphics = true;
- false
+ PtyRenderState::Visible
+ } else {
+ PtyRenderState::Hidden
+ };
+ let mut deferred_graphics = false;
+ let rendered_retained = match retained_render_plan(RetainedRenderInput {
+ needs_full_render,
+ needs_graphics_render,
+ pty,
+ }) {
+ RetainedRenderPlan::Full => false,
+ RetainedRenderPlan::Graphics => {
+ match self.render_retained_graphics_update_and_stream() {
+ RetainedGraphicsOutcome::Sent => true,
+ RetainedGraphicsOutcome::Deferred => {
+ deferred_graphics = true;
+ false
+ }
+ RetainedGraphicsOutcome::Fallback => false,
}
- RetainedGraphicsOutcome::Fallback => false,
}
- } else {
- pty_dirty && !needs_full_render && self.render_retained_pty_update_and_stream()
+ RetainedRenderPlan::Pty => self.render_retained_pty_update_and_stream(),
+ RetainedRenderPlan::HiddenPty => {
+ crate::render_prof::event("render.skipped.hidden_sources");
+ true
+ }
};
if deferred_graphics {
needs_render = false;
@@ -737,7 +799,7 @@ impl HeadlessServer {
}
}
LoopEvent::RenderRequested => {
- if self.app.render_dirty.load(Ordering::Acquire) {
+ if self.app.render_dirty.is_pending() {
needs_render = true;
}
}
@@ -3396,6 +3458,85 @@ impl HeadlessServer {
}
}
+ fn pty_sources_visible_to_any_render_target(
+ &self,
+ sources: &HashSet<crate::layout::PaneId>,
+ ) -> bool {
+ let mut has_app_target = false;
+ let mut direct_terminal_targets = HashSet::new();
+ for client in self
+ .clients
+ .values()
+ .filter(|client| client.writer.is_some())
+ {
+ match &client.mode {
+ ClientConnectionMode::App if client.is_full_app_client() => {
+ has_app_target = true;
+ }
+ ClientConnectionMode::TerminalAttach { terminal_id }
+ | ClientConnectionMode::TerminalObserve { terminal_id } => {
+ direct_terminal_targets.insert(terminal_id.as_str());
+ }
+ ClientConnectionMode::App => {}
+ }
+ }
+ if !has_app_target && direct_terminal_targets.is_empty() {
+ return false;
+ }
+
+ sources.iter().any(|&pane_id| {
+ let terminal_id = self.terminal_id_for_pane(pane_id);
+ (has_app_target && (terminal_id.is_none() || self.app_surface_contains_pane(pane_id)))
+ || terminal_id
+ .is_none_or(|source| direct_terminal_targets.contains(source.as_str()))
+ })
+ }
+
+ fn terminal_id_for_pane(
+ &self,
+ pane_id: crate::layout::PaneId,
+ ) -> Option<&crate::terminal::TerminalId> {
+ if let Some(popup) = self
+ .app
+ .state
+ .popup_pane
+ .as_ref()
+ .filter(|popup| popup.pane_id == pane_id)
+ {
+ return Some(&popup.terminal_id);
+ }
+ self.app
+ .find_pane(pane_id)
+ .map(|(_, pane)| &pane.attached_terminal_id)
+ }
+
+ fn app_surface_contains_pane(&self, pane_id: crate::layout::PaneId) -> bool {
+ if self
+ .app
+ .state
+ .popup_pane
+ .as_ref()
+ .is_some_and(|popup| popup.pane_id == pane_id)
+ {
+ return true;
+ }
+ let Some(workspace) = self
+ .app
+ .state
+ .active
+ .and_then(|ws_idx| self.app.state.workspaces.get(ws_idx))
+ else {
+ return false;
+ };
+ let Some(tab) = workspace.active_tab() else {
+ return false;
+ };
+ if !tab.panes.contains_key(&pane_id) {
+ return false;
+ }
+ !tab.zoomed || tab.layout.focused() == pane_id
+ }
+
fn render_retained_pty_update_and_stream(&mut self) -> bool {
crate::render_prof::event("retained.attempt");
let retained_started = crate::render_prof::timer();
@@ -4478,6 +4619,42 @@ mod tests {
#[path = "pane_graphics.rs"]
mod pane_graphics_tests;
+ #[test]
+ fn retained_render_plan_covers_each_render_path() {
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: true,
+ needs_graphics_render: true,
+ pty: PtyRenderState::Hidden,
+ }),
+ RetainedRenderPlan::Full
+ );
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: false,
+ needs_graphics_render: true,
+ pty: PtyRenderState::Hidden,
+ }),
+ RetainedRenderPlan::Graphics
+ );
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: false,
+ needs_graphics_render: false,
+ pty: PtyRenderState::Visible,
+ }),
+ RetainedRenderPlan::Pty
+ );
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: false,
+ needs_graphics_render: false,
+ pty: PtyRenderState::Hidden,
+ }),
+ RetainedRenderPlan::HiddenPty
+ );
+ }
+
fn test_headless_server() -> HeadlessServer {
test_headless_server_with_event_hub(api::EventHub::default())
}
@@ -4827,6 +5004,38 @@ mod tests {
(server, client_rx, pane_id)
}
+ fn hidden_pty_visibility_test_server(
+ client_sizes: &[(u16, u16)],
+ ) -> (HeadlessServer, crate::layout::PaneId) {
+ let mut server = test_headless_server();
+ let mut workspace = crate::workspace::Workspace::test_new("test");
+ let background_tab = workspace.test_add_tab(Some("background"));
+ let background_pane = workspace.tabs[background_tab].root_pane;
+ server.app.state.workspaces = vec![workspace];
+ server.app.state.active = Some(0);
+ server.app.state.selected = 0;
+ server.app.state.mode = crate::app::Mode::Terminal;
+
+ for (index, &terminal_size) in client_sizes.iter().enumerate() {
+ let client_id = index as u64 + 1;
+ let (client_tx, _client_control_rx, _client_rx) = test_client_writer();
+ server.clients.insert(
+ client_id,
+ ClientConnection::new(
+ terminal_size,
+ crate::kitty_graphics::HostCellSize::default(),
+ crate::terminal_theme::TerminalTheme::default(),
+ None,
+ client_id,
+ RenderEncoding::SemanticFrame,
+ Some(client_tx),
+ ),
+ );
+ }
+
+ (server, background_pane)
+ }
+
fn assert_frame_data_eq(actual: &FrameData, expected: &FrameData) {
assert_eq!(
(actual.width, actual.height),
@@ -8076,6 +8285,117 @@ next_tab = ""
);
}
+ #[test]
+ fn inactive_tab_pty_source_is_hidden_until_tab_focus() {
+ let (server, background_pane) = hidden_pty_visibility_test_server(&[]);
+ let sources = HashSet::from([background_pane]);
+ assert!(!server.pty_sources_visible_to_any_render_target(&sources));
+
+ let (mut server, background_pane) =
+ hidden_pty_visibility_test_server(&[(120, 40), (44, 20)]);
+ let sources = HashSet::from([background_pane]);
+ assert!(!server.pty_sources_visible_to_any_render_target(&sources));
+
+ server.app.state.workspaces[0].switch_tab(1);
+ assert!(server.pty_sources_visible_to_any_render_target(&sources));
+ }
+
+ #[tokio::test]
+ async fn hidden_pty_output_appears_after_switching_to_its_tab() {
+ let mut server = test_headless_server();
+ let mut workspace = crate::workspace::Workspace::test_new("test");
+ let background_tab = workspace.test_add_tab(Some("background"));
+ let background_pane = workspace.tabs[background_tab].root_pane;
+ workspace.insert_test_runtime(
+ background_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b"before"),
+ );
+ server.app.state.workspaces = vec![workspace];
+ server.app.state.active = Some(0);
+ server.app.state.selected = 0;
+ server.app.state.mode = crate::app::Mode::Terminal;
+
+ let (client_tx, _client_control_rx, client_rx) = test_client_writer();
+ server.clients.insert(
+ 1,
+ ClientConnection::new(
+ (80, 24),
+ crate::kitty_graphics::HostCellSize::default(),
+ crate::terminal_theme::TerminalTheme::default(),
+ None,
+ 1,
+ RenderEncoding::SemanticFrame,
+ Some(client_tx),
+ ),
+ );
+ server.foreground_client_id = Some(1);
+ server.sync_foreground_client_state();
+ server.resize_shared_runtime_to_effective_size();
+ server.render_and_stream();
+ let _initial_frame = client_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("initial frame");
+
+ let runtime = server
+ .app
+ .state
+ .runtime_for_pane_in_workspace(&server.app.terminal_runtimes, 0, background_pane)
+ .expect("background runtime");
+ runtime.test_process_pty_bytes(b"\rhidden-update");
+ assert!(server.app.render_dirty.request_pty(background_pane));
+ let request = server.app.render_dirty.take();
+ let pty = if server.pty_sources_visible_to_any_render_target(&request.pty_sources) {
+ PtyRenderState::Visible
+ } else {
+ PtyRenderState::Hidden
+ };
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: false,
+ needs_graphics_render: false,
+ pty,
+ }),
+ RetainedRenderPlan::HiddenPty
+ );
+ assert!(client_rx.recv_timeout(Duration::from_millis(50)).is_err());
+
+ server.app.state.workspaces[0].switch_tab(background_tab);
+ server.render_and_stream();
+ let visible_frame = read_server_frame(
+ client_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("frame after tab switch"),
+ );
+ assert!(frame_text(&visible_frame).contains("hidden-update"));
+ }
+
+ #[test]
+ fn direct_terminal_observer_keeps_hidden_pty_source_renderable() {
+ let (mut server, background_pane) = hidden_pty_visibility_test_server(&[]);
+ let terminal_id = server.app.state.workspaces[0]
+ .terminal_id(background_pane)
+ .expect("background terminal id")
+ .to_string();
+ let (client_tx, _client_control_rx, _client_rx) = test_client_writer();
+ server.clients.insert(
+ 1,
+ ClientConnection::new_with_mode(
+ ClientConnectionMode::TerminalObserve { terminal_id },
+ None,
+ (80, 24),
+ crate::kitty_graphics::HostCellSize::default(),
+ crate::terminal_theme::TerminalTheme::default(),
+ None,
+ 1,
+ RenderEncoding::SemanticFrame,
+ false,
+ Some(client_tx),
+ ),
+ );
+
+ assert!(server.pty_sources_visible_to_any_render_target(&HashSet::from([background_pane])));
+ }
+
#[tokio::test]
async fn retained_pty_update_streams_dirty_row_from_last_frame() {
let (mut server, client_rx, pane_id) = retained_test_server(b"aaaa");
diff --git a/src/server/notifications.rs b/src/server/notifications.rs
index 79ff63ca..b78cb6df 100644
--- a/src/server/notifications.rs
+++ b/src/server/notifications.rs
@@ -124,7 +124,7 @@ mod tests {
&crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
- std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ std::sync::Arc::new(crate::render_signal::RenderSignal::new()),
)
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);