perf: batch TUI log forwarding on a 250ms tick instead of one repaint per line

This commit is contained in:
aykhans committed 2026-08-17 20:46:33 +04:00
1 parent c195ca2ab2
commit 9602595216
1 file changed
+49 -8
+49 -8
View File
@@ -2,6 +2,7 @@ package sarin
import ( import (
"context" "context"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
@@ -15,6 +16,16 @@ import (
type tickMsg time.Time type tickMsg time.Time
// logBatch carries the log lines accumulated since the last repaint.
type logBatch []runtimeLog
const (
// logBoxLines is how many lines the log box shows at once.
logBoxLines = 8
// logBatchInterval is how often accumulated logs are handed to the program.
logBatchInterval = 250 * time.Millisecond
)
var ( var (
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#d1d1d1")) helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#d1d1d1"))
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FC5B5B")).Bold(true) errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FC5B5B")).Bold(true)
@@ -96,8 +107,10 @@ func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
return m, nil return m, nil
case runtimeLog: case logBatch:
m.logs = append(m.logs[1:], renderRuntimeLog(msg)) for _, entry := range msg {
m.logs = append(m.logs[1:], renderRuntimeLog(entry))
}
if m.ctx.Err() != nil { if m.ctx.Err() != nil {
return m, tea.Quit return m, tea.Quit
} }
@@ -174,8 +187,10 @@ func (m infiniteProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
return m, nil return m, nil
case runtimeLog: case logBatch:
m.logs = append(m.logs[1:], renderRuntimeLog(msg)) for _, entry := range msg {
m.logs = append(m.logs[1:], renderRuntimeLog(entry))
}
if m.ctx.Err() != nil { if m.ctx.Err() != nil {
m.quit = true m.quit = true
return m, tea.Quit return m, tea.Quit
@@ -247,7 +262,7 @@ func (s sarin) streamProgress(
progress.WithFillCharacters(progress.DefaultFullCharFullBlock, progress.DefaultEmptyCharBlock), progress.WithFillCharacters(progress.DefaultFullCharFullBlock, progress.DefaultEmptyCharBlock),
), ),
startTime: time.Now(), startTime: time.Now(),
logs: make([]string, 8), logs: make([]string, logBoxLines),
counter: counter, counter: counter,
maxValue: total, maxValue: total,
showBar: showBar, showBar: showBar,
@@ -278,7 +293,7 @@ func (s sarin) streamProgress(
), ),
startTime: time.Now(), startTime: time.Now(),
counter: counter, counter: counter,
logs: make([]string, 8), logs: make([]string, logBoxLines),
showBar: showBar, showBar: showBar,
ctx: ctx, ctx: ctx,
stop: stopCtrl.Stop, stop: stopCtrl.Stop,
@@ -291,9 +306,35 @@ func (s sarin) streamProgress(
stopCtrl.AttachProgram(program) stopCtrl.AttachProgram(program)
defer stopCtrl.AttachProgram(nil) defer stopCtrl.AttachProgram(nil)
// Bubble Tea repaints once per message, so forwarding every log made the run
// wait on the terminal. Send only what the box shows, on a fixed cadence.
go func() { go func() {
for msg := range logChannel { ticker := time.NewTicker(logBatchInterval)
program.Send(msg) defer ticker.Stop()
pending := make(logBatch, 0, logBoxLines)
flush := func() {
if len(pending) == 0 {
return
}
program.Send(slices.Clone(pending))
pending = pending[:0]
}
for {
select {
case entry, ok := <-logChannel:
if !ok {
flush()
return
}
if len(pending) == logBoxLines {
pending = append(pending[:0], pending[1:]...)
}
pending = append(pending, entry)
case <-ticker.C:
flush()
}
} }
}() }()