Skip to content

Commit 90ec44d

Browse files
committed
⚡ fix: optimize message rendering in chat component
Refactor the message rendering logic in the chat component to utilize goroutines for parallel processing. This improves performance by reducing rendering time for large message sets. Also, update the MarkdownRenderer to use a sync.Map for caching, improving access efficiency and thread safety.
1 parent 5cdcb8f commit 90ec44d

2 files changed

Lines changed: 58 additions & 45 deletions

File tree

cmd/vibeaura/chat.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,22 +1191,35 @@ func (m *model) renderMessages() string {
11911191
// Sync renderer width with viewport
11921192
m.md.SetWidth(m.viewport.Width)
11931193

1194-
var sb strings.Builder
1194+
// Use goroutines to render messages in parallel
1195+
rendered := make([]string, len(m.messages))
1196+
var wg sync.WaitGroup
1197+
11951198
for i, msg := range m.messages {
1196-
content := msg
1197-
// If it's an AI message (starts with the VibeAuracle label), render markdown for the content part
1198-
if strings.HasPrefix(msg, aiStyle.Render("VibeAuracle: ")) {
1199-
rawContent := strings.TrimPrefix(msg, aiStyle.Render("VibeAuracle: "))
1200-
// Only render markdown if it's not currently streaming (too slow/flickery)
1201-
if !strings.HasSuffix(rawContent, subtleStyle.Render("▌")) {
1202-
content = aiStyle.Render("VibeAuracle:") + "\n" + m.md.Render(rawContent)
1199+
wg.Add(1)
1200+
go func(idx int, raw string) {
1201+
defer wg.Done()
1202+
1203+
content := raw
1204+
// If it's an AI message, render markdown
1205+
if strings.HasPrefix(raw, aiStyle.Render("VibeAuracle: ")) {
1206+
rawContent := strings.TrimPrefix(raw, aiStyle.Render("VibeAuracle: "))
1207+
// Only render markdown if it's not currently streaming
1208+
if !strings.HasSuffix(rawContent, subtleStyle.Render("▌")) {
1209+
content = aiStyle.Render("VibeAuracle:") + "\n" + m.md.Render(rawContent)
1210+
}
12031211
}
1204-
}
12051212

1206-
// Use lipgloss to wrap the message to the viewport width precisely.
1207-
wrapped := lipgloss.NewStyle().Width(m.viewport.Width).Render(content)
1208-
sb.WriteString(wrapped)
1209-
if i < len(m.messages)-1 {
1213+
// Wrap the message
1214+
rendered[idx] = lipgloss.NewStyle().Width(m.viewport.Width).Render(content)
1215+
}(i, msg)
1216+
}
1217+
wg.Wait()
1218+
1219+
var sb strings.Builder
1220+
for i, r := range rendered {
1221+
sb.WriteString(r)
1222+
if i < len(rendered)-1 {
12101223
sb.WriteString("\n\n")
12111224
}
12121225
}

internal/reactor/markdown.go

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,48 @@ import (
77
)
88

99
type MarkdownRenderer struct {
10-
renderer *glamour.TermRenderer
11-
cache map[string]string
12-
width int
13-
mu sync.RWMutex
10+
cache sync.Map
11+
width int
12+
mu sync.Mutex // Protects width and pool recreation
13+
pool *sync.Pool
1414
}
1515

1616
func NewMarkdownRenderer(width int) *MarkdownRenderer {
17-
r, _ := glamour.NewTermRenderer(
18-
glamour.WithAutoStyle(),
19-
glamour.WithWordWrap(width-4),
20-
)
21-
return &MarkdownRenderer{
22-
renderer: r,
23-
cache: make(map[string]string),
24-
width: width,
17+
mr := &MarkdownRenderer{
18+
width: width,
2519
}
20+
mr.recreatePool(width)
21+
return mr
2622
}
2723

28-
func (m *MarkdownRenderer) Render(content string) string {
29-
m.mu.RLock()
30-
// Quick check if width changed (invalidate cache if so)
31-
if cached, ok := m.cache[content]; ok {
32-
m.mu.RUnlock()
33-
return cached
24+
func (m *MarkdownRenderer) recreatePool(width int) {
25+
m.pool = &sync.Pool{
26+
New: func() interface{} {
27+
r, _ := glamour.NewTermRenderer(
28+
glamour.WithAutoStyle(),
29+
glamour.WithWordWrap(width-4),
30+
)
31+
return r
32+
},
3433
}
35-
m.mu.RUnlock()
36-
37-
m.mu.Lock()
38-
defer m.mu.Unlock()
34+
}
3935

40-
// Double check cache after lock
41-
if cached, ok := m.cache[content]; ok {
42-
return cached
36+
func (m *MarkdownRenderer) Render(content string) string {
37+
// 1. Concurrent-safe cache check
38+
if cached, ok := m.cache.Load(content); ok {
39+
return cached.(string)
4340
}
4441

45-
rendered, err := m.renderer.Render(content)
42+
// 2. Get a renderer from the pool
43+
r := m.pool.Get().(*glamour.TermRenderer)
44+
defer m.pool.Put(r)
45+
46+
rendered, err := r.Render(content)
4647
if err != nil {
4748
return content
4849
}
4950

50-
m.cache[content] = rendered
51+
m.cache.Store(content, rendered)
5152
return rendered
5253
}
5354

@@ -58,9 +59,8 @@ func (m *MarkdownRenderer) SetWidth(width int) {
5859
return
5960
}
6061
m.width = width
61-
m.cache = make(map[string]string)
62-
m.renderer, _ = glamour.NewTermRenderer(
63-
glamour.WithAutoStyle(),
64-
glamour.WithWordWrap(width-4),
65-
)
62+
// Invalidate cache and pool for new width
63+
m.cache = sync.Map{}
64+
m.recreatePool(width)
6665
}
66+

0 commit comments

Comments
 (0)