Skip to content

Cache listTitleWidth result to avoid per-frame O(N×V) lipgloss.Width recomputation #49

Description

@gadflysu

Problem

listTitleWidth() is called once per visible row inside renderRowFull(), which is called for every row in every View() invocation. In stateList mode the method iterates over all sessions twice — once for titles, once for CWD strings — calling lipgloss.Width(display.Sanitize(...)) on each to find the natural maximum widths:

// picker/model.go  listTitleWidth()
for _, s := range m.sessions {
    if w := lipgloss.Width(display.Sanitize(s.Title)); w > maxNaturalTitle { ... }
}
for _, s := range m.sessions {
    if w := lipgloss.Width(display.Sanitize(s.CWDDisplay)); w > maxDirW { ... }
}

lipgloss.Width calls ansi.stringWidth → FirstGraphemeCluster → uax29/graphemes for Unicode grapheme segmentation — not a trivial operation.

With V visible rows and N total sessions, each frame triggers V × 2N width computations. For V=30, N=200 that is 12,000 calls per View(), repeated every 120 ms tick.

Evidence

CPU profiling via sample(1) on a live aps process shows the hot path:

bubbletea.eventLoop (120 ms tick)
  → View() → renderList() → renderRowFull() ×V
      → listTitleWidth()
          → lipgloss.Width(Sanitize(title))  ×N
          → lipgloss.Width(Sanitize(cwd))    ×N
              → ansi.stringWidth → FirstGraphemeCluster
                → uax29/graphemes Unicode grapheme split   ← CPU hot spot

listTitleWidth accounted for the majority of user-space samples in the profile. By contrast, idColW and msgColW are already computed once in newModel and never recalculated.

Fix

Add two cached fields to Model:

naturalTitleW int  // max lipgloss.Width(Sanitize(s.Title))   across m.sessions
naturalDirW   int  // max lipgloss.Width(Sanitize(s.CWDDisplay)) across m.sessions

Compute them in a helper recomputeNaturalWidths() and call it from:

  • newModel (initial)
  • applyRefresh (sessions added or updated)

listTitleWidth() reads the cached values directly — no per-call iteration.

Non-goals

  • Does not change visible layout or column widths
  • Does not affect stateListPreview path (already skips the natural-width bonus)

Plan

docs/agent/plan-issue-49-cache-list-title-width.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestperformancePerformance improvement or optimization

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions