mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 08:15:56 +01:00
files - speed up glob matching for file events in extension host (#305962)
* files - speed up glob matching for file events in extension host * partially restore old behaviour
This commit is contained in:
@@ -355,9 +355,7 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P
|
||||
...options,
|
||||
equals: ignoreCase ? equalsIgnoreCase : (a: string, b: string) => a === b,
|
||||
endsWith: ignoreCase ? endsWithIgnoreCase : (str: string, candidate: string) => str.endsWith(candidate),
|
||||
// TODO: the '!isLinux' part below is to keep current behavior unchanged, but it should probably be removed
|
||||
// in favor of passing correct options from the caller.
|
||||
isEqualOrParent: (base: string, candidate: string) => isEqualOrParent(base, candidate, !isLinux || ignoreCase)
|
||||
isEqualOrParent: (base: string, candidate: string) => isEqualOrParent(base, candidate, options.ignoreCase ?? !isLinux /* preserve old behaviour for when option is not adopted */)
|
||||
};
|
||||
|
||||
// Check cache
|
||||
@@ -371,13 +369,13 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P
|
||||
let match: RegExpExecArray | null;
|
||||
if (T1.test(pattern)) {
|
||||
parsedPattern = trivia1(pattern.substring(4), pattern, internalOptions); // common pattern: **/*.txt just need endsWith check
|
||||
} else if (match = T2.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/some.txt just need basename check
|
||||
} else if (match = T2.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/some.txt just need basename check
|
||||
parsedPattern = trivia2(match[1], pattern, internalOptions);
|
||||
} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
|
||||
} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
|
||||
parsedPattern = trivia3(pattern, internalOptions);
|
||||
} else if (match = T4.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/something/else just need endsWith check
|
||||
} else if (match = T4.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/something/else just need endsWith check
|
||||
parsedPattern = trivia4and5(match[1].substring(1), pattern, true, internalOptions);
|
||||
} else if (match = T5.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: something/else just need equals check
|
||||
} else if (match = T5.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: something/else just need equals check
|
||||
parsedPattern = trivia4and5(match[1], pattern, false, internalOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Emitter, Event, AsyncEmitter, IWaitUntil, IWaitUntilData } from '../../../base/common/event.js';
|
||||
import { GLOBSTAR, GLOB_SPLIT, IRelativePattern, parse } from '../../../base/common/glob.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { URI, UriComponents } from '../../../base/common/uri.js';
|
||||
import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';
|
||||
import type * as vscode from 'vscode';
|
||||
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation, MainContext, IRelativePatternDto } from './extHost.protocol.js';
|
||||
@@ -52,7 +52,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
|
||||
return Boolean(this._config & 0b100);
|
||||
}
|
||||
|
||||
constructor(mainContext: IMainContext, configuration: ExtHostConfigProvider, fileSystemInfo: ExtHostFileSystemInfo, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions) {
|
||||
constructor(mainContext: IMainContext, configuration: ExtHostConfigProvider, fileSystemInfo: ExtHostFileSystemInfo, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<LazyRevivedFileSystemEvents>, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions) {
|
||||
this._config = 0;
|
||||
if (options.ignoreCreateEvents) {
|
||||
this._config += 0b001;
|
||||
@@ -68,7 +68,18 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
|
||||
!((fileSystemInfo.getCapabilities(Schemas.file) ?? 0) & FileSystemProviderCapabilities.PathCaseSensitive) :
|
||||
fileSystemInfo.extUri.ignorePathCasing(URI.revive(globPattern.baseUri));
|
||||
|
||||
const parsedPattern = parse(globPattern, { ignoreCase });
|
||||
// Performance: pre-lowercase pattern and paths to use fast case-sensitive
|
||||
// matching instead of repeated case-insensitive comparisons in the hot loop.
|
||||
// By normalizing to lowercase upfront, we enforce `ignoreCase: false` so the
|
||||
// glob parser uses strict `===` / `endsWith` instead of character-by-character
|
||||
// case-folding on every comparison.
|
||||
let matchGlob: string | IRelativePattern = globPattern;
|
||||
if (ignoreCase) {
|
||||
matchGlob = typeof globPattern === 'string'
|
||||
? globPattern.toLowerCase()
|
||||
: { base: globPattern.base.toLowerCase(), pattern: globPattern.pattern.toLowerCase() };
|
||||
}
|
||||
const parsedPattern = parse(matchGlob, { ignoreCase: false /* speeds up matching, but requires us to lowercase paths and patterns */ });
|
||||
|
||||
// 1.64.x behavior change: given the new support to watch any folder
|
||||
// we start to ignore events outside the workspace when only a string
|
||||
@@ -95,25 +106,22 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
|
||||
}
|
||||
|
||||
if (!options.ignoreCreateEvents) {
|
||||
for (const created of events.created) {
|
||||
const uri = URI.revive(created);
|
||||
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
|
||||
for (const { uri, lowerCaseFsPath } of events.created) {
|
||||
if (parsedPattern(ignoreCase ? lowerCaseFsPath : uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
|
||||
this._onDidCreate.fire(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!options.ignoreChangeEvents) {
|
||||
for (const changed of events.changed) {
|
||||
const uri = URI.revive(changed);
|
||||
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
|
||||
for (const { uri, lowerCaseFsPath } of events.changed) {
|
||||
if (parsedPattern(ignoreCase ? lowerCaseFsPath : uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
|
||||
this._onDidChange.fire(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!options.ignoreDeleteEvents) {
|
||||
for (const deleted of events.deleted) {
|
||||
const uri = URI.revive(deleted);
|
||||
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
|
||||
for (const { uri, lowerCaseFsPath } of events.deleted) {
|
||||
if (parsedPattern(ignoreCase ? lowerCaseFsPath : uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
|
||||
this._onDidDelete.fire(uri);
|
||||
}
|
||||
}
|
||||
@@ -247,18 +255,28 @@ interface IExtensionListener<E> {
|
||||
(e: E): any;
|
||||
}
|
||||
|
||||
class LazyRevivedFileSystemEvents implements FileSystemEvents {
|
||||
interface RevivedFileSystemEvent {
|
||||
readonly uri: URI;
|
||||
readonly lowerCaseFsPath: string;
|
||||
}
|
||||
|
||||
class LazyRevivedFileSystemEvents {
|
||||
|
||||
readonly session: number | undefined;
|
||||
|
||||
private _created = new Lazy(() => this._events.created.map(URI.revive) as URI[]);
|
||||
get created(): URI[] { return this._created.value; }
|
||||
private _created = new Lazy(() => this._events.created.map(LazyRevivedFileSystemEvents._revive));
|
||||
get created(): RevivedFileSystemEvent[] { return this._created.value; }
|
||||
|
||||
private _changed = new Lazy(() => this._events.changed.map(URI.revive) as URI[]);
|
||||
get changed(): URI[] { return this._changed.value; }
|
||||
private _changed = new Lazy(() => this._events.changed.map(LazyRevivedFileSystemEvents._revive));
|
||||
get changed(): RevivedFileSystemEvent[] { return this._changed.value; }
|
||||
|
||||
private _deleted = new Lazy(() => this._events.deleted.map(URI.revive) as URI[]);
|
||||
get deleted(): URI[] { return this._deleted.value; }
|
||||
private _deleted = new Lazy(() => this._events.deleted.map(LazyRevivedFileSystemEvents._revive));
|
||||
get deleted(): RevivedFileSystemEvent[] { return this._deleted.value; }
|
||||
|
||||
private static _revive(uriComponents: UriComponents): RevivedFileSystemEvent {
|
||||
const uri = URI.revive(uriComponents);
|
||||
return { uri, lowerCaseFsPath: uri.fsPath.toLowerCase() };
|
||||
}
|
||||
|
||||
constructor(private readonly _events: FileSystemEvents) {
|
||||
this.session = this._events.session;
|
||||
@@ -267,7 +285,7 @@ class LazyRevivedFileSystemEvents implements FileSystemEvents {
|
||||
|
||||
export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServiceShape {
|
||||
|
||||
private readonly _onFileSystemEvent = new Emitter<FileSystemEvents>();
|
||||
private readonly _onFileSystemEvent = new Emitter<LazyRevivedFileSystemEvents>();
|
||||
|
||||
private readonly _onDidRenameFile = new Emitter<vscode.FileRenameEvent>();
|
||||
private readonly _onDidCreateFile = new Emitter<vscode.FileCreateEvent>();
|
||||
|
||||
@@ -8,20 +8,34 @@ import { IMainContext } from '../../common/extHost.protocol.js';
|
||||
import { NullLogService } from '../../../../platform/log/common/log.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
import { ExtHostFileSystemInfo } from '../../common/extHostFileSystemInfo.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { FileSystemProviderCapabilities } from '../../../../platform/files/common/files.js';
|
||||
import { IExtHostWorkspace } from '../../common/extHostWorkspace.js';
|
||||
import { RelativePattern } from '../../common/extHostTypes.js';
|
||||
import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';
|
||||
import { ExtHostConfigProvider } from '../../common/extHostConfiguration.js';
|
||||
|
||||
suite('ExtHostFileSystemEventService', () => {
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('FileSystemWatcher ignore events properties are reversed #26851', function () {
|
||||
const protocol: IMainContext = {
|
||||
getProxy: () => { return undefined!; },
|
||||
set: undefined!,
|
||||
dispose: undefined!,
|
||||
assertRegistered: undefined!,
|
||||
drain: undefined!
|
||||
};
|
||||
|
||||
const protocol: IMainContext = {
|
||||
getProxy: () => { return undefined!; },
|
||||
set: undefined!,
|
||||
dispose: undefined!,
|
||||
assertRegistered: undefined!,
|
||||
drain: undefined!
|
||||
};
|
||||
const protocolWithProxy: IMainContext = {
|
||||
getProxy: () => ({ $watch() { }, $unwatch() { }, dispose() { } }) as never,
|
||||
set: undefined!,
|
||||
dispose: undefined!,
|
||||
assertRegistered: undefined!,
|
||||
drain: undefined!
|
||||
};
|
||||
|
||||
test('FileSystemWatcher ignore events properties are reversed #26851', function () {
|
||||
|
||||
const fileSystemInfo = new ExtHostFileSystemInfo();
|
||||
|
||||
@@ -38,4 +52,102 @@ suite('ExtHostFileSystemEventService', () => {
|
||||
watcher2.dispose();
|
||||
});
|
||||
|
||||
test('FileSystemWatcher matches case-insensitively via pre-lowercasing', function () {
|
||||
const fileSystemInfo = new ExtHostFileSystemInfo();
|
||||
// Default: no PathCaseSensitive capability → ignoreCase=true for string patterns
|
||||
|
||||
const workspace: Pick<IExtHostWorkspace, 'getWorkspaceFolder'> = {
|
||||
getWorkspaceFolder: () => ({ uri: URI.file('/workspace'), name: 'test', index: 0 })
|
||||
};
|
||||
|
||||
const service = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!);
|
||||
const watcher = service.createFileSystemWatcher(workspace as IExtHostWorkspace, undefined!, fileSystemInfo, undefined!, '**/*.TXT', {});
|
||||
|
||||
const created: URI[] = [];
|
||||
const sub = watcher.onDidCreate(uri => created.push(uri));
|
||||
|
||||
// lowercase path should match uppercase pattern on case-insensitive fs
|
||||
service.$onFileEvent({
|
||||
session: undefined,
|
||||
created: [URI.file('/workspace/file.txt')],
|
||||
changed: [],
|
||||
deleted: []
|
||||
});
|
||||
|
||||
assert.strictEqual(created.length, 1);
|
||||
|
||||
sub.dispose();
|
||||
watcher.dispose();
|
||||
});
|
||||
|
||||
test('FileSystemWatcher matches case-sensitively when PathCaseSensitive', function () {
|
||||
const fileSystemInfo = new ExtHostFileSystemInfo();
|
||||
fileSystemInfo.$acceptProviderInfos(URI.file('/'), FileSystemProviderCapabilities.PathCaseSensitive);
|
||||
|
||||
const workspace: Pick<IExtHostWorkspace, 'getWorkspaceFolder'> = {
|
||||
getWorkspaceFolder: () => ({ uri: URI.file('/workspace'), name: 'test', index: 0 })
|
||||
};
|
||||
|
||||
const service = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!);
|
||||
const watcher = service.createFileSystemWatcher(workspace as IExtHostWorkspace, undefined!, fileSystemInfo, undefined!, '**/*.TXT', {});
|
||||
|
||||
const created: URI[] = [];
|
||||
const sub = watcher.onDidCreate(uri => created.push(uri));
|
||||
|
||||
// lowercase path should NOT match uppercase pattern on case-sensitive fs
|
||||
service.$onFileEvent({
|
||||
session: undefined,
|
||||
created: [URI.file('/workspace/file.txt')],
|
||||
changed: [],
|
||||
deleted: []
|
||||
});
|
||||
|
||||
assert.strictEqual(created.length, 0);
|
||||
|
||||
// uppercase path SHOULD match
|
||||
service.$onFileEvent({
|
||||
session: undefined,
|
||||
created: [URI.file('/workspace/file.TXT')],
|
||||
changed: [],
|
||||
deleted: []
|
||||
});
|
||||
|
||||
assert.strictEqual(created.length, 1);
|
||||
|
||||
sub.dispose();
|
||||
watcher.dispose();
|
||||
});
|
||||
|
||||
test('FileSystemWatcher matches relative pattern case-insensitively via pre-lowercasing', function () {
|
||||
const fileSystemInfo = new ExtHostFileSystemInfo();
|
||||
fileSystemInfo.$acceptProviderInfos(URI.file('/'), FileSystemProviderCapabilities.FileReadWrite); // no PathCaseSensitive → ignoreCase=true
|
||||
|
||||
const workspace: Pick<IExtHostWorkspace, 'getWorkspaceFolder'> = {
|
||||
getWorkspaceFolder: () => ({ uri: URI.file('/workspace'), name: 'test', index: 0 })
|
||||
};
|
||||
|
||||
const configProvider = {
|
||||
getConfiguration: () => ({ get: () => ({}) })
|
||||
} as unknown as ExtHostConfigProvider;
|
||||
|
||||
const service = new ExtHostFileSystemEventService(protocolWithProxy, new NullLogService(), undefined!);
|
||||
const watcher = service.createFileSystemWatcher(workspace as IExtHostWorkspace, configProvider, fileSystemInfo, nullExtensionDescription, new RelativePattern('/Workspace', '**/*.TXT'), {});
|
||||
|
||||
const created: URI[] = [];
|
||||
const sub = watcher.onDidCreate(uri => created.push(uri));
|
||||
|
||||
// lowercase path should match mixed-case base + uppercase extension on case-insensitive fs
|
||||
service.$onFileEvent({
|
||||
session: undefined,
|
||||
created: [URI.file('/workspace/file.txt')],
|
||||
changed: [],
|
||||
deleted: []
|
||||
});
|
||||
|
||||
assert.strictEqual(created.length, 1);
|
||||
|
||||
sub.dispose();
|
||||
watcher.dispose();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user