Skip to content

[Feature]: perf: cross-layer performance optimization across TUI / Desktop / Provider / EventWire #5433

Description

@ADX15xs

Version line

v2 — Go rewrite (1.x), main-v2 (active development)

What problem does this solve?

Reasonix has two frontend runtimes — a TUI (Bubbletea) and a Desktop app (Wails + React) — that share a common control.Controller backend. Over time, profiling revealed three categories of performance bottlenecks:

  1. Rendering frame rate (TUI) — The transcript view re-wraps the entire conversation on every token delta; lipgloss styles and goldmark renderers are recreated every frame; the status bar is built twice per frame (once in Update counting lines, once in View displaying them).
  2. IPC frequency (Desktop) — Every streaming token delta is emitted as a separate Wails bridge call; the Go side allocates a new string per event via Emit; no coalescing or backpressure exists; the React side rebuilds component trees on every update without useDeferredValue.
  3. Allocation / GC pressure (cross-layer) — SSE stream parsing copies every line from []byte to string; tool call arguments use += for delta accumulation (O(n²)); shell command output accumulates via string concatenation; JSON parsing for the todo panel fires every frame regardless of whether the data changed; project files are re-read from disk on every access.

These issues are most visible during long streaming conversations (>10k tokens), repeated tool call execution, and large shell command outputs — all common in daily use.

Proposed solution

Deliver three batches of zero-risk, independently verifiable optimisations across all four layers:

Batch 1 — Immediate gains (low risk):

  • Replace SSE string operations with zero-copy []byte (scanner.Bytes() + bytes.HasPrefix/bytes.Equal/bytes.TrimSpace)
  • Cache lipgloss Style objects at the package level, rebuilt only when width or colour changes (wrap, theme, queue indicator styles)
  • Cache goldmark mdRenderer instances per terminal width via sync.Map
  • Replace += string concatenation with strings.Builder in connectorBlock, MCP view width calculations, and shell output accumulation
  • Incremental flushableMarkdownPrefix using a lastParagraphBreak byte offset
  • File caching (loadProjectsFile with a 5-second TTL and sync.RWMutex)
  • Single-pass streamedRows rune scanning instead of strings.Split + regex
  • Replace map[event.Kind]string with a [KindCount]string array in the wire layer
  • Extract event.Coalesce() as a shared exported function for continuous deltas

Batch 2 — Core optimisations (medium risk):

  • Incremental transcript wrapping (wrapDirtyFrom + lineWrapCounts), only re-wrapping the dirty tail instead of the entire transcript
  • Eliminate double status-bar construction via buildStatusLines caching (one call in Update, zero in View)
  • Reuse package-level buffers (renderRowsBuf/renderBarBuf/blankRow) in renderTranscript, plus strings.Builder output assembly
  • Desktop Go event coalescing: buffer consecutive Text/Reasoning deltas, flush on non-coalescable events, async emitter with ring-buffer backpressure (drop oldest 1/4 at queue capacity 1024)
  • Desktop React: memo on Transcript/StatusBar/UserMessage components, useCallback stabilisation for handleToggleColdPage/handleToggleWarmTurn, 77 useCallback wrappers in App.tsx
  • Wire type deduplication (remove desktop/wire.go, share eventwire.ToWire)
  • scrollVersion keyed on stable identifiers (id + kind + status) only, ignoring volatile text.length
  • return s guards in applyEvent reducer to return the same reference when nothing changed

Batch 3 — Dependent / cross-cutting optimisations:

  • Tool call argument accumulation via argsBuilders map[int]*strings.Builder in both OpenAI and Anthropic SSE handlers
  • Rewrite coalesceEventDeltas with a single strings.Builder per coalesced run, eliminating O(n²) string copy during long flush intervals
  • Cache JSON parsing in the todo panel (todoParsedArgs/todoParsedTodos, invalidated via setTodoArgs/refreshTodoCache)
  • Cache bottom-panel row count (buildBottomPanelsRowCount() called once in Update instead of rendering twice)
  • useDeferredValue(text) in MarkdownRenderer to defer expensive markdown rendering during streaming; extract REMARK_PLUGINS/REHYPE_PLUGINS as module-level constants

All optimisations preserve the control.Controller transport-agnostic contract, maintain existing test coverage, and are independently verifiable (go vet + go test + npx tsc).

Metadata

Metadata

Assignees

No one assigned

    Labels

    agentCore agent loop (internal/agent, internal/control)desktopWails desktop app (desktop/**)enhancementNew feature or requestproviderModel providers & selection (internal/provider)tuiTerminal UI / CLI (internal/cli, internal/control)v2Go rewrite (1.x) — main-v2 branch, active development

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions