mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Remove repeated row copying from terminal redraw ingestion (#26118)
This commit is contained in:
@@ -73,7 +73,7 @@ export function appendNormalizedToMultilineTailBufferUnwindowed(
|
||||
ensureCursorRow()
|
||||
rows[cursorRow]!.completed = false
|
||||
}
|
||||
const writeChar = (char: string): void => {
|
||||
const writeText = (text: string): void => {
|
||||
ensureCursorRow()
|
||||
markCursorRowRewritten()
|
||||
const row = rows[cursorRow]!
|
||||
@@ -82,9 +82,9 @@ export function appendNormalizedToMultilineTailBufferUnwindowed(
|
||||
}
|
||||
row.text =
|
||||
cursorColumn >= row.text.length
|
||||
? `${row.text}${char}`
|
||||
: `${row.text.slice(0, cursorColumn)}${char}${row.text.slice(cursorColumn + 1)}`
|
||||
cursorColumn += 1
|
||||
? `${row.text}${text}`
|
||||
: `${row.text.slice(0, cursorColumn)}${text}${row.text.slice(cursorColumn + text.length)}`
|
||||
cursorColumn += text.length
|
||||
}
|
||||
const eraseLine = (mode: number): void => {
|
||||
ensureCursorRow()
|
||||
@@ -100,13 +100,28 @@ export function appendNormalizedToMultilineTailBufferUnwindowed(
|
||||
}
|
||||
}
|
||||
|
||||
let textStart = 0
|
||||
const writePendingText = (end: number): void => {
|
||||
if (end > textStart) {
|
||||
writeText(
|
||||
end === textStart + 1 ? normalizedChunk[textStart]! : normalizedChunk.slice(textStart, end)
|
||||
)
|
||||
}
|
||||
textStart = end + 1
|
||||
}
|
||||
for (let index = 0; index < normalizedChunk.length; index += 1) {
|
||||
const char = normalizedChunk[index]
|
||||
if (char === '\n') {
|
||||
writePendingText(index)
|
||||
ensureCursorRow()
|
||||
rows[cursorRow]!.completed = true
|
||||
newCompleteLines += 1
|
||||
retainNewlyCompletedLine(trimTerminalLineRight(rows[cursorRow]!.text))
|
||||
// Own only retained text; a short erase-to-end result can otherwise pin a large span.
|
||||
const row = rows[cursorRow]!
|
||||
const trimmed = trimTerminalLineRight(row.text)
|
||||
const completedLine = ownRetainedString(trimmed)
|
||||
row.text = trimmed === row.text ? completedLine : ownRetainedString(row.text)
|
||||
retainNewlyCompletedLine(completedLine)
|
||||
cursorRow += 1
|
||||
cursorColumn = 0
|
||||
ensureCursorRow()
|
||||
@@ -114,19 +129,23 @@ export function appendNormalizedToMultilineTailBufferUnwindowed(
|
||||
continue
|
||||
}
|
||||
if (char === '\r') {
|
||||
writePendingText(index)
|
||||
cursorColumn = 0
|
||||
continue
|
||||
}
|
||||
if (char === '\u0008') {
|
||||
writePendingText(index)
|
||||
cursorColumn = Math.max(0, cursorColumn - 1)
|
||||
continue
|
||||
}
|
||||
if (char === '\u001b') {
|
||||
writePendingText(index)
|
||||
const parsed = parseAnsiControlSequence(normalizedChunk, index)
|
||||
if (!parsed) {
|
||||
continue
|
||||
}
|
||||
index = parsed.endIndex
|
||||
textStart = index + 1
|
||||
if (parsed.kind !== 'csi' || !hasCanonicalNumericCsiParams(parsed.params)) {
|
||||
continue
|
||||
}
|
||||
@@ -145,8 +164,8 @@ export function appendNormalizedToMultilineTailBufferUnwindowed(
|
||||
}
|
||||
continue
|
||||
}
|
||||
writeChar(char)
|
||||
}
|
||||
writePendingText(normalizedChunk.length)
|
||||
|
||||
return finalizeRetainedTerminalRows(
|
||||
rows,
|
||||
@@ -186,7 +205,10 @@ function finalizeRetainedTerminalRows(
|
||||
newlyCompletedLines: string[]
|
||||
} {
|
||||
let truncated = initialTruncated
|
||||
let retainedRows = rows.map((row) => ({ ...row, text: trimTerminalLineRight(row.text) }))
|
||||
let retainedRows = rows.map((row) => {
|
||||
const trimmed = trimTerminalLineRight(row.text)
|
||||
return { ...row, text: trimmed === row.text ? row.text : ownRetainedString(trimmed) }
|
||||
})
|
||||
|
||||
if (retainedRows.length > MAX_TAIL_LINES + 1) {
|
||||
const removeCount = retainedRows.length - (MAX_TAIL_LINES + 1)
|
||||
@@ -242,10 +264,7 @@ function finalizeRetainedTerminalRows(
|
||||
|
||||
return {
|
||||
lines,
|
||||
// Why only the partial: redraw rows are built character by character and never sliced from
|
||||
// the chunk, but the partial is re-sliced from its own row on every chunk, so it alone can
|
||||
// accumulate a backing string across frames. Owning the rows too costs 20-36% on TUI floods
|
||||
// for no measured retention.
|
||||
// Completed rows own their storage at newline; the final partial may still reference a span.
|
||||
partialLine: ownRetainedString(partialLine),
|
||||
redrawCursor,
|
||||
truncated,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { appendNormalizedToTailBuffer } from './terminal-tail-buffer'
|
||||
|
||||
describe('retained terminal redraw text spans', () => {
|
||||
it('overwrites a long row without copying its suffix for every code unit', () => {
|
||||
const previous = 'p'.repeat(16 * 1024)
|
||||
const replacement = 'x'.repeat(previous.length)
|
||||
const slice = vi.spyOn(String.prototype, 'slice')
|
||||
let copiedSuffixes = 0
|
||||
try {
|
||||
const result = appendNormalizedToTailBuffer([previous], '', `\x1b[1A\r${replacement}\n`)
|
||||
copiedSuffixes = slice.mock.calls.length
|
||||
expect(result.lines).toEqual([replacement])
|
||||
expect(result.newlyCompletedLines).toEqual([replacement])
|
||||
expect(result.redrawCursor).toBeNull()
|
||||
} finally {
|
||||
slice.mockRestore()
|
||||
}
|
||||
expect(copiedSuffixes).toBeLessThan(20)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['shorter overwrite', '\x1b[1A\rXY\n', 'XYcdef'],
|
||||
['extending overwrite', '\x1b[1A\r123456789\n', '123456789'],
|
||||
['erase to end', '\x1b[1A\rXY\x1b[KZ\n', 'XYZ'],
|
||||
['erase to start', '\x1b[1A\rXY\x1b[2C!\x1b[1KZ\n', ' Z'],
|
||||
['erase whole line', '\x1b[1A\rXY\x1b[2KZ\n', ' Z'],
|
||||
['backspace', '\x1b[1A\rXY\bZ\n', 'XZcdef'],
|
||||
['carriage return', '\x1b[1A\rXY\rZ\n', 'ZYcdef'],
|
||||
['split surrogate overwrite', '\x1b[1A\r😀\bZ\n', '\ud83dZcdef'],
|
||||
[
|
||||
'Unicode and non-special controls',
|
||||
'\x1b[1A\r漢😀e\u0301\ud800\t\u0000\udfff\bZ\n',
|
||||
'漢😀e\u0301\ud800\t\u0000Z'
|
||||
]
|
||||
])('preserves %s', (_name, chunk, expected) => {
|
||||
const result = appendNormalizedToTailBuffer(['abcdef'], '', chunk)
|
||||
expect(result).toEqual({
|
||||
lines: [expected],
|
||||
partialLine: '',
|
||||
redrawCursor: null,
|
||||
truncated: false,
|
||||
newCompleteLines: 1,
|
||||
newlyCompletedLines: [expected]
|
||||
})
|
||||
})
|
||||
|
||||
it('fills a cursor gap before a text span and retains its code-unit column', () => {
|
||||
const result = appendNormalizedToTailBuffer(['abcdef'], '', '\x1b[1A\x1b[9G😀')
|
||||
expect(result.lines).toEqual([])
|
||||
expect(result.partialLine).toBe('abcdef 😀')
|
||||
expect(result.redrawCursor).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps completed trailing spaces available to a later cursor-up rewrite', () => {
|
||||
const result = appendNormalizedToTailBuffer(['abcdef'], '', '\x1b[1A\rab \n\x1b[1A\x1b[5GX\n')
|
||||
expect(result.lines).toEqual(['ab Xf'])
|
||||
expect(result.newlyCompletedLines).toEqual(['ab f', 'ab Xf'])
|
||||
})
|
||||
})
|
||||
@@ -79,6 +79,21 @@ describe('retained terminal tail row storage', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not pin overwritten multiline redraw chunks through completed rows', () => {
|
||||
let lines: string[] = []
|
||||
const before = collectHeap()
|
||||
for (let index = 0; index < 200; index += 1) {
|
||||
const chunk = `\x1b[0A${'x'.repeat(64 * 1024)}\rstep ${index} done\x1b[K\n`
|
||||
const next = appendNormalizedToTailBuffer(lines, '', chunk)
|
||||
lines = next.lines
|
||||
}
|
||||
const retained = collectHeap() - before
|
||||
|
||||
expect(lines).toHaveLength(200)
|
||||
expect(lines.at(-1)).toBe('step 199 done')
|
||||
expect(retained).toBeLessThan(4 * 1024 * 1024)
|
||||
})
|
||||
|
||||
it('owns the redraw partial line without re-owning carried rows', () => {
|
||||
const own = vi.spyOn(ownership, 'ownRetainedString')
|
||||
try {
|
||||
@@ -93,7 +108,7 @@ describe('retained terminal tail row storage', () => {
|
||||
|
||||
expect(redrawn.lines).toEqual(['row one'])
|
||||
expect(redrawn.partialLine).toBe('rewritten two')
|
||||
// One call for the partial only: redraw rows are built character by character.
|
||||
// Only the final partial is retained; carried rows need no copies.
|
||||
expect(own).toHaveBeenCalledTimes(1)
|
||||
expect(own).toHaveBeenLastCalledWith('rewritten two')
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user