Skip to content

Commit 21c9645

Browse files
committed
Improve PTY batching and clean stale tmux test sockets
1 parent f8f9aa4 commit 21c9645

6 files changed

Lines changed: 131 additions & 11 deletions

File tree

cmd/amux/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,13 @@ func runTUI() {
121121
// Initialize logging
122122
home, _ := os.UserHomeDir()
123123
logDir := filepath.Join(home, ".amux", "logs")
124-
if err := logging.Initialize(logDir, logging.LevelDebug); err != nil {
124+
if err := logging.Initialize(logDir, logging.LevelInfo); err != nil {
125125
fmt.Fprintf(os.Stderr, "Warning: could not initialize logging: %v\n", err)
126126
}
127127
defer logging.Close()
128128

129+
cleanupStaleTestTmuxSockets()
130+
129131
logging.Info("Starting amux")
130132

131133
startSignalDebug()

cmd/amux/tmux_socket_janitor.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//go:build !windows
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"net"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"time"
12+
13+
"github.com/andyrewlee/amux/internal/logging"
14+
)
15+
16+
const staleSocketDialTimeout = 75 * time.Millisecond
17+
18+
func cleanupStaleTestTmuxSockets() {
19+
removed := 0
20+
for _, dir := range tmuxSocketDirs() {
21+
entries, err := os.ReadDir(dir)
22+
if err != nil {
23+
continue
24+
}
25+
for _, entry := range entries {
26+
if entry.IsDir() {
27+
continue
28+
}
29+
name := entry.Name()
30+
if !strings.HasPrefix(name, "amux-test-") && !strings.HasPrefix(name, "amux-e2e-check-") {
31+
continue
32+
}
33+
info, err := entry.Info()
34+
if err != nil {
35+
continue
36+
}
37+
if info.Mode()&os.ModeSocket == 0 {
38+
continue
39+
}
40+
socketPath := filepath.Join(dir, name)
41+
if isLiveUnixSocket(socketPath) {
42+
continue
43+
}
44+
if err := os.Remove(socketPath); err == nil {
45+
removed++
46+
}
47+
}
48+
}
49+
if removed > 0 {
50+
logging.Info("Removed %d stale tmux test sockets", removed)
51+
}
52+
}
53+
54+
func tmuxSocketDirs() []string {
55+
uid := os.Getuid()
56+
candidates := []string{
57+
filepath.Join("/tmp", fmt.Sprintf("tmux-%d", uid)),
58+
filepath.Join("/private/tmp", fmt.Sprintf("tmux-%d", uid)),
59+
}
60+
seen := make(map[string]struct{}, len(candidates))
61+
out := make([]string, 0, len(candidates))
62+
for _, dir := range candidates {
63+
if _, ok := seen[dir]; ok {
64+
continue
65+
}
66+
seen[dir] = struct{}{}
67+
out = append(out, dir)
68+
}
69+
return out
70+
}
71+
72+
func isLiveUnixSocket(path string) bool {
73+
dialer := net.Dialer{Timeout: staleSocketDialTimeout}
74+
conn, err := dialer.Dial("unix", path)
75+
if err != nil {
76+
return false
77+
}
78+
_ = conn.Close()
79+
return true
80+
}

internal/app/defaults.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const (
3333
tmuxActivityPrefilter = 120 * time.Second
3434

3535
// tmuxActivityInterval controls how often we scan tmux sessions for activity.
36-
tmuxActivityInterval = 2 * time.Second
36+
tmuxActivityInterval = 5 * time.Second
3737

3838
// tmuxActivitySettleScans is how many successful activity scans are required
3939
// before dashboard "active workspace" indicators are shown.

internal/ui/center/model_pty_config.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import "time"
44

55
// PTY constants
66
const (
7-
ptyFlushQuiet = 4 * time.Millisecond
8-
ptyFlushMaxInterval = 16 * time.Millisecond
9-
ptyFlushQuietAlt = 8 * time.Millisecond
10-
ptyFlushMaxAlt = 32 * time.Millisecond
7+
ptyFlushQuiet = 12 * time.Millisecond
8+
ptyFlushMaxInterval = 48 * time.Millisecond
9+
ptyFlushQuietAlt = 24 * time.Millisecond
10+
ptyFlushMaxAlt = 96 * time.Millisecond
1111
// Inactive tabs still need to advance their terminal state, but can flush less frequently.
1212
ptyFlushInactiveMultiplier = 4
1313
ptyFlushInactiveHeavyMultiplier = 8
@@ -21,7 +21,7 @@ const (
2121
ptyFlushChunkSizeActive = 256 * 1024
2222
ptyReadBufferSize = 32 * 1024
2323
ptyReadQueueSize = 64
24-
ptyFrameInterval = time.Second / 60
24+
ptyFrameInterval = time.Second / 24
2525
ptyMaxPendingBytes = 512 * 1024
2626
ptyMaxBufferedBytes = 8 * 1024 * 1024
2727
ptyReaderStallTimeout = 10 * time.Second

internal/ui/common/pty_reader.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"github.com/andyrewlee/amux/internal/safego"
1111
)
1212

13+
const ptyIdleHeartbeatInterval = time.Second
14+
1315
// PTYReaderConfig configures the shared PTY read loop.
1416
type PTYReaderConfig struct {
1517
Label string // safego goroutine label
@@ -79,8 +81,30 @@ func RunPTYReader(
7981
}
8082
})
8183

82-
ticker := time.NewTicker(cfg.FrameInterval)
83-
defer ticker.Stop()
84+
heartbeatTicker := time.NewTicker(ptyIdleHeartbeatInterval)
85+
defer heartbeatTicker.Stop()
86+
var flushTicker *time.Ticker
87+
var flushTick <-chan time.Time
88+
startFlushTicker := func() {
89+
if flushTicker != nil {
90+
return
91+
}
92+
flushInterval := cfg.FrameInterval
93+
if flushInterval <= 0 {
94+
flushInterval = 40 * time.Millisecond
95+
}
96+
flushTicker = time.NewTicker(flushInterval)
97+
flushTick = flushTicker.C
98+
}
99+
stopFlushTicker := func() {
100+
if flushTicker == nil {
101+
return
102+
}
103+
flushTicker.Stop()
104+
flushTicker = nil
105+
flushTick = nil
106+
}
107+
defer stopFlushTicker()
84108

85109
var pending []byte
86110
var stoppedErr error
@@ -110,14 +134,23 @@ func RunPTYReader(
110134
return
111135
}
112136
pending = append(pending, data...)
137+
startFlushTicker()
113138
if len(pending) >= cfg.MaxPendingBytes {
114139
if !SendPTYMsg(msgCh, cancel, factory.Output(pending)) {
115140
close(msgCh)
116141
return
117142
}
118143
pending = nil
144+
if stoppedErr == nil {
145+
stopFlushTicker()
146+
}
147+
}
148+
if stoppedErr != nil && len(pending) == 0 {
149+
SendPTYMsg(msgCh, cancel, factory.Stopped(stoppedErr))
150+
close(msgCh)
151+
return
119152
}
120-
case <-ticker.C:
153+
case <-flushTick:
121154
beat()
122155
if len(pending) > 0 {
123156
if !SendPTYMsg(msgCh, cancel, factory.Output(pending)) {
@@ -126,11 +159,16 @@ func RunPTYReader(
126159
}
127160
pending = nil
128161
}
162+
if len(pending) == 0 {
163+
stopFlushTicker()
164+
}
129165
if stoppedErr != nil {
130166
SendPTYMsg(msgCh, cancel, factory.Stopped(stoppedErr))
131167
close(msgCh)
132168
return
133169
}
170+
case <-heartbeatTicker.C:
171+
beat()
134172
}
135173
}
136174
}

internal/ui/sidebar/terminal_pty_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const (
1414
ptyFlushChunkSize = 32 * 1024
1515
ptyReadBufferSize = 32 * 1024
1616
ptyReadQueueSize = 32
17-
ptyFrameInterval = time.Second / 60
17+
ptyFrameInterval = time.Second / 24
1818
ptyMaxPendingBytes = 256 * 1024
1919
ptyReaderStallTimeout = 10 * time.Second
2020
ptyMaxBufferedBytes = 4 * 1024 * 1024

0 commit comments

Comments
 (0)