Files
webterm/webterm/session.go
T
GitHub Copilot 2c5d3c72f9 Pause VT parser for idle sessions to eliminate CPU waste
When no WebSocket client is connected, the session's readLoop still
processes every byte of terminal output through the go-te VT parser
(tracker.Feed), Screen.Draw grapheme segmentation, and string
allocations — even though nobody is consuming the screen state.
For programs like btop inside tmux that produce continuous full-screen
redraws, this causes sustained CPU usage and GC pressure over hours.

Fix: after a 10-second idle threshold (no client connected), skip
tracker.Feed() and only maintain the replay buffer. When a client
reconnects (UpdateConnector) or a screenshot is requested
(GetScreenSnapshot), rebuild the tracker by replaying the buffer
through a fresh VT parser instance.

Changes:
- Add idleSince atomic timestamp + MarkIdle() to Session interface
- handleOutput() skips tracker.Feed when idle > threshold
- UpdateConnector() clears idle flag and rebuilds tracker from replay
- GetScreenSnapshot() rebuilds stale tracker on-demand for screenshots
- Wire MarkIdle() call into handleWebSocket cleanup (client disconnect)
- Add TestIdleTrackerPauseAndRebuild covering the full lifecycle
2026-02-18 09:21:18 +00:00

66 lines
1.5 KiB
Go

package webterm
import (
"github.com/rcarmo/webterm/internal/terminalstate"
)
type SessionConnector interface {
OnData(data []byte)
OnBinary(payload []byte)
OnMeta(meta map[string]any)
OnClose()
}
type Session interface {
Open(width, height int) error
Start(connector SessionConnector) error
Close() error
Wait() error
SetTerminalSize(width, height int) error
SendBytes(data []byte) bool
SendMeta(meta map[string]any) bool
IsRunning() bool
GetReplayBuffer() []byte
GetScreenSnapshot() terminalstate.Snapshot
ForceRedraw() error
UpdateConnector(connector SessionConnector)
MarkIdle()
}
type noopConnector struct{}
func (noopConnector) OnData([]byte) {}
func (noopConnector) OnBinary([]byte) {}
func (noopConnector) OnMeta(map[string]any) {}
func (noopConnector) OnClose() {}
func dispatchSessionOutput(filtered []byte, tracker *terminalstate.Tracker, replay *ReplayBuffer, connector SessionConnector) {
if len(filtered) == 0 {
return
}
replay.Add(filtered)
hasVisualChange := false
if tracker != nil {
_ = tracker.Feed(filtered)
hasVisualChange = tracker.ConsumeActivityChanged()
}
connector.OnData(filtered)
if hasVisualChange {
connector.OnMeta(map[string]any{"screen_changed": true})
}
}
func snapshotFromTracker(tracker *terminalstate.Tracker, width, height int) terminalstate.Snapshot {
if tracker != nil {
return tracker.Snapshot()
}
if height < 0 {
height = 0
}
return terminalstate.Snapshot{
Width: width,
Height: height,
Buffer: make([][]terminalstate.Cell, height),
}
}