From 1ac980ec96350c9a0893dac33f87528a0cb6c91f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 26 Jul 2019 12:26:07 -0700 Subject: [PATCH 1/4] File move --- src/testRunner/tsconfig.json | 2 +- .../unittests/{tsbuildWatchMode.ts => tsbuild/watchMode.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/testRunner/unittests/{tsbuildWatchMode.ts => tsbuild/watchMode.ts} (98%) diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 659dd426b79..a2ae6e478f3 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -61,7 +61,6 @@ "unittests/semver.ts", "unittests/shimMap.ts", "unittests/transform.ts", - "unittests/tsbuildWatchMode.ts", "unittests/config/commandLineParsing.ts", "unittests/config/configurationExtension.ts", "unittests/config/convertCompilerOptionsFromJson.ts", @@ -103,6 +102,7 @@ "unittests/tsbuild/resolveJsonModule.ts", "unittests/tsbuild/sample.ts", "unittests/tsbuild/transitiveReferences.ts", + "unittests/tsbuild/watchMode.ts", "unittests/tscWatch/consoleClearing.ts", "unittests/tscWatch/emit.ts", "unittests/tscWatch/emitAndErrorUpdates.ts", diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuild/watchMode.ts similarity index 98% rename from src/testRunner/unittests/tsbuildWatchMode.ts rename to src/testRunner/unittests/tsbuild/watchMode.ts index a630e28f1d8..9d052ae4af0 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuild/watchMode.ts @@ -33,7 +33,7 @@ namespace ts.tscWatch { return [f, host.getModifiedTime(f), host.writtenFiles.has(host.toFullPath(f))] as OutputFileStamp; } - describe("unittests:: tsbuild-watch program updates", () => { + describe("unittests:: tsbuild:: watchMode:: program updates", () => { const project = "sample1"; const enum SubProject { core = "core", From 4efcfb7120dd6b6bf5f65b2dea847f343dca91d9 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 26 Jul 2019 12:41:00 -0700 Subject: [PATCH 2/4] Some refactoring --- src/testRunner/unittests/tsbuild/watchMode.ts | 76 ++++++++++++------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/src/testRunner/unittests/tsbuild/watchMode.ts b/src/testRunner/unittests/tsbuild/watchMode.ts index 9d052ae4af0..4546e4e75c9 100644 --- a/src/testRunner/unittests/tsbuild/watchMode.ts +++ b/src/testRunner/unittests/tsbuild/watchMode.ts @@ -113,13 +113,33 @@ namespace ts.tscWatch { } } - const core = subProjectFiles(SubProject.core, /*anotherModuleAndSomeDecl*/ true); - const logic = subProjectFiles(SubProject.logic); - const tests = subProjectFiles(SubProject.tests); - const ui = subProjectFiles(SubProject.ui); - const allFiles: ReadonlyArray = [libFile, ...core, ...logic, ...tests, ...ui]; - const testProjectExpectedWatchedFiles = [core[0], core[1], core[2]!, ...logic, ...tests].map(f => f.path.toLowerCase()); // tslint:disable-line no-unnecessary-type-assertion (TODO: type assertion should be necessary) - const testProjectExpectedWatchedDirectoriesRecursive = [projectPath(SubProject.core), projectPath(SubProject.logic)]; + let core: SubProjectFiles; + let logic: SubProjectFiles; + let tests: SubProjectFiles; + let ui: SubProjectFiles; + let allFiles: ReadonlyArray; + let testProjectExpectedWatchedFiles: string[]; + let testProjectExpectedWatchedDirectoriesRecursive: string[]; + + before(() => { + core = subProjectFiles(SubProject.core, /*anotherModuleAndSomeDecl*/ true); + logic = subProjectFiles(SubProject.logic); + tests = subProjectFiles(SubProject.tests); + ui = subProjectFiles(SubProject.ui); + allFiles = [libFile, ...core, ...logic, ...tests, ...ui]; + testProjectExpectedWatchedFiles = [core[0], core[1], core[2]!, ...logic, ...tests].map(f => f.path.toLowerCase()); // tslint:disable-line no-unnecessary-type-assertion (TODO: type assertion should be necessary) + testProjectExpectedWatchedDirectoriesRecursive = [projectPath(SubProject.core), projectPath(SubProject.logic)]; + }); + + after(() => { + core = undefined!; + logic = undefined!; + tests = undefined!; + ui = undefined!; + allFiles = undefined!; + testProjectExpectedWatchedFiles = undefined!; + testProjectExpectedWatchedDirectoriesRecursive = undefined!; + }); function createSolutionInWatchMode(allFiles: ReadonlyArray, defaultOptions?: BuildOptions, disableConsoleClears?: boolean) { const host = createTsBuildWatchSystem(allFiles, { currentDirectory: projectsLocation }); @@ -172,9 +192,9 @@ namespace ts.tscWatch { content: `export const newFileConst = 30;` }; - function verifyProjectChanges(allFiles: ReadonlyArray) { + function verifyProjectChanges(allFilesGetter: () => ReadonlyArray) { function createSolutionInWatchModeToVerifyChanges(additionalFiles?: ReadonlyArray<[SubProject, string]>) { - const host = createSolutionInWatchMode(allFiles); + const host = createSolutionInWatchMode(allFilesGetter()); return { host, verifyChangeWithFile, verifyChangeAfterTimeout, verifyWatches }; function verifyChangeWithFile(fileName: string, content: string, local?: boolean) { @@ -277,19 +297,21 @@ export class someClass2 { }`); } describe("with simple project reference graph", () => { - verifyProjectChanges(allFiles); + verifyProjectChanges(() => allFiles); }); describe("with circular project reference", () => { - const [coreTsconfig, ...otherCoreFiles] = core; - const circularCoreConfig: File = { - path: coreTsconfig.path, - content: JSON.stringify({ - compilerOptions: { composite: true, declaration: true }, - references: [{ path: "../tests", circular: true }] - }) - }; - verifyProjectChanges([libFile, circularCoreConfig, ...otherCoreFiles, ...logic, ...tests]); + verifyProjectChanges(() => { + const [coreTsconfig, ...otherCoreFiles] = core; + const circularCoreConfig: File = { + path: coreTsconfig.path, + content: JSON.stringify({ + compilerOptions: { composite: true, declaration: true }, + references: [{ path: "../tests", circular: true }] + }) + }; + return [libFile, circularCoreConfig, ...otherCoreFiles, ...logic, ...tests]; + }); }); }); @@ -681,9 +703,9 @@ let x: string = 10;`); const coreIndexDts = projectFileName(SubProject.core, "index.d.ts"); const coreAnotherModuleDts = projectFileName(SubProject.core, "anotherModule.d.ts"); const logicIndexDts = projectFileName(SubProject.logic, "index.d.ts"); - const expectedWatchedFiles = [core[0], logic[0], ...tests, libFile].map(f => f.path).concat([coreIndexDts, coreAnotherModuleDts, logicIndexDts].map(f => f.toLowerCase())); + const expectedWatchedFiles = () => [core[0], logic[0], ...tests, libFile].map(f => f.path).concat([coreIndexDts, coreAnotherModuleDts, logicIndexDts].map(f => f.toLowerCase())); const expectedWatchedDirectoriesRecursive = projectSystem.getTypeRootsFromLocation(projectPath(SubProject.tests)); - const expectedProgramFiles = [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, logicIndexDts]; + const expectedProgramFiles = () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, logicIndexDts]; function createSolutionAndWatchMode() { return createSolutionAndWatchModeOfProject(allFiles, projectsLocation, `${project}/${SubProject.tests}`, tests[0].path, getOutputFileStamps); @@ -694,12 +716,12 @@ let x: string = 10;`); } function verifyWatches(host: TsBuildWatchSystem, withTsserver?: boolean) { - verifyWatchesOfProject(host, withTsserver ? expectedWatchedFiles.filter(f => f !== tests[1].path.toLowerCase()) : expectedWatchedFiles, expectedWatchedDirectoriesRecursive); + verifyWatchesOfProject(host, withTsserver ? expectedWatchedFiles().filter(f => f !== tests[1].path.toLowerCase()) : expectedWatchedFiles(), expectedWatchedDirectoriesRecursive); } function verifyScenario( edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, - expectedFilesAfterEdit: ReadonlyArray + expectedFilesAfterEdit: () => ReadonlyArray ) { it("with tsc-watch", () => { const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); @@ -708,7 +730,7 @@ let x: string = 10;`); host.checkTimeoutQueueLengthAndRun(1); checkOutputErrorsIncremental(host, emptyArray); - checkProgramActualFiles(watch(), expectedFilesAfterEdit); + checkProgramActualFiles(watch(), expectedFilesAfterEdit()); }); @@ -718,7 +740,7 @@ let x: string = 10;`); edit(host, solutionBuilder); host.checkTimeoutQueueLengthAndRun(2); - checkProjectActualFiles(service, tests[0].path, [tests[0].path, ...expectedFilesAfterEdit]); + checkProjectActualFiles(service, tests[0].path, [tests[0].path, ...expectedFilesAfterEdit()]); }); } @@ -729,7 +751,7 @@ let x: string = 10;`); verifyDependencies(watch, coreIndexDts, [coreIndexDts]); verifyDependencies(watch, coreAnotherModuleDts, [coreAnotherModuleDts]); verifyDependencies(watch, logicIndexDts, [logicIndexDts, coreAnotherModuleDts]); - verifyDependencies(watch, tests[1].path, expectedProgramFiles.filter(f => f !== libFile.path)); + verifyDependencies(watch, tests[1].path, expectedProgramFiles().filter(f => f !== libFile.path)); }); it("with tsserver", () => { @@ -769,7 +791,7 @@ export function gfoo() { })); solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath, ConfigFileProgramReloadLevel.Full); solutionBuilder.buildNextInvalidatedProject(); - }, [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")]); + }, () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")]); }); }); From 2db8a13d815822149da7b506ca2ae21e3b7f0f1f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 26 Jul 2019 14:06:33 -0700 Subject: [PATCH 3/4] Remove project status, watches etc when project is no longer part of build order --- src/compiler/tsbuild.ts | 47 +++++++- src/compiler/utilities.ts | 24 +++- src/harness/virtualFileSystemWithWatch.ts | 2 +- src/testRunner/tsconfig.json | 1 + .../unittests/tsbuild/watchEnvironment.ts | 111 ++++++++++++++++++ src/testRunner/unittests/tsbuild/watchMode.ts | 8 ++ 6 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 src/testRunner/unittests/tsbuild/watchEnvironment.ts diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 29911f42906..ba051ac23b2 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -564,8 +564,51 @@ namespace ts { } function getBuildOrder(state: SolutionBuilderState) { - return state.buildOrder || - (state.buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)))); + return state.buildOrder || createStateBuildOrder(state); + } + + function createStateBuildOrder(state: SolutionBuilderState) { + const buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f))); + if (arrayIsEqualTo(state.buildOrder, buildOrder)) return state.buildOrder!; + + // Clear all to ResolvedConfigFilePaths cache to start fresh + state.resolvedConfigFilePaths.clear(); + const currentProjects = arrayToSet( + buildOrder, + resolved => toResolvedConfigFilePath(state, resolved) + ) as ConfigFileMap; + + const noopOnDelete = { onDeleteValue: noop }; + // Config file cache + mutateMapSkippingNewValues(state.configFileCache, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectStatus, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.buildInfoChecked, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.builderPrograms, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.diagnostics, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectPendingBuild, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectErrorsReported, currentProjects, noopOnDelete); + + // Remove watches for the program no longer in the solution + if (state.watch) { + mutateMapSkippingNewValues( + state.allWatchedConfigFiles, + currentProjects, + { onDeleteValue: closeFileWatcher } + ); + + mutateMapSkippingNewValues( + state.allWatchedWildcardDirectories, + currentProjects, + { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcherOf) } + ); + + mutateMapSkippingNewValues( + state.allWatchedInputFiles, + currentProjects, + { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) } + ); + } + return state.buildOrder = buildOrder; } function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index d13e12a6f19..aa6e974c8e8 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4466,8 +4466,7 @@ namespace ts { map.clear(); } - export interface MutateMapOptions { - createNewValue(key: string, valueInNewMap: U): T; + export interface MutateMapSkippingNewValuesOptions { onDeleteValue(existingValue: T, key: string): void; /** @@ -4482,8 +4481,12 @@ namespace ts { /** * Mutates the map with newMap such that keys in map will be same as newMap. */ - export function mutateMap(map: Map, newMap: ReadonlyMap, options: MutateMapOptions) { - const { createNewValue, onDeleteValue, onExistingValue } = options; + export function mutateMapSkippingNewValues( + map: Map, + newMap: ReadonlyMap, + options: MutateMapSkippingNewValuesOptions + ) { + const { onDeleteValue, onExistingValue } = options; // Needs update map.forEach((existingValue, key) => { const valueInNewMap = newMap.get(key); @@ -4497,7 +4500,20 @@ namespace ts { onExistingValue(existingValue, valueInNewMap, key); } }); + } + export interface MutateMapOptions extends MutateMapSkippingNewValuesOptions { + createNewValue(key: string, valueInNewMap: U): T; + } + + /** + * Mutates the map with newMap such that keys in map will be same as newMap. + */ + export function mutateMap(map: Map, newMap: ReadonlyMap, options: MutateMapOptions) { + // Needs update + mutateMapSkippingNewValues(map, newMap, options); + + const { createNewValue } = options; // Add new values that are not already present newMap.forEach((valueInNewMap, key) => { if (!map.has(key)) { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index b8e3d189781..0535d832374 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -405,7 +405,7 @@ interface Array {}` return s; } - private now() { + now() { this.time += timeIncrements; return new Date(this.time); } diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index a2ae6e478f3..7d0cea1ea64 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -102,6 +102,7 @@ "unittests/tsbuild/resolveJsonModule.ts", "unittests/tsbuild/sample.ts", "unittests/tsbuild/transitiveReferences.ts", + "unittests/tsbuild/watchEnvironment.ts", "unittests/tsbuild/watchMode.ts", "unittests/tscWatch/consoleClearing.ts", "unittests/tscWatch/emit.ts", diff --git a/src/testRunner/unittests/tsbuild/watchEnvironment.ts b/src/testRunner/unittests/tsbuild/watchEnvironment.ts new file mode 100644 index 00000000000..f97c92a89d4 --- /dev/null +++ b/src/testRunner/unittests/tsbuild/watchEnvironment.ts @@ -0,0 +1,111 @@ +namespace ts.tscWatch { + describe("unittests:: tsbuild:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => { + it("watchFile on same file multiple times because file is part of multiple projects", () => { + const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`; + let maxPkgs = 4; + const configPath = `${project}/tsconfig.json`; + const typing: File = { + path: `${project}/typings/xterm.d.ts`, + content: "export const typing = 10;" + }; + + const allPkgFiles = pkgs(pkgFiles); + const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project }); + writePkgReferences(); + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true }); + solutionBuilder.build(); + checkOutputErrorsInitial(system, emptyArray, /*disableConsoleClears*/ undefined, [ + `Projects in this build: \r\n${ + concatenate( + pkgs(index => ` * pkg${index}/tsconfig.json`), + [" * tsconfig.json"] + ).join("\r\n")}\n\n`, + ...flatArray(pkgs(index => [ + `Project 'pkg${index}/tsconfig.json' is out of date because output file 'pkg${index}/index.js' does not exist\n\n`, + `Building project '${project}/pkg${index}/tsconfig.json'...\n\n` + ])) + ]); + + const watchFilesDetailed = arrayToMap(flatArray(allPkgFiles), f => f.path, () => 1); + watchFilesDetailed.set(configPath, 1); + watchFilesDetailed.set(typing.path, maxPkgs); + checkWatchedFilesDetailed(system, watchFilesDetailed); + system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); + verifyInvoke(); + + // Make change + maxPkgs--; + writePkgReferences(); + system.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(system, emptyArray); + const lastFiles = last(allPkgFiles); + lastFiles.forEach(f => watchFilesDetailed.delete(f.path)); + watchFilesDetailed.set(typing.path, maxPkgs); + checkWatchedFilesDetailed(system, watchFilesDetailed); + system.writeFile(typing.path, typing.content); + verifyInvoke(); + + // Make change to remove all the watches + maxPkgs = 0; + writePkgReferences(); + system.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(system, [ + `tsconfig.json(1,10): error TS18002: The 'files' list in config file '${configPath}' is empty.\n` + ]); + checkWatchedFilesDetailed(system, [configPath], 1); + + system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); + system.checkTimeoutQueueLength(0); + + function flatArray(arr: T[][]): readonly T[] { + return flatMap(arr, identity); + } + function pkgs(cb: (index: number) => T): T[] { + const result: T[] = []; + for (let index = 0; index < maxPkgs; index++) { + result.push(cb(index)); + } + return result; + } + function createPkgReference(index: number) { + return { path: `./pkg${index}` }; + } + function pkgFiles(index: number): File[] { + return [ + { + path: `${project}/pkg${index}/index.ts`, + content: `export const pkg${index} = ${index};` + }, + { + path: `${project}/pkg${index}/tsconfig.json`, + content: JSON.stringify({ + complerOptions: { composite: true }, + include: [ + "**/*.ts", + "../typings/xterm.d.ts" + ] + }) + } + ]; + } + function writePkgReferences() { + system.writeFile(configPath, JSON.stringify({ + files: [], + include: [], + references: pkgs(createPkgReference) + })); + } + function verifyInvoke() { + pkgs(() => system.checkTimeoutQueueLengthAndRun(1)); + checkOutputErrorsIncremental(system, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [ + ...flatArray(pkgs(index => [ + `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`, + `Building project '${project}/pkg${index}/tsconfig.json'...\n\n`, + `Updating unchanged output timestamps of project '${project}/pkg${index}/tsconfig.json'...\n\n` + ])) + ]); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsbuild/watchMode.ts b/src/testRunner/unittests/tsbuild/watchMode.ts index 4546e4e75c9..bd7653b6bea 100644 --- a/src/testRunner/unittests/tsbuild/watchMode.ts +++ b/src/testRunner/unittests/tsbuild/watchMode.ts @@ -16,11 +16,19 @@ namespace ts.tscWatch { return host; } + export function createSolutionBuilder(system: WatchedSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { const host = createSolutionBuilderHost(system); + host.now = system.now.bind(system); return ts.createSolutionBuilder(host, rootNames, defaultOptions || {}); } + export function createSolutionBuilderWithWatchHost(system: WatchedSystem) { + const host = ts.createSolutionBuilderWithWatchHost(system); + host.now = system.now.bind(system); + return host; + } + function createSolutionBuilderWithWatch(system: TsBuildWatchSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { const host = createSolutionBuilderWithWatchHost(system); const solutionBuilder = ts.createSolutionBuilderWithWatch(host, rootNames, defaultOptions || { watch: true }); From b84f13d7cf10b5cdbfd1510fdd9c2e75d9543251 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 29 Jul 2019 15:32:53 -0700 Subject: [PATCH 4/4] Use single stats watcher per filename Fixes #28690 --- src/compiler/sys.ts | 52 ++++- src/harness/virtualFileSystemWithWatch.ts | 35 ++- .../unittests/tsbuild/watchEnvironment.ts | 215 ++++++++++-------- .../unittests/tscWatch/watchEnvironment.ts | 2 +- 4 files changed, 194 insertions(+), 110 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 7049821c90f..50080e0da7b 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -304,6 +304,53 @@ namespace ts { } } + /* @internal */ + export function createSingleFileWatcherPerName( + watchFile: HostWatchFile, + useCaseSensitiveFileNames: boolean + ): HostWatchFile { + interface SingleFileWatcher { + watcher: FileWatcher; + refCount: number; + } + const cache = createMap(); + const callbacksCache = createMultiMap(); + const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + + return (fileName, callback, pollingInterval) => { + const path = toCanonicalFileName(fileName); + const existing = cache.get(path); + if (existing) { + existing.refCount++; + } + else { + cache.set(path, { + watcher: watchFile( + fileName, + (fileName, eventKind) => forEach( + callbacksCache.get(path), + cb => cb(fileName, eventKind) + ), + pollingInterval + ), + refCount: 1 + }); + } + callbacksCache.add(path, callback); + + return { + close: () => { + const watcher = Debug.assertDefined(cache.get(path)); + callbacksCache.remove(path, callback); + watcher.refCount--; + if (watcher.refCount) return; + cache.delete(path); + closeFileWatcherOf(watcher); + } + }; + }; + } + /** * Returns true if file status changed */ @@ -695,6 +742,7 @@ namespace ts { const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; const tscWatchFile = process.env.TSC_WATCHFILE; const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY; + const fsWatchFile = createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames); let dynamicPollingWatchFile: HostWatchFile | undefined; const nodeSystem: System = { args: process.argv.slice(2), @@ -835,7 +883,7 @@ namespace ts { return useNonPollingWatchers ? createNonPollingWatchFile() : // Default to do not use polling interval as it is before this experiment branch - (fileName, callback) => fsWatchFile(fileName, callback); + (fileName, callback) => fsWatchFile(fileName, callback, /*pollingInterval*/ undefined); } function getWatchDirectory(): HostWatchDirectory { @@ -916,7 +964,7 @@ namespace ts { } } - function fsWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { + function fsWatchFileWorker(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { _fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged); let eventKind: FileWatcherEventKind; return { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 0535d832374..13b87e1c91a 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -314,6 +314,11 @@ interface Array {}` invokeFileDeleteCreateAsPartInsteadOfChange: boolean; } + export enum Tsc_WatchFile { + DynamicPolling = "DynamicPriorityPolling", + SingleFileWatcherPerName = "SingleFileWatcherPerName" + } + export enum Tsc_WatchDirectory { WatchFile = "RecursiveDirectoryUsingFsWatchFile", NonRecursiveWatchDirectory = "RecursiveDirectoryUsingNonRecursiveWatchDirectory", @@ -339,7 +344,7 @@ interface Array {}` readonly watchedFiles = createMultiMap(); private readonly executingFilePath: string; private readonly currentDirectory: string; - private readonly dynamicPriorityWatchFile: HostWatchFile | undefined; + private readonly customWatchFile: HostWatchFile | undefined; private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined; public require: ((initialPath: string, moduleName: string) => server.RequireResult) | undefined; @@ -349,9 +354,23 @@ interface Array {}` this.executingFilePath = this.getHostSpecificPath(executingFilePath); this.currentDirectory = this.getHostSpecificPath(currentDirectory); this.reloadFS(fileOrFolderorSymLinkList); - this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ? - createDynamicPriorityPollingWatchFile(this) : - undefined; + const tscWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") as Tsc_WatchFile; + switch (tscWatchFile) { + case Tsc_WatchFile.DynamicPolling: + this.customWatchFile = createDynamicPriorityPollingWatchFile(this); + break; + case Tsc_WatchFile.SingleFileWatcherPerName: + this.customWatchFile = createSingleFileWatcherPerName( + this.watchFileWorker.bind(this), + this.useCaseSensitiveFileNames + ); + break; + case undefined: + break; + default: + Debug.assertNever(tscWatchFile); + } + const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY") as Tsc_WatchDirectory; if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium); @@ -854,10 +873,14 @@ interface Array {}` } watchFile(fileName: string, cb: FileWatcherCallback, pollingInterval: number) { - if (this.dynamicPriorityWatchFile) { - return this.dynamicPriorityWatchFile(fileName, cb, pollingInterval); + if (this.customWatchFile) { + return this.customWatchFile(fileName, cb, pollingInterval); } + return this.watchFileWorker(fileName, cb); + } + + private watchFileWorker(fileName: string, cb: FileWatcherCallback) { const path = this.toFullPath(fileName); const callback: TestFileWatcher = { fileName, cb }; this.watchedFiles.add(path, callback); diff --git a/src/testRunner/unittests/tsbuild/watchEnvironment.ts b/src/testRunner/unittests/tsbuild/watchEnvironment.ts index f97c92a89d4..d20dd7a0022 100644 --- a/src/testRunner/unittests/tsbuild/watchEnvironment.ts +++ b/src/testRunner/unittests/tsbuild/watchEnvironment.ts @@ -1,111 +1,124 @@ namespace ts.tscWatch { describe("unittests:: tsbuild:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => { - it("watchFile on same file multiple times because file is part of multiple projects", () => { - const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`; - let maxPkgs = 4; - const configPath = `${project}/tsconfig.json`; - const typing: File = { - path: `${project}/typings/xterm.d.ts`, - content: "export const typing = 10;" - }; + describe("when watchFile can create multiple watchers per file", () => { + verifyWatchFileOnMultipleProjects(/*singleWatchPerFile*/ false); + }); - const allPkgFiles = pkgs(pkgFiles); - const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project }); - writePkgReferences(); - const host = createSolutionBuilderWithWatchHost(system); - const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true }); - solutionBuilder.build(); - checkOutputErrorsInitial(system, emptyArray, /*disableConsoleClears*/ undefined, [ - `Projects in this build: \r\n${ - concatenate( - pkgs(index => ` * pkg${index}/tsconfig.json`), - [" * tsconfig.json"] - ).join("\r\n")}\n\n`, - ...flatArray(pkgs(index => [ - `Project 'pkg${index}/tsconfig.json' is out of date because output file 'pkg${index}/index.js' does not exist\n\n`, - `Building project '${project}/pkg${index}/tsconfig.json'...\n\n` - ])) - ]); + describe("when watchFile is single watcher per file", () => { + verifyWatchFileOnMultipleProjects( + /*singleWatchPerFile*/ true, + arrayToMap(["TSC_WATCHFILE"], identity, () => TestFSWithWatch.Tsc_WatchFile.SingleFileWatcherPerName) + ); + }); - const watchFilesDetailed = arrayToMap(flatArray(allPkgFiles), f => f.path, () => 1); - watchFilesDetailed.set(configPath, 1); - watchFilesDetailed.set(typing.path, maxPkgs); - checkWatchedFilesDetailed(system, watchFilesDetailed); - system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); - verifyInvoke(); + function verifyWatchFileOnMultipleProjects(singleWatchPerFile: boolean, environmentVariables?: Map) { + it("watchFile on same file multiple times because file is part of multiple projects", () => { + const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`; + let maxPkgs = 4; + const configPath = `${project}/tsconfig.json`; + const typing: File = { + path: `${project}/typings/xterm.d.ts`, + content: "export const typing = 10;" + }; - // Make change - maxPkgs--; - writePkgReferences(); - system.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(system, emptyArray); - const lastFiles = last(allPkgFiles); - lastFiles.forEach(f => watchFilesDetailed.delete(f.path)); - watchFilesDetailed.set(typing.path, maxPkgs); - checkWatchedFilesDetailed(system, watchFilesDetailed); - system.writeFile(typing.path, typing.content); - verifyInvoke(); - - // Make change to remove all the watches - maxPkgs = 0; - writePkgReferences(); - system.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(system, [ - `tsconfig.json(1,10): error TS18002: The 'files' list in config file '${configPath}' is empty.\n` - ]); - checkWatchedFilesDetailed(system, [configPath], 1); - - system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); - system.checkTimeoutQueueLength(0); - - function flatArray(arr: T[][]): readonly T[] { - return flatMap(arr, identity); - } - function pkgs(cb: (index: number) => T): T[] { - const result: T[] = []; - for (let index = 0; index < maxPkgs; index++) { - result.push(cb(index)); - } - return result; - } - function createPkgReference(index: number) { - return { path: `./pkg${index}` }; - } - function pkgFiles(index: number): File[] { - return [ - { - path: `${project}/pkg${index}/index.ts`, - content: `export const pkg${index} = ${index};` - }, - { - path: `${project}/pkg${index}/tsconfig.json`, - content: JSON.stringify({ - complerOptions: { composite: true }, - include: [ - "**/*.ts", - "../typings/xterm.d.ts" - ] - }) - } - ]; - } - function writePkgReferences() { - system.writeFile(configPath, JSON.stringify({ - files: [], - include: [], - references: pkgs(createPkgReference) - })); - } - function verifyInvoke() { - pkgs(() => system.checkTimeoutQueueLengthAndRun(1)); - checkOutputErrorsIncremental(system, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [ + const allPkgFiles = pkgs(pkgFiles); + const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project, environmentVariables }); + writePkgReferences(); + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true }); + solutionBuilder.build(); + checkOutputErrorsInitial(system, emptyArray, /*disableConsoleClears*/ undefined, [ + `Projects in this build: \r\n${ + concatenate( + pkgs(index => ` * pkg${index}/tsconfig.json`), + [" * tsconfig.json"] + ).join("\r\n")}\n\n`, ...flatArray(pkgs(index => [ - `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`, - `Building project '${project}/pkg${index}/tsconfig.json'...\n\n`, - `Updating unchanged output timestamps of project '${project}/pkg${index}/tsconfig.json'...\n\n` + `Project 'pkg${index}/tsconfig.json' is out of date because output file 'pkg${index}/index.js' does not exist\n\n`, + `Building project '${project}/pkg${index}/tsconfig.json'...\n\n` ])) ]); - } - }); + + const watchFilesDetailed = arrayToMap(flatArray(allPkgFiles), f => f.path, () => 1); + watchFilesDetailed.set(configPath, 1); + watchFilesDetailed.set(typing.path, singleWatchPerFile ? 1 : maxPkgs); + checkWatchedFilesDetailed(system, watchFilesDetailed); + system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); + verifyInvoke(); + + // Make change + maxPkgs--; + writePkgReferences(); + system.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(system, emptyArray); + const lastFiles = last(allPkgFiles); + lastFiles.forEach(f => watchFilesDetailed.delete(f.path)); + watchFilesDetailed.set(typing.path, singleWatchPerFile ? 1 : maxPkgs); + checkWatchedFilesDetailed(system, watchFilesDetailed); + system.writeFile(typing.path, typing.content); + verifyInvoke(); + + // Make change to remove all the watches + maxPkgs = 0; + writePkgReferences(); + system.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(system, [ + `tsconfig.json(1,10): error TS18002: The 'files' list in config file '${configPath}' is empty.\n` + ]); + checkWatchedFilesDetailed(system, [configPath], 1); + + system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); + system.checkTimeoutQueueLength(0); + + function flatArray(arr: T[][]): readonly T[] { + return flatMap(arr, identity); + } + function pkgs(cb: (index: number) => T): T[] { + const result: T[] = []; + for (let index = 0; index < maxPkgs; index++) { + result.push(cb(index)); + } + return result; + } + function createPkgReference(index: number) { + return { path: `./pkg${index}` }; + } + function pkgFiles(index: number): File[] { + return [ + { + path: `${project}/pkg${index}/index.ts`, + content: `export const pkg${index} = ${index};` + }, + { + path: `${project}/pkg${index}/tsconfig.json`, + content: JSON.stringify({ + complerOptions: { composite: true }, + include: [ + "**/*.ts", + "../typings/xterm.d.ts" + ] + }) + } + ]; + } + function writePkgReferences() { + system.writeFile(configPath, JSON.stringify({ + files: [], + include: [], + references: pkgs(createPkgReference) + })); + } + function verifyInvoke() { + pkgs(() => system.checkTimeoutQueueLengthAndRun(1)); + checkOutputErrorsIncremental(system, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [ + ...flatArray(pkgs(index => [ + `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`, + `Building project '${project}/pkg${index}/tsconfig.json'...\n\n`, + `Updating unchanged output timestamps of project '${project}/pkg${index}/tsconfig.json'...\n\n` + ])) + ]); + } + }); + } }); } diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index d30e5854652..ad07d8d1610 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -9,7 +9,7 @@ namespace ts.tscWatch { }; const files = [file1, libFile]; const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); + environmentVariables.set("TSC_WATCHFILE", TestFSWithWatch.Tsc_WatchFile.DynamicPolling); const host = createWatchedSystem(files, { environmentVariables }); const watch = createWatchOfFilesAndCompilerOptions([file1.path], host);