From aca643ec36da0f253793268a6ff301dc92e3df11 Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 21 Mar 2017 16:17:28 +0100 Subject: [PATCH] with @jrieken use BufferedContent to optimize output appending fixes #22804 --- .../api/node/mainThreadOutputService.ts | 2 +- .../parts/output/browser/outputActions.ts | 22 +-- .../parts/output/browser/outputServices.ts | 162 ++++++++++++------ .../workbench/parts/output/common/output.ts | 46 +++-- 4 files changed, 147 insertions(+), 85 deletions(-) diff --git a/src/vs/workbench/api/node/mainThreadOutputService.ts b/src/vs/workbench/api/node/mainThreadOutputService.ts index b0a32a6202d..6694cc7cd8f 100644 --- a/src/vs/workbench/api/node/mainThreadOutputService.ts +++ b/src/vs/workbench/api/node/mainThreadOutputService.ts @@ -43,7 +43,7 @@ export class MainThreadOutputService extends MainThreadOutputServiceShape { } private _getChannel(channelId: string, label: string): IOutputChannel { - if (Registry.as(Extensions.OutputChannels).getChannels().every(channel => channel.id !== channelId)) { + if (!Registry.as(Extensions.OutputChannels).getChannel(channelId)) { Registry.as(Extensions.OutputChannels).registerChannel(channelId, label); } diff --git a/src/vs/workbench/parts/output/browser/outputActions.ts b/src/vs/workbench/parts/output/browser/outputActions.ts index 5a0f7622cf3..ef225a8f426 100644 --- a/src/vs/workbench/parts/output/browser/outputActions.ts +++ b/src/vs/workbench/parts/output/browser/outputActions.ts @@ -107,9 +107,10 @@ export class SwitchOutputActionItem extends SelectActionItem { action: IAction, @IOutputService private outputService: IOutputService ) { - super(null, action, SwitchOutputActionItem.getChannelLabels(outputService), Math.max(0, SwitchOutputActionItem.getChannelLabels(outputService).indexOf(outputService.getActiveChannel().label))); - this.toDispose.push(this.outputService.onOutputChannel(this.onOutputChannel, this)); - this.toDispose.push(this.outputService.onActiveOutputChannel(this.onOutputChannel, this)); + super(null, action, [], 0); + this.toDispose.push(this.outputService.onOutputChannel(() => this.setOptions(this.getOptions(), this.getSelected(undefined)))); + this.toDispose.push(this.outputService.onActiveOutputChannel(activeChannelId => this.setOptions(this.getOptions(), this.getSelected(activeChannelId)))); + this.setOptions(this.getOptions(), this.getSelected(this.outputService.getActiveChannel().id)); } protected getActionContext(option: string): string { @@ -118,16 +119,15 @@ export class SwitchOutputActionItem extends SelectActionItem { return channel ? channel.id : option; } - private onOutputChannel(): void { - let channels = SwitchOutputActionItem.getChannelLabels(this.outputService); - let selected = Math.max(0, channels.indexOf(this.outputService.getActiveChannel().label)); - - this.setOptions(channels, selected); + private getOptions(): string[] { + return this.outputService.getChannels().map(c => c.label); } - private static getChannelLabels(outputService: IOutputService): string[] { - const contributedChannels = outputService.getChannels().map(channelData => channelData.label); + private getSelected(outputId: string): number { + if (!outputId) { + return undefined; + } - return contributedChannels.sort(); // sort by name + return Math.max(0, this.outputService.getChannels().map(c => c.id).indexOf(outputId)); } } diff --git a/src/vs/workbench/parts/output/browser/outputServices.ts b/src/vs/workbench/parts/output/browser/outputServices.ts index 4a584ff2f99..24a803cd2bc 100644 --- a/src/vs/workbench/parts/output/browser/outputServices.ts +++ b/src/vs/workbench/parts/output/browser/outputServices.ts @@ -6,6 +6,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import strings = require('vs/base/common/strings'); import Event, { Emitter } from 'vs/base/common/event'; +import { binarySearch } from 'vs/base/common/arrays'; import URI from 'vs/base/common/uri'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEditor } from 'vs/platform/editor/common/editor'; @@ -13,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/platform'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { IOutputChannelIdentifier, OutputEditors, IOutputEvent, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, MAX_OUTPUT_LENGTH, OUTPUT_SCHEME, OUTPUT_MIME } from 'vs/workbench/parts/output/common/output'; +import { IOutputChannelIdentifier, OutputEditors, IOutputEvent, IOutputChannel, IOutputService, IOutputDelta, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, MAX_OUTPUT_LENGTH, OUTPUT_SCHEME, OUTPUT_MIME } from 'vs/workbench/parts/output/common/output'; import { OutputPanel } from 'vs/workbench/parts/output/browser/outputPanel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -28,11 +29,61 @@ import { Position } from 'vs/editor/common/core/position'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; +class BufferedContent { + + private data: string[] = []; + private dataIds: number[] = []; + private idPool = 0; + private length = 0; + + public append(content: string): void { + this.data.push(content); + this.dataIds.push(++this.idPool); + this.length += content.length; + this.trim(); + } + + public clear(): void { + this.data.length = 0; + this.dataIds.length = 0; + this.length = 0; + } + + private trim(): void { + if (this.length < MAX_OUTPUT_LENGTH * 1.5) { + return; + } + + while (this.length > MAX_OUTPUT_LENGTH && this.data.length) { + this.dataIds.shift(); + const removed = this.data.shift(); + this.length -= removed.length; + } + } + + public value(previousDelta?: IOutputDelta): IOutputDelta { + let idx = -1; + if (previousDelta) { + idx = binarySearch(this.dataIds, previousDelta.id, (a, b) => a - b); + } + + const id = this.idPool; + if (idx >= 0) { + const value = strings.removeAnsiEscapeCodes(this.data.slice(idx).join('')); + return { value, id, append: true }; + } else { + const value = strings.removeAnsiEscapeCodes(this.data.join('')); + return { value, id }; + } + } +} + export class OutputService implements IOutputService { public _serviceBrand: any; - private receivedOutput: Map; + private receivedOutput: Map = new Map(); + private channels: Map = new Map(); private activeChannelId: string; @@ -56,8 +107,6 @@ export class OutputService implements IOutputService { this._onOutputChannel = new Emitter(); this._onActiveOutputChannel = new Emitter(); - this.receivedOutput = new Map(); - const channels = this.getChannels(); this.activeChannelId = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, channels && channels.length > 0 ? channels[0].id : null); @@ -82,26 +131,30 @@ export class OutputService implements IOutputService { } public getChannel(id: string): IOutputChannel { - const channelData = this.getChannels().filter(channelData => channelData.id === id).pop(); + if (!this.channels.has(id)) { + const channelData = Registry.as(Extensions.OutputChannels).getChannel(id); - const self = this; - return { - id, - label: channelData ? channelData.label : id, - get output() { - return self.getOutput(id); - }, - get scrollLock() { - return self._outputContentProvider.scrollLock(id); - }, - set scrollLock(value: boolean) { - self._outputContentProvider.setScrollLock(id, value); - }, - append: (output: string) => this.append(id, output), - show: (preserveFocus: boolean) => this.showOutput(id, preserveFocus), - clear: () => this.clearOutput(id), - dispose: () => this.removeOutput(id) - }; + const self = this; + this.channels.set(id, { + id, + label: channelData ? channelData.label : id, + getOutput(before?: IOutputDelta) { + return self.getOutput(id, before); + }, + get scrollLock() { + return self._outputContentProvider.scrollLock(id); + }, + set scrollLock(value: boolean) { + self._outputContentProvider.setScrollLock(id, value); + }, + append: (output: string) => this.append(id, output), + show: (preserveFocus: boolean) => this.showOutput(id, preserveFocus), + clear: () => this.clearOutput(id), + dispose: () => this.removeOutput(id) + }); + } + + return this.channels.get(id); } public getChannels(): IOutputChannelIdentifier[] { @@ -112,34 +165,37 @@ export class OutputService implements IOutputService { // Initialize if (!this.receivedOutput.has(channelId)) { - this.receivedOutput.set(channelId, ''); + this.receivedOutput.set(channelId, new BufferedContent()); this._onOutputChannel.fire(channelId); // emit event that we have a new channel } - // Sanitize - output = strings.removeAnsiEscapeCodes(output); - // Store if (output) { - this.receivedOutput.set(channelId, strings.appendWithLimit(this.receivedOutput.get(channelId), output, MAX_OUTPUT_LENGTH)); + const channel = this.receivedOutput.get(channelId); + channel.append(output); } - this._onOutput.fire({ output: output, channelId: channelId }); + this._onOutput.fire({ channelId: channelId, isClear: false }); } public getActiveChannel(): IOutputChannel { return this.getChannel(this.activeChannelId); } - private getOutput(channelId: string): string { - return this.receivedOutput.get(channelId) || ''; + private getOutput(channelId: string, before: IOutputDelta): IOutputDelta { + if (this.receivedOutput.has(channelId)) { + return this.receivedOutput.get(channelId).value(before); + } + + return undefined; } private clearOutput(channelId: string): void { - this.receivedOutput.set(channelId, ''); - - this._onOutput.fire({ channelId: channelId, output: null /* indicator to clear output */ }); + if (this.receivedOutput.has(channelId)) { + this.receivedOutput.get(channelId).clear(); + this._onOutput.fire({ channelId: channelId, isClear: true }); + } } private removeOutput(channelId: string): void { @@ -179,10 +235,9 @@ class OutputContentProvider implements ITextModelContentProvider { private static OUTPUT_DELAY = 300; - private bufferedOutput: { [channel: string]: string; }; + private bufferedOutput = new Map(); private appendOutputScheduler: { [channel: string]: RunOnceScheduler; }; private channelIdsWithScrollLock: Set = new Set(); - private toDispose: IDisposable[]; constructor( @@ -191,7 +246,6 @@ class OutputContentProvider implements ITextModelContentProvider { @IModeService private modeService: IModeService, @IPanelService private panelService: IPanelService ) { - this.bufferedOutput = Object.create(null); this.appendOutputScheduler = Object.create(null); this.toDispose = []; @@ -215,15 +269,10 @@ class OutputContentProvider implements ITextModelContentProvider { } // Append to model - if (e.output) { - this.bufferedOutput[e.channelId] = strings.appendWithLimit(this.bufferedOutput[e.channelId] || '', e.output, MAX_OUTPUT_LENGTH); - this.scheduleOutputAppend(e.channelId); - } - - // Clear from model - else if (e.output === null) { - this.bufferedOutput[e.channelId] = ''; + if (e.isClear) { model.setValue(''); + } else { + this.scheduleOutputAppend(e.channelId); } } @@ -236,10 +285,6 @@ class OutputContentProvider implements ITextModelContentProvider { return; // only if the output channel is visible } - if (!this.bufferedOutput[channel]) { - return; // only if we have any output to show - } - let scheduler = this.appendOutputScheduler[channel]; if (!scheduler) { scheduler = new RunOnceScheduler(() => { @@ -274,15 +319,17 @@ class OutputContentProvider implements ITextModelContentProvider { return; // only react if we have a known model } - const bufferedOutput = this.bufferedOutput[channel]; - this.bufferedOutput[channel] = ''; - if (!bufferedOutput) { - return; // return if nothing to append + const bufferedOutput = this.bufferedOutput.get(channel); + const newOutput = this.outputService.getChannel(channel).getOutput(bufferedOutput); + if (!newOutput) { + model.setValue(''); + return; } + this.bufferedOutput.set(channel, newOutput); // just fill in the full (trimmed) output if we exceed max length - if (model.getValueLength() + bufferedOutput.length > MAX_OUTPUT_LENGTH) { - model.setValue(this.outputService.getChannel(channel).output); + if (!newOutput.append) { + model.setValue(newOutput.value); } // otherwise append @@ -290,7 +337,7 @@ class OutputContentProvider implements ITextModelContentProvider { const lastLine = model.getLineCount(); const lastLineMaxColumn = model.getLineMaxColumn(lastLine); - model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), bufferedOutput)]); + model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), newOutput.value)]); } if (!this.channelIdsWithScrollLock.has(channel)) { @@ -319,7 +366,8 @@ class OutputContentProvider implements ITextModelContentProvider { } public provideTextContent(resource: URI): TPromise { - const content = this.outputService.getChannel(resource.fsPath).output; + const output = this.outputService.getChannel(resource.fsPath).getOutput(); + const content = output ? output.value : ''; let codeEditorModel = this.modelService.getModel(resource); if (!codeEditorModel) { diff --git a/src/vs/workbench/parts/output/common/output.ts b/src/vs/workbench/parts/output/common/output.ts index 7b67e67ec1a..2b2a798b09b 100644 --- a/src/vs/workbench/parts/output/common/output.ts +++ b/src/vs/workbench/parts/output/common/output.ts @@ -48,8 +48,8 @@ export const CONTEXT_IN_OUTPUT = new RawContextKey('inOutput', false); * The output event informs when new output got received. */ export interface IOutputEvent { - output: string; - channelId?: string; + channelId: string; + isClear: boolean; } export const IOutputService = createDecorator(OUTPUT_SERVICE_ID); @@ -93,6 +93,12 @@ export interface IOutputService { onActiveOutputChannel: Event; } +export interface IOutputDelta { + readonly value: string; + readonly id: number; + readonly append?: boolean; +} + export interface IOutputChannel { /** @@ -105,11 +111,6 @@ export interface IOutputChannel { */ label: string; - /** - * Returns the received output content. - */ - output: string; - /** * Returns the value indicating whether the channel has scroll locked. */ @@ -120,6 +121,12 @@ export interface IOutputChannel { */ append(output: string): void; + /** + * Returns the received output content. + * If a delta is passed, returns only the content that came after the passed delta. + */ + getOutput(previousDelta?: IOutputDelta): IOutputDelta; + /** * Opens the output for this channel. */ @@ -153,6 +160,11 @@ export interface IOutputChannelRegistry { */ getChannels(): IOutputChannelIdentifier[]; + /** + * Returns the channel with the passed id. + */ + getChannel(id: string): IOutputChannelIdentifier; + /** * Remove the output channel with the passed id. */ @@ -160,24 +172,26 @@ export interface IOutputChannelRegistry { } class OutputChannelRegistry implements IOutputChannelRegistry { - private channels: IOutputChannelIdentifier[]; - - constructor() { - this.channels = []; - } + private channels = new Map(); public registerChannel(id: string, label: string): void { - if (this.channels.every(channel => channel.id !== id)) { - this.channels.push({ id, label }); + if (!this.channels.has(id)) { + this.channels.set(id, { id, label }); } } public getChannels(): IOutputChannelIdentifier[] { - return this.channels; + const result: IOutputChannelIdentifier[] = []; + this.channels.forEach(value => result.push(value)); + return result; + } + + public getChannel(id: string): IOutputChannelIdentifier { + return this.channels.get(id); } public removeChannel(id: string): void { - this.channels = this.channels.filter(channel => channel.id !== id); + this.channels.delete(id); } }