Skip to content

Commit 8e81ea6

Browse files
authored
watcher - defer realpath to only when needed (#237600)
1 parent 91990b4 commit 8e81ea6

File tree

1 file changed

+33
-40
lines changed

1 file changed

+33
-40
lines changed

src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts

+33-40
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { realpath } from '../../../../../base/node/extpath.js';
1717
import { Promises } from '../../../../../base/node/pfs.js';
1818
import { FileChangeType, IFileChange } from '../../../common/files.js';
1919
import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, isWatchRequestWithCorrelation } from '../../../common/watcher.js';
20+
import { Lazy } from '../../../../../base/common/lazy.js';
2021

2122
export class NodeJSFileWatcherLibrary extends Disposable {
2223

@@ -55,6 +56,28 @@ export class NodeJSFileWatcherLibrary extends Disposable {
5556

5657
private readonly cts = new CancellationTokenSource();
5758

59+
private readonly realPath = new Lazy(async () => {
60+
61+
// This property is intentionally `Lazy` and not using `realcase()` as the counterpart
62+
// in the recursive watcher because of the amount of paths this watcher is dealing with.
63+
// We try as much as possible to avoid even needing `realpath()` if we can because even
64+
// that method does an `lstat()` per segment of the path.
65+
66+
let result = this.request.path;
67+
68+
try {
69+
result = await realpath(this.request.path);
70+
71+
if (this.request.path !== result) {
72+
this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${this.request.path}, real: ${result})`);
73+
}
74+
} catch (error) {
75+
// ignore
76+
}
77+
78+
return result;
79+
});
80+
5881
readonly ready = this.watch();
5982

6083
private _isReusingRecursiveWatcher = false;
@@ -76,19 +99,13 @@ export class NodeJSFileWatcherLibrary extends Disposable {
7699

77100
private async watch(): Promise<void> {
78101
try {
79-
const realPath = await this.normalizePath(this.request);
102+
const stat = await promises.stat(this.request.path);
80103

81104
if (this.cts.token.isCancellationRequested) {
82105
return;
83106
}
84107

85-
const stat = await promises.stat(realPath);
86-
87-
if (this.cts.token.isCancellationRequested) {
88-
return;
89-
}
90-
91-
this._register(await this.doWatch(realPath, stat.isDirectory()));
108+
this._register(await this.doWatch(stat.isDirectory()));
92109
} catch (error) {
93110
if (error.code !== 'ENOENT') {
94111
this.error(error);
@@ -106,46 +123,21 @@ export class NodeJSFileWatcherLibrary extends Disposable {
106123
this.onDidWatchFail?.();
107124
}
108125

109-
private async normalizePath(request: INonRecursiveWatchRequest): Promise<string> {
110-
let realPath = request.path;
111-
112-
try {
113-
114-
// Check for symbolic link
115-
realPath = await realpath(request.path);
116-
117-
// Note: we used to also call `realcase()` here, but
118-
// that operation is very expensive for large amounts
119-
// of files and is actually not needed for single
120-
// file/folder watching where we report on the original
121-
// path anyway.
122-
// (https://github.com/microsoft/vscode/issues/237351)
123-
124-
if (request.path !== realPath) {
125-
this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${request.path}, real: ${realPath})`);
126-
}
127-
} catch (error) {
128-
// ignore
129-
}
130-
131-
return realPath;
132-
}
133-
134-
private async doWatch(realPath: string, isDirectory: boolean): Promise<IDisposable> {
126+
private async doWatch(isDirectory: boolean): Promise<IDisposable> {
135127
const disposables = new DisposableStore();
136128

137-
if (this.doWatchWithExistingWatcher(realPath, isDirectory, disposables)) {
129+
if (this.doWatchWithExistingWatcher(isDirectory, disposables)) {
138130
this.trace(`reusing an existing recursive watcher for ${this.request.path}`);
139131
this._isReusingRecursiveWatcher = true;
140132
} else {
141133
this._isReusingRecursiveWatcher = false;
142-
await this.doWatchWithNodeJS(realPath, isDirectory, disposables);
134+
await this.doWatchWithNodeJS(isDirectory, disposables);
143135
}
144136

145137
return disposables;
146138
}
147139

148-
private doWatchWithExistingWatcher(realPath: string, isDirectory: boolean, disposables: DisposableStore): boolean {
140+
private doWatchWithExistingWatcher(isDirectory: boolean, disposables: DisposableStore): boolean {
149141
if (isDirectory) {
150142
// Recursive watcher re-use is currently not enabled for when
151143
// folders are watched. this is because the dispatching in the
@@ -162,7 +154,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
162154
}
163155

164156
if (error) {
165-
const watchDisposable = await this.doWatch(realPath, isDirectory);
157+
const watchDisposable = await this.doWatch(isDirectory);
166158
if (!disposables.isDisposed) {
167159
disposables.add(watchDisposable);
168160
} else {
@@ -188,7 +180,8 @@ export class NodeJSFileWatcherLibrary extends Disposable {
188180
return false;
189181
}
190182

191-
private async doWatchWithNodeJS(realPath: string, isDirectory: boolean, disposables: DisposableStore): Promise<void> {
183+
private async doWatchWithNodeJS(isDirectory: boolean, disposables: DisposableStore): Promise<void> {
184+
const realPath = await this.realPath.value;
192185

193186
// macOS: watching samba shares can crash VSCode so we do
194187
// a simple check for the file path pointing to /Volumes
@@ -407,7 +400,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
407400
if (fileExists) {
408401
this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
409402

410-
watcherDisposables.add(await this.doWatch(realPath, false));
403+
watcherDisposables.add(await this.doWatch(false));
411404
}
412405

413406
// File seems to be really gone, so emit a deleted and failed event

0 commit comments

Comments
 (0)