From 8a912886340b92116abb7e357fe36f873f135460 Mon Sep 17 00:00:00 2001 From: chrmarti Date: Mon, 18 Jul 2016 10:07:58 -0700 Subject: [PATCH] #55: Batch results for IPC (#9380) --- src/vs/base/parts/ipc/test/node/ipc.perf.ts | 122 ++++++++++++++++++ .../base/parts/ipc/test/node/testService.ts | 37 +++++- .../services/search/node/rawSearchService.ts | 22 +++- .../workbench/services/search/node/search.ts | 5 +- .../services/search/node/searchService.ts | 38 ++++-- .../search/test/node/searchService.test.ts | 101 +++++++++++++++ 6 files changed, 305 insertions(+), 20 deletions(-) create mode 100644 src/vs/base/parts/ipc/test/node/ipc.perf.ts create mode 100644 src/vs/workbench/services/search/test/node/searchService.test.ts diff --git a/src/vs/base/parts/ipc/test/node/ipc.perf.ts b/src/vs/base/parts/ipc/test/node/ipc.perf.ts new file mode 100644 index 00000000000..2694f1ab0fa --- /dev/null +++ b/src/vs/base/parts/ipc/test/node/ipc.perf.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; +import uri from 'vs/base/common/uri'; +import { always } from 'vs/base/common/async'; +import { ITestChannel, TestServiceClient, ITestService } from './testService'; + +function createClient(): Client { + return new Client(uri.parse(require.toUrl('bootstrap')).fsPath, { + serverName: 'TestServer', + env: { AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true } + }); +} + +// Rename to ipc.perf.test.ts and run with ./scripts/test.sh --grep IPC.performance --timeout 60000 +suite('IPC performance', () => { + + test('increasing batch size', () => { + if (process.env['VSCODE_PID']) { + return; // TODO@Ben find out why test fails when run from within VS Code + } + + const client = createClient(); + const channel = client.getChannel('test'); + const service = new TestServiceClient(channel); + + const runs = [ + { batches: 250000, size: 1 }, + { batches: 2500, size: 100 }, + { batches: 500, size: 500 }, + { batches: 250, size: 1000 }, + { batches: 50, size: 5000 }, + { batches: 25, size: 10000 }, + // { batches: 10, size: 25000 }, + // { batches: 5, size: 50000 }, + // { batches: 1, size: 250000 }, + ]; + const dataSizes = [ + 100, + 250, + ]; + let i = 0, j = 0; + const result = measure(service, 10, 10, 250) // warm-up + .then(() => { + return (function nextRun() { + if (i >= runs.length) { + if (++j >= dataSizes.length) { + return; + } + i = 0; + } + const run = runs[i++]; + return measure(service, run.batches, run.size, dataSizes[j]) + .then(() => { + return nextRun(); + }); + })(); + }); + + return always(result, () => client.dispose()); + }); + + test('increasing raw data size', () => { + if (process.env['VSCODE_PID']) { + return; // TODO@Ben find out why test fails when run from within VS Code + } + + const client = createClient(); + const channel = client.getChannel('test'); + const service = new TestServiceClient(channel); + + const runs = [ + { batches: 250000, dataSize: 100 }, + { batches: 25000, dataSize: 1000 }, + { batches: 2500, dataSize: 10000 }, + { batches: 1250, dataSize: 20000 }, + { batches: 500, dataSize: 50000 }, + { batches: 250, dataSize: 100000 }, + { batches: 125, dataSize: 200000 }, + { batches: 50, dataSize: 500000 }, + { batches: 25, dataSize: 1000000 }, + ]; + let i = 0; + const result = measure(service, 10, 10, 250) // warm-up + .then(() => { + return (function nextRun() { + if (i >= runs.length) { + return; + } + const run = runs[i++]; + return measure(service, run.batches, 1, run.dataSize) + .then(() => { + return nextRun(); + }); + })(); + }); + + return always(result, () => client.dispose()); + }); + + function measure(service: ITestService, batches: number, size: number, dataSize: number) { + const start = Date.now(); + let hits = 0; + let count = 0; + return service.batchPerf(batches, size, dataSize) + .then(() => { + console.log(`Batches: ${batches}, size: ${size}, dataSize: ${dataSize}, n: ${batches * size * dataSize}, duration: ${Date.now() - start}`); + assert.strictEqual(hits, batches); + assert.strictEqual(count, batches * size); + }, err => assert.fail(err), + batch => { + hits++; + count += batch.length; + }); + } +}); \ No newline at end of file diff --git a/src/vs/base/parts/ipc/test/node/testService.ts b/src/vs/base/parts/ipc/test/node/testService.ts index f6c4a7235fc..1dd0def4f70 100644 --- a/src/vs/base/parts/ipc/test/node/testService.ts +++ b/src/vs/base/parts/ipc/test/node/testService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { TPromise, PPromise } from 'vs/base/common/winjs.base'; import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; import Event, { Emitter } from 'vs/base/common/event'; @@ -17,6 +17,7 @@ export interface ITestService { marco(): TPromise; pong(ping:string): TPromise<{ incoming:string, outgoing:string }>; cancelMe(): TPromise; + batchPerf(batches: number, size: number, dataSize: number): PPromise; } export class TestService implements ITestService { @@ -24,6 +25,8 @@ export class TestService implements ITestService { private _onMarco = new Emitter(); onMarco: Event = this._onMarco.event; + private _data = 'abcdefghijklmnopqrstuvwxyz'; + marco(): TPromise { this._onMarco.fire({ answer: 'polo' }); return TPromise.as('polo'); @@ -36,12 +39,39 @@ export class TestService implements ITestService { cancelMe(): TPromise { return TPromise.timeout(100).then(() => true); } + + batchPerf(batches: number, size: number, dataSize: number): PPromise { + while(this._data.length < dataSize) { + this._data += this._data; + } + const self = this; + return new PPromise((complete, error, progress) => { + let j = 0; + function send() { + if (j >= batches) { + complete(null); + return; + } + j++; + const batch = []; + for (let i = 0; i < size; i++) { + batch.push({ + prop: `${i}${self._data}`.substr(0, dataSize) + }); + } + progress(batch); + process.nextTick(send); + }; + process.nextTick(send); + }); + } } export interface ITestChannel extends IChannel { call(command: 'marco'): TPromise; call(command: 'pong', ping: string): TPromise; call(command: 'cancelMe'): TPromise; + call(command: 'batchPerf', args: { batches: number; size: number; dataSize: number; }): PPromise; call(command: string, ...args: any[]): TPromise; } @@ -55,6 +85,7 @@ export class TestChannel implements ITestChannel { case 'cancelMe': return this.testService.cancelMe(); case 'marco': return this.testService.marco(); case 'event:marco': return eventToCall(this.testService.onMarco); + case 'batchPerf': return this.testService.batchPerf(args[0].batches, args[0].size, args[0].dataSize); default: return TPromise.wrapError(new Error('command not found')); } } @@ -80,4 +111,8 @@ export class TestServiceClient implements ITestService { cancelMe(): TPromise { return this.channel.call('cancelMe'); } + + batchPerf(batches: number, size: number, dataSize: number): PPromise { + return this.channel.call('batchPerf', { batches, size, dataSize }); + } } \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index cb2dec09f9f..a538122c5f0 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -18,10 +18,12 @@ import {IRawSearchService, IRawSearch, ISerializedSearchProgressItem, ISerialize export class SearchService implements IRawSearchService { + private static BATCH_SIZE = 500; + public fileSearch(config: IRawSearch): PPromise { let engine = new FileSearchEngine(config); - return this.doSearch(engine); + return this.doSearch(engine, SearchService.BATCH_SIZE); } public textSearch(config: IRawSearch): PPromise { @@ -34,18 +36,30 @@ export class SearchService implements IRawSearchService { maxFilesize: MAX_FILE_SIZE })); - return this.doSearch(engine); + return this.doSearch(engine, SearchService.BATCH_SIZE); } - private doSearch(engine: ISearchEngine): PPromise { + public doSearch(engine: ISearchEngine, batchSize?: number): PPromise { return new PPromise((c, e, p) => { + let batch = []; engine.search((match) => { if (match) { - p(match); + if (batchSize) { + batch.push(match); + if (batchSize > 0 && batch.length >= batchSize) { + p(batch); + batch = []; + } + } else { + p(match); + } } }, (progress) => { p(progress); }, (error, isLimitHit) => { + if (batch.length) { + p(batch); + } if (error) { e(error); } else { diff --git a/src/vs/workbench/services/search/node/search.ts b/src/vs/workbench/services/search/node/search.ts index 4b56097d299..bdcc7680c62 100644 --- a/src/vs/workbench/services/search/node/search.ts +++ b/src/vs/workbench/services/search/node/search.ts @@ -40,6 +40,5 @@ export interface ISerializedFileMatch { lineMatches?: ILineMatch[]; } -export interface ISerializedSearchProgressItem extends ISerializedFileMatch, IProgress { - // Marker interface to indicate the possible values for progress calls from the engine -} +// Type of the possible values for progress calls from the engine +export type ISerializedSearchProgressItem = ISerializedFileMatch | ISerializedFileMatch[] | IProgress; diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index e2ffd52dcf6..653b7ef6402 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -17,7 +17,7 @@ import {IUntitledEditorService} from 'vs/workbench/services/untitled/common/unti import {IModelService} from 'vs/editor/common/services/modelService'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; -import {IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, IRawSearchService} from './search'; +import {IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedFileMatch, IRawSearchService} from './search'; import {ISearchChannel, SearchChannelClient} from './searchIpc'; export class SearchService implements ISearchService { @@ -187,7 +187,7 @@ export class SearchService implements ISearchService { } } -class DiskSearch { +export class DiskSearch { private raw: IRawSearchService; @@ -211,7 +211,6 @@ class DiskSearch { } public search(query: ISearchQuery): PPromise { - let result: IFileMatch[] = []; let request: PPromise; let rawSearch: IRawSearch = { @@ -234,6 +233,11 @@ class DiskSearch { request = this.raw.textSearch(rawSearch); } + return DiskSearch.collectResults(request); + } + + public static collectResults(request: PPromise): PPromise { + let result: IFileMatch[] = []; return new PPromise((c, e, p) => { request.done((complete) => { c({ @@ -242,17 +246,17 @@ class DiskSearch { }); }, e, (data) => { + // Matches + if (Array.isArray(data)) { + const fileMatches = data.map(d => this.createFileMatch(d)); + result = result.concat(fileMatches); + fileMatches.forEach(p); + } + // Match - if (data.path) { - let fileMatch = new FileMatch(uri.file(data.path)); + else if ((data).path) { + const fileMatch = this.createFileMatch(data); result.push(fileMatch); - - if (data.lineMatches) { - for (let j = 0; j < data.lineMatches.length; j++) { - fileMatch.lineMatches.push(new LineMatch(data.lineMatches[j].preview, data.lineMatches[j].lineNumber, data.lineMatches[j].offsetAndLengths)); - } - } - p(fileMatch); } @@ -263,4 +267,14 @@ class DiskSearch { }); }, () => request.cancel()); } + + private static createFileMatch(data: ISerializedFileMatch): FileMatch { + let fileMatch = new FileMatch(uri.file(data.path)); + if (data.lineMatches) { + for (let j = 0; j < data.lineMatches.length; j++) { + fileMatch.lineMatches.push(new LineMatch(data.lineMatches[j].preview, data.lineMatches[j].lineNumber, data.lineMatches[j].offsetAndLengths)); + } + } + return fileMatch; + } } \ No newline at end of file diff --git a/src/vs/workbench/services/search/test/node/searchService.test.ts b/src/vs/workbench/services/search/test/node/searchService.test.ts new file mode 100644 index 00000000000..333e9205b5c --- /dev/null +++ b/src/vs/workbench/services/search/test/node/searchService.test.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * 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 assert = require('assert'); + +import {IProgress} from 'vs/platform/search/common/search'; +import {ISearchEngine, ISerializedFileMatch} from 'vs/workbench/services/search/node/search'; +import {SearchService as RawSearchService} from 'vs/workbench/services/search/node/rawSearchService'; +import {DiskSearch} from 'vs/workbench/services/search/node/searchService'; + + +class TestSearchEngine implements ISearchEngine { + + constructor(private result: () => ISerializedFileMatch) { + } + + public search(onResult: (match: ISerializedFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, isLimitHit: boolean) => void): void { + const self = this; + (function next() { + process.nextTick(() => { + const result = self.result(); + if (!result) { + done(null, false); + } else { + onResult(result); + next(); + } + }); + })(); + } + + public cancel(): void { + } +} + +suite('SearchService', () => { + + test('Individual results', function () { + const path = '/some/where'; + let i = 5; + const engine = new TestSearchEngine(() => i-- && { path }); + const service = new RawSearchService(); + + let results = 0; + return service.doSearch(engine) + .then(() => { + assert.strictEqual(results, 5); + }, null, value => { + if (!Array.isArray(value)) { + assert.strictEqual((value).path, path); + results++; + } else { + assert.fail(value); + } + }); + }); + + test('Batch results', function () { + const path = '/some/where'; + let i = 25; + const engine = new TestSearchEngine(() => i-- && { path }); + const service = new RawSearchService(); + + const results = []; + return service.doSearch(engine, 10) + .then(() => { + assert.deepStrictEqual(results, [10, 10, 5]); + }, null, value => { + if (Array.isArray(value)) { + value.forEach(match => { + assert.strictEqual(match.path, path); + }); + results.push(value.length); + } else { + assert.fail(value); + } + }); + }); + + test('Collect batched results', function () { + const path = '/some/where'; + let i = 25; + const engine = new TestSearchEngine(() => i-- && { path }); + const service = new RawSearchService(); + const diskSearch = new DiskSearch(false); + + const progressResults = []; + return DiskSearch.collectResults(service.doSearch(engine, 10)) + .then(result => { + assert.strictEqual(result.results.length, 25, 'Result'); + assert.strictEqual(progressResults.length, 25, 'Progress'); + }, null, match => { + assert.strictEqual(match.resource.path, path); + progressResults.push(match); + }); + }); +}); \ No newline at end of file -- GitLab