perf(ui): кап буфера логов 10MB, O(1) вытеснение, дебаунс рендера
Some checks failed
CI / test (pull_request) Failing after 1m21s
CI / build-and-package (amd64, linux) (pull_request) Failing after 1m9s
CI / build-and-package (amd64, windows) (pull_request) Successful in 31s

- LogBuffer: ленивое вытеснение через start + инкрементальный total убирает
  O(n^2) копирование entries[1:] при большом числе записей
- LogPanel: лимит 10MB (maxLogBytes), перерисовка ленты коалесцируется
  (200мс) — строка не пересобирается на каждый append
- build-publish-ui.ps1: корень проекта задаётся через PROJECT_PATH в
  .gitea-creds (относительно scripts/), без него — папка рядом со скриптом
- тесты: живые записи Entries/Render, кап по байтам
This commit is contained in:
ki.sagidullin
2026-08-21 23:01:54 +05:00
parent a762a1df80
commit ee47785e1a
4 changed files with 147 additions and 30 deletions

View File

@@ -4,6 +4,7 @@ package desktop
import (
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
@@ -12,8 +13,13 @@ import (
"github.com/kamelion/ratatoskr-go/internal/ui"
)
// maxLogLen — обрезка буфера панели «Логи», чтобы не расти бесконечно.
const maxLogLen = 200_000
// maxLogBytes — кап суммарного объёма текста буфера «Логи» (байты), чтобы
// не расти бесконечно и не копить мегабайты в памяти.
const maxLogBytes = 10 * 1024 * 1024
// renderFlushMs — дебаунс-интервал перерисовки ленты (мс): при всплеске
// логов рендер выполняется не на каждую запись, а раз в этот интервал.
const renderFlushMs = 200
// LogPanel — Fyne-реализация ui.LogPanel (спец 12.8, 12.9): поток системных
// логов с фильтром по уровням (error/warning/info/debug).
@@ -24,25 +30,31 @@ const maxLogLen = 200_000
// widget.PopUp с widget.Check по каждому уровню; изменение набора сразу
// перерисовывает ленту — и по уже загруженным записям, и для новых.
//
// Состояние (buf/selected) доступается только с потока Fyne: Append приходит
// из горутин Hub и заворачивается в fyne.Do, колбэки чекбоксов Fyne вызывает
// сам на своём потоке.
// Состояние (buf/selected/pending/flushScheduled/lastFlush) доступается только
// с потока Fyne: Append приходит из горутин Hub и заворачивается в fyne.Do,
// колбэки чекбоксов Fyne вызывает сам на своём потоке. Перерисовка ленты
// коалесцируется (см. scheduleFlush), т.к. строка ленты пересобирается
// целиком и при капе в 10MB на каждый append была бы дорогой.
type LogPanel struct {
label *widget.Label
btn *widget.Button // кнопка-дропдаун фильтра уровней
buf *ui.LogBuffer // буфер записей, старые вытесняются
selected map[string]bool // уровень → показывать (по умолчанию все)
pending bool // буфер изменился, рендер отложен (поток Fyne)
flushScheduled bool // фоновый флаш уже запущен (поток Fyne)
lastFlush int64 // мс последнего рендера (поток Fyne)
}
// NewLogPanel создаёт LogPanel со всеми включёнными уровнями.
// NewLogPanel создаёт LogPanel с уровнями error/warning/info (debug скрыт).
func NewLogPanel() *LogPanel {
p := &LogPanel{
buf: ui.NewLogBuffer(maxLogLen),
buf: ui.NewLogBuffer(maxLogBytes),
selected: make(map[string]bool, len(ui.LogLevels)),
}
for _, lvl := range ui.LogLevels {
p.selected[lvl] = true
p.selected[lvl] = lvl != ui.LogLevelDebug
}
p.label = widget.NewLabel("")
@@ -68,11 +80,42 @@ func (p *LogPanel) Append(level, text string) {
dropped := p.buf.Append(lvl, text)
// Перерисовка нужна, если запись видима или вытеснение задело ленту.
if p.selected[lvl] || dropped {
p.render()
p.pending = true
p.scheduleFlush()
}
})
}
// scheduleFlush планирует отложенный рендер через renderFlushMs (или сразу,
// если с прошлого рендера прошло больше интервала). Все вызовы — с потока
// Fyne; фоновый поток только спит и по таймеру отправляет flush через fyne.Do,
// поэтому защита от повторного планирования (flushScheduled) работает без
// блокировок.
func (p *LogPanel) scheduleFlush() {
if p.flushScheduled {
return
}
p.flushScheduled = true
delay := int64(renderFlushMs) - (time.Now().UnixMilli() - p.lastFlush)
if delay < 1 {
delay = 1
}
go func() {
time.Sleep(time.Duration(delay) * time.Millisecond)
fyne.Do(p.flush)
}()
}
// flush выполняет отложенную перерисовку ленты (на потоке Fyne).
func (p *LogPanel) flush() {
p.flushScheduled = false
if p.pending {
p.pending = false
p.lastFlush = time.Now().UnixMilli()
p.render()
}
}
// render перестраивает текст ленты из буфера с учётом выбранного набора
// уровней. Пустой набор → пустая лента (без ошибок).
func (p *LogPanel) render() {