Files
webterm/internal/terminalstate/tracker.go
T
GitHub Copilot 82ccfeb6a9 perf: reduce input tarpitting and speed up large-display rendering
Input tarpitting fixes:
- terminal.ts: batch stdin writes with 10ms coalescing window (flushes
  immediately for large payloads like paste), replacing per-keystroke
  WebSocket messages with fewer, larger frames
- server.go: replace per-message time.After() with a reusable timer to
  eliminate GC pressure from repeated key input
- server.go: coalesce queued stdin writes (up to 4KB) into a single PTY
  write to reduce syscall overhead

Screenshot/rendering pipeline optimizations:
- tracker.go: stop forcing empty cells to space; let exporters decide
  what to render, drastically reducing work for mostly-blank terminals
- tracker.go: use clear() for dirty map instead of delete-in-loop
- svg_exporter.go: skip visually empty cells (blank glyph, default BG,
  no reverse/underline); still render background rects for colored or
  reverse-video cells
- png_exporter.go: add color parsing cache to avoid redundant hex
  parsing per cell; add empty cell fast-path; short-circuit blend math
  for coverage 0 and 255

Dashboard thumbnail concurrency:
- server.go: replace single-flight screenshot fetching with limited
  parallelism (2-4 concurrent requests based on hardwareConcurrency)
  so large dashboards with many tiles update faster

Also fixes typo in dashboard JS (tetagBySlug -> etagBySlug) that
silently broke ETag caching for screenshot refreshes.

Bumps version to 1.3.32.
2026-02-26 19:57:43 +00:00

160 lines
3.2 KiB
Go

package terminalstate
import (
"fmt"
"strconv"
"strings"
"sync"
"github.com/rcarmo/go-te/pkg/te"
)
var ansi16Names = [...]string{
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"brightblack",
"brightred",
"brightgreen",
"brightyellow",
"brightblue",
"brightmagenta",
"brightcyan",
"brightwhite",
}
type Cell struct {
Data string `json:"data"`
FG string `json:"fg"`
BG string `json:"bg"`
Bold bool `json:"bold"`
Italics bool `json:"italics"`
Underscore bool `json:"underscore"`
Reverse bool `json:"reverse"`
}
type Snapshot struct {
Width int `json:"width"`
Height int `json:"height"`
Buffer [][]Cell `json:"buffer"`
HasChanges bool `json:"has_changes"`
}
type Tracker struct {
mu sync.Mutex
screen *te.DiffScreen
stream *te.ByteStream
changeCounter uint64
lastActivityCounter uint64
lastSnapshotCounter uint64
}
func NewTracker(width, height int) *Tracker {
screen := te.NewDiffScreen(width, height)
return &Tracker{
screen: screen,
stream: te.NewByteStream(screen, false),
}
}
func (t *Tracker) Feed(data []byte) error {
if len(data) == 0 {
return nil
}
t.mu.Lock()
defer t.mu.Unlock()
if err := t.stream.Feed(data); err != nil {
return err
}
if len(t.screen.Dirty) > 0 {
t.changeCounter++
// Clear dirty set so subsequent feeds detect new changes
clear(t.screen.Dirty)
}
return nil
}
func (t *Tracker) Resize(width, height int) {
t.mu.Lock()
defer t.mu.Unlock()
if width == t.screen.Columns && height == t.screen.Lines {
return
}
t.screen.Resize(height, width)
t.changeCounter++
}
func (t *Tracker) Snapshot() Snapshot {
t.mu.Lock()
defer t.mu.Unlock()
snapshot := Snapshot{
Width: t.screen.Columns,
Height: t.screen.Lines,
HasChanges: t.changeCounter > t.lastSnapshotCounter,
Buffer: make([][]Cell, t.screen.Lines),
}
t.lastSnapshotCounter = t.changeCounter
for row := 0; row < t.screen.Lines; row++ {
line := make([]Cell, t.screen.Columns)
for col := 0; col < t.screen.Columns; col++ {
raw := t.screen.Buffer[row][col]
data := raw.Data
line[col] = Cell{
Data: data,
FG: colorToString(raw.Attr.Fg),
BG: colorToString(raw.Attr.Bg),
Bold: raw.Attr.Bold,
Italics: raw.Attr.Italics,
Underscore: raw.Attr.Underline,
Reverse: raw.Attr.Reverse,
}
}
snapshot.Buffer[row] = line
}
return snapshot
}
func (t *Tracker) ConsumeActivityChanged() bool {
t.mu.Lock()
defer t.mu.Unlock()
if t.changeCounter > t.lastActivityCounter {
t.lastActivityCounter = t.changeCounter
return true
}
return false
}
func colorToString(color te.Color) string {
if color.Name != "" {
name := strings.ToLower(strings.TrimPrefix(color.Name, "#"))
if len(name) == 6 {
if _, err := strconv.ParseUint(name, 16, 32); err == nil {
return name
}
}
return name
}
switch color.Mode {
case te.ColorDefault:
return "default"
case te.ColorANSI16:
if int(color.Index) < len(ansi16Names) {
return ansi16Names[color.Index]
}
return "default"
case te.ColorANSI256:
return fmt.Sprintf("%d", color.Index)
case te.ColorTrueColor:
return "default"
default:
return "default"
}
}