Skip to content

Commit bf903eb

Browse files
authored
Merge pull request microsoft#32613 from microsoft/singleHostFsWatchFile
Create only single StatFileWatcher through node
2 parents 9243415 + b84f13d commit bf903eb

File tree

8 files changed

+330
-45
lines changed

8 files changed

+330
-45
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/compiler/tsbuild.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,8 +564,51 @@ namespace ts {
564564
}
565565

566566
function getBuildOrder(state: SolutionBuilderState) {
567-
return state.buildOrder ||
568-
(state.buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f))));
567+
return state.buildOrder || createStateBuildOrder(state);
568+
}
569+
570+
function createStateBuildOrder(state: SolutionBuilderState) {
571+
const buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)));
572+
if (arrayIsEqualTo(state.buildOrder, buildOrder)) return state.buildOrder!;
573+
574+
// Clear all to ResolvedConfigFilePaths cache to start fresh
575+
state.resolvedConfigFilePaths.clear();
576+
const currentProjects = arrayToSet(
577+
buildOrder,
578+
resolved => toResolvedConfigFilePath(state, resolved)
579+
) as ConfigFileMap<true>;
580+
581+
const noopOnDelete = { onDeleteValue: noop };
582+
// Config file cache
583+
mutateMapSkippingNewValues(state.configFileCache, currentProjects, noopOnDelete);
584+
mutateMapSkippingNewValues(state.projectStatus, currentProjects, noopOnDelete);
585+
mutateMapSkippingNewValues(state.buildInfoChecked, currentProjects, noopOnDelete);
586+
mutateMapSkippingNewValues(state.builderPrograms, currentProjects, noopOnDelete);
587+
mutateMapSkippingNewValues(state.diagnostics, currentProjects, noopOnDelete);
588+
mutateMapSkippingNewValues(state.projectPendingBuild, currentProjects, noopOnDelete);
589+
mutateMapSkippingNewValues(state.projectErrorsReported, currentProjects, noopOnDelete);
590+
591+
// Remove watches for the program no longer in the solution
592+
if (state.watch) {
593+
mutateMapSkippingNewValues(
594+
state.allWatchedConfigFiles,
595+
currentProjects,
596+
{ onDeleteValue: closeFileWatcher }
597+
);
598+
599+
mutateMapSkippingNewValues(
600+
state.allWatchedWildcardDirectories,
601+
currentProjects,
602+
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcherOf) }
603+
);
604+
605+
mutateMapSkippingNewValues(
606+
state.allWatchedInputFiles,
607+
currentProjects,
608+
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) }
609+
);
610+
}
611+
return state.buildOrder = buildOrder;
569612
}
570613

571614
function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined) {

src/compiler/utilities.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4466,8 +4466,7 @@ namespace ts {
44664466
map.clear();
44674467
}
44684468

4469-
export interface MutateMapOptions<T, U> {
4470-
createNewValue(key: string, valueInNewMap: U): T;
4469+
export interface MutateMapSkippingNewValuesOptions<T, U> {
44714470
onDeleteValue(existingValue: T, key: string): void;
44724471

44734472
/**
@@ -4482,8 +4481,12 @@ namespace ts {
44824481
/**
44834482
* Mutates the map with newMap such that keys in map will be same as newMap.
44844483
*/
4485-
export function mutateMap<T, U>(map: Map<T>, newMap: ReadonlyMap<U>, options: MutateMapOptions<T, U>) {
4486-
const { createNewValue, onDeleteValue, onExistingValue } = options;
4484+
export function mutateMapSkippingNewValues<T, U>(
4485+
map: Map<T>,
4486+
newMap: ReadonlyMap<U>,
4487+
options: MutateMapSkippingNewValuesOptions<T, U>
4488+
) {
4489+
const { onDeleteValue, onExistingValue } = options;
44874490
// Needs update
44884491
map.forEach((existingValue, key) => {
44894492
const valueInNewMap = newMap.get(key);
@@ -4497,7 +4500,20 @@ namespace ts {
44974500
onExistingValue(existingValue, valueInNewMap, key);
44984501
}
44994502
});
4503+
}
4504+
4505+
export interface MutateMapOptions<T, U> extends MutateMapSkippingNewValuesOptions<T, U> {
4506+
createNewValue(key: string, valueInNewMap: U): T;
4507+
}
4508+
4509+
/**
4510+
* Mutates the map with newMap such that keys in map will be same as newMap.
4511+
*/
4512+
export function mutateMap<T, U>(map: Map<T>, newMap: ReadonlyMap<U>, options: MutateMapOptions<T, U>) {
4513+
// Needs update
4514+
mutateMapSkippingNewValues(map, newMap, options);
45004515

4516+
const { createNewValue } = options;
45014517
// Add new values that are not already present
45024518
newMap.forEach((valueInNewMap, key) => {
45034519
if (!map.has(key)) {

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 30 additions & 7 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);
@@ -405,7 +424,7 @@ interface Array<T> {}`
405424
return s;
406425
}
407426

408-
private now() {
427+
now() {
409428
this.time += timeIncrements;
410429
return new Date(this.time);
411430
}
@@ -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);

src/testRunner/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
"unittests/semver.ts",
6262
"unittests/shimMap.ts",
6363
"unittests/transform.ts",
64-
"unittests/tsbuildWatchMode.ts",
6564
"unittests/config/commandLineParsing.ts",
6665
"unittests/config/configurationExtension.ts",
6766
"unittests/config/convertCompilerOptionsFromJson.ts",
@@ -103,6 +102,8 @@
103102
"unittests/tsbuild/resolveJsonModule.ts",
104103
"unittests/tsbuild/sample.ts",
105104
"unittests/tsbuild/transitiveReferences.ts",
105+
"unittests/tsbuild/watchEnvironment.ts",
106+
"unittests/tsbuild/watchMode.ts",
106107
"unittests/tscWatch/consoleClearing.ts",
107108
"unittests/tscWatch/emit.ts",
108109
"unittests/tscWatch/emitAndErrorUpdates.ts",
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
namespace ts.tscWatch {
2+
describe("unittests:: tsbuild:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => {
3+
describe("when watchFile can create multiple watchers per file", () => {
4+
verifyWatchFileOnMultipleProjects(/*singleWatchPerFile*/ false);
5+
});
6+
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+
});
13+
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+
};
23+
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+
]);
41+
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();
48+
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();
60+
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);
75+
}
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));
80+
}
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+
}
123+
});
124+
}

0 commit comments

Comments
 (0)