mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
perf(tui): lazily seed virtual history heights (#16523)
This commit is contained in:
parent
9b55365f6f
commit
98d75dea5a
3 changed files with 75 additions and 22 deletions
39
ui-tui/src/__tests__/useVirtualHistoryHeights.test.ts
Normal file
39
ui-tui/src/__tests__/useVirtualHistoryHeights.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { ensureVirtualItemHeight } from '../hooks/useVirtualHistory.js'
|
||||||
|
|
||||||
|
describe('ensureVirtualItemHeight', () => {
|
||||||
|
it('reuses cached heights without invoking the estimator', () => {
|
||||||
|
const heights = new Map([['a', 7]])
|
||||||
|
const estimateHeight = vi.fn(() => 99)
|
||||||
|
|
||||||
|
expect(ensureVirtualItemHeight(heights, 'a', 0, 4, estimateHeight)).toBe(7)
|
||||||
|
expect(estimateHeight).not.toHaveBeenCalled()
|
||||||
|
expect(heights.get('a')).toBe(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lazily seeds missing heights from the estimator', () => {
|
||||||
|
const heights = new Map<string, number>()
|
||||||
|
const estimateHeight = vi.fn((index: number) => 10 + index)
|
||||||
|
|
||||||
|
expect(ensureVirtualItemHeight(heights, 'b', 2, 4, estimateHeight)).toBe(12)
|
||||||
|
expect(estimateHeight).toHaveBeenCalledTimes(1)
|
||||||
|
expect(estimateHeight).toHaveBeenCalledWith(2, 'b')
|
||||||
|
expect(heights.get('b')).toBe(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the default estimate when no estimator is provided', () => {
|
||||||
|
const heights = new Map<string, number>()
|
||||||
|
|
||||||
|
expect(ensureVirtualItemHeight(heights, 'c', 0, 4)).toBe(4)
|
||||||
|
expect(heights.get('c')).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normalizes non-positive estimates to a minimum of one row', () => {
|
||||||
|
const heights = new Map<string, number>()
|
||||||
|
const estimateHeight = vi.fn(() => 0)
|
||||||
|
|
||||||
|
expect(ensureVirtualItemHeight(heights, 'd', 0, 0, estimateHeight)).toBe(1)
|
||||||
|
expect(heights.get('d')).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -218,23 +218,15 @@ export function useMainApp(gw: GatewayClient) {
|
||||||
return cache
|
return cache
|
||||||
}, [heightCacheKey])
|
}, [heightCacheKey])
|
||||||
|
|
||||||
const initialHeights = useMemo(() => {
|
const estimateRowHeight = useCallback(
|
||||||
const out = new Map<string, number>()
|
(index: number) =>
|
||||||
|
estimatedMsgHeight(virtualRows[index]!.msg, cols, {
|
||||||
for (const row of virtualRows) {
|
compact: ui.compact,
|
||||||
out.set(
|
details: detailsVisible,
|
||||||
row.key,
|
limitHistory: index < virtualRows.length - FULL_RENDER_TAIL_ITEMS
|
||||||
heightCache.get(row.key) ??
|
}),
|
||||||
estimatedMsgHeight(row.msg, cols, {
|
[cols, detailsVisible, ui.compact, virtualRows]
|
||||||
compact: ui.compact,
|
)
|
||||||
details: detailsVisible,
|
|
||||||
limitHistory: row.index < virtualRows.length - FULL_RENDER_TAIL_ITEMS
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}, [cols, detailsVisible, heightCache, ui.compact, virtualRows])
|
|
||||||
|
|
||||||
const syncHeightCache = useCallback(
|
const syncHeightCache = useCallback(
|
||||||
(heights: ReadonlyMap<string, number>) => {
|
(heights: ReadonlyMap<string, number>) => {
|
||||||
|
|
@ -250,7 +242,8 @@ export function useMainApp(gw: GatewayClient) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const virtualHistory = useVirtualHistory(scrollRef, virtualRows, cols, {
|
const virtualHistory = useVirtualHistory(scrollRef, virtualRows, cols, {
|
||||||
initialHeights,
|
estimateHeight: estimateRowHeight,
|
||||||
|
initialHeights: heightCache,
|
||||||
liveTailActive: turnLiveTailActive,
|
liveTailActive: turnLiveTailActive,
|
||||||
onHeightsChange: syncHeightCache
|
onHeightsChange: syncHeightCache
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -76,12 +76,32 @@ export const shouldSetVirtualClamp = ({
|
||||||
viewportHeight: number
|
viewportHeight: number
|
||||||
}) => itemCount > 0 && viewportHeight > 0 && !sticky && !liveTailActive
|
}) => itemCount > 0 && viewportHeight > 0 && !sticky && !liveTailActive
|
||||||
|
|
||||||
|
export const ensureVirtualItemHeight = (
|
||||||
|
heights: Map<string, number>,
|
||||||
|
key: string,
|
||||||
|
index: number,
|
||||||
|
estimate: number,
|
||||||
|
estimateHeight?: (index: number, key: string) => number
|
||||||
|
) => {
|
||||||
|
const cached = heights.get(key)
|
||||||
|
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return Math.max(1, Math.floor(cached))
|
||||||
|
}
|
||||||
|
|
||||||
|
const seeded = Math.max(1, Math.floor(estimateHeight?.(index, key) ?? estimate))
|
||||||
|
heights.set(key, seeded)
|
||||||
|
|
||||||
|
return seeded
|
||||||
|
}
|
||||||
|
|
||||||
export function useVirtualHistory(
|
export function useVirtualHistory(
|
||||||
scrollRef: RefObject<ScrollBoxHandle | null>,
|
scrollRef: RefObject<ScrollBoxHandle | null>,
|
||||||
items: readonly { key: string }[],
|
items: readonly { key: string }[],
|
||||||
columns: number,
|
columns: number,
|
||||||
{
|
{
|
||||||
estimate = ESTIMATE,
|
estimate = ESTIMATE,
|
||||||
|
estimateHeight,
|
||||||
initialHeights,
|
initialHeights,
|
||||||
liveTailActive = false,
|
liveTailActive = false,
|
||||||
onHeightsChange,
|
onHeightsChange,
|
||||||
|
|
@ -208,7 +228,7 @@ export function useVirtualHistory(
|
||||||
arr[0] = 0
|
arr[0] = 0
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
arr[i + 1] = arr[i]! + Math.max(1, Math.floor(heights.current.get(items[i]!.key) ?? estimate))
|
arr[i + 1] = arr[i]! + ensureVirtualItemHeight(heights.current, items[i]!.key, i, estimate, estimateHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
offsetsCache.current = { arr, n, version: offsetVersion.current }
|
offsetsCache.current = { arr, n, version: offsetVersion.current }
|
||||||
|
|
@ -280,7 +300,7 @@ export function useVirtualHistory(
|
||||||
let coverage = 0
|
let coverage = 0
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
coverage += heights.current.get(items[i]!.key) ?? PESSIMISTIC
|
coverage += ensureVirtualItemHeight(heights.current, items[i]!.key, i, PESSIMISTIC, estimateHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sticky) {
|
if (sticky) {
|
||||||
|
|
@ -288,13 +308,13 @@ export function useVirtualHistory(
|
||||||
|
|
||||||
while (start > minStart && coverage < needed) {
|
while (start > minStart && coverage < needed) {
|
||||||
start--
|
start--
|
||||||
coverage += heights.current.get(items[start]!.key) ?? PESSIMISTIC
|
coverage += ensureVirtualItemHeight(heights.current, items[start]!.key, start, PESSIMISTIC, estimateHeight)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const maxEnd = Math.min(n, start + maxMounted)
|
const maxEnd = Math.min(n, start + maxMounted)
|
||||||
|
|
||||||
while (end < maxEnd && coverage < needed) {
|
while (end < maxEnd && coverage < needed) {
|
||||||
coverage += heights.current.get(items[end]!.key) ?? PESSIMISTIC
|
coverage += ensureVirtualItemHeight(heights.current, items[end]!.key, end, PESSIMISTIC, estimateHeight)
|
||||||
end++
|
end++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -498,6 +518,7 @@ interface MeasuredNode {
|
||||||
interface VirtualHistoryOptions {
|
interface VirtualHistoryOptions {
|
||||||
coldStartCount?: number
|
coldStartCount?: number
|
||||||
estimate?: number
|
estimate?: number
|
||||||
|
estimateHeight?: (index: number, key: string) => number
|
||||||
initialHeights?: ReadonlyMap<string, number>
|
initialHeights?: ReadonlyMap<string, number>
|
||||||
liveTailActive?: boolean
|
liveTailActive?: boolean
|
||||||
maxMounted?: number
|
maxMounted?: number
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue