Files
deepseek-harness/packages/ui/tui/tests/transcript-card-cache.spec.ts
T
Turtle 5329bef04d perf(tui): incremental step timing and card render caches for long sessions
Resuming a long session (196k events, 2.2k steps, 1.8k tool cards) took
~12s to render and ~800ms to echo one keystroke:

- Every step's timing footer called stepTimingAt, which replayed the whole
  event log per footer - O(steps x events) on the initial render.
- pi-tui re-renders every component each frame and relies on per-component
  line caches, but ToolCardComponent/ContextCardComponent built throwaway
  Text/Markdown instances inside render(width), re-wrapping every settled
  card's output on every keystroke.

Replace the per-footer replay with one shared StepTimingTracker per chat
mount (single O(events) cursor over the append-only log), and cache card
rows by width via CardLineCache, dropped by every state mutator and
invalidate().

Measured (tmux 200x50, 196k-event session): resume prompt-ready ~12s -> ~7.6s;
per-keystroke echo ~800ms median -> ~11ms.
2026-08-04 00:37:10 +08:00

75 lines
2.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import { createToolResultMessage, CallId } from '@deepseek-ai/dsh-llm'
import { ContextCardComponent, ToolCardComponent } from '../src/components/transcript.ts'
import { parseArguments } from '../src/components/content.ts'
import { createPalette, markdownTheme } from '../src/components/theme.ts'
const palette = createPalette(false)
const mdTheme = markdownTheme(palette)
function toolCard(): ToolCardComponent {
return new ToolCardComponent('bash', parseArguments('{"command":"ls"}'), undefined, 10, 2_000, palette, mdTheme)
}
function toolResult(text: string): Extract<SessionEvent, { type: 'tool/result' }>['data'] {
const message = createToolResultMessage({
callId: CallId('call-1'),
content: [{ type: 'text', text }],
isError: false,
})
return { turn: 1, step: 1, message }
}
// pi-tui re-renders every component each frame; the cards must serve repeat
// same-width renders from their line cache and drop it on every state change.
describe('transcript card render caches', () => {
it('tool card: repeat same-width renders return the cached rows', () => {
const card = toolCard()
const first = card.render(80)
expect(card.render(80)).toBe(first)
const narrower = card.render(60)
expect(narrower).not.toBe(first)
expect(card.render(60)).toBe(narrower)
})
it('tool card: result, visibility, and invalidate() each drop the cache', () => {
const card = toolCard()
const pending = card.render(80)
card.updateResult(toolResult('output line'))
const settled = card.render(80)
expect(settled).not.toBe(pending)
expect(settled.join('\n')).toContain('●')
card.setVisibility('hidden')
expect(card.render(80)).toEqual([])
card.setVisibility('collapsed')
const restored = card.render(80)
expect(restored).toEqual(settled)
expect(restored).not.toBe(settled)
card.invalidate()
expect(card.render(80)).not.toBe(restored)
})
it('context card: caches by width and drops on setExpanded and invalidate()', () => {
const card = new ContextCardComponent('workspace-context', 'line one\nline two', 10, palette)
const first = card.render(80)
expect(card.render(80)).toBe(first)
// Same width across the mutation, so a hit here would prove a kept cache.
card.setExpanded(true)
const expanded = card.render(80)
expect(expanded).not.toBe(first)
expect(card.render(80)).toBe(expanded)
card.invalidate()
const reRendered = card.render(80)
expect(reRendered).not.toBe(expanded)
expect(reRendered).toEqual(expanded)
expect(card.render(60)).not.toBe(reRendered)
})
})