diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 1cde9257d52..7138319d459 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -173,13 +173,27 @@ suite('window namespace tests', () => { const dataEvents: { name: string, data: string }[] = []; const closeEvents: string[] = []; const reg1 = window.onDidOpenTerminal(e => openEvents.push(e.name)); - const reg2 = window.onDidWriteTerminalData(e => dataEvents.push({ name: e.terminal.name, data: e.data })); + + let resolveOnceDataWritten: (() => void) | undefined; + + const reg2 = window.onDidWriteTerminalData(e => { + dataEvents.push({ name: e.terminal.name, data: e.data }); + + resolveOnceDataWritten!(); + }); + const reg3 = window.onDidCloseTerminal(e => { closeEvents.push(e.name); - if (closeEvents.length === 2) { - deepEqual(openEvents, [ 'test1', 'test2' ]); - deepEqual(dataEvents, [ { name: 'test1', data: 'write1' }, { name: 'test2', data: 'write2' } ]); - deepEqual(closeEvents, [ 'test1', 'test2' ]); + + if (closeEvents.length === 1) { + deepEqual(openEvents, ['test1']); + deepEqual(dataEvents, [{ name: 'test1', data: 'write1' }]); + deepEqual(closeEvents, ['test1']); + } else if (closeEvents.length === 2) { + deepEqual(openEvents, ['test1', 'test2']); + deepEqual(dataEvents, [{ name: 'test1', data: 'write1' }, { name: 'test2', data: 'write2' }]); + deepEqual(closeEvents, ['test1', 'test2']); + reg1.dispose(); reg2.dispose(); reg3.dispose(); @@ -192,22 +206,30 @@ suite('window namespace tests', () => { window.createTerminal({ name: 'test1', pty: { onDidWrite: term1Write.event, onDidClose: term1Close.event, - open: () => { + open: async () => { term1Write.fire('write1'); + + // Wait until the data is written + await new Promise(resolve => { resolveOnceDataWritten = resolve; }); term1Close.fire(); + const term2Write = new EventEmitter(); const term2Close = new EventEmitter(); window.createTerminal({ name: 'test2', pty: { onDidWrite: term2Write.event, onDidClose: term2Close.event, - open: () => { + open: async () => { term2Write.fire('write2'); + + // Wait until the data is written + await new Promise(resolve => { resolveOnceDataWritten = resolve; }); + term2Close.fire(); }, - close: () => {} + close: () => { } }}); }, - close: () => {} + close: () => { } }}); }); }); @@ -225,8 +247,8 @@ suite('window namespace tests', () => { }); const pty: Pseudoterminal = { onDidWrite: new EventEmitter().event, - open: () => {}, - close: () => {} + open: () => { }, + close: () => { } }; window.createTerminal({ name: 'c', pty }); }); @@ -303,7 +325,7 @@ suite('window namespace tests', () => { open: () => { overrideDimensionsEmitter.fire({ columns: 10, rows: 5 }); }, - close: () => {} + close: () => { } }; const terminal = window.createTerminal({ name: 'foo', pty }); }); diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index cce3bcfa8cc..8344b323c7e 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -12,6 +12,7 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { ITerminalInstanceService, ITerminalService, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { @@ -327,16 +328,23 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape * listeners are removed. */ class TerminalDataEventTracker extends Disposable { + private readonly _bufferer: TerminalDataBufferer; + constructor( private readonly _callback: (id: number, data: string) => void, @ITerminalService private readonly _terminalService: ITerminalService ) { super(); + + this._register(this._bufferer = new TerminalDataBufferer()); + this._terminalService.terminalInstances.forEach(instance => this._registerInstance(instance)); this._register(this._terminalService.onInstanceCreated(instance => this._registerInstance(instance))); + this._register(this._terminalService.onInstanceDisposed(instance => this._bufferer.stopBuffering(instance.id))); } private _registerInstance(instance: ITerminalInstance): void { - this._register(instance.onData(e => this._callback(instance.id, e))); + // Buffer data events to reduce the amount of messages going to the extension host + this._register(this._bufferer.startBuffering(instance.id, instance.onData, this._callback)); } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 368f057d8d8..16a1cafd977 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -12,6 +12,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { EXT_HOST_CREATION_DELAY, ITerminalChildProcess, ITerminalDimensions } from 'vs/workbench/contrib/terminal/common/terminal'; import { timeout } from 'vs/base/common/async'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape { @@ -286,9 +287,13 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ protected readonly _onDidWriteTerminalData: Emitter; public get onDidWriteTerminalData(): Event { return this._onDidWriteTerminalData && this._onDidWriteTerminalData.event; } + private readonly _bufferer: TerminalDataBufferer; + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService ) { + this._bufferer = new TerminalDataBufferer(); + this._proxy = extHostRpc.getProxy(MainContext.MainThreadTerminalService); this._onDidWriteTerminalData = new Emitter({ onFirstListenerAdd: () => this._proxy.$startSendingDataEvents(), @@ -464,8 +469,11 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ protected _setupExtHostProcessListeners(id: number, p: ITerminalChildProcess): void { p.onProcessReady((e: { pid: number, cwd: string }) => this._proxy.$sendProcessReady(id, e.pid, e.cwd)); p.onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title)); - p.onProcessData(data => this._proxy.$sendProcessData(id, data)); + + // Buffer data events to reduce the amount of messages going to the renderer + this._bufferer.startBuffering(id, p.onProcessData, this._proxy.$sendProcessData); p.onProcessExit(exitCode => this._onProcessExit(id, exitCode)); + if (p.onProcessOverrideDimensions) { p.onProcessOverrideDimensions(e => this._proxy.$sendOverrideDimensions(id, e)); } @@ -504,6 +512,8 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ } private _onProcessExit(id: number, exitCode: number): void { + this._bufferer.stopBuffering(id); + // Remove process reference delete this._terminalProcesses[id]; diff --git a/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts b/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts new file mode 100644 index 00000000000..8b2008bf29b --- /dev/null +++ b/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +interface TerminalDataBuffer extends IDisposable { + data: string[]; + timeoutId: any; +} + +export class TerminalDataBufferer implements IDisposable { + private readonly _terminalBufferMap = new Map(); + + dispose() { + for (const buffer of this._terminalBufferMap.values()) { + buffer.dispose(); + } + } + + startBuffering(id: number, event: Event, callback: (id: number, data: string) => void, throttleBy: number = 5): IDisposable { + let disposable: IDisposable; + disposable = event((e: string) => { + let buffer = this._terminalBufferMap.get(id); + if (buffer) { + buffer.data.push(e); + + return; + } + + const timeoutId = setTimeout(() => { + this._terminalBufferMap.delete(id); + callback(id, buffer!.data.join('')); + }, throttleBy); + buffer = { + data: [e], + timeoutId: timeoutId, + dispose: () => { + clearTimeout(timeoutId); + this._terminalBufferMap.delete(id); + disposable.dispose(); + } + }; + this._terminalBufferMap.set(id, buffer); + }); + return disposable; + } + + stopBuffering(id: number) { + const buffer = this._terminalBufferMap.get(id); + if (buffer) { + buffer.dispose(); + } + } +} diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts new file mode 100644 index 00000000000..54a2e4be0ab --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter } from 'vs/base/common/event'; +import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; + +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +suite('Workbench - TerminalDataBufferer', () => { + let bufferer: TerminalDataBufferer; + + setup(async () => { + bufferer = new TerminalDataBufferer(); + }); + + test('start', async () => { + let terminalOnData = new Emitter(); + let counter = 0; + let data: string | undefined; + + bufferer.startBuffering(1, terminalOnData.event, (id, e) => { + counter++; + data = e; + }, 0); + + terminalOnData.fire('1'); + terminalOnData.fire('2'); + terminalOnData.fire('3'); + + await wait(0); + + terminalOnData.fire('4'); + + assert.equal(counter, 1); + assert.equal(data, '123'); + + await wait(0); + + assert.equal(counter, 2); + assert.equal(data, '4'); + }); + + test('start 2', async () => { + let terminal1OnData = new Emitter(); + let terminal1Counter = 0; + let terminal1Data: string | undefined; + + bufferer.startBuffering(1, terminal1OnData.event, (id, e) => { + terminal1Counter++; + terminal1Data = e; + }, 0); + + let terminal2OnData = new Emitter(); + let terminal2Counter = 0; + let terminal2Data: string | undefined; + + bufferer.startBuffering(2, terminal2OnData.event, (id, e) => { + terminal2Counter++; + terminal2Data = e; + }, 0); + + terminal1OnData.fire('1'); + terminal2OnData.fire('4'); + terminal1OnData.fire('2'); + terminal2OnData.fire('5'); + terminal1OnData.fire('3'); + terminal2OnData.fire('6'); + terminal2OnData.fire('7'); + + assert.equal(terminal1Counter, 0); + assert.equal(terminal1Data, undefined); + assert.equal(terminal2Counter, 0); + assert.equal(terminal2Data, undefined); + + await wait(0); + + assert.equal(terminal1Counter, 1); + assert.equal(terminal1Data, '123'); + assert.equal(terminal2Counter, 1); + assert.equal(terminal2Data, '4567'); + }); + + test('stop', async () => { + let terminalOnData = new Emitter(); + let counter = 0; + let data: string | undefined; + + bufferer.startBuffering(1, terminalOnData.event, (id, e) => { + counter++; + data = e; + }, 0); + + terminalOnData.fire('1'); + terminalOnData.fire('2'); + terminalOnData.fire('3'); + + bufferer.stopBuffering(1); + + await wait(0); + + assert.equal(counter, 0); + assert.equal(data, undefined); + }); + + test('start 2 stop 1', async () => { + let terminal1OnData = new Emitter(); + let terminal1Counter = 0; + let terminal1Data: string | undefined; + + bufferer.startBuffering(1, terminal1OnData.event, (id, e) => { + terminal1Counter++; + terminal1Data = e; + }, 0); + + let terminal2OnData = new Emitter(); + let terminal2Counter = 0; + let terminal2Data: string | undefined; + + bufferer.startBuffering(2, terminal2OnData.event, (id, e) => { + terminal2Counter++; + terminal2Data = e; + }, 0); + + + terminal1OnData.fire('1'); + terminal2OnData.fire('4'); + terminal1OnData.fire('2'); + terminal2OnData.fire('5'); + terminal1OnData.fire('3'); + terminal2OnData.fire('6'); + terminal2OnData.fire('7'); + + assert.equal(terminal1Counter, 0); + assert.equal(terminal1Data, undefined); + assert.equal(terminal2Counter, 0); + assert.equal(terminal2Data, undefined); + + bufferer.stopBuffering(1); + await wait(0); + + assert.equal(terminal1Counter, 0); + assert.equal(terminal1Data, undefined); + assert.equal(terminal2Counter, 1); + assert.equal(terminal2Data, '4567'); + }); + + test('dispose', async () => { + let terminal1OnData = new Emitter(); + let terminal1Counter = 0; + let terminal1Data: string | undefined; + + bufferer.startBuffering(1, terminal1OnData.event, (id, e) => { + terminal1Counter++; + terminal1Data = e; + }, 0); + + let terminal2OnData = new Emitter(); + let terminal2Counter = 0; + let terminal2Data: string | undefined; + + bufferer.startBuffering(2, terminal2OnData.event, (id, e) => { + terminal2Counter++; + terminal2Data = e; + }, 0); + + + terminal1OnData.fire('1'); + terminal2OnData.fire('4'); + terminal1OnData.fire('2'); + terminal2OnData.fire('5'); + terminal1OnData.fire('3'); + terminal2OnData.fire('6'); + terminal2OnData.fire('7'); + + assert.equal(terminal1Counter, 0); + assert.equal(terminal1Data, undefined); + assert.equal(terminal2Counter, 0); + assert.equal(terminal2Data, undefined); + + bufferer.dispose(); + await wait(0); + + assert.equal(terminal1Counter, 0); + assert.equal(terminal1Data, undefined); + assert.equal(terminal2Counter, 0); + assert.equal(terminal2Data, undefined); + }); +});