Skip to content

Commit c52a05a

Browse files
cpcloudcursoragent
andcommitted
perf(app): lazy tab reload after mutations, add benchmarks
Replace reloadAll() in mutation paths (save, undo, redo, inline edit) with reloadAfterMutation() which only refreshes the effective tab and marks all others as stale. Stale tabs are lazily reloaded when the user navigates to them (tab switch, dashboard jump, FK link follow). Before: every mutation reloaded all 5 tabs + lookups + house + dashboard ~3ms, 680KB, 15,670 allocs per mutation After: only the active tab + dashboard (if visible) ~489us (dashboard hidden), ~1ms (dashboard visible) 6.2x faster with dashboard hidden, 3x with it open Also adds comprehensive benchmarks for render, reload, and data layer hot paths to track regressions. refs #53 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bb505df commit c52a05a

File tree

7 files changed

+470
-12
lines changed

7 files changed

+470
-12
lines changed

internal/app/bench_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright 2026 Phillip Cloud
2+
// Licensed under the Apache License, Version 2.0
3+
4+
package app
5+
6+
import (
7+
"path/filepath"
8+
"testing"
9+
"time"
10+
11+
"github.com/cpcloud/micasa/internal/data"
12+
"github.com/cpcloud/micasa/internal/fake"
13+
)
14+
15+
// benchModel returns a Model populated with demo data, sized for a
16+
// realistic terminal. Reusable across benchmarks.
17+
func benchModel(b *testing.B) *Model {
18+
b.Helper()
19+
path := filepath.Join(b.TempDir(), "bench.db")
20+
store, err := data.Open(path)
21+
if err != nil {
22+
b.Fatal(err)
23+
}
24+
b.Cleanup(func() { _ = store.Close() })
25+
if err := store.AutoMigrate(); err != nil {
26+
b.Fatal(err)
27+
}
28+
if err := store.SeedDefaults(); err != nil {
29+
b.Fatal(err)
30+
}
31+
if err := store.SeedDemoDataFrom(fake.New(42)); err != nil {
32+
b.Fatal(err)
33+
}
34+
m, err := NewModel(store, Options{DBPath: path})
35+
if err != nil {
36+
b.Fatal(err)
37+
}
38+
m.width = 120
39+
m.height = 40
40+
m.showDashboard = false
41+
if err := m.reloadAllTabs(); err != nil {
42+
b.Fatal(err)
43+
}
44+
return m
45+
}
46+
47+
func BenchmarkView(b *testing.B) {
48+
m := benchModel(b)
49+
b.ResetTimer()
50+
for b.Loop() {
51+
_ = m.View()
52+
}
53+
}
54+
55+
func BenchmarkViewDashboard(b *testing.B) {
56+
m := benchModel(b)
57+
m.showDashboard = true
58+
if err := m.loadDashboardAt(time.Now()); err != nil {
59+
b.Fatal(err)
60+
}
61+
b.ResetTimer()
62+
for b.Loop() {
63+
_ = m.View()
64+
}
65+
}
66+
67+
func BenchmarkReloadAll(b *testing.B) {
68+
m := benchModel(b)
69+
b.ResetTimer()
70+
for b.Loop() {
71+
m.reloadAll()
72+
}
73+
}
74+
75+
func BenchmarkReloadActiveTab(b *testing.B) {
76+
m := benchModel(b)
77+
b.ResetTimer()
78+
for b.Loop() {
79+
_ = m.reloadActiveTab()
80+
}
81+
}
82+
83+
func BenchmarkReloadAfterMutation(b *testing.B) {
84+
m := benchModel(b)
85+
b.ResetTimer()
86+
for b.Loop() {
87+
m.reloadAfterMutation()
88+
}
89+
}
90+
91+
func BenchmarkReloadAfterMutationWithDashboard(b *testing.B) {
92+
m := benchModel(b)
93+
m.showDashboard = true
94+
if err := m.loadDashboardAt(time.Now()); err != nil {
95+
b.Fatal(err)
96+
}
97+
b.ResetTimer()
98+
for b.Loop() {
99+
m.reloadAfterMutation()
100+
}
101+
}
102+
103+
func BenchmarkLoadDashboard(b *testing.B) {
104+
m := benchModel(b)
105+
now := time.Now()
106+
b.ResetTimer()
107+
for b.Loop() {
108+
_ = m.loadDashboardAt(now)
109+
}
110+
}
111+
112+
func BenchmarkColumnWidths(b *testing.B) {
113+
m := benchModel(b)
114+
tab := m.activeTab()
115+
visSpecs, visCells, _, _, _ := visibleProjection(tab)
116+
sepW := 3
117+
b.ResetTimer()
118+
for b.Loop() {
119+
_ = columnWidths(visSpecs, visCells, 120, sepW)
120+
}
121+
}
122+
123+
func BenchmarkNaturalWidths(b *testing.B) {
124+
m := benchModel(b)
125+
tab := m.activeTab()
126+
visSpecs, visCells, _, _, _ := visibleProjection(tab)
127+
b.ResetTimer()
128+
for b.Loop() {
129+
_ = naturalWidths(visSpecs, visCells)
130+
}
131+
}
132+
133+
func BenchmarkVisibleProjection(b *testing.B) {
134+
m := benchModel(b)
135+
tab := m.activeTab()
136+
b.ResetTimer()
137+
for b.Loop() {
138+
_, _, _, _, _ = visibleProjection(tab)
139+
}
140+
}
141+
142+
func BenchmarkComputeTableViewport(b *testing.B) {
143+
m := benchModel(b)
144+
tab := m.activeTab()
145+
sep := m.styles.TableSeparator.Render(" │ ")
146+
b.ResetTimer()
147+
for b.Loop() {
148+
_ = computeTableViewport(tab, 120, sep, m.styles)
149+
}
150+
}
151+
152+
func BenchmarkTableView(b *testing.B) {
153+
m := benchModel(b)
154+
tab := m.activeTab()
155+
b.ResetTimer()
156+
for b.Loop() {
157+
_ = m.tableView(tab)
158+
}
159+
}
160+
161+
func BenchmarkDashboardView(b *testing.B) {
162+
m := benchModel(b)
163+
m.showDashboard = true
164+
if err := m.loadDashboardAt(time.Now()); err != nil {
165+
b.Fatal(err)
166+
}
167+
b.ResetTimer()
168+
for b.Loop() {
169+
_ = m.dashboardView(30)
170+
}
171+
}
172+
173+
func BenchmarkBuildBaseView(b *testing.B) {
174+
m := benchModel(b)
175+
b.ResetTimer()
176+
for b.Loop() {
177+
_ = m.buildBaseView()
178+
}
179+
}
180+
181+
func BenchmarkDimBackground(b *testing.B) {
182+
m := benchModel(b)
183+
base := m.buildBaseView()
184+
b.ResetTimer()
185+
for b.Loop() {
186+
_ = dimBackground(base)
187+
}
188+
}

