1
1
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:
Wez Furlong 2023-01-21 15:15:58 -07:00
parent ca7024aee3
commit 33f25e9ce6
No known key found for this signature in database
GPG Key ID: 7A7F66A31EC9B387
9 changed files with 137 additions and 9 deletions

View File

@ -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`

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)]

View File

@ -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 {

View File

@ -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),

View File

@ -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));
}

View File

@ -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)]