diff --git a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts index e604726676759f43453208fc0c734a15f840a1a8..50c0516109ba817a917eef8299033244fe1420cc 100644 --- a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts +++ b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts @@ -2,7 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; + +import { ITextBufferBuilder, ITextBufferFactory, ITextBuffer, DefaultEndOfLine } from 'vs/editor/common/model'; +import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder'; +import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; export function doBenchmark(id: string, ts: T[], fn: (t: T) => void) { let columns: string[] = [id]; @@ -13,4 +16,55 @@ export function doBenchmark(id: string, ts: T[], fn: (t: T) => void) { columns.push(`${(diff[0] * 1000 + diff[1] / 1000000).toFixed(3)} ms`); } console.log('|' + columns.join('\t|') + '|'); -} \ No newline at end of file +} + +export interface IBenchmark { + name: string; + /** + * Before each cycle, this function will be called to create TextBufferFactory + */ + buildBuffer: (textBufferBuilder: ITextBufferBuilder) => ITextBufferFactory; + /** + * Before each cycle, this function will be called to do pre-work for text buffer. + * This will be called onece `buildBuffer` is finished. + */ + preCycle: (textBuffer: ITextBuffer) => void; + /** + * The function we are benchmarking + */ + fn: (textBuffer: ITextBuffer) => void; +} + +export class BenchmarkSuite { + name: string; + benchmarks: IBenchmark[]; + + constructor(suiteOptions: { name: string }) { + this.name = suiteOptions.name; + this.benchmarks = []; + } + + add(benchmark: IBenchmark) { + this.benchmarks.push(benchmark); + } + + run() { + console.log(`|${this.name}\t|line buffer\t|piece table\t|`); + console.log('|---|---|---|'); + for (let i = 0; i < this.benchmarks.length; i++) { + let benchmark = this.benchmarks[i]; + let columns: string[] = [benchmark.name]; + [new LinesTextBufferBuilder(), new PieceTreeTextBufferBuilder()].forEach((builder: ITextBufferBuilder) => { + let factory = benchmark.buildBuffer(builder); + let buffer = factory.create(DefaultEndOfLine.LF); + benchmark.preCycle(buffer); + var start = process.hrtime(); + benchmark.fn(buffer); + var diff = process.hrtime(start); + columns.push(`${(diff[0] * 1000 + diff[1] / 1000000).toFixed(3)} ms`); + }); + console.log('|' + columns.join('\t|') + '|'); + } + console.log('\n'); + } +} diff --git a/src/vs/editor/test/common/model/benchmark/operations.benchmark.ts b/src/vs/editor/test/common/model/benchmark/operations.benchmark.ts index 53d8754b04c7d0957e5be2e37d76a6c3631a8f5b..02c7e90958e52d44b6a871a0d4ad6a8ce6991010 100644 --- a/src/vs/editor/test/common/model/benchmark/operations.benchmark.ts +++ b/src/vs/editor/test/common/model/benchmark/operations.benchmark.ts @@ -2,45 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; -import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder'; -import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { ITextBuffer, IIdentifiedSingleEditOperation, EndOfLinePreference } from 'vs/editor/common/model'; -import { generateRandomEdits, createMockBuffer, createMockText, generateSequentialInserts } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils'; +import { ITextBufferBuilder, EndOfLinePreference } from 'vs/editor/common/model'; +import { BenchmarkSuite } from 'vs/editor/test/common/model/benchmark/benchmarkUtils'; +import { generateRandomChunkWithLF, generateRandomEdits, generateSequentialInserts, getRandomInt } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils'; import { Range } from 'vs/editor/common/core/range'; -import { doBenchmark } from 'vs/editor/test/common/model/benchmark/benchmarkUtils'; -let readLinesBenchmark = function (id: string, buffers: ITextBuffer[]) { - doBenchmark(id, buffers, (buffer) => { - for (let j = 0, len = buffer.getLineCount(); j < len; j++) { - var str = buffer.getLineContent(j + 1); - let firstChar = str.charCodeAt(0); - let lastChar = str.charCodeAt(str.length - 1); - firstChar = firstChar - lastChar; - lastChar = firstChar + lastChar; - firstChar = lastChar - firstChar; - } - }); -}; - -let appyEditsBenchmark = function (id: string, buffers: ITextBuffer[], edits: IIdentifiedSingleEditOperation[]) { - doBenchmark(id, buffers, (buffer) => { - for (let j = 0; j < edits.length; j++) { - buffer.applyEdits([edits[j]], false); - } - }); -}; - -let getValueBenchmark = function (id: string, buffers: ITextBuffer[], eol: EndOfLinePreference = EndOfLinePreference.LF): void { - doBenchmark(id, buffers, (buffer) => { - const lineCount = buffer.getLineCount(); - const fullModelRange = new Range(1, 1, lineCount, buffer.getLineLength(lineCount) + 1); - buffer.getValueInRange(fullModelRange, eol); - }); -}; - -let suites = [ +let fileSizes = [1, 1000, 64 * 1000, 32 * 1000 * 1000]; +let editTypes = [ { id: 'random edits', generateEdits: generateRandomEdits @@ -51,18 +20,119 @@ let suites = [ } ]; -let text = createMockText(1000, 0, 50); +for (let fileSize of fileSizes) { + let chunks = []; + + let chunkCnt = Math.floor(fileSize / (64 * 1000)); + if (chunkCnt === 0) { + chunks.push(generateRandomChunkWithLF(fileSize, fileSize)); + } else { + let chunk = generateRandomChunkWithLF(64 * 1000, 64 * 1000); + // try to avoid OOM + for (let j = 0; j < chunkCnt; j++) { + chunks.push(Buffer.from(chunk + j).toString()); + } + } + + for (let editType of editTypes) { + const edits = editType.generateEdits(chunks, 1000); + + let editsSuite = new BenchmarkSuite({ + name: `File Size: ${fileSize}Byte, ${editType.id}`, + }); + + for (let i of [10, 100, 1000]) { + editsSuite.add({ + name: `apply ${i} edits`, + buildBuffer: (textBufferBuilder: ITextBufferBuilder) => { + chunks.forEach(ck => textBufferBuilder.acceptChunk(ck)); + return textBufferBuilder.finish(); + }, + preCycle: (textBuffer) => { + return textBuffer; + }, + fn: (textBuffer) => { + // for line model, this loop doesn't reflect the real situation. + for (let k = 0; k < edits.length && k < i; k++) { + textBuffer.applyEdits([edits[k]], false); + } + } + }); -for (let i = 0, len = suites.length; i < len; i++) { - console.log(`\n|${suites[i].id}\t|line buffer\t|piece table\t|`); - console.log('|---|---|---|'); - for (let j of [10, 100, 1000]) { - let linesTextBuffer = createMockBuffer(text, new LinesTextBufferBuilder()); - let pieceTreeTextBuffer = createMockBuffer(text, new PieceTreeTextBufferBuilder()); - let edits = suites[i].generateEdits(text, j); + editsSuite.add({ + name: `Read all lines after ${i} edits`, + buildBuffer: (textBufferBuilder: ITextBufferBuilder) => { + chunks.forEach(ck => textBufferBuilder.acceptChunk(ck)); + return textBufferBuilder.finish(); + }, + preCycle: (textBuffer) => { + for (let k = 0; k < edits.length && k < i; k++) { + textBuffer.applyEdits([edits[k]], false); + } + return textBuffer; + }, + fn: (textBuffer) => { + for (let j = 0, len = textBuffer.getLineCount(); j < len; j++) { + var str = textBuffer.getLineContent(j + 1); + let firstChar = str.charCodeAt(0); + let lastChar = str.charCodeAt(str.length - 1); + firstChar = firstChar - lastChar; + lastChar = firstChar + lastChar; + firstChar = lastChar - firstChar; + } + } + }); + + editsSuite.add({ + name: `Read 10 random windows after ${i} edits`, + buildBuffer: (textBufferBuilder: ITextBufferBuilder) => { + chunks.forEach(ck => textBufferBuilder.acceptChunk(ck)); + return textBufferBuilder.finish(); + }, + preCycle: (textBuffer) => { + for (let k = 0; k < edits.length && k < i; k++) { + textBuffer.applyEdits([edits[k]], false); + } + return textBuffer; + }, + fn: (textBuffer) => { + for (let i = 0; i < 10; i++) { + let minLine = 1; + let maxLine = textBuffer.getLineCount(); + let startLine = getRandomInt(minLine, Math.max(minLine, maxLine - 100)); + let endLine = Math.min(maxLine, startLine + 100); + for (let j = startLine; j < endLine; j++) { + var str = textBuffer.getLineContent(j + 1); + let firstChar = str.charCodeAt(0); + let lastChar = str.charCodeAt(str.length - 1); + firstChar = firstChar - lastChar; + lastChar = firstChar + lastChar; + firstChar = lastChar - firstChar; + } + } + } + }); + + editsSuite.add({ + name: `save file after ${i} edits`, + buildBuffer: (textBufferBuilder: ITextBufferBuilder) => { + chunks.forEach(ck => textBufferBuilder.acceptChunk(ck)); + return textBufferBuilder.finish(); + }, + preCycle: (textBuffer) => { + for (let k = 0; k < edits.length && k < i; k++) { + textBuffer.applyEdits([edits[k]], false); + } + return textBuffer; + }, + fn: (textBuffer) => { + const lineCount = textBuffer.getLineCount(); + const fullModelRange = new Range(1, 1, lineCount, textBuffer.getLineLength(lineCount) + 1); + textBuffer.getValueInRange(fullModelRange, EndOfLinePreference.LF); + } + }); + } - appyEditsBenchmark(`apply ${j} edits`, [linesTextBuffer, pieceTreeTextBuffer], edits); - readLinesBenchmark(`getLineContent after ${j} edits`, [linesTextBuffer, pieceTreeTextBuffer]); - getValueBenchmark(`save after ${j} edits`, [linesTextBuffer, pieceTreeTextBuffer]); + editsSuite.run(); } } \ No newline at end of file diff --git a/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts b/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts index e8131268dc1bbddc15890d10aa874d2313052868..b805c477637e5bbe3fea2764382d19f8dfadde5f 100644 --- a/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts +++ b/src/vs/editor/test/common/model/benchmark/searchNReplace.benchmark.ts @@ -4,25 +4,47 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder'; -import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { IIdentifiedSingleEditOperation, ITextBuffer } from 'vs/editor/common/model'; -import { createMockText, createMockBuffer, generateRandomReplaces } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils'; -import { doBenchmark } from 'vs/editor/test/common/model/benchmark/benchmarkUtils'; - -let appyEditsBenchmark = function (id: string, buffers: ITextBuffer[], edits: IIdentifiedSingleEditOperation[]) { - doBenchmark(id, buffers, buffer => { - buffer.applyEdits(edits, false); +import { ITextBufferBuilder } from 'vs/editor/common/model'; +import { generateRandomReplaces, generateRandomChunkWithLF } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils'; +import { BenchmarkSuite } from 'vs/editor/test/common/model/benchmark/benchmarkUtils'; + +let fileSizes = [1, 1000, 64 * 1000, 32 * 1000 * 1000]; + +for (let fileSize of fileSizes) { + let chunks = []; + + let chunkCnt = Math.floor(fileSize / (64 * 1000)); + if (chunkCnt === 0) { + chunks.push(generateRandomChunkWithLF(fileSize, fileSize)); + } else { + let chunk = generateRandomChunkWithLF(64 * 1000, 64 * 1000); + // try to avoid OOM + for (let j = 0; j < chunkCnt; j++) { + chunks.push(Buffer.from(chunk + j).toString()); + } + } + + let replaceSuite = new BenchmarkSuite({ + name: `File Size: ${fileSize}Byte`, }); -}; -let text = createMockText(1000, 50, 100); + let edits = generateRandomReplaces(chunks, 500, 5, 10); + + for (let i of [10, 100, 500]) { + replaceSuite.add({ + name: `replace ${i} occurrences`, + buildBuffer: (textBufferBuilder: ITextBufferBuilder) => { + chunks.forEach(ck => textBufferBuilder.acceptChunk(ck)); + return textBufferBuilder.finish(); + }, + preCycle: (textBuffer) => { + return textBuffer; + }, + fn: (textBuffer) => { + textBuffer.applyEdits(edits.slice(0, i), false); + } + }); + } -console.log(`\n|replace all\t|line buffer\t|piece table\t|`); -console.log('|---|---|---|'); -for (let i of [10, 100, 500, 1000]) { - let linesTextBuffer = createMockBuffer(text, new LinesTextBufferBuilder()); - let pieceTreeTextBuffer = createMockBuffer(text, new PieceTreeTextBufferBuilder()); - let edits = generateRandomReplaces(text, i, 5, 10); - appyEditsBenchmark(`replace ${i} occurrences`, [linesTextBuffer, pieceTreeTextBuffer], edits); + replaceSuite.run(); } \ No newline at end of file diff --git a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts index f5c1e69c424227793674f929dcdfdeec0a92495d..b8ea951b3184ed299ece802d8e3dea33c5c70bcd 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts @@ -32,14 +32,24 @@ export function getRandomString(minLength: number, maxLength: number): string { return r; } -export function generateRandomEdits(str: string, editCnt: number): IIdentifiedSingleEditOperation[] { - let lines = str.split(/\r\n|\r|\n/); +export function generateRandomEdits(chunks: string[], editCnt: number): IIdentifiedSingleEditOperation[] { + let lines = []; + for (let i = 0; i < chunks.length; i++) { + let newLines = chunks[i].split(/\r\n|\r|\n/); + if (lines.length === 0) { + lines.push(...newLines); + } else { + newLines[0] = lines[lines.length - 1] + newLines[0]; + lines.splice(lines.length - 1, 1, ...newLines); + } + } + let ops: IIdentifiedSingleEditOperation[] = []; for (let i = 0; i < editCnt; i++) { let line = getRandomInt(1, lines.length); - let startColumn = getRandomInt(1, lines[line - 1].length + 1); - let endColumn = getRandomInt(startColumn, lines[line - 1].length + 1); + let startColumn = getRandomInt(1, Math.max(lines[line - 1].length, 1)); + let endColumn = getRandomInt(startColumn, Math.max(lines[line - 1].length, startColumn)); let text: string = ''; if (Math.random() < .5) { text = getRandomString(5, 10); @@ -55,8 +65,18 @@ export function generateRandomEdits(str: string, editCnt: number): IIdentifiedSi return ops; } -export function generateSequentialInserts(str: string, editCnt: number): IIdentifiedSingleEditOperation[] { - let lines = str.split(/\r\n|\r|\n/); +export function generateSequentialInserts(chunks: string[], editCnt: number): IIdentifiedSingleEditOperation[] { + let lines = []; + for (let i = 0; i < chunks.length; i++) { + let newLines = chunks[i].split(/\r\n|\r|\n/); + if (lines.length === 0) { + lines.push(...newLines); + } else { + newLines[0] = lines[lines.length - 1] + newLines[0]; + lines.splice(lines.length - 1, 1, ...newLines); + } + } + let ops: IIdentifiedSingleEditOperation[] = []; for (let i = 0; i < editCnt; i++) { @@ -67,7 +87,7 @@ export function generateSequentialInserts(str: string, editCnt: number): IIdenti text = '\n'; lines.push(''); } else { - text = getRandomString(5, 10); + text = getRandomString(1, 2); lines[line - 1] += text; } @@ -80,8 +100,18 @@ export function generateSequentialInserts(str: string, editCnt: number): IIdenti return ops; } -export function generateRandomReplaces(str: string, editCnt: number, searchStringLen: number, replaceStringLen: number): IIdentifiedSingleEditOperation[] { - let lines = str.split(/\r\n|\r|\n/); +export function generateRandomReplaces(chunks: string[], editCnt: number, searchStringLen: number, replaceStringLen: number): IIdentifiedSingleEditOperation[] { + let lines = []; + for (let i = 0; i < chunks.length; i++) { + let newLines = chunks[i].split(/\r\n|\r|\n/); + if (lines.length === 0) { + lines.push(...newLines); + } else { + newLines[0] = lines[lines.length - 1] + newLines[0]; + lines.splice(lines.length - 1, 1, ...newLines); + } + } + let ops: IIdentifiedSingleEditOperation[] = []; let chunkSize = Math.max(1, Math.floor(lines.length / editCnt)); let chunkCnt = Math.floor(lines.length / chunkSize);