Skip to content

Commit d5d7a2a

Browse files
committed
Merge pull request microsoft#6026 from zhengbli/i6016
Fix too many watcher instances issue
2 parents 18da7ba + dbfe862 commit d5d7a2a

File tree

5 files changed

+140
-34
lines changed

5 files changed

+140
-34
lines changed

src/compiler/core.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,9 @@ namespace ts {
616616
return path.substr(0, rootLength) + normalized.join(directorySeparator);
617617
}
618618

619-
export function getDirectoryPath(path: string) {
619+
export function getDirectoryPath(path: Path): Path;
620+
export function getDirectoryPath(path: string): string;
621+
export function getDirectoryPath(path: string): any {
620622
return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator)));
621623
}
622624

@@ -874,4 +876,11 @@ namespace ts {
874876
}
875877
return copiedList;
876878
}
879+
880+
export function createGetCanonicalFileName(useCaseSensitivefileNames: boolean): (fileName: string) => string {
881+
return useCaseSensitivefileNames
882+
? ((fileName) => fileName)
883+
: ((fileName) => fileName.toLowerCase());
884+
}
885+
877886
}

src/compiler/sys.ts

+124-22
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
/// <reference path="core.ts"/>
22

33
namespace ts {
4+
export type FileWatcherCallback = (path: string, removed?: boolean) => void;
5+
export type DirectoryWatcherCallback = (path: string) => void;
6+
47
export interface System {
58
args: string[];
69
newLine: string;
710
useCaseSensitiveFileNames: boolean;
811
write(s: string): void;
912
readFile(path: string, encoding?: string): string;
1013
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
11-
watchFile?(path: string, callback: (path: string, removed?: boolean) => void): FileWatcher;
12-
watchDirectory?(path: string, callback: (path: string) => void, recursive?: boolean): FileWatcher;
14+
watchFile?(path: Path, callback: FileWatcherCallback): FileWatcher;
15+
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
1316
resolvePath(path: string): string;
1417
fileExists(path: string): boolean;
1518
directoryExists(path: string): boolean;
@@ -22,15 +25,20 @@ namespace ts {
2225
}
2326

2427
interface WatchedFile {
25-
fileName: string;
26-
callback: (fileName: string, removed?: boolean) => void;
27-
mtime: Date;
28+
filePath: Path;
29+
callback: FileWatcherCallback;
30+
mtime?: Date;
2831
}
2932

3033
export interface FileWatcher {
3134
close(): void;
3235
}
3336

37+
export interface DirectoryWatcher extends FileWatcher {
38+
directoryPath: Path;
39+
referenceCount: number;
40+
}
41+
3442
declare var require: any;
3543
declare var module: any;
3644
declare var process: any;
@@ -62,8 +70,8 @@ namespace ts {
6270
readFile(path: string): string;
6371
writeFile(path: string, contents: string): void;
6472
readDirectory(path: string, extension?: string, exclude?: string[]): string[];
65-
watchFile?(path: string, callback: (path: string, removed?: boolean) => void): FileWatcher;
66-
watchDirectory?(path: string, callback: (path: string) => void, recursive?: boolean): FileWatcher;
73+
watchFile?(path: string, callback: FileWatcherCallback): FileWatcher;
74+
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
6775
};
6876

6977
export var sys: System = (function () {
@@ -221,7 +229,7 @@ namespace ts {
221229

222230
// average async stat takes about 30 microseconds
223231
// set chunk size to do 30 files in < 1 millisecond
224-
function createWatchedFileSet(interval = 2500, chunkSize = 30) {
232+
function createPollingWatchedFileSet(interval = 2500, chunkSize = 30) {
225233
let watchedFiles: WatchedFile[] = [];
226234
let nextFileToCheck = 0;
227235
let watchTimer: any;
@@ -236,13 +244,13 @@ namespace ts {
236244
return;
237245
}
238246

239-
_fs.stat(watchedFile.fileName, (err: any, stats: any) => {
247+
_fs.stat(watchedFile.filePath, (err: any, stats: any) => {
240248
if (err) {
241-
watchedFile.callback(watchedFile.fileName);
249+
watchedFile.callback(watchedFile.filePath);
242250
}
243251
else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) {
244-
watchedFile.mtime = getModifiedTime(watchedFile.fileName);
245-
watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0);
252+
watchedFile.mtime = getModifiedTime(watchedFile.filePath);
253+
watchedFile.callback(watchedFile.filePath, watchedFile.mtime.getTime() === 0);
246254
}
247255
});
248256
}
@@ -270,11 +278,11 @@ namespace ts {
270278
}, interval);
271279
}
272280

273-
function addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile {
281+
function addFile(filePath: Path, callback: FileWatcherCallback): WatchedFile {
274282
const file: WatchedFile = {
275-
fileName,
283+
filePath,
276284
callback,
277-
mtime: getModifiedTime(fileName)
285+
mtime: getModifiedTime(filePath)
278286
};
279287

280288
watchedFiles.push(file);
@@ -297,6 +305,88 @@ namespace ts {
297305
};
298306
}
299307

308+
function createWatchedFileSet() {
309+
const dirWatchers = createFileMap<DirectoryWatcher>();
310+
// One file can have multiple watchers
311+
const fileWatcherCallbacks = createFileMap<FileWatcherCallback[]>();
312+
return { addFile, removeFile };
313+
314+
function reduceDirWatcherRefCountForFile(filePath: Path) {
315+
const dirPath = getDirectoryPath(filePath);
316+
if (dirWatchers.contains(dirPath)) {
317+
const watcher = dirWatchers.get(dirPath);
318+
watcher.referenceCount -= 1;
319+
if (watcher.referenceCount <= 0) {
320+
watcher.close();
321+
dirWatchers.remove(dirPath);
322+
}
323+
}
324+
}
325+
326+
function addDirWatcher(dirPath: Path): void {
327+
if (dirWatchers.contains(dirPath)) {
328+
const watcher = dirWatchers.get(dirPath);
329+
watcher.referenceCount += 1;
330+
return;
331+
}
332+
333+
const watcher: DirectoryWatcher = _fs.watch(
334+
dirPath,
335+
{ persistent: true },
336+
(eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath)
337+
);
338+
watcher.referenceCount = 1;
339+
dirWatchers.set(dirPath, watcher);
340+
return;
341+
}
342+
343+
function addFileWatcherCallback(filePath: Path, callback: FileWatcherCallback): void {
344+
if (fileWatcherCallbacks.contains(filePath)) {
345+
fileWatcherCallbacks.get(filePath).push(callback);
346+
}
347+
else {
348+
fileWatcherCallbacks.set(filePath, [callback]);
349+
}
350+
}
351+
352+
function addFile(filePath: Path, callback: FileWatcherCallback): WatchedFile {
353+
addFileWatcherCallback(filePath, callback);
354+
addDirWatcher(getDirectoryPath(filePath));
355+
356+
return { filePath, callback };
357+
}
358+
359+
function removeFile(watchedFile: WatchedFile) {
360+
removeFileWatcherCallback(watchedFile.filePath, watchedFile.callback);
361+
reduceDirWatcherRefCountForFile(watchedFile.filePath);
362+
}
363+
364+
function removeFileWatcherCallback(filePath: Path, callback: FileWatcherCallback) {
365+
if (fileWatcherCallbacks.contains(filePath)) {
366+
const newCallbacks = copyListRemovingItem(callback, fileWatcherCallbacks.get(filePath));
367+
if (newCallbacks.length === 0) {
368+
fileWatcherCallbacks.remove(filePath);
369+
}
370+
else {
371+
fileWatcherCallbacks.set(filePath, newCallbacks);
372+
}
373+
}
374+
}
375+
376+
/**
377+
* @param watcherPath is the path from which the watcher is triggered.
378+
*/
379+
function fileEventHandler(eventName: string, relativefileName: string, baseDirPath: Path) {
380+
// When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined"
381+
const filePath = relativefileName === undefined ? undefined : toPath(relativefileName, baseDirPath, getCanonicalPath);
382+
if (eventName === "change" && fileWatcherCallbacks.contains(filePath)) {
383+
for (const fileCallback of fileWatcherCallbacks.get(filePath)) {
384+
fileCallback(filePath);
385+
}
386+
}
387+
}
388+
}
389+
300390
// REVIEW: for now this implementation uses polling.
301391
// The advantage of polling is that it works reliably
302392
// on all os and with network mounted files.
@@ -310,8 +400,13 @@ namespace ts {
310400
// changes for large reference sets? If so, do we want
311401
// to increase the chunk size or decrease the interval
312402
// time dynamically to match the large reference set?
403+
const pollingWatchedFileSet = createPollingWatchedFileSet();
313404
const watchedFileSet = createWatchedFileSet();
314405

406+
function isNode4OrLater(): boolean {
407+
return parseInt(process.version.charAt(1)) >= 4;
408+
}
409+
315410
const platform: string = _os.platform();
316411
// win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive
317412
const useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin";
@@ -405,29 +500,38 @@ namespace ts {
405500
},
406501
readFile,
407502
writeFile,
408-
watchFile: (fileName, callback) => {
503+
watchFile: (filePath, callback) => {
409504
// Node 4.0 stablized the `fs.watch` function on Windows which avoids polling
410505
// and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649
411506
// and https://github.com/Microsoft/TypeScript/issues/4643), therefore
412507
// if the current node.js version is newer than 4, use `fs.watch` instead.
413-
const watchedFile = watchedFileSet.addFile(fileName, callback);
508+
const watchSet = isNode4OrLater() ? watchedFileSet : pollingWatchedFileSet;
509+
const watchedFile = watchSet.addFile(filePath, callback);
414510
return {
415-
close: () => watchedFileSet.removeFile(watchedFile)
511+
close: () => watchSet.removeFile(watchedFile)
416512
};
417513
},
418514
watchDirectory: (path, callback, recursive) => {
419515
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
420516
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
517+
let options: any;
518+
if (isNode4OrLater() && (process.platform === "win32" || process.platform === "darwin")) {
519+
options = { persistent: true, recursive: !!recursive };
520+
}
521+
else {
522+
options = { persistent: true };
523+
}
524+
421525
return _fs.watch(
422526
path,
423-
{ persistent: true, recursive: !!recursive },
527+
options,
424528
(eventName: string, relativeFileName: string) => {
425529
// In watchDirectory we only care about adding and removing files (when event name is
426530
// "rename"); changes made within files are handled by corresponding fileWatchers (when
427531
// event name is "change")
428532
if (eventName === "rename") {
429533
// When deleting a file, the passed baseFileName is null
430-
callback(!relativeFileName ? relativeFileName : normalizePath(ts.combinePaths(path, relativeFileName)));
534+
callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(path, relativeFileName)));
431535
};
432536
}
433537
);
@@ -511,5 +615,3 @@ namespace ts {
511615
}
512616
})();
513617
}
514-
515-

src/compiler/tsc.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ namespace ts {
334334
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
335335
}
336336
if (configFileName) {
337-
configFileWatcher = sys.watchFile(configFileName, configFileChanged);
337+
const configFilePath = toPath(configFileName, sys.getCurrentDirectory(), createGetCanonicalFileName(sys.useCaseSensitiveFileNames));
338+
configFileWatcher = sys.watchFile(configFilePath, configFileChanged);
338339
}
339340
if (sys.watchDirectory && configFileName) {
340341
const directory = ts.getDirectoryPath(configFileName);
@@ -442,7 +443,8 @@ namespace ts {
442443
const sourceFile = hostGetSourceFile(fileName, languageVersion, onError);
443444
if (sourceFile && compilerOptions.watch) {
444445
// Attach a file watcher
445-
sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (fileName: string, removed?: boolean) => sourceFileChanged(sourceFile, removed));
446+
const filePath = toPath(sourceFile.fileName, sys.getCurrentDirectory(), createGetCanonicalFileName(sys.useCaseSensitiveFileNames));
447+
sourceFile.fileWatcher = sys.watchFile(filePath, (fileName: string, removed?: boolean) => sourceFileChanged(sourceFile, removed));
446448
}
447449
return sourceFile;
448450
}

src/server/editorServices.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,7 @@ namespace ts.server {
10021002
info.setFormatOptions(this.getFormatCodeOptions());
10031003
this.filenameToScriptInfo[fileName] = info;
10041004
if (!info.isOpen) {
1005-
info.fileWatcher = this.host.watchFile(fileName, _ => { this.watchedFileChanged(fileName); });
1005+
info.fileWatcher = this.host.watchFile(<Path>fileName, _ => { this.watchedFileChanged(fileName); });
10061006
}
10071007
}
10081008
}
@@ -1215,7 +1215,7 @@ namespace ts.server {
12151215
}
12161216
}
12171217
project.finishGraph();
1218-
project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project));
1218+
project.projectFileWatcher = this.host.watchFile(<Path>configFilename, _ => this.watchedProjectConfigFileChanged(project));
12191219
this.log("Add recursive watcher for: " + ts.getDirectoryPath(configFilename));
12201220
project.directoryWatcher = this.host.watchDirectory(
12211221
ts.getDirectoryPath(configFilename),

src/services/services.ts

-7
Original file line numberDiff line numberDiff line change
@@ -2018,13 +2018,6 @@ namespace ts {
20182018
return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, sourceFile.languageVersion, version, /*setNodeParents*/ true);
20192019
}
20202020

2021-
export function createGetCanonicalFileName(useCaseSensitivefileNames: boolean): (fileName: string) => string {
2022-
return useCaseSensitivefileNames
2023-
? ((fileName) => fileName)
2024-
: ((fileName) => fileName.toLowerCase());
2025-
}
2026-
2027-
20282021
export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory = ""): DocumentRegistry {
20292022
// Maps from compiler setting target (ES3, ES5, etc.) to all the cached documents we have
20302023
// for those settings.

0 commit comments

Comments
 (0)