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:

  1. 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).
  2. Ignored Backpressure: Library WebSocket standar sering membungkus non-blocking socket ke dalam unbuffered/buffered channel internal. Jika panggilan WriteMessage berjalan tanpa memedulikan status kernel send buffer (SO_SNDBUF) atau status flush, proxy terus menarik data dari PTY dan menyimpannya di memory heap.
  3. 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:

  1. Simulasi Traffic Throttling: Batasi bandwidth link loopback menggunakan command tc di Linux:
    sudo tc qdisc add dev lo root tbf rate 2mbit burst 32kbit latency 400ms
  2. 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 atau pidstat -r. Memori proxy harus stabil konstan di bawah 25MB tanpa lonjakan linear.
  3. 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.