From 19197e30864f22f1b7e29b9f005d3fe6d2bae728 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 4 May 2026 11:38:39 +0000 Subject: [PATCH] fix(ui): cache glamour renderers The theme prep overhaul in 755f6fa made the main markdown renderer and thinking block markdown renderer heavier, which exasperated a gap in perf. Basically every token chunk would clear would clear the per-item render cache and force a re-render. This update memoizes renderers with markdown and should generally give us a nice increase in perf compared to what we had before the theme rendering. --- internal/ui/common/markdown.go | 36 ++++++++++++++++++++++++++++++++-- internal/ui/model/ui.go | 1 + 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/internal/ui/common/markdown.go b/internal/ui/common/markdown.go index 6123711d9a6353c5ee96c11765859479112aef70..8ba45bccd263350607763af4813f2b6b047a1d7f 100644 --- a/internal/ui/common/markdown.go +++ b/internal/ui/common/markdown.go @@ -2,6 +2,7 @@ package common import ( "image/color" + "sync" "charm.land/glamour/v2" "github.com/alecthomas/chroma/v2/formatters" @@ -18,24 +19,55 @@ func init() { formatters.Register(formatterName, xchroma.Formatter(zero, nil)) } +var ( + mdCacheMu sync.Mutex + mdCache = map[int]*glamour.TermRenderer{} + quietMDCache = map[int]*glamour.TermRenderer{} +) + // MarkdownRenderer returns a glamour [glamour.TermRenderer] configured with -// the given styles and width. +// the given styles and width. Renderers are memoized per width and shared +// across callers; call InvalidateMarkdownRendererCache when the active +// styles change. func MarkdownRenderer(sty *styles.Styles, width int) *glamour.TermRenderer { + mdCacheMu.Lock() + defer mdCacheMu.Unlock() + if r, ok := mdCache[width]; ok { + return r + } r, _ := glamour.NewTermRenderer( glamour.WithStyles(sty.Markdown), glamour.WithWordWrap(width), glamour.WithChromaFormatter(formatterName), ) + mdCache[width] = r return r } // QuietMarkdownRenderer returns a glamour [glamour.TermRenderer] with no colors -// (plain text with structure) and the given width. +// (plain text with structure) and the given width. Renderers are memoized per +// width and shared across callers. func QuietMarkdownRenderer(sty *styles.Styles, width int) *glamour.TermRenderer { + mdCacheMu.Lock() + defer mdCacheMu.Unlock() + if r, ok := quietMDCache[width]; ok { + return r + } r, _ := glamour.NewTermRenderer( glamour.WithStyles(sty.QuietMarkdown), glamour.WithWordWrap(width), glamour.WithChromaFormatter(formatterName), ) + quietMDCache[width] = r return r } + +// InvalidateMarkdownRendererCache drops every cached renderer. Call this +// whenever the active styles change so subsequent renderers pick up the new +// ansi.StyleConfig. +func InvalidateMarkdownRendererCache() { + mdCacheMu.Lock() + defer mdCacheMu.Unlock() + mdCache = map[int]*glamour.TermRenderer{} + quietMDCache = map[int]*glamour.TermRenderer{} +} diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 8dac275f28cce91c38134265220f84bd611eecf1..75eead0149b2145166557e4aff6050cd40206ae5 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -3116,6 +3116,7 @@ func (m *UI) refreshStyles() { ) m.todoSpinner.Style = t.Pills.TodoSpinner m.status.help.Styles = t.Help + common.InvalidateMarkdownRendererCache() m.chat.InvalidateRenderCaches() }