diff options
| author | akbash <akbash@herdr.dev> | 2026-08-18 04:37:11 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-18 04:37:11 +0300 |
| commit | 2d24950ad9a02096921bc764e2e3dcd8900c3366 (patch) | |
| tree | b6884a29425922dfed083bb7108415ae56725a45 /src/server | |
| parent | d497ee14dd3f2b5325a16d0de779ed6bd62178ee (diff) | |
fix: isolate hidden pane render cadence (#2892)
refs #2890
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/headless.rs | 89 |
1 files changed, 77 insertions, 12 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs index 4231b073..ae0d7b20 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -698,8 +698,18 @@ impl HeadlessServer { self.stream_host_mouse_capture_mode(); self.stream_host_keyboard_enhancement_flags(); - // 7. Render virtually and stream frames. - if needs_render && self.app.can_render_now(now) { + // 7. Render virtually and stream frames. Hidden-only PTY work keeps a + // bounded classification cadence without delaying presentation work + // that joins the same coalesced request. + let render_cadence_due = self.app.can_render_now(now); + if needs_render + && (render_cadence_due + || (self.app.can_present_now(now) + && self.has_pending_presentation_work( + needs_full_render, + needs_graphics_render, + ))) + { crate::render_prof::event("render.attempt"); let render_request = self.app.render_dirty.take(); let pty_dirty = !render_request.pty_sources.is_empty(); @@ -744,11 +754,12 @@ impl HeadlessServer { PtyRenderState::Hidden }; let mut deferred_graphics = false; - let rendered_retained = match retained_render_plan(RetainedRenderInput { + let render_plan = retained_render_plan(RetainedRenderInput { needs_full_render, needs_graphics_render, pty, - }) { + }); + let rendered_retained = match render_plan { RetainedRenderPlan::Full => false, RetainedRenderPlan::Graphics => { match self.render_retained_graphics_update_and_stream() { @@ -774,7 +785,8 @@ impl HeadlessServer { crate::render_prof::event("full_render.invoke"); self.render_and_stream(); } - self.app.last_render_at = Some(now); + self.app + .record_render_attempt(now, render_plan != RetainedRenderPlan::HiddenPty); needs_render = false; needs_full_render = false; needs_graphics_render = false; @@ -4069,10 +4081,32 @@ impl HeadlessServer { } } - fn pty_sources_visible_to_any_render_target( + fn has_pending_presentation_work( &self, - sources: &HashSet<crate::layout::PaneId>, + needs_full_render: bool, + needs_graphics_render: bool, ) -> bool { + if needs_full_render + || needs_graphics_render + || self.app.render_dirty.has_generic_or_terminal_title() + { + return true; + } + + let (has_app_target, direct_terminal_targets) = self.pty_render_targets(); + if !has_app_target && direct_terminal_targets.is_empty() { + return false; + } + self.app.render_dirty.has_pty_source_matching(|pane_id| { + self.pty_source_visible_to_render_targets( + pane_id, + has_app_target, + &direct_terminal_targets, + ) + }) + } + + fn pty_render_targets(&self) -> (bool, HashSet<&str>) { let mut has_app_target = false; let mut direct_terminal_targets = HashSet::new(); for client in self @@ -4091,15 +4125,35 @@ impl HeadlessServer { ClientConnectionMode::App => {} } } + (has_app_target, direct_terminal_targets) + } + + fn pty_source_visible_to_render_targets( + &self, + pane_id: crate::layout::PaneId, + has_app_target: bool, + direct_terminal_targets: &HashSet<&str>, + ) -> bool { + 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 pty_sources_visible_to_any_render_target( + &self, + sources: &HashSet<crate::layout::PaneId>, + ) -> bool { + let (has_app_target, direct_terminal_targets) = self.pty_render_targets(); 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())) + sources.iter().copied().any(|pane_id| { + self.pty_source_visible_to_render_targets( + pane_id, + has_app_target, + &direct_terminal_targets, + ) }) } @@ -9627,6 +9681,17 @@ next_tab = "" } #[test] + fn visible_source_wakes_pending_hidden_work() { + let (server, background_pane) = hidden_pty_visibility_test_server(&[(120, 40)]); + let visible_pane = server.app.state.workspaces[0].tabs[0].root_pane; + + assert!(server.app.render_dirty.request_pty(background_pane)); + assert!(!server.has_pending_presentation_work(false, false)); + assert!(server.app.render_dirty.request_pty(visible_pane)); + assert!(server.has_pending_presentation_work(false, false)); + } + + #[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]); |
