diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index 76e13cc72..5a5cb3522 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -1182,32 +1182,52 @@ describe('Buffer', () => { }); describe('line string cache cleanup', () => { - it('should clear shared cache entries with a single timer', async () => { - buffer.fillViewportRows(); - buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 1)); - buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 1)); + it('should clear shared cache entries with a single timer', () => { + const originalSetTimeout = globalThis.setTimeout; + const originalClearTimeout = globalThis.clearTimeout; + let timeoutId = 0; + const scheduledTimeouts = new Map void>(); + (globalThis as any).setTimeout = ((handler: (...args: any[]) => void) => { + const id = ++timeoutId; + scheduledTimeouts.set(id, () => handler()); + return id as ReturnType; + }) as typeof setTimeout; + (globalThis as any).clearTimeout = ((id: ReturnType) => { + scheduledTimeouts.delete(id as unknown as number); + }) as typeof clearTimeout; + try { + buffer.fillViewportRows(); + buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 1)); + buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 1)); - assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`); - assert.equal(buffer.translateBufferLineToString(1, false), `b${' '.repeat(INIT_COLS - 1)}`); + assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`); + assert.equal(buffer.translateBufferLineToString(1, false), `b${' '.repeat(INIT_COLS - 1)}`); - const cache = (buffer as any)._stringCache; - assert.equal(cache.entries.size, 2); - const previousTimer = (cache as any)._clearTimeout; - assert.notEqual(previousTimer, undefined); + const cache = (buffer as any)._stringCache; + assert.equal(cache.entries.size, 2); + const previousTimer = (cache as any)._clearTimeout.value; + assert.notEqual(previousTimer, undefined); - // Another translation should refresh the same shared timer, not create a second queue. - assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`); - assert.notEqual((cache as any)._clearTimeout, undefined); - assert.notEqual((cache as any)._clearTimeout, previousTimer); + // Another translation should refresh the same shared timer, not create a second queue. + assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`); + assert.notEqual((cache as any)._clearTimeout.value, undefined); + assert.notEqual((cache as any)._clearTimeout.value, previousTimer); - await new Promise(r => setTimeout(r, 70)); + for (const timeout of scheduledTimeouts.values()) { + timeout(); + } + scheduledTimeouts.clear(); - assert.equal(cache.entries.size, 0); - assert.equal((cache as any)._clearTimeout, undefined); + assert.equal(cache.entries.size, 0); + assert.equal((cache as any)._clearTimeout.value, undefined); - // Cache entries should be recreated lazily after a timer-based reset. - assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`); - assert.equal(cache.entries.size, 1); + // Cache entries should be recreated lazily after a timer-based reset. + assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`); + assert.equal(cache.entries.size, 1); + } finally { + globalThis.setTimeout = originalSetTimeout; + globalThis.clearTimeout = originalClearTimeout; + } }); it('should reset line string cache state on clear and resize', () => { @@ -1217,11 +1237,11 @@ describe('Buffer', () => { const cache = (buffer as any)._stringCache; assert.equal(cache.entries.size, 1); - assert.notEqual((cache as any)._clearTimeout, undefined); + assert.notEqual((cache as any)._clearTimeout.value, undefined); buffer.clear(); assert.equal(cache.entries.size, 0); - assert.equal((cache as any)._clearTimeout, undefined); + assert.equal((cache as any)._clearTimeout.value, undefined); buffer.fillViewportRows(); buffer.lines.get(0)!.setCell(0, createCellData(0, 'b', 1)); @@ -1230,7 +1250,7 @@ describe('Buffer', () => { buffer.resize(INIT_COLS - 1, INIT_ROWS); assert.equal(cache.entries.size, 0); - assert.equal((cache as any)._clearTimeout, undefined); + assert.equal((cache as any)._clearTimeout.value, undefined); }); }); diff --git a/src/common/buffer/BufferLineStringCache.ts b/src/common/buffer/BufferLineStringCache.ts index 9382eaf83..547afc85f 100644 --- a/src/common/buffer/BufferLineStringCache.ts +++ b/src/common/buffer/BufferLineStringCache.ts @@ -5,14 +5,22 @@ import type { IBufferLineStringCache, IBufferLineStringCacheEntry } from 'common/buffer/BufferLine'; import { disposableTimeout } from 'common/Async'; +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import type { IDisposable } from 'common/Lifecycle'; -const STRING_CACHE_CLEAR_DELAY_MS = 50; +const enum Constants { + CacheTtlMs = 15000 +} -export class BufferLineStringCache implements IBufferLineStringCache, IDisposable { +export class BufferLineStringCache extends Disposable implements IBufferLineStringCache { public generation: number = 0; public readonly entries: Set = new Set(); - private _clearTimeout: IDisposable | undefined; + private readonly _clearTimeout = this._register(new MutableDisposable()); + + constructor() { + super(); + this._register(toDisposable(() => this.entries.clear())); + } public touch(): void { this._scheduleClear(); @@ -30,8 +38,7 @@ export class BufferLineStringCache implements IBufferLineStringCache, IDisposabl } public clear(): void { - this._clearTimeout?.dispose(); - this._clearTimeout = undefined; + this._clearTimeout.clear(); this.generation++; for (const entry of this.entries) { entry.value = undefined; @@ -40,15 +47,10 @@ export class BufferLineStringCache implements IBufferLineStringCache, IDisposabl this.entries.clear(); } - public dispose(): void { - this.clear(); - } - private _scheduleClear(): void { - this._clearTimeout?.dispose(); - this._clearTimeout = disposableTimeout(() => { - this._clearTimeout = undefined; + this._clearTimeout.value = disposableTimeout(() => { + this._clearTimeout.clear(); this.clear(); - }, STRING_CACHE_CLEAR_DELAY_MS); + }, Constants.CacheTtlMs); } } diff --git a/src/common/buffer/BufferSet.test.ts b/src/common/buffer/BufferSet.test.ts index 072095346..8809a3cb6 100644 --- a/src/common/buffer/BufferSet.test.ts +++ b/src/common/buffer/BufferSet.test.ts @@ -91,13 +91,13 @@ describe('BufferSet', () => { const oldCache = oldNormal._stringCache; assert.equal(oldCache.entries.size, 1); - assert.notEqual(oldCache._clearTimeout, undefined); + assert.notEqual(oldCache._clearTimeout.value, undefined); bufferSet.reset(); assert.notEqual(bufferSet.normal, oldNormal); assert.equal(oldCache.entries.size, 0); - assert.equal(oldCache._clearTimeout, undefined); + assert.equal(oldCache._clearTimeout.value, undefined); }); it('should dispose both buffers when disposed', () => { @@ -112,15 +112,15 @@ describe('BufferSet', () => { const normalCache = normal._stringCache; const altCache = alt._stringCache; - assert.notEqual(normalCache._clearTimeout, undefined); - assert.notEqual(altCache._clearTimeout, undefined); + assert.notEqual(normalCache._clearTimeout.value, undefined); + assert.notEqual(altCache._clearTimeout.value, undefined); bufferSet.dispose(); assert.equal(normalCache.entries.size, 0); assert.equal(altCache.entries.size, 0); - assert.equal(normalCache._clearTimeout, undefined); - assert.equal(altCache._clearTimeout, undefined); + assert.equal(normalCache._clearTimeout.value, undefined); + assert.equal(altCache._clearTimeout.value, undefined); }); }); });