From d3dedf10aaefb14fc2f3f03c109bf4f87c43a1cf Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 26 Apr 2026 17:03:38 -0500 Subject: [PATCH] revert(tui): drop DeferredMd, profiling showed it was neutral MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profiled with scripts/profile-tui.py under hold-PageUp + hold-wheel. The placeholder → microtask-upgrade pattern did not reduce renderer p99 (63ms → 63ms) or max (96ms → 142ms, slightly worse). Each fresh row still pays the Md cost — just on a follow-up commit instead of inline — and the follow-up commit shows up as a second heavy frame a few ms later. The real bottlenecks turned out to be: 1. wheel step too large (fixed in 7ca16eea) 2. outer terminal ANSI parse throughput (diagnosing next) 3. React commit frequency during hold-scroll (needs coalescing) None of which DeferredMd addresses. Clearing the complexity so the next experiments land on a simpler substrate. --- ui-tui/src/components/deferredMarkdown.tsx | 90 ---------------------- ui-tui/src/components/messageLine.tsx | 9 +-- ui-tui/src/components/thinking.tsx | 2 +- 3 files changed, 3 insertions(+), 98 deletions(-) delete mode 100644 ui-tui/src/components/deferredMarkdown.tsx diff --git a/ui-tui/src/components/deferredMarkdown.tsx b/ui-tui/src/components/deferredMarkdown.tsx deleted file mode 100644 index 55d984a32a..0000000000 --- a/ui-tui/src/components/deferredMarkdown.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// DeferredMd — renders a lightweight placeholder on first mount and -// upgrades to full markdown + syntax highlighting in a subsequent -// transition commit. Spreads the parse cost off the scroll critical path. -// -// Why: profiling shows the 63-112ms renderer spikes during hold-PageUp -// correlate with fresh MessageLine mounts running the markdown tokenizer -// + syntax highlighting synchronously. The new row is added by -// useVirtualHistory's slide step; React commits the tree; Ink lays out -// Yoga; stdout writes the result. All in one hitch frame. -// -// With this wrapper, the hitch frame lays out a pre-wrapped plain -// (Yoga only needs to wrap width-known strings — no tokenizer, no -// highlighter, no inline regex walk), then a follow-up commit re-renders -// the same row with full markdown. The follow-up is gated on a -// queueMicrotask so Ink has a chance to paint the placeholder before -// React starts the Md-heavy upgrade work. -// -// Upgrade cache: once a given (theme, text, compact) tuple has been -// rendered as full Md, we remember it so remounts (scroll-out then -// scroll-back) don't pay the placeholder round-trip again — they mount -// straight into the upgraded subtree, which Md internally memoizes -// on text identity, so there's no re-tokenization either. - -import { Text } from '@hermes/ink' -import { memo, useEffect, useState } from 'react' - -import type { Theme } from '../theme.js' - -import { Md, stripInlineMarkup } from './markdown.js' - -// Theme object is stable per-session; key upgrades under it so palette -// swaps naturally retrigger (colors differ → render changes). -const upgraded = new WeakMap>() - -const cacheKey = (compact: boolean | undefined, text: string) => (compact ? `c:${text}` : `x:${text}`) - -const hasUpgraded = (t: Theme, key: string) => upgraded.get(t)?.has(key) ?? false - -const markUpgraded = (t: Theme, key: string) => { - const bucket = upgraded.get(t) ?? new Set() - - bucket.add(key) - upgraded.set(t, bucket) -} - -export const DeferredMd = memo(function DeferredMd({ color, compact, t, text }: DeferredMdProps) { - const key = cacheKey(compact, text) - const [ready, setReady] = useState(() => hasUpgraded(t, key) || !text) - - useEffect(() => { - if (ready) { - return - } - - let cancelled = false - - queueMicrotask(() => { - if (cancelled) { - return - } - - markUpgraded(t, key) - setReady(true) - }) - - return () => { - cancelled = true - } - }, [key, ready, t]) - - if (ready) { - return - } - - // Placeholder: strip inline markup so the visible width approximately - // matches the final Md layout (bold/italic/links are width-neutral or - // collapse to anchor text). Line breaks preserved — Ink's wrap="wrap" - // lays the plain text out as blocks at the right column count. - // Using directly (no Box wrapper) so there's no column-flex - // decision for Yoga — it just wraps a string. - return {stripInlineMarkup(text)} -}) - -interface DeferredMdProps { - /** Fallback color for the placeholder text (typically the role's body color). */ - color?: string - compact?: boolean - t: Theme - text: string -} diff --git a/ui-tui/src/components/messageLine.tsx b/ui-tui/src/components/messageLine.tsx index fe7c8076a1..a3d3f5844a 100644 --- a/ui-tui/src/components/messageLine.tsx +++ b/ui-tui/src/components/messageLine.tsx @@ -9,7 +9,7 @@ import { boundedLiveRenderText, compactPreview, hasAnsi, isPasteBackedText, stri import type { Theme } from '../theme.js' import type { ActiveTool, DetailsMode, Msg, SectionVisibility } from '../types.js' -import { DeferredMd } from './deferredMarkdown.js' +import { Md } from './markdown.js' import { StreamingMd } from './streamingMarkdown.js' import { ToolTrail } from './thinking.js' import { TodoPanel } from './todoPanel.js' @@ -107,12 +107,7 @@ export const MessageLine = memo(function MessageLine({ // streamingMarkdown.tsx for the cost model. ) : ( - // Deferred markdown: plain-text placeholder on first mount, upgrade - // to full Md on a queued microtask. Spreads the tokenizer + syntax - // cost off the scroll critical path so hold-PageUp doesn't hitch - // on fresh assistant rows entering overscan. See - // deferredMarkdown.tsx for the trade-offs. - + ) } diff --git a/ui-tui/src/components/thinking.tsx b/ui-tui/src/components/thinking.tsx index 0fd47315a9..03ecf8c86e 100644 --- a/ui-tui/src/components/thinking.tsx +++ b/ui-tui/src/components/thinking.tsx @@ -899,7 +899,7 @@ export const ToolTrail = memo(function ToolTrail({ return duration ? ( <> {label} - + {duration}