internal/app/dashboard.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -776,8 +776,13 @@ func (m *Model) dashJump() {
776776
entry := nav[m.dashCursor]
777777
m.showDashboard = false
778778
m.active = tabIndex(entry.Tab)
779-
_ = m.reloadActiveTab()
780-
if tab := m.activeTab(); tab != nil {
779+
tab := m.activeTab()
780+
if tab != nil && tab.Stale {
781+
_ = m.reloadIfStale(tab)
782+
} else {
783+
_ = m.reloadActiveTab()
784+
}
785+
if tab != nil {
781786
selectRowByID(tab, entry.ID)
782787
}
783788
}

internal/app/forms.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1113,13 +1113,14 @@ func (m *Model) openDatePicker(
11131113
m.editID = &id
11141114
m.formKind = kind
11151115
m.formData = values
1116+
savedKind := kind
11161117
m.openCalendar(dateField, func() {
11171118
m.snapshotForUndo()
11181119
if err := m.handleFormSubmit(); err != nil {
11191120
m.setStatusError(err.Error())
11201121
} else {
11211122
m.setStatusInfo("Saved.")
1122-
m.reloadAll()
1123+
m.reloadAfterFormSave(savedKind)
11231124
}
11241125
m.formKind = formNone
11251126
m.formData = nil

internal/app/lazy_reload_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2026 Phillip Cloud
2+
// Licensed under the Apache License, Version 2.0
3+
4+
package app
5+
6+
import (
7+
"testing"
8+
)
9+
10+
func TestReloadAfterMutationMarksOtherTabsStale(t *testing.T) {
11+
m := newTestModelWithDemoData(t, 42)
12+
m.width = 120
13+
m.height = 40
14+
15+
// Start on the Projects tab (index 0).
16+
m.active = 0
17+
m.reloadAfterMutation()
18+
19+
// Active tab (0) should NOT be stale.
20+
if m.tabs[0].Stale {
21+
t.Error("active tab should not be stale after reloadAfterMutation")
22+
}
23+
24+
// All other tabs should be stale.
25+
for i := 1; i < len(m.tabs); i++ {
26+
if !m.tabs[i].Stale {
27+
t.Errorf("tab %d (%s) should be stale after mutation on tab 0", i, m.tabs[i].Name)
28+
}
29+
}
30+
}
31+
32+
func TestNavigatingToStaleTabClearsStaleFlag(t *testing.T) {
33+
m := newTestModelWithDemoData(t, 42)
34+
m.width = 120
35+
m.height = 40
36+
37+
// Simulate a mutation on tab 0 to mark others stale.
38+
m.active = 0
39+
m.reloadAfterMutation()
40+
41+
// Navigate to the next tab.
42+
m.nextTab()
43+
if m.active != 1 {
44+
t.Fatalf("expected active=1, got %d", m.active)
45+
}
46+
47+
// After navigation, the new active tab should not be stale.
48+
if m.tabs[1].Stale {
49+
t.Error("tab 1 should not be stale after navigating to it")
50+
}
51+
52+
// But tab 2 should still be stale (we haven't visited it).
53+
if !m.tabs[2].Stale {
54+
t.Error("tab 2 should still be stale")
55+
}
56+
}
57+
58+
func TestPrevTabClearsStaleFlag(t *testing.T) {
59+
m := newTestModelWithDemoData(t, 42)
60+
m.width = 120
61+
m.height = 40
62+
63+
// Start on tab 2, mutate to mark others stale.
64+
m.active = 2
65+
m.reloadAfterMutation()
66+
67+
// Navigate backward.
68+
m.prevTab()
69+
if m.active != 1 {
70+
t.Fatalf("expected active=1, got %d", m.active)
71+
}
72+
if m.tabs[1].Stale {
73+
t.Error("tab 1 should not be stale after navigating to it via prevTab")
74+
}
75+
}
76+
77+
func TestReloadAllClearsAllStaleFlags(t *testing.T) {
78+
m := newTestModelWithDemoData(t, 42)
79+
m.width = 120
80+
m.height = 40
81+
82+
// Mark tabs stale.
83+
for i := range m.tabs {
84+
m.tabs[i].Stale = true
85+
}
86+
87+
// reloadAllTabs resets all data, and reloadIfStale clears per-tab.
88+
m.reloadAll()
89+
90+
// After reloadAll, no tabs should be stale (they were all freshly loaded).
91+
for i := range m.tabs {
92+
if m.tabs[i].Stale {
93+
t.Errorf("tab %d (%s) should not be stale after reloadAll", i, m.tabs[i].Name)
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)