mirror of
https://github.com/wez/wezterm.git
synced 2024-12-26 23:04:49 +03:00
reduce latency when heavily using foreground process info
The tcgetpgrp call appears to have high variance in latency, ranging from 200-700us on my system. If you have 10 tabs and mouse over the tab bar, that's around 7ms spent per frame just figuring out the foreground process; that doesn't include actually extracting the process executable or current working directory paths. This was exacerbated by the mouse move events triggering a tab bar recompute on every pixel of mouse movement. This commit takes the following steps to resolve this: * We now only re-compute the tab bar when the UI item is changed by a mouse movement * A simple single-item cache is now used on unix that allows the caller to proceed quickly with stale-but-probably-still-mostly-accurate data while queuing up an update to a background thread which can absorb the latency. The result of this is that hovering over several tabs in quick succession no longer takes a noticeable length of time to render the hover, but the consequence is that the contents of a given tab may be stale by 300-400ms. I think that trade-off is worth while. We already have a similar trade-off on Windows, although we don't yet do the updates in a different thread on Windows. Perhaps in a follow up commit? refs: https://github.com/wez/wezterm/issues/2991
This commit is contained in:
parent
ca7024aee3
commit
33f25e9ce6
@ -85,6 +85,8 @@ As features stabilize some brief notes about them will accumulate here.
|
||||
[#2986](https://github.com/wez/wezterm/discussions/2986)
|
||||
* modal overlays like CharSelect and the command palette sometimes wouldn't
|
||||
render when first activated until pressing a key.
|
||||
* lag when making heavy use of foreground process information in tab titles.
|
||||
[#2991](https://github.com/wez/wezterm/issues/2991)
|
||||
|
||||
#### Changed
|
||||
* `CTRL-SHIFT-P` now activates the new command palette, instead of `PaneSelect`
|
||||
|
@ -20,6 +20,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::convert::TryInto;
|
||||
use std::io::{Result as IoResult, Write};
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use termwiz::escape::csi::{Sgr, CSI};
|
||||
@ -34,6 +35,8 @@ use wezterm_term::{
|
||||
SemanticZone, StableRowIndex, Terminal, TerminalConfiguration, TerminalSize,
|
||||
};
|
||||
|
||||
const PROC_INFO_CACHE_TTL: Duration = Duration::from_millis(300);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ProcessState {
|
||||
Running {
|
||||
@ -55,6 +58,63 @@ struct CachedProcInfo {
|
||||
foreground: LocalProcessInfo,
|
||||
}
|
||||
|
||||
/// This is a bit horrible; it can take 700us to tcgetpgrp, so if we have
|
||||
/// 10 tabs open and run the mouse over them, hovering them each in turn,
|
||||
/// we can spend 7ms per evaluation of the tab bar state on fetching those
|
||||
/// pids alone, which can easily lead to stuttering when moving the mouse
|
||||
/// over all of the tabs.
|
||||
///
|
||||
/// This implements a cache holding that fg process and the often queried
|
||||
/// cwd and process path that allows for stale reads to proceed quickly
|
||||
/// while the writes can happen in a background thread.
|
||||
#[cfg(unix)]
|
||||
#[derive(Clone)]
|
||||
struct CachedLeaderInfo {
|
||||
updated: Instant,
|
||||
fd: std::os::fd::RawFd,
|
||||
pid: u32,
|
||||
path: Option<PathBuf>,
|
||||
current_working_dir: Option<PathBuf>,
|
||||
updating: bool,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl CachedLeaderInfo {
|
||||
fn new(fd: Option<std::os::fd::RawFd>) -> Self {
|
||||
let mut me = Self {
|
||||
updated: Instant::now(),
|
||||
fd: fd.unwrap_or(-1),
|
||||
pid: 0,
|
||||
path: None,
|
||||
current_working_dir: None,
|
||||
updating: false,
|
||||
};
|
||||
me.update();
|
||||
me
|
||||
}
|
||||
|
||||
fn can_update(&self) -> bool {
|
||||
self.fd != -1 && !self.updating
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.pid = unsafe { libc::tcgetpgrp(self.fd) } as u32;
|
||||
if self.pid > 0 {
|
||||
self.path = LocalProcessInfo::executable_path(self.pid);
|
||||
self.current_working_dir = LocalProcessInfo::current_working_dir(self.pid);
|
||||
} else {
|
||||
self.path.take();
|
||||
self.current_working_dir.take();
|
||||
}
|
||||
self.updated = Instant::now();
|
||||
self.updating = false;
|
||||
}
|
||||
|
||||
fn expired(&self) -> bool {
|
||||
self.updated.elapsed() > PROC_INFO_CACHE_TTL
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocalPane {
|
||||
pane_id: PaneId,
|
||||
terminal: Mutex<Terminal>,
|
||||
@ -64,6 +124,8 @@ pub struct LocalPane {
|
||||
domain_id: DomainId,
|
||||
tmux_domain: Mutex<Option<Arc<TmuxDomainState>>>,
|
||||
proc_list: Mutex<Option<CachedProcInfo>>,
|
||||
#[cfg(unix)]
|
||||
leader: Arc<Mutex<Option<CachedLeaderInfo>>>,
|
||||
command_description: String,
|
||||
}
|
||||
|
||||
@ -407,18 +469,18 @@ impl Pane for LocalPane {
|
||||
|
||||
fn get_foreground_process_name(&self) -> Option<String> {
|
||||
#[cfg(unix)]
|
||||
if let Some(pid) = self.pty.lock().process_group_leader() {
|
||||
if let Some(path) = LocalProcessInfo::executable_path(pid as u32) {
|
||||
{
|
||||
let leader = self.get_leader();
|
||||
if let Some(path) = &leader.path {
|
||||
return Some(path.to_string_lossy().to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Some(fg) = self.divine_foreground_process() {
|
||||
return Some(fg.executable.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn can_close_without_prompting(&self, _reason: CloseReason) -> bool {
|
||||
@ -862,16 +924,44 @@ impl LocalPane {
|
||||
domain_id,
|
||||
tmux_domain: Mutex::new(None),
|
||||
proc_list: Mutex::new(None),
|
||||
#[cfg(unix)]
|
||||
leader: Arc::new(Mutex::new(None)),
|
||||
command_description,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_leader(&self) -> CachedLeaderInfo {
|
||||
let mut leader = self.leader.lock();
|
||||
|
||||
if let Some(info) = leader.as_mut() {
|
||||
// If stale, queue up some work in another thread to update.
|
||||
// Right now, we'll return the stale data.
|
||||
if info.expired() && info.can_update() {
|
||||
info.updating = true;
|
||||
let leader_ref = Arc::clone(&self.leader);
|
||||
std::thread::spawn(move || {
|
||||
let mut leader = leader_ref.lock();
|
||||
if let Some(leader) = leader.as_mut() {
|
||||
leader.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
leader.replace(CachedLeaderInfo::new(self.pty.lock().as_raw_fd()));
|
||||
}
|
||||
|
||||
(*leader).clone().unwrap()
|
||||
}
|
||||
|
||||
fn divine_current_working_dir(&self) -> Option<Url> {
|
||||
#[cfg(unix)]
|
||||
if let Some(pid) = self.pty.lock().process_group_leader() {
|
||||
if let Some(path) = LocalProcessInfo::current_working_dir(pid as u32) {
|
||||
{
|
||||
let leader = self.get_leader();
|
||||
if let Some(path) = &leader.current_working_dir {
|
||||
return Url::parse(&format!("file://localhost{}", path.display())).ok();
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@ -893,7 +983,7 @@ impl LocalPane {
|
||||
let expired = force_refresh
|
||||
|| proc_list
|
||||
.as_ref()
|
||||
.map(|info| info.updated.elapsed() > Duration::from_millis(300))
|
||||
.map(|info| info.updated.elapsed() > PROC_INFO_CACHE_TTL)
|
||||
.unwrap_or(true);
|
||||
|
||||
if expired {
|
||||
|
@ -984,6 +984,11 @@ impl portable_pty::MasterPty for WrappedSshPty {
|
||||
let _ = inner.check_connected();
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn as_raw_fd(&self) -> Option<std::os::fd::RawFd> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for PtyWriter {
|
||||
|
@ -156,4 +156,9 @@ impl MasterPty for TmuxPty {
|
||||
fn process_group_leader(&self) -> Option<libc::pid_t> {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn as_raw_fd(&self) -> Option<std::os::fd::RawFd> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,13 @@ pub trait MasterPty {
|
||||
#[cfg(unix)]
|
||||
fn process_group_leader(&self) -> Option<libc::pid_t>;
|
||||
|
||||
/// If get_termios() and process_group_leader() are both implemented and
|
||||
/// return Some, then as_raw_fd() should return the same underlying fd
|
||||
/// associated with the stream. This is to enable applications that
|
||||
/// "know things" to query similar information for themselves.
|
||||
#[cfg(unix)]
|
||||
fn as_raw_fd(&self) -> Option<std::os::fd::RawFd>;
|
||||
|
||||
/// If applicable to the type of the tty, return the termios
|
||||
/// associated with the stream
|
||||
#[cfg(unix)]
|
||||
|
@ -240,6 +240,11 @@ impl MasterPty for Master {
|
||||
// N/A: there is no local process
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn as_raw_fd(&self) -> Option<std::os::fd::RawFd> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct Reader {
|
||||
|
@ -323,6 +323,10 @@ impl MasterPty for UnixMasterPty {
|
||||
Ok(Box::new(UnixMasterWriter { fd }))
|
||||
}
|
||||
|
||||
fn as_raw_fd(&self) -> Option<std::os::fd::RawFd> {
|
||||
Some(self.fd.0.as_raw_fd())
|
||||
}
|
||||
|
||||
fn process_group_leader(&self) -> Option<libc::pid_t> {
|
||||
match unsafe { libc::tcgetpgrp(self.fd.0.as_raw_fd()) } {
|
||||
pid if pid > 0 => Some(pid),
|
||||
|
@ -184,6 +184,8 @@ impl super::TermWindow {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let prior_ui_item = self.last_ui_item.clone();
|
||||
|
||||
let ui_item = if matches!(self.current_mouse_capture, None | Some(MouseCapture::UI)) {
|
||||
let ui_item = self.resolve_ui_item(&event);
|
||||
|
||||
@ -209,7 +211,7 @@ impl super::TermWindow {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(item) = ui_item {
|
||||
if let Some(item) = ui_item.clone() {
|
||||
if capture_mouse {
|
||||
self.current_mouse_capture = Some(MouseCapture::UI);
|
||||
}
|
||||
@ -231,6 +233,10 @@ impl super::TermWindow {
|
||||
capture_mouse,
|
||||
);
|
||||
}
|
||||
|
||||
if prior_ui_item != ui_item {
|
||||
self.update_title_post_status();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mouse_leave_impl(&mut self, context: &dyn WindowOps) {
|
||||
@ -445,7 +451,6 @@ impl super::TermWindow {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.update_title_post_status();
|
||||
context.set_cursor(Some(MouseCursor::Arrow));
|
||||
}
|
||||
|
||||
|
@ -77,6 +77,11 @@ impl portable_pty::MasterPty for SshPty {
|
||||
// It's not local, so there's no meaningful leader
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn as_raw_fd(&self) -> Option<std::os::fd::RawFd> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
Loading…
Reference in New Issue
Block a user