revert(tui): drop DeferredMd, profiling showed it was neutral

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.
This commit is contained in:
Brooklyn Nicholson 2026-04-26 17:03:38 -05:00
parent 7ca16eea56
commit d3dedf10aa
3 changed files with 3 additions and 98 deletions

View file

@ -1,90 +0,0 @@
// DeferredMd — renders a lightweight <Text> placeholder on first mount and
// upgrades to full <Md> 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 <Text>
// (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 <Md> 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<Theme, Set<string>>()
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<string>()
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 <Md compact={compact} t={t} text={text} />
}
// 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 <Text> directly (no Box wrapper) so there's no column-flex
// decision for Yoga — it just wraps a string.
return <Text color={color ?? undefined}>{stripInlineMarkup(text)}</Text>
})
interface DeferredMdProps {
/** Fallback color for the placeholder text (typically the role's body color). */
color?: string
compact?: boolean
t: Theme
text: string
}

View file

@ -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.
<StreamingMd compact={compact} t={t} text={boundedLiveRenderText(msg.text)} />
) : (
// 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.
<DeferredMd color={body} compact={compact} t={t} text={msg.text} />
<Md compact={compact} t={t} text={msg.text} />
)
}

View file

@ -899,7 +899,7 @@ export const ToolTrail = memo(function ToolTrail({
return duration ? (
<>
{label}
<Text color={t.color.dim} dim>
<Text color={t.color.statusFg} dim>
{duration}
</Text>
</>