Reduce shell integration prompt latency (#2109)
* Reduce shell integration prompt latency Three changes to cut ~10-15ms from every precmd/preexec cycle: 1. Use zsh/net/unix (zsocket) for socket sends when available. Eliminates fork+exec of ncat/socat/nc for every telemetry send (~3ms per send, 3-4 sends per prompt cycle). Falls back to external tools if the module is unavailable. 2. Replace _cmux_kill_process_tree (synchronous /bin/ps -ax | awk) with direct kill in _cmux_stop_pr_poll_loop. The tree-kill enumerated all system processes on every command (~5-13ms). Orphaned children (gh, sleep) finish on their own within seconds. 3. Minor savings: guard _cmux_patch_ghostty_semantic_redraw after first success, make _cmux_clear_pr_for_panel async, cache bash send tool. * Address review: process-group kill, fix clear_pr race, reorder bash init 1. Use kill -KILL -- -$PID (process-group kill) instead of plain kill. Background jobs are process-group leaders, so this kills all descendants (gh, sleep) without /bin/ps overhead. 2. Keep bash _cmux_clear_pr_for_panel synchronous to prevent race with the next report_pr from the poll loop. Zsh version uses _cmux_send_bg which is synchronous when zsocket is available. 3. Move _cmux_detect_send_tool after _cmux_fix_path in bash so the cached tool lookup runs with the final PATH. --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
parent
23253e6ddf
commit
71828fe86e
2 changed files with 72 additions and 49 deletions
|
|
@ -1,26 +1,35 @@
|
|||
# cmux shell integration for bash
|
||||
|
||||
# Cache which send tool is available to avoid repeated PATH lookups.
|
||||
_CMUX_SEND_TOOL=""
|
||||
_cmux_detect_send_tool() {
|
||||
if command -v ncat >/dev/null 2>&1; then
|
||||
_CMUX_SEND_TOOL=ncat
|
||||
elif command -v socat >/dev/null 2>&1; then
|
||||
_CMUX_SEND_TOOL=socat
|
||||
elif command -v nc >/dev/null 2>&1; then
|
||||
_CMUX_SEND_TOOL=nc
|
||||
fi
|
||||
}
|
||||
# Detection deferred to after _cmux_fix_path (end of file).
|
||||
|
||||
_cmux_send() {
|
||||
local payload="$1"
|
||||
if command -v ncat >/dev/null 2>&1; then
|
||||
case "$_CMUX_SEND_TOOL" in
|
||||
ncat)
|
||||
printf '%s\n' "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
|
||||
elif command -v socat >/dev/null 2>&1; then
|
||||
;;
|
||||
socat)
|
||||
printf '%s\n' "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH" >/dev/null 2>&1
|
||||
elif command -v nc >/dev/null 2>&1; then
|
||||
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
|
||||
#
|
||||
# Important: macOS/BSD nc will often wait for the peer to close the socket
|
||||
# after it has finished writing. cmux keeps the connection open, so
|
||||
# a plain `nc -U` can hang indefinitely and leak background processes.
|
||||
#
|
||||
# Prefer flags that guarantee we exit after sending, and fall back to a
|
||||
# short timeout so we never block sidebar updates.
|
||||
;;
|
||||
nc)
|
||||
if printf '%s\n' "$payload" | nc -N -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1; then
|
||||
:
|
||||
else
|
||||
printf '%s\n' "$payload" | nc -w 1 -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_cmux_restore_scrollback_once() {
|
||||
|
|
@ -271,6 +280,7 @@ _cmux_clear_pr_for_panel() {
|
|||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
# Synchronous: must arrive before the next report_pr from the poll loop.
|
||||
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
}
|
||||
|
||||
|
|
@ -445,9 +455,10 @@ _cmux_run_pr_probe_with_timeout() {
|
|||
|
||||
_cmux_stop_pr_poll_loop() {
|
||||
if [[ -n "$_CMUX_PR_POLL_PID" ]]; then
|
||||
# Use SIGKILL directly to avoid blocking sleep in preexec.
|
||||
# The poll loop is lightweight and safe to kill abruptly.
|
||||
_cmux_kill_process_tree "$_CMUX_PR_POLL_PID" KILL
|
||||
# Process-group kill: background jobs are process-group leaders, so
|
||||
# negative PID kills the loop + all descendants (gh, sleep) without
|
||||
# the synchronous /bin/ps + awk of tree-kill (~5-13ms).
|
||||
kill -KILL -- -"$_CMUX_PR_POLL_PID" 2>/dev/null || true
|
||||
_CMUX_PR_POLL_PID=""
|
||||
fi
|
||||
}
|
||||
|
|
@ -702,4 +713,6 @@ _cmux_fix_path() {
|
|||
_cmux_fix_path
|
||||
unset -f _cmux_fix_path
|
||||
|
||||
_cmux_detect_send_tool
|
||||
|
||||
_cmux_install_prompt_command
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
# cmux shell integration for zsh
|
||||
# Injected automatically — do not source manually
|
||||
|
||||
# Prefer zsh/net/unix for socket sends (no fork, ~0.2ms per send vs ~3ms
|
||||
# for fork+exec of ncat/socat/nc). Falls back to external tools if the
|
||||
# module is unavailable.
|
||||
typeset -g _CMUX_HAS_ZSOCKET=0
|
||||
if zmodload zsh/net/unix 2>/dev/null; then
|
||||
_CMUX_HAS_ZSOCKET=1
|
||||
fi
|
||||
|
||||
_cmux_send() {
|
||||
local payload="$1"
|
||||
if (( _CMUX_HAS_ZSOCKET )); then
|
||||
local fd
|
||||
zsocket "$CMUX_SOCKET_PATH" 2>/dev/null || return 1
|
||||
fd=$REPLY
|
||||
print -u $fd -r -- "$payload" 2>/dev/null
|
||||
exec {fd}>&- 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
if command -v ncat >/dev/null 2>&1; then
|
||||
print -r -- "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
|
||||
elif command -v socat >/dev/null 2>&1; then
|
||||
print -r -- "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH" >/dev/null 2>&1
|
||||
elif command -v nc >/dev/null 2>&1; then
|
||||
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
|
||||
#
|
||||
# Important: macOS/BSD nc will often wait for the peer to close the socket
|
||||
# after it has finished writing. cmux keeps the connection open, so
|
||||
# a plain `nc -U` can hang indefinitely and leak background processes.
|
||||
#
|
||||
# Prefer flags that guarantee we exit after sending, and fall back to a
|
||||
# short timeout so we never block sidebar updates.
|
||||
if print -r -- "$payload" | nc -N -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1; then
|
||||
:
|
||||
else
|
||||
|
|
@ -24,6 +32,16 @@ _cmux_send() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Fire-and-forget send: synchronous when zsocket is available (fast, no fork),
|
||||
# backgrounded otherwise.
|
||||
_cmux_send_bg() {
|
||||
if (( _CMUX_HAS_ZSOCKET )); then
|
||||
_cmux_send "$1"
|
||||
else
|
||||
{ _cmux_send "$1" } >/dev/null 2>&1 &!
|
||||
fi
|
||||
}
|
||||
|
||||
_cmux_restore_scrollback_once() {
|
||||
local path="${CMUX_RESTORE_SCROLLBACK_FILE:-}"
|
||||
[[ -n "$path" ]] || return 0
|
||||
|
|
@ -337,9 +355,7 @@ _cmux_report_tty_once() {
|
|||
[[ -n "$payload" ]] || return 0
|
||||
|
||||
_CMUX_TTY_REPORTED=1
|
||||
{
|
||||
_cmux_send "$payload"
|
||||
} >/dev/null 2>&1 &!
|
||||
_cmux_send_bg "$payload"
|
||||
}
|
||||
|
||||
_cmux_report_shell_activity_state() {
|
||||
|
|
@ -350,9 +366,7 @@ _cmux_report_shell_activity_state() {
|
|||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
[[ "$_CMUX_SHELL_ACTIVITY_LAST" == "$state" ]] && return 0
|
||||
_CMUX_SHELL_ACTIVITY_LAST="$state"
|
||||
{
|
||||
_cmux_send "report_shell_state $state --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 &!
|
||||
_cmux_send_bg "report_shell_state $state --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
}
|
||||
|
||||
_cmux_ports_kick() {
|
||||
|
|
@ -362,9 +376,7 @@ _cmux_ports_kick() {
|
|||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
_CMUX_PORTS_LAST_RUN=$EPOCHSECONDS
|
||||
{
|
||||
_cmux_send "ports_kick --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 &!
|
||||
_cmux_send_bg "ports_kick --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
}
|
||||
|
||||
_cmux_report_git_branch_for_path() {
|
||||
|
|
@ -392,7 +404,7 @@ _cmux_clear_pr_for_panel() {
|
|||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
_cmux_send_bg "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
}
|
||||
|
||||
_cmux_pr_output_indicates_no_pull_request() {
|
||||
|
|
@ -567,9 +579,10 @@ _cmux_run_pr_probe_with_timeout() {
|
|||
|
||||
_cmux_stop_pr_poll_loop() {
|
||||
if [[ -n "$_CMUX_PR_POLL_PID" ]]; then
|
||||
# Use SIGKILL directly to avoid blocking sleep in preexec.
|
||||
# The poll loop is lightweight and safe to kill abruptly.
|
||||
_cmux_kill_process_tree "$_CMUX_PR_POLL_PID" KILL
|
||||
# Process-group kill: background jobs are process-group leaders, so
|
||||
# negative PID kills the loop + all descendants (gh, sleep) without
|
||||
# the synchronous /bin/ps + awk of tree-kill (~5-13ms).
|
||||
kill -KILL -- -"$_CMUX_PR_POLL_PID" 2>/dev/null || true
|
||||
_CMUX_PR_POLL_PID=""
|
||||
fi
|
||||
}
|
||||
|
|
@ -682,7 +695,7 @@ _cmux_precmd() {
|
|||
_cmux_report_shell_activity_state prompt
|
||||
|
||||
# Handle cases where Ghostty integration initializes after this file.
|
||||
_cmux_patch_ghostty_semantic_redraw
|
||||
(( _CMUX_GHOSTTY_SEMANTIC_PATCHED )) || _cmux_patch_ghostty_semantic_redraw
|
||||
|
||||
if [[ -z "$_CMUX_TTY_NAME" ]]; then
|
||||
local t
|
||||
|
|
@ -717,11 +730,8 @@ _cmux_precmd() {
|
|||
# This is also the simplest way to test sidebar directory behavior end-to-end.
|
||||
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
|
||||
_CMUX_PWD_LAST_PWD="$pwd"
|
||||
{
|
||||
# Quote to preserve spaces.
|
||||
local qpwd="${pwd//\"/\\\"}"
|
||||
_cmux_send "report_pwd \"${qpwd}\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 &!
|
||||
_cmux_send_bg "report_pwd \"${qpwd}\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
fi
|
||||
|
||||
# Git branch/dirty: update immediately on directory change, otherwise every ~3s.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue