Skip to content

Commit 9fa406a

Browse files
committed
⚡ perf: implement O(1) incremental rendering and width-persistent caching
1 parent 90ec44d commit 9fa406a

2 files changed

Lines changed: 85 additions & 39 deletions

File tree

cmd/vibeaura/chat.go

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,23 @@ type model struct {
129129
// Dynamic Commands from Extensions
130130
dynamicCommands map[string]brain.CLICommand
131131

132-
// Non-blocking Engine
133-
reactor *reactor.Reactor
134-
md *reactor.MarkdownRenderer
132+
// Non-blocking Engine
133+
reactor *reactor.Reactor
134+
md *reactor.MarkdownRenderer
135135
lastRenderTime time.Time
136-
}
137136

138-
type layoutMsg struct {
139-
content string
140-
wasAtBottom bool
141-
wasAtTop bool
142-
prevOffset int
143-
}
137+
// Memoization
138+
lastViewportWidth int
139+
lastMessageCount int
140+
memoizedView string
141+
}
142+
143+
type layoutMsg struct {
144+
content string
145+
wasAtBottom bool
146+
wasAtTop bool
147+
prevOffset int
148+
}
144149
type recordTickMsg time.Time
145150

146151
type checkUpdateTickMsg time.Time
@@ -1191,11 +1196,31 @@ func (m *model) renderMessages() string {
11911196
// Sync renderer width with viewport
11921197
m.md.SetWidth(m.viewport.Width)
11931198

1194-
// Use goroutines to render messages in parallel
1195-
rendered := make([]string, len(m.messages))
1199+
// O(1) optimization: If width hasn't changed AND history hasn't changed, return memoized
1200+
if m.lastViewportWidth == m.viewport.Width && m.lastMessageCount == len(m.messages) && m.memoizedView != "" {
1201+
return m.memoizedView
1202+
}
1203+
1204+
// O(1) incremental optimization: If width is SAME but messages grew, only render NEW items
1205+
isIncremental := m.lastViewportWidth == m.viewport.Width && len(m.messages) > m.lastMessageCount && m.memoizedView != ""
1206+
1207+
startIndex := 0
1208+
var sb strings.Builder
1209+
1210+
if isIncremental {
1211+
startIndex = m.lastMessageCount
1212+
sb.WriteString(m.memoizedView)
1213+
if startIndex > 0 {
1214+
sb.WriteString("\n\n")
1215+
}
1216+
}
1217+
1218+
// Only iterate through what's actually new
1219+
newMessages := m.messages[startIndex:]
1220+
rendered := make([]string, len(newMessages))
11961221
var wg sync.WaitGroup
11971222

1198-
for i, msg := range m.messages {
1223+
for i, msg := range newMessages {
11991224
wg.Add(1)
12001225
go func(idx int, raw string) {
12011226
defer wg.Done()
@@ -1206,7 +1231,8 @@ func (m *model) renderMessages() string {
12061231
rawContent := strings.TrimPrefix(raw, aiStyle.Render("VibeAuracle: "))
12071232
// Only render markdown if it's not currently streaming
12081233
if !strings.HasSuffix(rawContent, subtleStyle.Render("▌")) {
1209-
content = aiStyle.Render("VibeAuracle:") + "\n" + m.md.Render(rawContent)
1234+
// Use the width-aware persistent cache (Internal O(1) hit)
1235+
content = aiStyle.Render("VibeAuracle:") + "\n" + m.md.Render(rawContent, m.viewport.Width)
12101236
}
12111237
}
12121238

@@ -1216,14 +1242,17 @@ func (m *model) renderMessages() string {
12161242
}
12171243
wg.Wait()
12181244

1219-
var sb strings.Builder
12201245
for i, r := range rendered {
12211246
sb.WriteString(r)
12221247
if i < len(rendered)-1 {
12231248
sb.WriteString("\n\n")
12241249
}
12251250
}
12261251

1252+
m.memoizedView = sb.String()
1253+
m.lastViewportWidth = m.viewport.Width
1254+
m.lastMessageCount = len(m.messages)
1255+
12271256
if m.brain.Config().UI.ShowReasoning {
12281257
sb.WriteString("\n\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("#5F5F5F")).Bold(true).Render(" ◆ AGENTIC REASONING TRACE") + "\n")
12291258
for _, log := range m.thinkingLog {

internal/reactor/markdown.go

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,41 @@ import (
77
)
88

99
type MarkdownRenderer struct {
10-
cache sync.Map
11-
width int
12-
mu sync.Mutex // Protects width and pool recreation
13-
pool *sync.Pool
10+
// width -> content hash -> rendered string
11+
caches map[int]*sync.Map
12+
pools map[int]*sync.Pool
13+
mu sync.RWMutex
1414
}
1515

1616
func NewMarkdownRenderer(width int) *MarkdownRenderer {
1717
mr := &MarkdownRenderer{
18-
width: width,
18+
caches: make(map[int]*sync.Map),
19+
pools: make(map[int]*sync.Pool),
1920
}
20-
mr.recreatePool(width)
21+
mr.getOrCreateResources(width)
2122
return mr
2223
}
2324

24-
func (m *MarkdownRenderer) recreatePool(width int) {
25-
m.pool = &sync.Pool{
25+
func (m *MarkdownRenderer) getOrCreateResources(width int) (*sync.Map, *sync.Pool) {
26+
m.mu.RLock()
27+
cache, okC := m.caches[width]
28+
pool, okP := m.pools[width]
29+
m.mu.RUnlock()
30+
31+
if okC && okP {
32+
return cache, pool
33+
}
34+
35+
m.mu.Lock()
36+
defer m.mu.Unlock()
37+
38+
// Double check
39+
if cache, ok := m.caches[width]; ok {
40+
return cache, m.pools[width]
41+
}
42+
43+
newCache := &sync.Map{}
44+
newPool := &sync.Pool{
2645
New: func() interface{} {
2746
r, _ := glamour.NewTermRenderer(
2847
glamour.WithAutoStyle(),
@@ -31,36 +50,34 @@ func (m *MarkdownRenderer) recreatePool(width int) {
3150
return r
3251
},
3352
}
53+
m.caches[width] = newCache
54+
m.pools[width] = newPool
55+
return newCache, newPool
3456
}
3557

36-
func (m *MarkdownRenderer) Render(content string) string {
37-
// 1. Concurrent-safe cache check
38-
if cached, ok := m.cache.Load(content); ok {
58+
func (m *MarkdownRenderer) Render(content string, width int) string {
59+
cache, pool := m.getOrCreateResources(width)
60+
61+
// 1. O(1) Cache hit
62+
if cached, ok := cache.Load(content); ok {
3963
return cached.(string)
4064
}
4165

42-
// 2. Get a renderer from the pool
43-
r := m.pool.Get().(*glamour.TermRenderer)
44-
defer m.pool.Put(r)
66+
// 2. Render only if missed
67+
r := pool.Get().(*glamour.TermRenderer)
68+
defer pool.Put(r)
4569

4670
rendered, err := r.Render(content)
4771
if err != nil {
4872
return content
4973
}
5074

51-
m.cache.Store(content, rendered)
75+
cache.Store(content, rendered)
5276
return rendered
5377
}
5478

5579
func (m *MarkdownRenderer) SetWidth(width int) {
56-
m.mu.Lock()
57-
defer m.mu.Unlock()
58-
if m.width == width {
59-
return
60-
}
61-
m.width = width
62-
// Invalidate cache and pool for new width
63-
m.cache = sync.Map{}
64-
m.recreatePool(width)
80+
m.getOrCreateResources(width)
6581
}
6682

83+

0 commit comments

Comments
 (0)