Skip to content

Commit b84f13d

Browse files
committed
Use single stats watcher per filename
Fixes microsoft#28690
1 parent 2db8a13 commit b84f13d

File tree

4 files changed

+192
-108
lines changed

4 files changed

+192
-108
lines changed

src/compiler/sys.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,53 @@ namespace ts {
304304
}
305305
}
306306

307+
/* @internal */
308+
export function createSingleFileWatcherPerName(
309+
watchFile: HostWatchFile,
310+
useCaseSensitiveFileNames: boolean
311+
): HostWatchFile {
312+
interface SingleFileWatcher {
313+
watcher: FileWatcher;
314+
refCount: number;
315+
}
316+
const cache = createMap<SingleFileWatcher>();
317+
const callbacksCache = createMultiMap<FileWatcherCallback>();
318+
const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
319+
320+
return (fileName, callback, pollingInterval) => {
321+
const path = toCanonicalFileName(fileName);
322+
const existing = cache.get(path);
323+
if (existing) {
324+
existing.refCount++;
325+
}
326+
else {
327+
cache.set(path, {
328+
watcher: watchFile(
329+
fileName,
330+
(fileName, eventKind) => forEach(
331+
callbacksCache.get(path),
332+
cb => cb(fileName, eventKind)
333+
),
334+
pollingInterval
335+
),
336+
refCount: 1
337+
});
338+
}
339+
callbacksCache.add(path, callback);
340+
341+
return {
342+
close: () => {
343+
const watcher = Debug.assertDefined(cache.get(path));
344+
callbacksCache.remove(path, callback);
345+
watcher.refCount--;
346+
if (watcher.refCount) return;
347+
cache.delete(path);
348+
closeFileWatcherOf(watcher);
349+
}
350+
};
351+
};
352+
}
353+
307354
/**
308355
* Returns true if file status changed
309356
*/
@@ -695,6 +742,7 @@ namespace ts {
695742
const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER;
696743
const tscWatchFile = process.env.TSC_WATCHFILE;
697744
const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY;
745+
const fsWatchFile = createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames);
698746
let dynamicPollingWatchFile: HostWatchFile | undefined;
699747
const nodeSystem: System = {
700748
args: process.argv.slice(2),
@@ -835,7 +883,7 @@ namespace ts {
835883
return useNonPollingWatchers ?
836884
createNonPollingWatchFile() :
837885
// Default to do not use polling interval as it is before this experiment branch
838-
(fileName, callback) => fsWatchFile(fileName, callback);
886+
(fileName, callback) => fsWatchFile(fileName, callback, /*pollingInterval*/ undefined);
839887
}
840888

841889
function getWatchDirectory(): HostWatchDirectory {
@@ -916,7 +964,7 @@ namespace ts {
916964
}
917965
}
918966

919-
function fsWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher {
967+
function fsWatchFileWorker(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher {
920968
_fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged);
921969
let eventKind: FileWatcherEventKind;
922970
return {

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,11 @@ interface Array<T> {}`
314314
invokeFileDeleteCreateAsPartInsteadOfChange: boolean;
315315
}
316316

317+
export enum Tsc_WatchFile {
318+
DynamicPolling = "DynamicPriorityPolling",
319+
SingleFileWatcherPerName = "SingleFileWatcherPerName"
320+
}
321+
317322
export enum Tsc_WatchDirectory {
318323
WatchFile = "RecursiveDirectoryUsingFsWatchFile",
319324
NonRecursiveWatchDirectory = "RecursiveDirectoryUsingNonRecursiveWatchDirectory",
@@ -339,7 +344,7 @@ interface Array<T> {}`
339344
readonly watchedFiles = createMultiMap<TestFileWatcher>();
340345
private readonly executingFilePath: string;
341346
private readonly currentDirectory: string;
342-
private readonly dynamicPriorityWatchFile: HostWatchFile | undefined;
347+
private readonly customWatchFile: HostWatchFile | undefined;
343348
private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined;
344349
public require: ((initialPath: string, moduleName: string) => server.RequireResult) | undefined;
345350

@@ -349,9 +354,23 @@ interface Array<T> {}`
349354
this.executingFilePath = this.getHostSpecificPath(executingFilePath);
350355
this.currentDirectory = this.getHostSpecificPath(currentDirectory);
351356
this.reloadFS(fileOrFolderorSymLinkList);
352-
this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ?
353-
createDynamicPriorityPollingWatchFile(this) :
354-
undefined;
357+
const tscWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") as Tsc_WatchFile;
358+
switch (tscWatchFile) {
359+
case Tsc_WatchFile.DynamicPolling:
360+
this.customWatchFile = createDynamicPriorityPollingWatchFile(this);
361+
break;
362+
case Tsc_WatchFile.SingleFileWatcherPerName:
363+
this.customWatchFile = createSingleFileWatcherPerName(
364+
this.watchFileWorker.bind(this),
365+
this.useCaseSensitiveFileNames
366+
);
367+
break;
368+
case undefined:
369+
break;
370+
default:
371+
Debug.assertNever(tscWatchFile);
372+
}
373+
355374
const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY") as Tsc_WatchDirectory;
356375
if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) {
357376
const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium);
@@ -854,10 +873,14 @@ interface Array<T> {}`
854873
}
855874

856875
watchFile(fileName: string, cb: FileWatcherCallback, pollingInterval: number) {
857-
if (this.dynamicPriorityWatchFile) {
858-
return this.dynamicPriorityWatchFile(fileName, cb, pollingInterval);
876+
if (this.customWatchFile) {
877+
return this.customWatchFile(fileName, cb, pollingInterval);
859878
}
860879

880+
return this.watchFileWorker(fileName, cb);
881+
}
882+
883+
private watchFileWorker(fileName: string, cb: FileWatcherCallback) {
861884
const path = this.toFullPath(fileName);
862885
const callback: TestFileWatcher = { fileName, cb };
863886
this.watchedFiles.add(path, callback);
Lines changed: 112 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,124 @@
11
namespace ts.tscWatch {
22
describe("unittests:: tsbuild:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => {
3-
it("watchFile on same file multiple times because file is part of multiple projects", () => {
4-
const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`;
5-
let maxPkgs = 4;
6-
const configPath = `${project}/tsconfig.json`;
7-
const typing: File = {
8-
path: `${project}/typings/xterm.d.ts`,
9-
content: "export const typing = 10;"
10-
};
3+
describe("when watchFile can create multiple watchers per file", () => {
4+
verifyWatchFileOnMultipleProjects(/*singleWatchPerFile*/ false);
5+
});
116

12-
const allPkgFiles = pkgs(pkgFiles);
13-
const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project });
14-
writePkgReferences();
15-
const host = createSolutionBuilderWithWatchHost(system);
16-
const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true });
17-
solutionBuilder.build();
18-
checkOutputErrorsInitial(system, emptyArray, /*disableConsoleClears*/ undefined, [
19-
`Projects in this build: \r\n${
20-
concatenate(
21-
pkgs(index => ` * pkg${index}/tsconfig.json`),
22-
[" * tsconfig.json"]
23-
).join("\r\n")}\n\n`,
24-
...flatArray(pkgs(index => [
25-
`Project 'pkg${index}/tsconfig.json' is out of date because output file 'pkg${index}/index.js' does not exist\n\n`,
26-
`Building project '${project}/pkg${index}/tsconfig.json'...\n\n`
27-
]))
28-
]);
7+
describe("when watchFile is single watcher per file", () => {
8+
verifyWatchFileOnMultipleProjects(
9+
/*singleWatchPerFile*/ true,
10+
arrayToMap(["TSC_WATCHFILE"], identity, () => TestFSWithWatch.Tsc_WatchFile.SingleFileWatcherPerName)
11+
);
12+
});
2913

30-
const watchFilesDetailed = arrayToMap(flatArray(allPkgFiles), f => f.path, () => 1);
31-
watchFilesDetailed.set(configPath, 1);
32-
watchFilesDetailed.set(typing.path, maxPkgs);
33-
checkWatchedFilesDetailed(system, watchFilesDetailed);
34-
system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`);
35-
verifyInvoke();
14+
function verifyWatchFileOnMultipleProjects(singleWatchPerFile: boolean, environmentVariables?: Map<string>) {
15+
it("watchFile on same file multiple times because file is part of multiple projects", () => {
16+
const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`;
17+
let maxPkgs = 4;
18+
const configPath = `${project}/tsconfig.json`;
19+
const typing: File = {
20+
path: `${project}/typings/xterm.d.ts`,
21+
content: "export const typing = 10;"
22+
};
3623

37-
// Make change
38-
maxPkgs--;
39-
writePkgReferences();
40-
system.checkTimeoutQueueLengthAndRun(1);
41-
checkOutputErrorsIncremental(system, emptyArray);
42-
const lastFiles = last(allPkgFiles);
43-
lastFiles.forEach(f => watchFilesDetailed.delete(f.path));
44-
watchFilesDetailed.set(typing.path, maxPkgs);
45-
checkWatchedFilesDetailed(system, watchFilesDetailed);
46-
system.writeFile(typing.path, typing.content);
47-
verifyInvoke();
24+
const allPkgFiles = pkgs(pkgFiles);
25+
const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project, environmentVariables });
26+
writePkgReferences();
27+
const host = createSolutionBuilderWithWatchHost(system);
28+
const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true });
29+
solutionBuilder.build();
30+
checkOutputErrorsInitial(system, emptyArray, /*disableConsoleClears*/ undefined, [
31+
`Projects in this build: \r\n${
32+
concatenate(
33+
pkgs(index => ` * pkg${index}/tsconfig.json`),
34+
[" * tsconfig.json"]
35+
).join("\r\n")}\n\n`,
36+
...flatArray(pkgs(index => [
37+
`Project 'pkg${index}/tsconfig.json' is out of date because output file 'pkg${index}/index.js' does not exist\n\n`,
38+
`Building project '${project}/pkg${index}/tsconfig.json'...\n\n`
39+
]))
40+
]);
4841

49-
// Make change to remove all the watches
50-
maxPkgs = 0;
51-
writePkgReferences();
52-
system.checkTimeoutQueueLengthAndRun(1);
53-
checkOutputErrorsIncremental(system, [
54-
`tsconfig.json(1,10): error TS18002: The 'files' list in config file '${configPath}' is empty.\n`
55-
]);
56-
checkWatchedFilesDetailed(system, [configPath], 1);
42+
const watchFilesDetailed = arrayToMap(flatArray(allPkgFiles), f => f.path, () => 1);
43+
watchFilesDetailed.set(configPath, 1);
44+
watchFilesDetailed.set(typing.path, singleWatchPerFile ? 1 : maxPkgs);
45+
checkWatchedFilesDetailed(system, watchFilesDetailed);
46+
system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`);
47+
verifyInvoke();
5748

58-
system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`);
59-
system.checkTimeoutQueueLength(0);
49+
// Make change
50+
maxPkgs--;
51+
writePkgReferences();
52+
system.checkTimeoutQueueLengthAndRun(1);
53+
checkOutputErrorsIncremental(system, emptyArray);
54+
const lastFiles = last(allPkgFiles);
55+
lastFiles.forEach(f => watchFilesDetailed.delete(f.path));
56+
watchFilesDetailed.set(typing.path, singleWatchPerFile ? 1 : maxPkgs);
57+
checkWatchedFilesDetailed(system, watchFilesDetailed);
58+
system.writeFile(typing.path, typing.content);
59+
verifyInvoke();
6060

61-
function flatArray<T>(arr: T[][]): readonly T[] {
62-
return flatMap(arr, identity);
63-
}
64-
function pkgs<T>(cb: (index: number) => T): T[] {
65-
const result: T[] = [];
66-
for (let index = 0; index < maxPkgs; index++) {
67-
result.push(cb(index));
61+
// Make change to remove all the watches
62+
maxPkgs = 0;
63+
writePkgReferences();
64+
system.checkTimeoutQueueLengthAndRun(1);
65+
checkOutputErrorsIncremental(system, [
66+
`tsconfig.json(1,10): error TS18002: The 'files' list in config file '${configPath}' is empty.\n`
67+
]);
68+
checkWatchedFilesDetailed(system, [configPath], 1);
69+
70+
system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`);
71+
system.checkTimeoutQueueLength(0);
72+
73+
function flatArray<T>(arr: T[][]): readonly T[] {
74+
return flatMap(arr, identity);
6875
}
69-
return result;
70-
}
71-
function createPkgReference(index: number) {
72-
return { path: `./pkg${index}` };
73-
}
74-
function pkgFiles(index: number): File[] {
75-
return [
76-
{
77-
path: `${project}/pkg${index}/index.ts`,
78-
content: `export const pkg${index} = ${index};`
79-
},
80-
{
81-
path: `${project}/pkg${index}/tsconfig.json`,
82-
content: JSON.stringify({
83-
complerOptions: { composite: true },
84-
include: [
85-
"**/*.ts",
86-
"../typings/xterm.d.ts"
87-
]
88-
})
76+
function pkgs<T>(cb: (index: number) => T): T[] {
77+
const result: T[] = [];
78+
for (let index = 0; index < maxPkgs; index++) {
79+
result.push(cb(index));
8980
}
90-
];
91-
}
92-
function writePkgReferences() {
93-
system.writeFile(configPath, JSON.stringify({
94-
files: [],
95-
include: [],
96-
references: pkgs(createPkgReference)
97-
}));
98-
}
99-
function verifyInvoke() {
100-
pkgs(() => system.checkTimeoutQueueLengthAndRun(1));
101-
checkOutputErrorsIncremental(system, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [
102-
...flatArray(pkgs(index => [
103-
`Project 'pkg${index}/tsconfig.json' is out of date because oldest output 'pkg${index}/index.js' is older than newest input 'typings/xterm.d.ts'\n\n`,
104-
`Building project '${project}/pkg${index}/tsconfig.json'...\n\n`,
105-
`Updating unchanged output timestamps of project '${project}/pkg${index}/tsconfig.json'...\n\n`
106-
]))
107-
]);
108-
}
109-
});
81+
return result;
82+
}
83+
function createPkgReference(index: number) {
84+
return { path: `./pkg${index}` };
85+
}
86+
function pkgFiles(index: number): File[] {
87+
return [
88+
{
89+
path: `${project}/pkg${index}/index.ts`,
90+
content: `export const pkg${index} = ${index};`
91+
},
92+
{
93+
path: `${project}/pkg${index}/tsconfig.json`,
94+
content: JSON.stringify({
95+
complerOptions: { composite: true },
96+
include: [
97+
"**/*.ts",
98+
"../typings/xterm.d.ts"
99+
]
100+
})
101+
}
102+
];
103+
}
104+
function writePkgReferences() {
105+
system.writeFile(configPath, JSON.stringify({
106+
files: [],
107+
include: [],
108+
references: pkgs(createPkgReference)
109+
}));
110+
}
111+
function verifyInvoke() {
112+
pkgs(() => system.checkTimeoutQueueLengthAndRun(1));
113+
checkOutputErrorsIncremental(system, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [
114+
...flatArray(pkgs(index => [
115+
`Project 'pkg${index}/tsconfig.json' is out of date because oldest output 'pkg${index}/index.js' is older than newest input 'typings/xterm.d.ts'\n\n`,
116+
`Building project '${project}/pkg${index}/tsconfig.json'...\n\n`,
117+
`Updating unchanged output timestamps of project '${project}/pkg${index}/tsconfig.json'...\n\n`
118+
]))
119+
]);
120+
}
121+
});
122+
}
110123
});
111124
}

src/testRunner/unittests/tscWatch/watchEnvironment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace ts.tscWatch {
99
};
1010
const files = [file1, libFile];
1111
const environmentVariables = createMap<string>();
12-
environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling");
12+
environmentVariables.set("TSC_WATCHFILE", TestFSWithWatch.Tsc_WatchFile.DynamicPolling);
1313
const host = createWatchedSystem(files, { environmentVariables });
1414
const watch = createWatchOfFilesAndCompilerOptions([file1.path], host);
1515

0 commit comments

Comments
 (0)