perf(ui): sort History once per redraw, drop duplicate jobs refreshes
Stage 4 of the GUI layout plan (F11, F12). History: the cell callback copied and sorted the whole event list on every call, and a full-window Refresh issues one call per visible cell — 126 sorts of a 300-element slice per redraw, measured. The sorted snapshot now lives in `rows`, refilled by `resort()` at build time, on a sort toggle, and from `refresh()`. The length callback moves to `len(rows)` with it: cells and the row count have to read the same slice, which was only incidentally true while each cell re-derived the order for itself. The column captions become a package-level array instead of a slice reallocated per header update, and the per-cell `TextStyle`/`Refresh()` pair goes — the template already carries the zero style and `SetText` refreshes. Jobs: `refreshView()` already re-reads the service snapshot and refreshes the list, so the six `list.Refresh()` calls that preceded it, and the duplicate `syncFromService()` in the pause handler, were redundant. The folder filter's early-return path never reaches `refreshView()`, so its `list.Refresh()` moves into that branch rather than being deleted. Both changes carry regression tests, each verified to fail against the behaviour it guards: `TestHistorySortToggleKeepsRowsInSync` for the cache-versus-length hazard, `TestToolbarButtonRedrawsRowAndDetails` for the handlers that now rely on `refreshView` alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/test"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
@@ -127,6 +129,95 @@ func TestLogFileName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHistorySortToggleKeepsRowsInSync is the regression guard for F11: the
|
||||
// table now reads one cached sorted snapshot instead of re-sorting inside every
|
||||
// cell callback, so the length callback and the cells have to be refilled
|
||||
// together. If either the sort toggle or refresh stops calling resort(), the
|
||||
// row count and the cell contents disagree — which no compiler check catches.
|
||||
func TestHistorySortToggleKeepsRowsInSync(t *testing.T) {
|
||||
testApp := test.NewApp()
|
||||
defer testApp.Quit()
|
||||
|
||||
events := []event{
|
||||
{Time: "2026-06-01 10:00:00", JobName: "A"},
|
||||
{Time: "2026-06-01 11:00:00", JobName: "B"},
|
||||
{Time: "2026-06-01 12:00:00", JobName: "C"},
|
||||
}
|
||||
content, refresh := newHistoryView(&events)
|
||||
table, ok := content.Objects[0].(*widget.Table)
|
||||
if !ok {
|
||||
t.Fatal("history view does not wrap a table")
|
||||
}
|
||||
|
||||
rowCount := func() int {
|
||||
t.Helper()
|
||||
rows, cols := table.Length()
|
||||
if cols != len(historyHeaders) {
|
||||
t.Errorf("column count = %d, want %d", cols, len(historyHeaders))
|
||||
}
|
||||
return rows
|
||||
}
|
||||
// Column 2 is the Job name, the field these fixtures vary.
|
||||
jobAt := func(row int) string {
|
||||
t.Helper()
|
||||
cell := table.CreateCell()
|
||||
table.UpdateCell(widget.TableCellID{Row: row, Col: 2}, cell)
|
||||
return cell.(*widget.Label).Text
|
||||
}
|
||||
// The sort toggle lives on the Time header cell, which is only wired up
|
||||
// when UpdateHeader runs for it.
|
||||
header := table.CreateHeader()
|
||||
table.UpdateHeader(widget.TableCellID{Row: -1, Col: 0}, header)
|
||||
timeHeader, ok := header.(*historyHeader)
|
||||
if !ok {
|
||||
t.Fatal("history table header is not a historyHeader")
|
||||
}
|
||||
assertOrder := func(when string, want ...string) {
|
||||
t.Helper()
|
||||
if got := rowCount(); got != len(want) {
|
||||
t.Fatalf("%s: row count = %d, want %d", when, got, len(want))
|
||||
}
|
||||
for row, name := range want {
|
||||
if got := jobAt(row); got != name {
|
||||
t.Errorf("%s: row %d = %q, want %q", when, row, got, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertOrder("ascending", "A", "B", "C")
|
||||
|
||||
test.Tap(timeHeader)
|
||||
assertOrder("descending", "C", "B", "A")
|
||||
|
||||
// A new run arrives while the table is sorted newest-first: it must be
|
||||
// counted and placed in the order currently on screen, not the build-time one.
|
||||
events = append(events, event{Time: "2026-06-01 13:00:00", JobName: "D"})
|
||||
refresh()
|
||||
assertOrder("descending after refresh", "D", "C", "B", "A")
|
||||
|
||||
test.Tap(timeHeader)
|
||||
assertOrder("ascending after refresh", "A", "B", "C", "D")
|
||||
}
|
||||
|
||||
// TestHistoryCellTemplateIsPlainText guards the dropped per-cell TextStyle
|
||||
// assignment: the template must already carry the zero style, since nothing
|
||||
// resets it any more.
|
||||
func TestHistoryCellTemplateIsPlainText(t *testing.T) {
|
||||
testApp := test.NewApp()
|
||||
defer testApp.Quit()
|
||||
|
||||
var events []event
|
||||
content, _ := newHistoryView(&events)
|
||||
table := content.Objects[0].(*widget.Table)
|
||||
label, ok := table.CreateCell().(*widget.Label)
|
||||
if !ok {
|
||||
t.Fatal("history cell template is not a label")
|
||||
}
|
||||
if label.TextStyle != (fyne.TextStyle{}) {
|
||||
t.Errorf("cell template TextStyle = %+v, want the zero value", label.TextStyle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEventUsesConsistentTimestampShape(t *testing.T) {
|
||||
ev := newEvent(1, "Job", "OK", "detail")
|
||||
if _, err := time.Parse("2006-01-02 15:04:05", ev.Time); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user