/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as path from 'path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IProgress } from 'vs/platform/search/common/search'; import { FileWalker } from 'vs/workbench/services/search/node/fileSearch'; import { ISerializedFileMatch, IRawSearch, ISearchEngine, ISerializedSearchSuccess } from './search'; import { ISearchWorker } from './worker/searchWorkerIpc'; import { ITextSearchWorkerProvider } from './textSearchWorkerProvider'; export class Engine implements ISearchEngine { private static readonly PROGRESS_FLUSH_CHUNK_SIZE = 50; // optimization: number of files to process before emitting progress event private config: IRawSearch; private walker: FileWalker; private walkerError: Error; private isCanceled = false; private isDone = false; private totalBytes = 0; private processedBytes = 0; private progressed = 0; private walkerIsDone = false; private limitReached = false; private numResults = 0; private workerProvider: ITextSearchWorkerProvider; private workers: ISearchWorker[]; private nextWorker = 0; constructor(config: IRawSearch, walker: FileWalker, workerProvider: ITextSearchWorkerProvider) { this.config = config; this.walker = walker; this.workerProvider = workerProvider; } cancel(): void { this.isCanceled = true; this.walker.cancel(); this.workers.forEach(w => { w.cancel() .then(null, onUnexpectedError); }); } initializeWorkers(): void { this.workers.forEach(w => { w.initialize() .then(null, onUnexpectedError); }); } search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void { this.workers = this.workerProvider.getWorkers(); this.initializeWorkers(); const fileEncoding = this.config.folderQueries.length === 1 ? this.config.folderQueries[0].fileEncoding || 'utf8' : 'utf8'; const progress = () => { if (++this.progressed % Engine.PROGRESS_FLUSH_CHUNK_SIZE === 0) { onProgress({ total: this.totalBytes, worked: this.processedBytes }); // buffer progress in chunks to reduce pressure } }; const unwind = (processed: number) => { this.processedBytes += processed; // Emit progress() unless we got canceled or hit the limit if (processed && !this.isDone && !this.isCanceled && !this.limitReached) { progress(); } // Emit done() if (!this.isDone && this.processedBytes === this.totalBytes && this.walkerIsDone) { this.isDone = true; done(this.walkerError, { type: 'success', limitHit: this.limitReached, stats: this.walker.getStats() }); } }; const run = (batch: string[], batchBytes: number): void => { const worker = this.workers[this.nextWorker]; this.nextWorker = (this.nextWorker + 1) % this.workers.length; const maxResults = this.config.maxResults && (this.config.maxResults - this.numResults); const searchArgs = { absolutePaths: batch, maxResults, pattern: this.config.contentPattern, fileEncoding }; worker.search(searchArgs).then(result => { if (!result || this.limitReached || this.isCanceled) { return unwind(batchBytes); } const matches = result.matches; onResult(matches); this.numResults += result.numMatches; if (this.config.maxResults && this.numResults >= this.config.maxResults) { // It's possible to go over maxResults like this, but it's much simpler than trying to extract the exact number // of file matches, line matches, and matches within a line to == maxResults. this.limitReached = true; } unwind(batchBytes); }, error => { // An error on the worker's end, not in reading the file, but in processing the batch. Log and continue. onUnexpectedError(error); unwind(batchBytes); }); }; // Walk over the file system let nextBatch: string[] = []; let nextBatchBytes = 0; const batchFlushBytes = 2 ** 20; // 1MB this.walker.walk(this.config.folderQueries, this.config.extraFiles, result => { let bytes = result.size || 1; this.totalBytes += bytes; // If we have reached the limit or we are canceled, ignore it if (this.limitReached || this.isCanceled) { return unwind(bytes); } // Indicate progress to the outside progress(); const absolutePath = result.base ? [result.base, result.relativePath].join(path.sep) : result.relativePath; nextBatch.push(absolutePath); nextBatchBytes += bytes; if (nextBatchBytes >= batchFlushBytes) { run(nextBatch, nextBatchBytes); nextBatch = []; nextBatchBytes = 0; } }, onProgress, (error, isLimitHit) => { this.walkerIsDone = true; this.walkerError = error; // Send any remaining paths to a worker, or unwind if we're stopping if (nextBatch.length) { if (this.limitReached || this.isCanceled) { unwind(nextBatchBytes); } else { run(nextBatch, nextBatchBytes); } } else { unwind(0); } }); } }