Răsfoiți Sursa

perf(ui/chat): cache help keybind URLs to avoid redundant goldmark parsing

ShortHelp() and FullHelp() are called every draw frame and both
independently call messageURLs(), which parses message content through
goldmark. Add a cursor-indexed cache so messageURLs() runs at most once
per cursor change instead of twice per frame. Cache is invalidated when
messages are added, edited, or deleted via invalidateRows().

Fixes COMP #9 and COMP #15.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
claude 1 lună în urmă
părinte
comite
f99e04d200
1 a modificat fișierele cu 41 adăugiri și 10 ștergeri
  1. 41 10
      internal/ui/chat/messages_list.go

+ 41 - 10
internal/ui/chat/messages_list.go

@@ -52,6 +52,11 @@ type messagesList struct {
 
 	lastWidth int
 
+	// Help cache: invalidated when cursor changes or messages are modified.
+	cachedHelpCursor int
+	cachedHelpURLs   []string
+	cachedHelpDirty  bool
+
 	fetchingMembers struct {
 		mu    sync.Mutex
 		value bool
@@ -77,12 +82,14 @@ type messagesListRow struct {
 
 func newMessagesList(cfg *config.Config, chatView *Model) *messagesList {
 	ml := &messagesList{
-		Model:           list.NewModel(),
-		cfg:             cfg,
-		chatView:        chatView,
-		renderer:        markdown.NewRenderer(cfg),
-		itemByID:        make(map[discord.MessageID]*tview.TextView),
-		expandedReplies: make(map[discord.MessageID]struct{}),
+		Model:            list.NewModel(),
+		cfg:              cfg,
+		chatView:         chatView,
+		renderer:         markdown.NewRenderer(cfg),
+		itemByID:         make(map[discord.MessageID]*tview.TextView),
+		expandedReplies:  make(map[discord.MessageID]struct{}),
+		cachedHelpCursor: -1,
+		cachedHelpDirty:  true,
 	}
 	ml.attachmentsPicker = newAttachmentsPicker(cfg, chatView)
 
@@ -319,6 +326,7 @@ func (ml *messagesList) rebuildRows() {
 
 func (ml *messagesList) invalidateRows() {
 	ml.rowsDirty = true
+	ml.cachedHelpDirty = true
 }
 
 // ensureRows lazily rebuilds list rows. This avoids repeated O(n) row rebuild
@@ -1197,6 +1205,29 @@ func (ml *messagesList) waitForChunkEvent() uint {
 	return ml.fetchingMembers.count
 }
 
+// cachedMessageURLs returns the URLs for the selected message, caching the
+// result of the expensive messageURLs parse so that ShortHelp and FullHelp
+// (both called every draw frame) only run the goldmark parser once per cursor
+// position change.
+func (ml *messagesList) cachedMessageURLs() ([]string, *discord.Message, bool) {
+	cursor := ml.Cursor()
+	if !ml.cachedHelpDirty && cursor == ml.cachedHelpCursor {
+		msg, err := ml.selectedMessage()
+		if err != nil {
+			return nil, nil, false
+		}
+		return ml.cachedHelpURLs, msg, true
+	}
+	msg, err := ml.selectedMessage()
+	if err != nil {
+		return nil, nil, false
+	}
+	ml.cachedHelpURLs = messageURLs(*msg)
+	ml.cachedHelpCursor = cursor
+	ml.cachedHelpDirty = false
+	return ml.cachedHelpURLs, msg, true
+}
+
 func (ml *messagesList) ShortHelp() []keybind.Keybind {
 	cfg := ml.cfg.Keybinds.MessagesList
 	help := []keybind.Keybind{
@@ -1206,13 +1237,13 @@ func (ml *messagesList) ShortHelp() []keybind.Keybind {
 		cfg.Search.Keybind,
 	}
 
-	if msg, err := ml.selectedMessage(); err == nil {
+	if urls, msg, ok := ml.cachedMessageURLs(); ok {
 		if me, err := ml.chatView.state.Cabinet.Me(); err == nil {
 			if msg.Author.ID != me.ID {
 				help = append(help, cfg.Reply.Keybind)
 			}
 		}
-		if len(messageURLs(*msg)) != 0 || len(msg.Attachments) != 0 {
+		if len(urls) != 0 || len(msg.Attachments) != 0 {
 			help = append(help, cfg.Open.Keybind)
 		}
 		if msg.Thread != nil {
@@ -1232,9 +1263,9 @@ func (ml *messagesList) FullHelp() [][]keybind.Keybind {
 	canDelete := false
 	canOpen := false
 	canOpenThread := false
-	if msg, err := ml.selectedMessage(); err == nil {
+	if urls, msg, ok := ml.cachedMessageURLs(); ok {
 		canSelectReply = msg.ReferencedMessage != nil
-		canOpen = len(messageURLs(*msg)) != 0 || len(msg.Attachments) != 0
+		canOpen = len(urls) != 0 || len(msg.Attachments) != 0
 		canOpenThread = msg.Thread != nil
 
 		if me, err := ml.chatView.state.Cabinet.Me(); err == nil {