From a8c9128841eb953a2d3afbe445fc15e130f5cb0d Mon Sep 17 00:00:00 2001 From: Nick Gregory Date: Wed, 7 Jan 2026 16:04:49 -0800 Subject: [PATCH 1/2] improve SearchLineCache performance --- addons/addon-search/src/SearchLineCache.ts | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/addons/addon-search/src/SearchLineCache.ts b/addons/addon-search/src/SearchLineCache.ts index 535a3cc5e9..175f3919cd 100644 --- a/addons/addon-search/src/SearchLineCache.ts +++ b/addons/addon-search/src/SearchLineCache.ts @@ -38,6 +38,9 @@ export class SearchLineCache extends Disposable { private _linesCache: LineCacheEntry[] | undefined; private _linesCacheTimeout = this._register(new MutableDisposable()); private _linesCacheDisposables = this._register(new MutableDisposable()); + // Track access to avoid recreating a timeout on every init call which occurs once per search + // result (findNext/findPrevious -> _highlightAllMatches -> find loop). + private _lastAccessTimestamp = 0; constructor(private readonly _terminal: Terminal) { super(); @@ -57,15 +60,34 @@ export class SearchLineCache extends Disposable { ); } - this._linesCacheTimeout.value = disposableTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE); + this._lastAccessTimestamp = Date.now(); + if (!this._linesCacheTimeout.value) { + this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE); + } } private _destroyLinesCache(): void { this._linesCache = undefined; + this._lastAccessTimestamp = 0; this._linesCacheDisposables.clear(); this._linesCacheTimeout.clear(); } + private _scheduleLinesCacheTimeout(delay: number): void { + this._linesCacheTimeout.value = disposableTimeout(() => { + if (!this._linesCache) { + return; + } + const now = Date.now(); + const elapsed = now - this._lastAccessTimestamp; + if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) { + this._destroyLinesCache(); + return; + } + this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed); + }, delay); + } + public getLineFromCache(row: number): LineCacheEntry | undefined { return this._linesCache?.[row]; } From bf7c95b6ccc4b65c4e55ce2a5eab9cb351567e0a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:50:51 -0800 Subject: [PATCH 2/2] Flush writes on resize Fixes #5597 --- src/common/CoreTerminal.ts | 4 ++++ src/common/input/WriteBuffer.test.ts | 19 +++++++++++++++++ src/common/input/WriteBuffer.ts | 32 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 5a46570ecb..dd312af11a 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -176,6 +176,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { x = Math.max(x, MINIMUM_COLS); y = Math.max(y, MINIMUM_ROWS); + // Flush pending writes before resize to avoid race conditions where async + // writes are processed with incorrect dimensions + this._writeBuffer.flushSync(); + this._bufferService.resize(x, y); } diff --git a/src/common/input/WriteBuffer.test.ts b/src/common/input/WriteBuffer.test.ts index 8910642301..c534bbae48 100644 --- a/src/common/input/WriteBuffer.test.ts +++ b/src/common/input/WriteBuffer.test.ts @@ -106,5 +106,24 @@ describe('WriteBuffer', () => { wb.writeSync('1', 10); assert.equal(last, '11'); // 1 + 10 sub calls = 11 }); + it('flushSync processes all pending writes', done => { + wb.write('a', () => { cbStack.push('a'); }); + wb.write('b', () => { cbStack.push('b'); }); + wb.write('c', () => { cbStack.push('c'); }); + wb.flushSync(); + assert.deepEqual(stack, ['a', 'b', 'c']); + assert.deepEqual(cbStack, ['a', 'b', 'c']); + wb.write('x', () => { cbStack.push('x'); }); + wb.write('', () => { + assert.deepEqual(stack, ['a', 'b', 'c', 'x', '']); + assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']); + done(); + }); + }); + it('flushSync with no pending writes is a no-op', () => { + wb.flushSync(); + assert.deepEqual(stack, []); + assert.deepEqual(cbStack, []); + }); }); }); diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index 801cf3efba..f6d83ba882 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -54,6 +54,38 @@ export class WriteBuffer extends Disposable { this._didUserInput = true; } + /** + * Flushes all pending writes synchronously. This is useful when you need to + * ensure all queued data is processed before performing an operation that + * depends upon everything being parsed like resize. + * + * Note: This is unreliable with async parser handlers as it does not wait for + * promises to resolve. + */ + public flushSync(): void { + // exit early if another sync write loop is active + if (this._isSyncWriting) { + return; + } + this._isSyncWriting = true; + + // Process all pending chunks synchronously + let chunk: string | Uint8Array | undefined; + while (chunk = this._writeBuffer.shift()) { + this._action(chunk); + const cb = this._callbacks.shift(); + if (cb) cb(); + } + + // Reset buffer state + this._pendingData = 0; + this._bufferOffset = 0x7FFFFFFF; + this._writeBuffer.length = 0; + this._callbacks.length = 0; + + this._isSyncWriting = false; + } + /** * @deprecated Unreliable, to be removed soon. */