From 89d4dd8952a1389ba72012a8efb42ee6ed1492d8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:53:14 -0700 Subject: [PATCH] perf(terminal): scan synchronized markers in linear work (#23015) --- .../foreground-output-scan.test.ts | 80 +++++++++++++++++++ .../pty-connection/foreground-output-scan.ts | 21 ++--- 2 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts diff --git a/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts new file mode 100644 index 00000000000..7d2a280eafc --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts @@ -0,0 +1,80 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + scanSynchronizedForegroundOutput, + SYNCHRONIZED_OUTPUT_START_SEQUENCE as start, + SYNCHRONIZED_OUTPUT_END_SEQUENCE as end +} from './foreground-output-scan' + +afterEach(() => vi.restoreAllMocks()) + +describe('foreground synchronized output scan', () => { + it.each([start, end])('bounds searched characters for a flood of %j markers', (marker) => { + const data = marker.repeat(8192) + const originalIndexOf = String.prototype.indexOf + let searchedCharacters = 0 + const spy = vi.spyOn(String.prototype, 'indexOf').mockImplementation(function ( + this: string, + search: string, + position = 0 + ): number { + const result = originalIndexOf.call(this, search, position) + if (this === data && (search === start || search === end)) { + searchedCharacters += (result < 0 ? data.length : result + search.length) - position + } + return result + }) + const result = scanSynchronizedForegroundOutput(data, '', false) + spy.mockRestore() + console.info(JSON.stringify({ markers: 8192, chars: data.length, searchedCharacters })) + expect(result).toEqual({ + started: marker === start, + ended: marker === end, + active: marker === start, + markerTail: data.slice(-7) + }) + expect(searchedCharacters).toBeLessThanOrEqual(data.length * 2) + }) + + it('preserves the last marker and records both transitions in the current chunk', () => { + expect(scanSynchronizedForegroundOutput(`${end}paint${start}`, '', true)).toEqual({ + started: true, + ended: true, + active: true, + markerTail: start.slice(1) + }) + expect(scanSynchronizedForegroundOutput(`${start}paint${end}`, '', false)).toEqual({ + started: true, + ended: true, + active: false, + markerTail: end.slice(1) + }) + }) + + it.each([start, end])('recognizes %j at every socket split', (marker) => { + for (let split = 1; split < marker.length; split++) { + const first = scanSynchronizedForegroundOutput(marker.slice(0, split), '', marker === end) + const second = scanSynchronizedForegroundOutput( + marker.slice(split), + first.markerTail, + first.active + ) + expect(second).toEqual({ + started: marker === start, + ended: marker === end, + active: marker === start, + markerTail: marker.slice(1) + }) + } + }) + + it('leaves the latch unchanged for empty output and malformed lookalikes', () => { + for (const data of ['', 'ordinary output', '\x1b[?2026x'.repeat(2048)]) { + expect(scanSynchronizedForegroundOutput(data, '', true)).toEqual({ + started: false, + ended: false, + active: true, + markerTail: data.slice(-7) + }) + } + }) +}) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.ts b/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.ts index 8d8d53eebb6..b74f44a6d09 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.ts @@ -58,27 +58,30 @@ export function scanSynchronizedForegroundOutput( let active = wasActive let started = false let ended = false - let offset = 0 + let startIndex = scanData.indexOf(SYNCHRONIZED_OUTPUT_START_SEQUENCE) + let endIndex = scanData.indexOf(SYNCHRONIZED_OUTPUT_END_SEQUENCE) - while (offset < scanData.length) { - const startIndex = scanData.indexOf(SYNCHRONIZED_OUTPUT_START_SEQUENCE, offset) - const endIndex = scanData.indexOf(SYNCHRONIZED_OUTPUT_END_SEQUENCE, offset) - if (startIndex === -1 && endIndex === -1) { - break - } + // Each marker search advances independently, so a missing counterpart is scanned only once. + while (startIndex !== -1 || endIndex !== -1) { if (endIndex !== -1 && (startIndex === -1 || endIndex < startIndex)) { active = false if (endIndex + SYNCHRONIZED_OUTPUT_END_SEQUENCE.length > currentChunkStartIndex) { ended = true } - offset = endIndex + SYNCHRONIZED_OUTPUT_END_SEQUENCE.length + endIndex = scanData.indexOf( + SYNCHRONIZED_OUTPUT_END_SEQUENCE, + endIndex + SYNCHRONIZED_OUTPUT_END_SEQUENCE.length + ) continue } active = true if (startIndex + SYNCHRONIZED_OUTPUT_START_SEQUENCE.length > currentChunkStartIndex) { started = true } - offset = startIndex + SYNCHRONIZED_OUTPUT_START_SEQUENCE.length + startIndex = scanData.indexOf( + SYNCHRONIZED_OUTPUT_START_SEQUENCE, + startIndex + SYNCHRONIZED_OUTPUT_START_SEQUENCE.length + ) } return {