Aplikasi Text User Interface (TUI) modern seperti htop, btop, atau custom dashboard sering merender puluhan frame per detik dengan stream ANSI escape codes berukuran besar. Saat menjembatani pseudoterminal (PTY) lokal ke frontend berbasis xterm.js melalui WebSocket proxy, kendala klasik muncul: browser mengalami frame drop, rendering tertunda (lag akumulatif), dan penggunaan memori server melonjak tajam.
Gejala Masalah: Lag Akumulatif dan Buffer Overflow
Saat aplikasi TUI me-refresh layar penuh pada frekuensi 30–60 FPS, puluhan kilobyte sequence ANSI diproduksi per frame. Masalah muncul ketika throughput produksi stdout PTY jauh melebihi laju transmisi jaringan TCP/WebSocket ke browser.
Gejala umum pada sistem:
- Input Lag Meningkat: Respon keystroke pengguna mengalami delay hingga beberapa detik karena antrean write buffer WebSocket menumpuk di sisi proxy.
- Tampilan Pecah atau Flickering: Potongan frame parsial terdorong sebelum frame sebelumnya selesai digambar oleh renderer terminal web.
- High Memory Footprint: Memory leak semu pada proxy akibat buffer alokasi internal WebSocket writer yang tidak kunjung di-drain oleh TCP socket stack.
Analisis Root Cause: Unthrottled Stdout Read dan Ketiadaan Backpressure
Akar masalah terletak pada pemisahan layer I/O yang tidak sinkron:
- Loop Read PTY Tanpa Batas: Goroutine atau event loop membaca dari master PTY secepat mungkin segera setelah kernel mengindikasikan file descriptor siap baca (ready-to-read).
- Ignored Backpressure: Library WebSocket standar sering membungkus non-blocking socket ke dalam unbuffered/buffered channel internal. Jika panggilan
WriteMessageberjalan tanpa memedulikan status kernel send buffer (SO_SNDBUF) atau status flush, proxy terus menarik data dari PTY dan menyimpannya di memory heap. - TCP Congestion Window: Jaringan lambat menurunkan ACK rate TCP. Send buffer sistem operasi terisi penuh, tetapi proses PTY lokal tetap berjalan bebas karena pipe descriptor tidak memiliki mekanisme throttling otomatis secara default.
Solusi Teknis: Backpressure Control dan Token Bucket di Go
Pola implementasi yang benar mengharuskan transfer rate dari PTY terikat langsung dengan status penulisan WebSocket. Jika socket sibuk atau send buffer penuh, loop read PTY harus menunda alokasi atau membaca dalam batch terkontrol menggunakan Token Bucket rate limiting.
package main
import (
"context"
"io"
"os"
"sync"
"time"
"github.com/gorilla/websocket"
"golang.org/x/time/rate"
)
type TUIProxy struct {
ptyMaster *os.File
wsConn *websocket.Conn
limiter *rate.Limiter
writeMu sync.Mutex
}
func NewTUIProxy(pty *os.File, ws *websocket.Conn, maxBytesPerSec int, burstSize int) *TUIProxy {
return &TUIProxy{
ptyMaster: pty,
wsConn: ws,
limiter: rate.NewLimiter(rate.Limit(maxBytesPerSec), burstSize),
}
}
// StreamToClient mengalirkan stdout PTY ke WebSocket dengan backpressure terukur.
func (p *TUIProxy) StreamToClient(ctx context.Context) error {
// Buffer dinamis: sesuaikan ukuran bacaan dengan viewport tipikal ANSI frame (4KB - 16KB)
buf := make([]byte, 16384)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// 1. Baca dari PTY descriptor
n, err := p.ptyMaster.Read(buf)
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if n == 0 {
continue
}
// 2. Terapkan token bucket throttling berdasarkan payload byte
if err := p.limiter.WaitN(ctx, n); err != nil {
return err
}
// 3. Serialized direct write dengan deadline write network
p.writeMu.Lock()
_ = p.wsConn.SetWriteDeadline(time.Now().Add(2 * time.Second))
err = p.wsConn.WriteMessage(websocket.BinaryMessage, buf[:n])
p.writeMu.Unlock()
if err != nil {
// Write timeout atau broken pipe: hentikan pembacaan PTY agar memori tidak bocor
return err
}
}
}
Dynamic Chunking untuk Menghindari Frame Tearing
Mengirim data setiap kali PTY mengembalikan potongan byte kecil (misal: 128 byte) memicu overhead framing TCP dan WebSocket. Sebaliknya, menggabungkan chunk data selama interval beberapa milidetik (batching 8-16ms) memungkinkan sinkronisasi mendekati frame refresh screen 60Hz browser tanpa memecah rangkaian sequence ANSI multi-byte.
Verifikasi Performa Transfer Byte
Verifikasi dilakukan untuk memastikan proxy tidak lagi mengakumulasi buffer data di memory heap saat throughput network dibatasi:
- Simulasi Traffic Throttling: Batasi bandwidth link loopback menggunakan command
tcdi Linux:sudo tc qdisc add dev lo root tbf rate 2mbit burst 32kbit latency 400ms - Observasi Resident Set Size (RSS): Jalankan program intensif TUI (seperti pemutar video terminal ASCII atau stress-test script
cat /dev/urandom). Amati metrik memory via pprof ataupidstat -r. Memori proxy harus stabil konstan di bawah 25MB tanpa lonjakan linear. - Evaluasi Latensi Browser: Periksa metrik Web Inspector pada browser client. Durasi buffer unparsed xterm.js harus berada pada angka ideal 0ms hingga 16ms tanpa lonjakan antrean frame tertahan.
Catatan: Jangan pernah mengalokasikan goroutine baru tanpa pembatas untuk setiap hasil
pty.Read(). Mengalirkan stream PTY ke dalam unbounded channel adalah penyebab utama kegagalan out-of-memory (OOM) pada WebSocket proxy.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!