From f6f465280d99197bfa828b02533afd2bf0bfbcea Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 20 Nov 2017 14:26:09 -0800 Subject: [PATCH 01/23] Fix the error callback of the present directory watcher --- src/compiler/sys.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 841dba8a52834..052a50ab0a9bf 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -291,12 +291,11 @@ namespace ts { callback ); dirWatcher.on("error", () => { - if (!directoryExists(directoryName)) { - // Deleting directory - watcher = watchMissingDirectory(); - // Call the callback for current directory - callback("rename", ""); - } + // Watch the missing directory + watcher.close(); + watcher = watchMissingDirectory(); + // Call the callback for current directory + callback("rename", ""); }); return dirWatcher; } From ed5673c13c98fc14e9e94db5534a84ff144fb1a7 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 20 Nov 2017 16:34:48 -0800 Subject: [PATCH 02/23] Simplify and correct the fsWatchFile callback handling with the stats invoked --- src/compiler/sys.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 052a50ab0a9bf..4fe1d8ed95413 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -232,26 +232,28 @@ namespace ts { function fsWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { _fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged); + let eventKind: FileWatcherEventKind; return { close: () => _fs.unwatchFile(fileName, fileChanged) }; function fileChanged(curr: any, prev: any) { - const isCurrZero = +curr.mtime === 0; - const isPrevZero = +prev.mtime === 0; - const created = !isCurrZero && isPrevZero; - const deleted = isCurrZero && !isPrevZero; - - const eventKind = created - ? FileWatcherEventKind.Created - : deleted - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; - - if (eventKind === FileWatcherEventKind.Changed && +curr.mtime <= +prev.mtime) { + if (+curr.mtime === 0) { + eventKind = FileWatcherEventKind.Deleted; + } + // previous event kind check is to ensure we send created event when file is restored or renamed twice (that is it disappears and reappears) + // since in that case the prevTime returned is same as prev time of event when file was deleted as per node documentation + else if (+prev.mtime === 0 || eventKind === FileWatcherEventKind.Deleted) { + eventKind = FileWatcherEventKind.Created; + } + // If there is no change in modified time, ignore the event + else if (+curr.mtime === +prev.mtime) { return; } - + else { + // File changed + eventKind = FileWatcherEventKind.Changed; + } callback(fileName, eventKind); } } From ca52f089da67b0b7b0cf28cda75b6f8f1c654fba Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 20 Nov 2017 17:14:59 -0800 Subject: [PATCH 03/23] Rearrange the code to have functions after executing code --- src/compiler/sys.ts | 266 ++++++++++++++++++++++---------------------- 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 4fe1d8ed95413..4920da700cc54 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -134,7 +134,140 @@ namespace ts { const _os = require("os"); const _crypto = require("crypto"); + const nodeVersion = getNodeMajorVersion(); + const isNode4OrLater = nodeVersion >= 4; + + const platform: string = _os.platform(); + const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + + const enum FileSystemEntryKind { + File, + Directory + } + const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; + const watchedFileSet = createWatchedFileSet(); + + const nodeSystem: System = { + args: process.argv.slice(2), + newLine: _os.EOL, + useCaseSensitiveFileNames, + write(s: string): void { + process.stdout.write(s); + }, + readFile, + writeFile, + watchFile: (fileName, callback, pollingInterval) => { + if (useNonPollingWatchers) { + const watchedFile = watchedFileSet.addFile(fileName, callback); + return { + close: () => watchedFileSet.removeFile(watchedFile) + }; + } + else { + return fsWatchFile(fileName, callback, pollingInterval); + } + }, + watchDirectory: (directoryName, callback, recursive) => { + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + return fsWatchDirectory(directoryName, (eventName, relativeFileName) => { + // In watchDirectory we only care about adding and removing files (when event name is + // "rename"); changes made within files are handled by corresponding fileWatchers (when + // event name is "change") + if (eventName === "rename") { + // When deleting a file, the passed baseFileName is null + callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); + } + }, recursive); + }, + resolvePath: path => _path.resolve(path), + fileExists, + directoryExists, + createDirectory(directoryName: string) { + if (!nodeSystem.directoryExists(directoryName)) { + _fs.mkdirSync(directoryName); + } + }, + getExecutingFilePath() { + return __filename; + }, + getCurrentDirectory() { + return process.cwd(); + }, + getDirectories, + getEnvironmentVariable(name: string) { + return process.env[name] || ""; + }, + readDirectory, + getModifiedTime(path) { + try { + return _fs.statSync(path).mtime; + } + catch (e) { + return undefined; + } + }, + createHash(data) { + const hash = _crypto.createHash("md5"); + hash.update(data); + return hash.digest("hex"); + }, + getMemoryUsage() { + if (global.gc) { + global.gc(); + } + return process.memoryUsage().heapUsed; + }, + getFileSize(path) { + try { + const stat = _fs.statSync(path); + if (stat.isFile()) { + return stat.size; + } + } + catch { /*ignore*/ } + return 0; + }, + exit(exitCode?: number): void { + process.exit(exitCode); + }, + realpath(path: string): string { + return _fs.realpathSync(path); + }, + debugMode: some(process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), + tryEnableSourceMapsForHost() { + try { + require("source-map-support").install(); + } + catch { + // Could not enable source maps. + } + }, + setTimeout, + clearTimeout, + clearScreen: () => { + process.stdout.write("\x1Bc"); + } + }; + return nodeSystem; + + function isFileSystemCaseSensitive(): boolean { + // win32\win64 are case insensitive platforms + if (platform === "win32" || platform === "win64") { + return false; + } + // If this file exists under a different case, we must be case-insensitve. + return !fileExists(swapCase(__filename)); + } + + /** Convert all lowercase chars to uppercase, and vice-versa */ + function swapCase(s: string): string { + return s.replace(/\w/g, (ch) => { + const up = ch.toUpperCase(); + return ch === up ? ch.toLowerCase() : up; + }); + } function createWatchedFileSet() { const dirWatchers = createMap(); @@ -205,30 +338,6 @@ namespace ts { } } } - const watchedFileSet = createWatchedFileSet(); - - const nodeVersion = getNodeMajorVersion(); - const isNode4OrLater = nodeVersion >= 4; - - function isFileSystemCaseSensitive(): boolean { - // win32\win64 are case insensitive platforms - if (platform === "win32" || platform === "win64") { - return false; - } - // If this file exists under a different case, we must be case-insensitve. - return !fileExists(swapCase(__filename)); - } - - /** Convert all lowercase chars to uppercase, and vice-versa */ - function swapCase(s: string): string { - return s.replace(/\w/g, (ch) => { - const up = ch.toUpperCase(); - return ch === up ? ch.toLowerCase() : up; - }); - } - - const platform: string = _os.platform(); - const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); function fsWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { _fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged); @@ -407,11 +516,6 @@ namespace ts { return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries); } - const enum FileSystemEntryKind { - File, - Directory - } - function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { try { const stat = _fs.statSync(path); @@ -436,110 +540,6 @@ namespace ts { function getDirectories(path: string): string[] { return filter(_fs.readdirSync(path), dir => fileSystemEntryExists(combinePaths(path, dir), FileSystemEntryKind.Directory)); } - - const nodeSystem: System = { - clearScreen: () => { - process.stdout.write("\x1Bc"); - }, - args: process.argv.slice(2), - newLine: _os.EOL, - useCaseSensitiveFileNames, - write(s: string): void { - process.stdout.write(s); - }, - readFile, - writeFile, - watchFile: (fileName, callback, pollingInterval) => { - if (useNonPollingWatchers) { - const watchedFile = watchedFileSet.addFile(fileName, callback); - return { - close: () => watchedFileSet.removeFile(watchedFile) - }; - } - else { - return fsWatchFile(fileName, callback, pollingInterval); - } - }, - watchDirectory: (directoryName, callback, recursive) => { - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - return fsWatchDirectory(directoryName, (eventName, relativeFileName) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); - } - }, recursive); - }, - resolvePath: path => _path.resolve(path), - fileExists, - directoryExists, - createDirectory(directoryName: string) { - if (!nodeSystem.directoryExists(directoryName)) { - _fs.mkdirSync(directoryName); - } - }, - getExecutingFilePath() { - return __filename; - }, - getCurrentDirectory() { - return process.cwd(); - }, - getDirectories, - getEnvironmentVariable(name: string) { - return process.env[name] || ""; - }, - readDirectory, - getModifiedTime(path) { - try { - return _fs.statSync(path).mtime; - } - catch (e) { - return undefined; - } - }, - createHash(data) { - const hash = _crypto.createHash("md5"); - hash.update(data); - return hash.digest("hex"); - }, - getMemoryUsage() { - if (global.gc) { - global.gc(); - } - return process.memoryUsage().heapUsed; - }, - getFileSize(path) { - try { - const stat = _fs.statSync(path); - if (stat.isFile()) { - return stat.size; - } - } - catch { /*ignore*/ } - return 0; - }, - exit(exitCode?: number): void { - process.exit(exitCode); - }, - realpath(path: string): string { - return _fs.realpathSync(path); - }, - debugMode: some(process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), - tryEnableSourceMapsForHost() { - try { - require("source-map-support").install(); - } - catch { - // Could not enable source maps. - } - }, - setTimeout, - clearTimeout - }; - return nodeSystem; } function getChakraSystem(): System { From 63a3a65ac7c75368bcd63cbfe4812b2d8df69e10 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 20 Nov 2017 18:03:18 -0800 Subject: [PATCH 04/23] Use toLowercase as utility function --- src/compiler/commandLineParser.ts | 20 +------------------- src/compiler/core.ts | 7 ++++--- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index eb9f1e3cf34a6..c2dd4c4407590 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -2020,7 +2020,7 @@ namespace ts { export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray = []): ExpandResult { basePath = normalizePath(basePath); - const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper; + const keyMapper = host.useCaseSensitiveFileNames ? identity : toLowerCase; // Literal file names (provided via the "files" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map later when when including @@ -2224,24 +2224,6 @@ namespace ts { } } - /** - * Gets a case sensitive key. - * - * @param key The original key. - */ - function caseSensitiveKeyMapper(key: string) { - return key; - } - - /** - * Gets a case insensitive key. - * - * @param key The original key. - */ - function caseInsensitiveKeyMapper(key: string) { - return key.toLowerCase(); - } - /** * Produces a cleaned version of compiler options with personally identifiying info (aka, paths) removed. * Also converts enum values back to strings. diff --git a/src/compiler/core.ts b/src/compiler/core.ts index d92a02661e8b4..75564779280ba 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1455,6 +1455,9 @@ namespace ts { /** Returns its argument. */ export function identity(x: T) { return x; } + /** Returns the lower case string */ + export function toLowerCase(x: string) { return x.toLowerCase(); } + /** Throws an error because a function is not implemented. */ export function notImplemented(): never { throw new Error("Not implemented"); @@ -2925,9 +2928,7 @@ namespace ts { export type GetCanonicalFileName = (fileName: string) => string; export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { - return useCaseSensitiveFileNames - ? ((fileName) => fileName) - : ((fileName) => fileName.toLowerCase()); + return useCaseSensitiveFileNames ? identity : toLowerCase; } /** From 7f32f9ac783b02ea38c2e46586c07f759f79c358 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 20 Nov 2017 18:11:14 -0800 Subject: [PATCH 05/23] Rename and restructure non polling watch file --- src/compiler/sys.ts | 111 ++++++++++++++++---------------------------- 1 file changed, 40 insertions(+), 71 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 4920da700cc54..093eadd9cefae 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -146,7 +146,6 @@ namespace ts { } const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; - const watchedFileSet = createWatchedFileSet(); const nodeSystem: System = { args: process.argv.slice(2), @@ -157,17 +156,7 @@ namespace ts { }, readFile, writeFile, - watchFile: (fileName, callback, pollingInterval) => { - if (useNonPollingWatchers) { - const watchedFile = watchedFileSet.addFile(fileName, callback); - return { - close: () => watchedFileSet.removeFile(watchedFile) - }; - } - else { - return fsWatchFile(fileName, callback, pollingInterval); - } - }, + watchFile: useNonPollingWatchers ? createNonPollingWatchFile() : fsWatchFile, watchDirectory: (directoryName, callback, recursive) => { // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) @@ -269,73 +258,53 @@ namespace ts { }); } - function createWatchedFileSet() { - const dirWatchers = createMap(); + function createNonPollingWatchFile() { // One file can have multiple watchers const fileWatcherCallbacks = createMultiMap(); - return { addFile, removeFile }; - - function reduceDirWatcherRefCountForFile(fileName: string) { - const dirName = getDirectoryPath(fileName); - const watcher = dirWatchers.get(dirName); - if (watcher) { - watcher.referenceCount -= 1; - if (watcher.referenceCount <= 0) { - watcher.close(); - dirWatchers.delete(dirName); - } - } - } - - function addDirWatcher(dirPath: string): void { - let watcher = dirWatchers.get(dirPath); - if (watcher) { - watcher.referenceCount += 1; - return; - } - watcher = fsWatchDirectory( - dirPath || ".", - (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath) - ) as DirectoryWatcher; - watcher.referenceCount = 1; - dirWatchers.set(dirPath, watcher); - return; - } + const dirWatchers = createMap(); + const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); + return nonPollingWatchFile; - function addFileWatcherCallback(filePath: string, callback: FileWatcherCallback): void { + function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback): FileWatcher { + const filePath = toCanonicalName(fileName); fileWatcherCallbacks.add(filePath, callback); + const dirPath = getDirectoryPath(filePath) || "."; + const watcher = dirWatchers.get(dirPath) || createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath); + watcher.referenceCount++; + return { + close: () => { + if (watcher.referenceCount === 1) { + watcher.close(); + dirWatchers.delete(dirPath); + } + else { + watcher.referenceCount--; + } + fileWatcherCallbacks.remove(filePath, callback); + } + }; } - function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { - addFileWatcherCallback(fileName, callback); - addDirWatcher(getDirectoryPath(fileName)); - - return { fileName, callback }; - } - - function removeFile(watchedFile: WatchedFile) { - removeFileWatcherCallback(watchedFile.fileName, watchedFile.callback); - reduceDirWatcherRefCountForFile(watchedFile.fileName); - } - - function removeFileWatcherCallback(filePath: string, callback: FileWatcherCallback) { - fileWatcherCallbacks.remove(filePath, callback); - } - - function fileEventHandler(eventName: string, relativeFileName: string | undefined, baseDirPath: string) { - // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" - const fileName = !isString(relativeFileName) - ? undefined - : ts.getNormalizedAbsolutePath(relativeFileName, baseDirPath); - // Some applications save a working file via rename operations - if ((eventName === "change" || eventName === "rename")) { - const callbacks = fileWatcherCallbacks.get(fileName); - if (callbacks) { - for (const fileCallback of callbacks) { - fileCallback(fileName, FileWatcherEventKind.Changed); + function createDirectoryWatcher(dirName: string, dirPath: string) { + const watcher = fsWatchDirectory( + dirName, + (_eventName: string, relativeFileName) => { + // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" + const fileName = !isString(relativeFileName) + ? undefined + : ts.getNormalizedAbsolutePath(relativeFileName, dirName); + // Some applications save a working file via rename operations + const callbacks = fileWatcherCallbacks.get(toCanonicalName(fileName)); + if (callbacks) { + for (const fileCallback of callbacks) { + fileCallback(fileName, FileWatcherEventKind.Changed); + } } } - } + ) as DirectoryWatcher; + watcher.referenceCount = 0; + dirWatchers.set(dirPath, watcher); + return watcher; } } From c8e2b9c7c0f5705916578a1b1dc367ea69dd06d1 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 14 Dec 2017 13:27:04 -0800 Subject: [PATCH 06/23] Simplify check output errors --- src/harness/unittests/tscWatchMode.ts | 177 +++++++++++++------------- 1 file changed, 86 insertions(+), 91 deletions(-) diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 9e01021155c72..8abc23f1ac7cc 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -81,64 +81,60 @@ namespace ts.tscWatch { checkOutputDoesNotContain(host, expectedNonAffectedFiles); } - enum ExpectedOutputErrorsPosition { - BeforeCompilationStarts, - AfterCompilationStarting, - AfterFileChangeDetected - } - function checkOutputErrors( host: WatchedSystem, + preErrorsWatchDiagnostic: DiagnosticMessage | undefined, errors: ReadonlyArray, - errorsPosition: ExpectedOutputErrorsPosition, - skipWaiting?: true + ...postErrorsWatchDiagnostics: DiagnosticMessage[] ) { const outputs = host.getOutput(); - const expectedOutputCount = errors.length + (skipWaiting ? 0 : 1) + 1; - assert.equal(outputs.length, expectedOutputCount, "Outputs = " + outputs.toString()); - let index: number; - - switch (errorsPosition) { - case ExpectedOutputErrorsPosition.AfterCompilationStarting: - assertWatchDiagnosticAt(host, 0, Diagnostics.Starting_compilation_in_watch_mode); - index = 1; - break; - case ExpectedOutputErrorsPosition.AfterFileChangeDetected: - assertWatchDiagnosticAt(host, 0, Diagnostics.File_change_detected_Starting_incremental_compilation); - index = 1; - break; - case ExpectedOutputErrorsPosition.BeforeCompilationStarts: - assertWatchDiagnosticAt(host, errors.length, Diagnostics.Starting_compilation_in_watch_mode); - index = 0; - break; + const expectedOutputCount = (preErrorsWatchDiagnostic ? 1 : 0) + errors.length + postErrorsWatchDiagnostics.length; + assert.equal(outputs.length, expectedOutputCount); + let index = 0; + if (preErrorsWatchDiagnostic) { + assertWatchDiagnostic(preErrorsWatchDiagnostic); } + // Verify errors + forEach(errors, assertDiagnostic); + forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic); + host.clearOutput(); - forEach(errors, error => { - assertDiagnosticAt(host, index, error); + function assertDiagnostic(diagnostic: Diagnostic) { + const expected = formatDiagnostic(diagnostic, host); + assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); index++; - }); - if (!skipWaiting) { - if (errorsPosition === ExpectedOutputErrorsPosition.BeforeCompilationStarts) { - assertWatchDiagnosticAt(host, index, ts.Diagnostics.Starting_compilation_in_watch_mode); - index += 1; - } - assertWatchDiagnosticAt(host, index, Diagnostics.Compilation_complete_Watching_for_file_changes); } - host.clearOutput(); + + function assertWatchDiagnostic(diagnosticMessage: DiagnosticMessage) { + const expected = getWatchDiagnosticWithoutDate(diagnosticMessage); + assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); + index++; + } + + function getOutputAtFailedMessage(caption: string, expectedOutput: string) { + return `Expected ${caption}: ${expectedOutput} at ${index} in ${JSON.stringify(outputs)}`; + } + + function getWatchDiagnosticWithoutDate(diagnosticMessage: DiagnosticMessage) { + return ` - ${flattenDiagnosticMessageText(getLocaleSpecificMessage(diagnosticMessage), host.newLine)}${host.newLine + host.newLine + host.newLine}`; + } + } + + function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray) { + checkOutputErrors(host, Diagnostics.Starting_compilation_in_watch_mode, errors, Diagnostics.Compilation_complete_Watching_for_file_changes); } - function assertDiagnosticAt(host: WatchedSystem, outputAt: number, diagnostic: Diagnostic) { - const output = host.getOutput()[outputAt]; - assert.equal(output, formatDiagnostic(diagnostic, host), "outputs[" + outputAt + "] is " + output); + function checkOutputErrorsInitialWithConfigErrors(host: WatchedSystem, errors: ReadonlyArray) { + checkOutputErrors(host, /*preErrorsWatchDiagnostic*/ undefined, errors, Diagnostics.Starting_compilation_in_watch_mode, Diagnostics.Compilation_complete_Watching_for_file_changes); } - function assertWatchDiagnosticAt(host: WatchedSystem, outputAt: number, diagnosticMessage: DiagnosticMessage) { - const output = host.getOutput()[outputAt]; - assert.isTrue(endsWith(output, getWatchDiagnosticWithoutDate(host, diagnosticMessage)), "outputs[" + outputAt + "] is " + output); + function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray) { + checkOutputErrors(host, Diagnostics.File_change_detected_Starting_incremental_compilation, errors, Diagnostics.Compilation_complete_Watching_for_file_changes); } - function getWatchDiagnosticWithoutDate(host: WatchedSystem, diagnosticMessage: DiagnosticMessage) { - return ` - ${flattenDiagnosticMessageText(getLocaleSpecificMessage(diagnosticMessage), host.newLine)}${host.newLine + host.newLine + host.newLine}`; + function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray, expectedExitCode: ExitStatus) { + checkOutputErrors(host, Diagnostics.File_change_detected_Starting_incremental_compilation, errors); + assert.equal(host.exitCode, expectedExitCode); } function getDiagnosticOfFileFrom(file: SourceFile, text: string, start: number, length: number, message: DiagnosticMessage): Diagnostic { @@ -358,16 +354,16 @@ namespace ts.tscWatch { checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, libFile.path]); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf(commonFile2Name), commonFile2Name.length, Diagnostics.File_0_not_found, commonFile2.path), getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf("y"), 1, Diagnostics.Cannot_find_name_0, "y") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); host.reloadFS([file1, commonFile2, libFile]); host.runQueuedTimeoutCallbacks(); checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, libFile.path, commonFile2.path]); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("should reflect change in config file", () => { @@ -695,15 +691,14 @@ namespace ts.tscWatch { const watch = createWatchModeWithConfigFile(config.path, host); checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); host.reloadFS([file1, file2, libFile]); host.checkTimeoutQueueLengthAndRun(1); - assert.equal(host.exitCode, ExitStatus.DiagnosticsPresent_OutputsSkipped); - checkOutputErrors(host, [ + checkOutputErrorsIncrementalWithExit(host, [ getDiagnosticWithoutFile(Diagnostics.File_0_not_found, config.path) - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected, /*skipWaiting*/ true); + ], ExitStatus.DiagnosticsPresent_OutputsSkipped); }); it("Proper errors: document is not contained in project", () => { @@ -806,21 +801,21 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([moduleFile, file1, libFile]); const watch = createWatchModeWithoutConfigFile([file1.path], host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); const moduleFileOldPath = moduleFile.path; const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("rename a module file and rename back should restore the states for configured projects", () => { @@ -838,21 +833,21 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); const watch = createWatchModeWithConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); const moduleFileOldPath = moduleFile.path; const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, configFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, configFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("types should load from config file path if config exists", () => { @@ -889,13 +884,13 @@ namespace ts.tscWatch { const host = createWatchedSystem([file1, libFile]); const watch = createWatchModeWithoutConfigFile([file1.path], host); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); host.reloadFS([file1, moduleFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("Configure file diagnostics events are generated when the config file has errors", () => { @@ -915,10 +910,10 @@ namespace ts.tscWatch { const host = createWatchedSystem([file, configFile, libFile]); const watch = createWatchModeWithConfigFile(configFile.path, host); - checkOutputErrors(host, [ + checkOutputErrorsInitialWithConfigErrors(host, [ getUnknownCompilerOption(watch(), configFile, "foo"), getUnknownCompilerOption(watch(), configFile, "allowJS") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.BeforeCompilationStarts); + ]); }); it("If config file doesnt have errors, they are not reported", () => { @@ -935,7 +930,7 @@ namespace ts.tscWatch { const host = createWatchedSystem([file, configFile, libFile]); createWatchModeWithConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); }); it("Reports errors when the config file changes", () => { @@ -952,7 +947,7 @@ namespace ts.tscWatch { const host = createWatchedSystem([file, configFile, libFile]); const watch = createWatchModeWithConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); configFile.content = `{ "compilerOptions": { @@ -961,16 +956,16 @@ namespace ts.tscWatch { }`; host.reloadFS([file, configFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getUnknownCompilerOption(watch(), configFile, "haha") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); configFile.content = `{ "compilerOptions": {} }`; host.reloadFS([file, configFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { @@ -1058,13 +1053,13 @@ namespace ts.tscWatch { getDiagnosticOfFile(watch().getCompilerOptions().configFile, configFile.content.indexOf('"declaration"'), '"declaration"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration") ]; const intialErrors = errors(); - checkOutputErrors(host, intialErrors, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, intialErrors); configFile.content = configFileContentWithoutCommentLine; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); const nowErrors = errors(); - checkOutputErrors(host, nowErrors, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, nowErrors); assert.equal(nowErrors[0].start, intialErrors[0].start - configFileContentComment.length); assert.equal(nowErrors[1].start, intialErrors[1].start - configFileContentComment.length); }); @@ -1725,7 +1720,7 @@ namespace ts.tscWatch { const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); // ensure that imported file was found - checkOutputErrors(host, [f1IsNotModule, cannotFindFoo], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); const originalFileExists = host.fileExists; { @@ -1741,11 +1736,11 @@ namespace ts.tscWatch { host.runQueuedTimeoutCallbacks(); // ensure file has correct number of errors after edit - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ f1IsNotModule, getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), cannotFindFoo - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); } { let fileExistsIsCalled = false; @@ -1765,9 +1760,9 @@ namespace ts.tscWatch { host.runQueuedTimeoutCallbacks(); // ensure file has correct number of errors after edit - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); assert.isTrue(fileExistsIsCalled); } @@ -1788,7 +1783,7 @@ namespace ts.tscWatch { host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [f1IsNotModule, cannotFindFoo], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); assert.isTrue(fileExistsCalled); } }); @@ -1823,16 +1818,16 @@ namespace ts.tscWatch { const watch = createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); fileExistsCalledForBar = false; root.content = `import {y} from "bar"`; host.reloadFS(files.concat(imported)); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); }); @@ -1865,20 +1860,20 @@ namespace ts.tscWatch { const watch = createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); fileExistsCalledForBar = false; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); fileExistsCalledForBar = false; host.reloadFS(filesWithImported); host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); }); @@ -1913,13 +1908,13 @@ declare module "fs" { const watch = createWatchModeWithoutConfigFile([root.path], host, { }); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); host.reloadFS(filesWithNodeType); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("works when included file with ambient module changes", () => { @@ -1955,14 +1950,14 @@ declare module "fs" { const watch = createWatchModeWithoutConfigFile([root.path, file.path], host, {}); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); file.content += fileContentWithFS; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("works when reusing program with files from external library", () => { @@ -1997,7 +1992,7 @@ declare module "fs" { const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); const expectedFiles: ExpectedFile[] = [ createExpectedEmittedFile(file1), createExpectedEmittedFile(file2), @@ -2016,7 +2011,7 @@ declare module "fs" { host.reloadFS(programFiles.concat(configFile)); host.runQueuedTimeoutCallbacks(); checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); verifyExpectedFiles(expectedFiles); From cabb2117025d3ed48a8c80b3ec0052022a4dc130 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 15 Dec 2017 16:41:49 -0800 Subject: [PATCH 07/23] Create polling interval as non optional parameter --- src/compiler/watch.ts | 21 +++---- src/compiler/watchUtilities.ts | 104 +++++++++++++++++++-------------- src/server/editorServices.ts | 48 +++++++-------- src/server/project.ts | 1 + 4 files changed, 92 insertions(+), 82 deletions(-) diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 79e26f5986cf8..4913e54f043fc 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -252,18 +252,19 @@ namespace ts { let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations let hasChangedAutomaticTypeDirectiveNames = false; // True if the automatic type directives have changed - const loggingEnabled = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; - const writeLog: (s: string) => void = loggingEnabled ? s => { system.write(s); system.write(system.newLine); } : noop; - const watchFile = compilerOptions.extendedDiagnostics ? ts.addFileWatcherWithLogging : loggingEnabled ? ts.addFileWatcherWithOnlyTriggerLogging : ts.addFileWatcher; - const watchFilePath = compilerOptions.extendedDiagnostics ? ts.addFilePathWatcherWithLogging : ts.addFilePathWatcher; - const watchDirectoryWorker = compilerOptions.extendedDiagnostics ? ts.addDirectoryWatcherWithLogging : ts.addDirectoryWatcher; + const watchLogLevel = compilerOptions.extendedDiagnostics ? WatchLogLevel.Verbose : + compilerOptions.diagnostics ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; + const writeLog: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? s => { system.write(s); system.write(system.newLine); } : noop; + const watchFile = createWatchFile(watchLogLevel, writeLog); + const watchFilePath = createWatchFilePath(watchLogLevel, writeLog); + const watchDirectoryWorker = createWatchDirectory(watchLogLevel, writeLog); watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; const directoryStructureHost = configFileName ? createCachedDirectoryStructureHost(system) : system; if (configFileName) { - watchFile(system, configFileName, scheduleProgramReload, writeLog); + watchFile(system, configFileName, scheduleProgramReload, /*pollingInterval*/ undefined); } const getCurrentDirectory = memoize(() => directoryStructureHost.getCurrentDirectory()); @@ -416,7 +417,7 @@ namespace ts { hostSourceFile.sourceFile = sourceFile; sourceFile.version = hostSourceFile.version.toString(); if (!hostSourceFile.fileWatcher) { - hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, path, writeLog); + hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, /*pollingInterval*/ undefined, path); } } else { @@ -429,7 +430,7 @@ namespace ts { let fileWatcher: FileWatcher; if (sourceFile) { sourceFile.version = "0"; - fileWatcher = watchFilePath(system, fileName, onSourceFileChange, path, writeLog); + fileWatcher = watchFilePath(system, fileName, onSourceFileChange, /*pollingInterval*/ undefined, path); sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); } else { @@ -599,11 +600,11 @@ namespace ts { } function watchDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { - return watchDirectoryWorker(system, directory, cb, flags, writeLog); + return watchDirectoryWorker(system, directory, cb, flags); } function watchMissingFilePath(missingFilePath: Path) { - return watchFilePath(system, missingFilePath, onMissingFileChange, missingFilePath, writeLog); + return watchFilePath(system, missingFilePath, onMissingFileChange, /*pollingInterval*/ undefined, missingFilePath); } function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) { diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 0bc6920479be4..70d7ca63ee914 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -90,75 +90,89 @@ namespace ts { return program.isEmittedFile(file); } - export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { - return host.watchFile(file, cb); + export enum WatchLogLevel { + None, + TriggerOnly, + Verbose } - export function addFileWatcherWithLogging(host: System, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, file, cb); - } + export type WatchFile = (host: System, file: string, callback: FileWatcherCallback, pollingInterval?: number, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; + export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; + export type WatchFilePath = (host: System, file: string, callback: FilePathWatcherCallback, pollingInterval: number | undefined, path: Path, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; + export type WatchDirectory = (host: System, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; - export function addFileWatcherWithOnlyTriggerLogging(host: System, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, file, cb); + export function createWatchFile(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFile { + const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFile); + return (host, file, callback, pollingInterval, detailInfo1, detailInfo2) => + createFileWatcher(host, file, callback, pollingInterval, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo); } - export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; - export function addFilePathWatcher(host: System, file: string, cb: FilePathWatcherCallback, path: Path): FileWatcher { - return host.watchFile(file, (fileName, eventKind) => cb(fileName, eventKind, path)); + export function createWatchFilePath(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFilePath { + const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFilePath); + return (host, file, callback, pollingInterval, path, detailInfo1, detailInfo2) => + createFileWatcher(host, file, callback, pollingInterval, path, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo); } - export function addFilePathWatcherWithLogging(host: System, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, file, cb, path); + export function createWatchDirectory(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchDirectory { + const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchDirectory); + return (host, directory, callback, flags, detailInfo1, detailInfo2) => + createFileWatcher(host, directory, callback, flags, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchDirectory, log, "DirectoryWatcher", getDetailWatchInfo); } - export function addFilePathWatcherWithOnlyTriggerLogging(host: System, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, file, cb, path); + function watchFile(host: System, file: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { + return host.watchFile(file, callback, pollingInterval); } - export function addDirectoryWatcher(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { - const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; - return host.watchDirectory(directory, cb, recursive); + function watchFilePath(host: System, file: string, callback: FilePathWatcherCallback, pollingInterval: number | undefined, path: Path): FileWatcher { + return host.watchFile(file, (fileName, eventKind) => callback(fileName, eventKind, path), pollingInterval); } - export function addDirectoryWatcherWithLogging(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { - const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; - return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, directory, cb, flags); + function watchDirectory(host: System, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { + return host.watchDirectory(directory, callback, (flags & WatchDirectoryFlags.Recursive) !== 0); } - export function addDirectoryWatcherWithOnlyTriggerLogging(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { - const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; - return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, directory, cb, flags); - } + export type WatchCallback = (fileName: string, cbOptional?: T, passThrough?: U) => void; + export type AddWatch = (host: System, file: string, cb: WatchCallback, flags: T, passThrough?: V, detailInfo1?: undefined, detailInfo2?: undefined) => FileWatcher; + export type GetDetailWatchInfo = (detailInfo1: X, detailInfo2: Y) => string; - type WatchCallback = (fileName: string, cbOptional1?: T, optional?: U) => void; - type AddWatch = (host: System, file: string, cb: WatchCallback, optional?: U) => FileWatcher; - function createWatcherWithLogging(addWatch: AddWatch, watcherCaption: string, log: (s: string) => void, logOnlyTrigger: boolean, host: System, file: string, cb: WatchCallback, optional?: U): FileWatcher { - const info = `PathInfo: ${file}`; - if (!logOnlyTrigger) { - log(`${watcherCaption}Added: ${info}`); + type CreateFileWatcher = (host: System, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined) => FileWatcher; + function getCreateFileWatcher(watchLogLevel: WatchLogLevel, addWatch: AddWatch): CreateFileWatcher { + switch (watchLogLevel) { + case WatchLogLevel.None: + return addWatch; + case WatchLogLevel.TriggerOnly: + return createFileWatcherWithLogging; + case WatchLogLevel.Verbose: + return createFileWatcherWithTriggerLogging; } - const watcher = addWatch(host, file, (fileName, cbOptional1?) => { - const optionalInfo = cbOptional1 !== undefined ? ` ${cbOptional1}` : ""; - log(`${watcherCaption}Trigger: ${fileName}${optionalInfo} ${info}`); - const start = timestamp(); - cb(fileName, cbOptional1, optional); - const elapsed = timestamp() - start; - log(`${watcherCaption}Elapsed: ${elapsed}ms Trigger: ${fileName}${optionalInfo} ${info}`); - }, optional); + } + + function createFileWatcherWithLogging(host: System, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { + log(`${watchCaption}:: Added:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`); + const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo); return { close: () => { - if (!logOnlyTrigger) { - log(`${watcherCaption}Close: ${info}`); - } + log(`${watchCaption}:: Close:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`); watcher.close(); } }; } + function createFileWatcherWithTriggerLogging(host: System, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { + return addWatch(host, file, (fileName, cbOptional) => { + const triggerredInfo = `${watchCaption}:: Triggered with ${fileName}${cbOptional !== undefined ? cbOptional : ""}:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`; + log(triggerredInfo); + const start = timestamp(); + cb(fileName, cbOptional, passThrough); + const elapsed = timestamp() - start; + log(`Elapsed:: ${elapsed}ms ${triggerredInfo}`); + }, flags); + } + + function getWatchInfo(file: string, flags: T, detailInfo1: X | undefined, detailInfo2: Y | undefined, getDetailWatchInfo: GetDetailWatchInfo | undefined) { + return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : ""}` + } + export function closeFileWatcher(watcher: FileWatcher) { watcher.close(); } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 664938a4cdc21..e6ba7fb3894bd 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -321,9 +321,13 @@ namespace ts.server { typesMapLocation?: string; } - type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, watchType: WatchType, project?: Project) => FileWatcher; - type WatchFilePath = (host: ServerHost, file: string, cb: FilePathWatcherCallback, path: Path, watchType: WatchType, project?: Project) => FileWatcher; - type WatchDirectory = (host: ServerHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, watchType: WatchType, project?: Project) => FileWatcher; + export type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, pollingInterval: number | undefined, watchType: WatchType, project?: Project) => FileWatcher; + export type WatchFilePath = (host: ServerHost, file: string, cb: FilePathWatcherCallback, pollingInterval: number | undefined, path: Path, watchType: WatchType, project?: Project) => FileWatcher; + export type WatchDirectory = (host: ServerHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, watchType: WatchType, project?: Project) => FileWatcher; + + function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) { + return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`; + } export class ProjectService { @@ -450,26 +454,12 @@ namespace ts.server { }; this.documentRegistry = createDocumentRegistry(this.host.useCaseSensitiveFileNames, this.currentDirectory); - if (this.logger.hasLevel(LogLevel.verbose)) { - this.watchFile = (host, file, cb, watchType, project) => ts.addFileWatcherWithLogging(host, file, cb, this.createWatcherLog(watchType, project)); - this.watchFilePath = (host, file, cb, path, watchType, project) => ts.addFilePathWatcherWithLogging(host, file, cb, path, this.createWatcherLog(watchType, project)); - this.watchDirectory = (host, dir, cb, flags, watchType, project) => ts.addDirectoryWatcherWithLogging(host, dir, cb, flags, this.createWatcherLog(watchType, project)); - } - else if (this.logger.loggingEnabled()) { - this.watchFile = (host, file, cb, watchType, project) => ts.addFileWatcherWithOnlyTriggerLogging(host, file, cb, this.createWatcherLog(watchType, project)); - this.watchFilePath = (host, file, cb, path, watchType, project) => ts.addFilePathWatcherWithOnlyTriggerLogging(host, file, cb, path, this.createWatcherLog(watchType, project)); - this.watchDirectory = (host, dir, cb, flags, watchType, project) => ts.addDirectoryWatcherWithOnlyTriggerLogging(host, dir, cb, flags, this.createWatcherLog(watchType, project)); - } - else { - this.watchFile = ts.addFileWatcher; - this.watchFilePath = ts.addFilePathWatcher; - this.watchDirectory = ts.addDirectoryWatcher; - } - } - - private createWatcherLog(watchType: WatchType, project: Project | undefined): (s: string) => void { - const detailedInfo = ` Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`; - return s => this.logger.info(s + detailedInfo); + const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose : + this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; + const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; + this.watchFile = createWatchFile(watchLogLevel, log, getDetailWatchInfo); + this.watchFilePath = createWatchFilePath(watchLogLevel, log, getDetailWatchInfo); + this.watchDirectory = createWatchDirectory(watchLogLevel, log, getDetailWatchInfo); } toPath(fileName: string) { @@ -755,8 +745,8 @@ namespace ts.server { } } - private onSourceFileChanged(fileName: NormalizedPath, eventKind: FileWatcherEventKind) { - const info = this.getScriptInfoForNormalizedPath(fileName); + private onSourceFileChanged(fileName: string, eventKind: FileWatcherEventKind, path: Path) { + const info = this.getScriptInfoForPath(path); if (!info) { this.logger.msg(`Error: got watch notification for unknown file: ${fileName}`); } @@ -1136,6 +1126,7 @@ namespace ts.server { this.host, configFileName, (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind), + /*pollingInterval*/ undefined, WatchType.ConfigFileForInferredRoot ); this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); @@ -1519,6 +1510,7 @@ namespace ts.server { this.host, configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind), + /*pollingInterval*/ undefined, WatchType.ConfigFilePath, project ); @@ -1741,10 +1733,12 @@ namespace ts.server { // do not watch files with mixed content - server doesn't know how to interpret it if (!info.isDynamicOrHasMixedContent()) { const { fileName } = info; - info.fileWatcher = this.watchFile( + info.fileWatcher = this.watchFilePath( this.host, fileName, - (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind), + (fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path), + /*pollingInterval*/ undefined, + info.path, WatchType.ClosedScriptInfo ); } diff --git a/src/server/project.ts b/src/server/project.ts index a25e6870bdab2..48a019dd97ef0 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -932,6 +932,7 @@ namespace ts.server { this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } }, + /*pollingInterval*/ undefined, WatchType.MissingFilePath, this ); From aa5e49a4d4debf376731a116c68c4c6ce185a68a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 18 Dec 2017 13:20:42 -0800 Subject: [PATCH 08/23] Update watchFile to take priority (which is currently polling interval being passed to the host) --- src/compiler/sys.ts | 17 ++++++++++ src/compiler/watch.ts | 13 +++---- src/compiler/watchUtilities.ts | 62 ++++++++++++++++++++++------------ src/server/editorServices.ts | 24 +++++-------- src/server/project.ts | 8 ++--- 5 files changed, 76 insertions(+), 48 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 093eadd9cefae..d08be68aaac4a 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -30,6 +30,23 @@ namespace ts { mtime?: Date; } + /* @internal */ + export enum WatchPriority { + High, + Medium, + Low + } + + const pollingIntervalsForPriority = [250, 1000, 4000]; + function pollingInterval(watchPriority: WatchPriority): number { + return pollingIntervalsForPriority[watchPriority]; + } + + /* @internal */ + export function watchFileUsingPriorityPollingInterval(host: System, fileName: string, callback: FileWatcherCallback, watchPriority: WatchPriority): FileWatcher { + return host.watchFile(fileName, callback, pollingInterval(watchPriority)); + } + /** * Partial interface of the System thats needed to support the caching of directory structure */ diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 4913e54f043fc..d5004ba39205b 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -255,16 +255,13 @@ namespace ts { const watchLogLevel = compilerOptions.extendedDiagnostics ? WatchLogLevel.Verbose : compilerOptions.diagnostics ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; const writeLog: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? s => { system.write(s); system.write(system.newLine); } : noop; - const watchFile = createWatchFile(watchLogLevel, writeLog); - const watchFilePath = createWatchFilePath(watchLogLevel, writeLog); - const watchDirectoryWorker = createWatchDirectory(watchLogLevel, writeLog); - watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; + const { watchFile, watchFilePath, watchDirectory: watchDirectoryWorker } = getWatchFactory(system, watchLogLevel, writeLog); const directoryStructureHost = configFileName ? createCachedDirectoryStructureHost(system) : system; if (configFileName) { - watchFile(system, configFileName, scheduleProgramReload, /*pollingInterval*/ undefined); + watchFile(system, configFileName, scheduleProgramReload, WatchPriority.Low); } const getCurrentDirectory = memoize(() => directoryStructureHost.getCurrentDirectory()); @@ -417,7 +414,7 @@ namespace ts { hostSourceFile.sourceFile = sourceFile; sourceFile.version = hostSourceFile.version.toString(); if (!hostSourceFile.fileWatcher) { - hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, /*pollingInterval*/ undefined, path); + hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, WatchPriority.High, path); } } else { @@ -430,7 +427,7 @@ namespace ts { let fileWatcher: FileWatcher; if (sourceFile) { sourceFile.version = "0"; - fileWatcher = watchFilePath(system, fileName, onSourceFileChange, /*pollingInterval*/ undefined, path); + fileWatcher = watchFilePath(system, fileName, onSourceFileChange, WatchPriority.High, path); sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); } else { @@ -604,7 +601,7 @@ namespace ts { } function watchMissingFilePath(missingFilePath: Path) { - return watchFilePath(system, missingFilePath, onMissingFileChange, /*pollingInterval*/ undefined, missingFilePath); + return watchFilePath(system, missingFilePath, onMissingFileChange, WatchPriority.Medium, missingFilePath); } function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) { diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 70d7ca63ee914..b9d6e34071224 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -96,43 +96,63 @@ namespace ts { Verbose } - export type WatchFile = (host: System, file: string, callback: FileWatcherCallback, pollingInterval?: number, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; + export type WatchFile = (host: System, file: string, callback: FileWatcherCallback, watchPriority: WatchPriority, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; - export type WatchFilePath = (host: System, file: string, callback: FilePathWatcherCallback, pollingInterval: number | undefined, path: Path, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; + export type WatchFilePath = (host: System, file: string, callback: FilePathWatcherCallback, watchPriority: WatchPriority, path: Path, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; export type WatchDirectory = (host: System, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; - export function createWatchFile(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFile { - const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFile); - return (host, file, callback, pollingInterval, detailInfo1, detailInfo2) => - createFileWatcher(host, file, callback, pollingInterval, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo); + export interface WatchFactory { + watchFile: WatchFile; + watchFilePath: WatchFilePath; + watchDirectory: WatchDirectory; } - export function createWatchFilePath(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFilePath { - const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFilePath); - return (host, file, callback, pollingInterval, path, detailInfo1, detailInfo2) => - createFileWatcher(host, file, callback, pollingInterval, path, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo); + export function getWatchFactory(host: System, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { + const value = host.getEnvironmentVariable("TSC_WATCHFILE"); + switch (value) { + case "PriorityPollingInterval": + // Use polling interval based on priority when create watch using host.watchFile + return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFileUsingPriorityPollingInterval, watchDirectory); + default: + return getDefaultWatchFactory(watchLogLevel, log, getDetailWatchInfo); + } } - export function createWatchDirectory(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchDirectory { - const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchDirectory); - return (host, directory, callback, flags, detailInfo1, detailInfo2) => - createFileWatcher(host, directory, callback, flags, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchDirectory, log, "DirectoryWatcher", getDetailWatchInfo); + export function getDefaultWatchFactory(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { + // Current behaviour in which polling interval is always 250 ms + return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFile, watchDirectory); } - function watchFile(host: System, file: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { - return host.watchFile(file, callback, pollingInterval); + function getWatchFactoryWith(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo: GetDetailWatchInfo | undefined, + watchFile: (host: System, file: string, callback: FileWatcherCallback, watchPriority: WatchPriority) => FileWatcher, + watchDirectory: (host: System, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags) => FileWatcher): WatchFactory { + const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFile); + const createFilePathWatcher: CreateFileWatcher = watchLogLevel === WatchLogLevel.None ? watchFilePath : createFileWatcher; + const createDirectoryWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchDirectory); + return { + watchFile: (host, file, callback, pollingInterval, detailInfo1, detailInfo2) => + createFileWatcher(host, file, callback, pollingInterval, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo), + watchFilePath: (host, file, callback, pollingInterval, path, detailInfo1, detailInfo2) => + createFilePathWatcher(host, file, callback, pollingInterval, path, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo), + watchDirectory: (host, directory, callback, flags, detailInfo1, detailInfo2) => + createDirectoryWatcher(host, directory, callback, flags, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchDirectory, log, "DirectoryWatcher", getDetailWatchInfo) + }; + + function watchFilePath(host: System, file: string, callback: FilePathWatcherCallback, watchPriority: WatchPriority, path: Path): FileWatcher { + return watchFile(host, file, (fileName, eventKind) => callback(fileName, eventKind, path), watchPriority); + } } - function watchFilePath(host: System, file: string, callback: FilePathWatcherCallback, pollingInterval: number | undefined, path: Path): FileWatcher { - return host.watchFile(file, (fileName, eventKind) => callback(fileName, eventKind, path), pollingInterval); + function watchFile(host: System, file: string, callback: FileWatcherCallback, _watchPriority: WatchPriority): FileWatcher { + return host.watchFile(file, callback); } function watchDirectory(host: System, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { return host.watchDirectory(directory, callback, (flags & WatchDirectoryFlags.Recursive) !== 0); } - export type WatchCallback = (fileName: string, cbOptional?: T, passThrough?: U) => void; - export type AddWatch = (host: System, file: string, cb: WatchCallback, flags: T, passThrough?: V, detailInfo1?: undefined, detailInfo2?: undefined) => FileWatcher; + type WatchCallback = (fileName: string, cbOptional?: T, passThrough?: U) => void; + type AddWatch = (host: System, file: string, cb: WatchCallback, flags: T, passThrough?: V, detailInfo1?: undefined, detailInfo2?: undefined) => FileWatcher; export type GetDetailWatchInfo = (detailInfo1: X, detailInfo2: Y) => string; type CreateFileWatcher = (host: System, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined) => FileWatcher; @@ -170,7 +190,7 @@ namespace ts { } function getWatchInfo(file: string, flags: T, detailInfo1: X | undefined, detailInfo2: Y | undefined, getDetailWatchInfo: GetDetailWatchInfo | undefined) { - return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : ""}` + return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : ""}`; } export function closeFileWatcher(watcher: FileWatcher) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e6ba7fb3894bd..43cb678d743f6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -410,11 +410,7 @@ namespace ts.server { private readonly seenProjects = createMap(); /*@internal*/ - readonly watchFile: WatchFile; - /*@internal*/ - readonly watchFilePath: WatchFilePath; - /*@internal*/ - readonly watchDirectory: WatchDirectory; + readonly watchFactory: WatchFactory; constructor(opts: ProjectServiceOptions) { this.host = opts.host; @@ -457,9 +453,7 @@ namespace ts.server { const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose : this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; - this.watchFile = createWatchFile(watchLogLevel, log, getDetailWatchInfo); - this.watchFilePath = createWatchFilePath(watchLogLevel, log, getDetailWatchInfo); - this.watchDirectory = createWatchDirectory(watchLogLevel, log, getDetailWatchInfo); + this.watchFactory = getDefaultWatchFactory(watchLogLevel, log, getDetailWatchInfo); } toPath(fileName: string) { @@ -792,7 +786,7 @@ namespace ts.server { */ /*@internal*/ watchWildcardDirectory(directory: Path, flags: WatchDirectoryFlags, project: ConfiguredProject) { - return this.watchDirectory( + return this.watchFactory.watchDirectory( this.host, directory, fileOrDirectory => { @@ -1122,11 +1116,11 @@ namespace ts.server { canonicalConfigFilePath: string, configFileExistenceInfo: ConfigFileExistenceInfo ) { - configFileExistenceInfo.configFileWatcherForRootOfInferredProject = this.watchFile( + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = this.watchFactory.watchFile( this.host, configFileName, (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind), - /*pollingInterval*/ undefined, + WatchPriority.Low, WatchType.ConfigFileForInferredRoot ); this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); @@ -1506,11 +1500,11 @@ namespace ts.server { project.configFileSpecs = configFileSpecs; // TODO: We probably should also watch the configFiles that are extended - project.configFileWatcher = this.watchFile( + project.configFileWatcher = this.watchFactory.watchFile( this.host, configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind), - /*pollingInterval*/ undefined, + WatchPriority.Low, WatchType.ConfigFilePath, project ); @@ -1733,11 +1727,11 @@ namespace ts.server { // do not watch files with mixed content - server doesn't know how to interpret it if (!info.isDynamicOrHasMixedContent()) { const { fileName } = info; - info.fileWatcher = this.watchFilePath( + info.fileWatcher = this.watchFactory.watchFilePath( this.host, fileName, (fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path), - /*pollingInterval*/ undefined, + WatchPriority.Medium, info.path, WatchType.ClosedScriptInfo ); diff --git a/src/server/project.ts b/src/server/project.ts index 48a019dd97ef0..c7ea875f072eb 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -376,7 +376,7 @@ namespace ts.server { /*@internal*/ watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { - return this.projectService.watchDirectory( + return this.projectService.watchFactory.watchDirectory( this.projectService.host, directory, cb, @@ -393,7 +393,7 @@ namespace ts.server { /*@internal*/ watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { - return this.projectService.watchDirectory( + return this.projectService.watchFactory.watchDirectory( this.projectService.host, directory, cb, @@ -916,7 +916,7 @@ namespace ts.server { } private addMissingFileWatcher(missingFilePath: Path) { - const fileWatcher = this.projectService.watchFile( + const fileWatcher = this.projectService.watchFactory.watchFile( this.projectService.host, missingFilePath, (fileName, eventKind) => { @@ -932,7 +932,7 @@ namespace ts.server { this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } }, - /*pollingInterval*/ undefined, + WatchPriority.Medium, WatchType.MissingFilePath, this ); From 976f3300447683318f3f3588d3162e6d41acc030 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 18 Dec 2017 19:43:56 -0800 Subject: [PATCH 09/23] Watch based on dynamic polling priority frequency queue --- src/compiler/sys.ts | 209 ++++++++++++++++++++++++++++++++- src/compiler/watchUtilities.ts | 16 ++- src/server/server.ts | 18 +-- 3 files changed, 223 insertions(+), 20 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index d08be68aaac4a..7edfd5dd50367 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -25,9 +25,9 @@ namespace ts { export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; export type DirectoryWatcherCallback = (fileName: string) => void; export interface WatchedFile { - fileName: string; - callback: FileWatcherCallback; - mtime?: Date; + readonly fileName: string; + readonly callback: FileWatcherCallback; + mtime: Date; } /* @internal */ @@ -37,7 +37,13 @@ namespace ts { Low } - const pollingIntervalsForPriority = [250, 1000, 4000]; + function getPriorityValues(highPriorityValue: number): [number, number, number] { + const mediumPriorityValue = highPriorityValue * 2; + const lowPriorityValue = mediumPriorityValue * 4; + return [highPriorityValue, mediumPriorityValue, lowPriorityValue]; + } + + const pollingIntervalsForPriority = getPriorityValues(250); function pollingInterval(watchPriority: WatchPriority): number { return pollingIntervalsForPriority[watchPriority]; } @@ -47,6 +53,201 @@ namespace ts { return host.watchFile(fileName, callback, pollingInterval(watchPriority)); } + /* @internal */ + export interface DynamicPriorityPollingStatsSet { + watchFile(fileName: string, callback: FileWatcherCallback, defaultPriority: WatchPriority): FileWatcher; + } + + /* @internal */ + export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time + /* @internal */ + export function createDynamicPriorityPollingStatsSet(host: System): DynamicPriorityPollingStatsSet { + if (!host.getModifiedTime || !host.setTimeout) { + throw notImplemented(); + } + + interface WatchedFile extends ts.WatchedFile { + isClosed?: boolean; + unchangedPolls: number; + } + + interface WatchPriorityQueue extends Array { + watchPriority: WatchPriority; + pollIndex: number; + } + + const chunkSizes = getPriorityValues(32); + const unChangedThresholds = getPriorityValues(32); + const watchedFiles: WatchedFile[] = []; + const changedFilesInLastPoll: WatchedFile[] = []; + const priorityQueues = [createPriorityQueue(WatchPriority.High), createPriorityQueue(WatchPriority.Medium), createPriorityQueue(WatchPriority.Low)]; + return { + watchFile + }; + + function watchFile(fileName: string, callback: FileWatcherCallback, defaultPriority: WatchPriority): FileWatcher { + const file: WatchedFile = { + fileName, + callback, + unchangedPolls: 0, + mtime: getModifiedTime(fileName) + }; + watchedFiles.push(file); + + addToPriorityQueue(file, defaultPriority); + return { + close: () => { + file.isClosed = true; + // Remove from watchedFiles + unorderedRemoveItem(watchedFiles, file); + // Do not update priority queue since that will happen as part of polling + } + }; + } + + function createPriorityQueue(watchPriority: WatchPriority): WatchPriorityQueue { + const queue = [] as WatchPriorityQueue; + queue.watchPriority = watchPriority; + queue.pollIndex = 0; + return queue; + } + + function pollPriorityQueue(queue: WatchPriorityQueue) { + const priority = queue.watchPriority; + queue.pollIndex = pollQueue(queue, priority, queue.pollIndex, chunkSizes[priority]); + // Set the next polling index and timeout + if (queue.length) { + scheduleNextPoll(priority); + } + else { + Debug.assert(queue.pollIndex === 0); + } + } + + function pollHighPriorityQueue(queue: WatchPriorityQueue) { + // Always poll complete list of changedFilesInLastPoll + pollQueue(changedFilesInLastPoll, WatchPriority.High, /*pollIndex*/ 0, changedFilesInLastPoll.length); + + // Finally do the actual polling of the queue + pollPriorityQueue(queue); + // Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue + // as pollPriorityQueue wont schedule for next poll + if (!queue.length && changedFilesInLastPoll.length) { + scheduleNextPoll(WatchPriority.High); + } + } + + function pollQueue(queue: WatchedFile[], priority: WatchPriority, pollIndex: number, chunkSize: number) { + // Max visit would be all elements of the queue + let needsVisit = queue.length; + let definedValueCopyToIndex = pollIndex; + const unChangedThreshold = unChangedThresholds[priority]; + for (let polled = 0; polled < chunkSize && needsVisit > 0; nextPollIndex(), needsVisit--) { + const watchedFile = queue[pollIndex]; + if (!watchedFile) { + continue; + } + else if (watchedFile.isClosed) { + queue[pollIndex] = undefined; + continue; + } + + polled++; + const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(watchedFile.fileName)); + if (watchedFile.isClosed) { + // Closed watcher as part of callback + queue[pollIndex] = undefined; + } + else if (fileChanged) { + watchedFile.unchangedPolls = 0; + // Changed files go to changedFilesInLastPoll queue + if (queue !== changedFilesInLastPoll && priority !== WatchPriority.High) { + queue[pollIndex] = undefined; + addChangedFileToHighPriorityQueue(watchedFile); + } + } + else if (watchedFile.unchangedPolls !== unChangedThreshold) { + watchedFile.unchangedPolls++; + } + else if (queue === changedFilesInLastPoll) { + // Restart unchangedPollCount for unchanged file and move to high priority queue + watchedFile.unchangedPolls = 0; + addToPriorityQueue(watchedFile, WatchPriority.High); + } + else if (priority !== WatchPriority.Low) { + watchedFile.unchangedPolls++; + queue[pollIndex] = undefined; + addToPriorityQueue(watchedFile, priority + 1); + } + + if (queue[pollIndex]) { + // Copy this file to the non hole location + if (definedValueCopyToIndex < pollIndex) { + queue[definedValueCopyToIndex] = watchedFile; + queue[pollIndex] = undefined; + } + definedValueCopyToIndex++; + } + } + + // Return next poll index + return pollIndex; + + function nextPollIndex() { + pollIndex++; + if (pollIndex === queue.length) { + if (definedValueCopyToIndex < pollIndex) { + // There are holes from nextDefinedValueIndex to end of queue, change queue size + queue.length = definedValueCopyToIndex; + } + pollIndex = 0; + definedValueCopyToIndex = 0; + } + } + } + + function addToPriorityQueue(file: WatchedFile, priority: WatchPriority) { + if (priorityQueues[priority].push(file) === 1) { + scheduleNextPoll(priority); + } + } + + function addChangedFileToHighPriorityQueue(file: WatchedFile) { + if (changedFilesInLastPoll.push(file) === 1 && !priorityQueues[WatchPriority.High].length) { + scheduleNextPoll(WatchPriority.High); + } + } + + function scheduleNextPoll(priority: WatchPriority) { + host.setTimeout(priority === WatchPriority.High ? pollHighPriorityQueue : pollPriorityQueue, pollingInterval(priority), priorityQueues[priority]); + } + + function getModifiedTime(fileName: string) { + return host.getModifiedTime(fileName) || missingFileModifiedTime; + } + } + + /** + * Returns true if file status changed + */ + /*@internal*/ + export function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean { + const oldTime = watchedFile.mtime.getTime(); + const newTime = modifiedTime.getTime(); + if (oldTime !== newTime) { + watchedFile.mtime = modifiedTime; + const eventKind = oldTime === 0 + ? FileWatcherEventKind.Created + : newTime === 0 + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; + watchedFile.callback(watchedFile.fileName, eventKind); + return true; + } + + return false; + } + /** * Partial interface of the System thats needed to support the caching of directory structure */ diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index b9d6e34071224..9ddaa8f2d6766 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -113,6 +113,9 @@ namespace ts { case "PriorityPollingInterval": // Use polling interval based on priority when create watch using host.watchFile return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFileUsingPriorityPollingInterval, watchDirectory); + case "DynamicPriorityPolling": + // Dynamically move frequently changing files to high frequency polling and non changing files to lower frequency + return getWatchFactoryWithDynamicPriorityPolling(host, watchLogLevel, log, getDetailWatchInfo); default: return getDefaultWatchFactory(watchLogLevel, log, getDetailWatchInfo); } @@ -143,6 +146,15 @@ namespace ts { } } + function getWatchFactoryWithDynamicPriorityPolling(host: System, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { + const pollingSet = createDynamicPriorityPollingStatsSet(host); + return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFile, watchDirectory); + + function watchFile(_host: System, file: string, callback: FileWatcherCallback, watchPriority: WatchPriority): FileWatcher { + return pollingSet.watchFile(file, callback, watchPriority); + } + } + function watchFile(host: System, file: string, callback: FileWatcherCallback, _watchPriority: WatchPriority): FileWatcher { return host.watchFile(file, callback); } @@ -161,9 +173,9 @@ namespace ts { case WatchLogLevel.None: return addWatch; case WatchLogLevel.TriggerOnly: - return createFileWatcherWithLogging; - case WatchLogLevel.Verbose: return createFileWatcherWithTriggerLogging; + case WatchLogLevel.Verbose: + return createFileWatcherWithLogging; } } diff --git a/src/server/server.ts b/src/server/server.ts index 8e53c4d51092a..c83a5988e70a0 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -684,11 +684,11 @@ namespace ts.server { return; } - fs.stat(watchedFile.fileName, (err: any, stats: any) => { + fs.stat(watchedFile.fileName, (err, stats) => { if (err) { if (err.code === "ENOENT") { if (watchedFile.mtime.getTime() !== 0) { - watchedFile.mtime = new Date(0); + watchedFile.mtime = missingFileModifiedTime; watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Deleted); } } @@ -697,17 +697,7 @@ namespace ts.server { } } else { - const oldTime = watchedFile.mtime.getTime(); - const newTime = stats.mtime.getTime(); - if (oldTime !== newTime) { - watchedFile.mtime = stats.mtime; - const eventKind = oldTime === 0 - ? FileWatcherEventKind.Created - : newTime === 0 - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; - watchedFile.callback(watchedFile.fileName, eventKind); - } + onWatchedFileStat(watchedFile, stats.mtime); } }); } @@ -741,7 +731,7 @@ namespace ts.server { callback, mtime: sys.fileExists(fileName) ? getModifiedTime(fileName) - : new Date(0) // Any subsequent modification will occur after this time + : missingFileModifiedTime // Any subsequent modification will occur after this time }; watchedFiles.push(file); From c3b9904190317a2fb1534749f9dc0d07b939bd3a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 22 Dec 2017 17:01:33 -0800 Subject: [PATCH 10/23] Add test to verify timeout queues --- src/compiler/sys.ts | 44 +++++++++++----- src/compiler/watchUtilities.ts | 2 +- src/harness/unittests/tscWatchMode.ts | 63 +++++++++++++++++++++++ src/harness/virtualFileSystemWithWatch.ts | 53 +++++++++++++------ 4 files changed, 130 insertions(+), 32 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 7edfd5dd50367..6428036a17ebb 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -60,6 +60,17 @@ namespace ts { /* @internal */ export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time + + const chunkSizeOrUnchangedThresholdsForPriority = getPriorityValues(32); + function chunkSize(watchPriority: WatchPriority) { + return chunkSizeOrUnchangedThresholdsForPriority[watchPriority]; + } + + /*@internal*/ + export function unChangedThreshold(watchPriority: WatchPriority) { + return chunkSizeOrUnchangedThresholdsForPriority[watchPriority]; + } + /* @internal */ export function createDynamicPriorityPollingStatsSet(host: System): DynamicPriorityPollingStatsSet { if (!host.getModifiedTime || !host.setTimeout) { @@ -74,10 +85,9 @@ namespace ts { interface WatchPriorityQueue extends Array { watchPriority: WatchPriority; pollIndex: number; + pollScheduled: boolean; } - const chunkSizes = getPriorityValues(32); - const unChangedThresholds = getPriorityValues(32); const watchedFiles: WatchedFile[] = []; const changedFilesInLastPoll: WatchedFile[] = []; const priorityQueues = [createPriorityQueue(WatchPriority.High), createPriorityQueue(WatchPriority.Medium), createPriorityQueue(WatchPriority.Low)]; @@ -109,18 +119,20 @@ namespace ts { const queue = [] as WatchPriorityQueue; queue.watchPriority = watchPriority; queue.pollIndex = 0; + queue.pollScheduled = false; return queue; } function pollPriorityQueue(queue: WatchPriorityQueue) { const priority = queue.watchPriority; - queue.pollIndex = pollQueue(queue, priority, queue.pollIndex, chunkSizes[priority]); + queue.pollIndex = pollQueue(queue, priority, queue.pollIndex, chunkSize(priority)); // Set the next polling index and timeout if (queue.length) { scheduleNextPoll(priority); } else { Debug.assert(queue.pollIndex === 0); + queue.pollScheduled = false; } } @@ -132,7 +144,7 @@ namespace ts { pollPriorityQueue(queue); // Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue // as pollPriorityQueue wont schedule for next poll - if (!queue.length && changedFilesInLastPoll.length) { + if (!queue.pollScheduled && changedFilesInLastPoll.length) { scheduleNextPoll(WatchPriority.High); } } @@ -141,7 +153,6 @@ namespace ts { // Max visit would be all elements of the queue let needsVisit = queue.length; let definedValueCopyToIndex = pollIndex; - const unChangedThreshold = unChangedThresholds[priority]; for (let polled = 0; polled < chunkSize && needsVisit > 0; nextPollIndex(), needsVisit--) { const watchedFile = queue[pollIndex]; if (!watchedFile) { @@ -161,17 +172,18 @@ namespace ts { else if (fileChanged) { watchedFile.unchangedPolls = 0; // Changed files go to changedFilesInLastPoll queue - if (queue !== changedFilesInLastPoll && priority !== WatchPriority.High) { + if (queue !== changedFilesInLastPoll) { queue[pollIndex] = undefined; addChangedFileToHighPriorityQueue(watchedFile); } } - else if (watchedFile.unchangedPolls !== unChangedThreshold) { + else if (watchedFile.unchangedPolls !== unChangedThreshold(priority)) { watchedFile.unchangedPolls++; } else if (queue === changedFilesInLastPoll) { // Restart unchangedPollCount for unchanged file and move to high priority queue - watchedFile.unchangedPolls = 0; + watchedFile.unchangedPolls = 1; + queue[pollIndex] = undefined; addToPriorityQueue(watchedFile, WatchPriority.High); } else if (priority !== WatchPriority.Low) { @@ -207,19 +219,23 @@ namespace ts { } function addToPriorityQueue(file: WatchedFile, priority: WatchPriority) { - if (priorityQueues[priority].push(file) === 1) { - scheduleNextPoll(priority); - } + priorityQueues[priority].push(file); + scheduleNextPollIfNotAlreadyScheduled(priority); } function addChangedFileToHighPriorityQueue(file: WatchedFile) { - if (changedFilesInLastPoll.push(file) === 1 && !priorityQueues[WatchPriority.High].length) { - scheduleNextPoll(WatchPriority.High); + changedFilesInLastPoll.push(file); + scheduleNextPollIfNotAlreadyScheduled(WatchPriority.High); + } + + function scheduleNextPollIfNotAlreadyScheduled(priority: WatchPriority) { + if (!priorityQueues[priority].pollScheduled) { + scheduleNextPoll(priority); } } function scheduleNextPoll(priority: WatchPriority) { - host.setTimeout(priority === WatchPriority.High ? pollHighPriorityQueue : pollPriorityQueue, pollingInterval(priority), priorityQueues[priority]); + priorityQueues[priority].pollScheduled = host.setTimeout(priority === WatchPriority.High ? pollHighPriorityQueue : pollPriorityQueue, pollingInterval(priority), priorityQueues[priority]); } function getModifiedTime(fileName: string) { diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 9ddaa8f2d6766..f35f952834afb 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -108,7 +108,7 @@ namespace ts { } export function getWatchFactory(host: System, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { - const value = host.getEnvironmentVariable("TSC_WATCHFILE"); + const value = host.getEnvironmentVariable && host.getEnvironmentVariable("TSC_WATCHFILE"); switch (value) { case "PriorityPollingInterval": // Use polling interval based on priority when create watch using host.watchFile diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 8abc23f1ac7cc..60361f56c1af5 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2113,4 +2113,67 @@ declare module "fs" { host.checkScreenClears(2); }); }); + + describe("tsc-watch with different polling/non polling options", () => { + it("watchFile using dynamic priority polling", () => { + const projectFolder = "/a/username/project"; + const file1: FileOrFolder = { + path: `${projectFolder}/typescript.ts`, + content: "var z = 10;" + }; + const files = [file1, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchModeWithoutConfigFile([file1.path], host); + + const initialProgram = watch(); + verifyProgram(); + + const mediumPriorityThreshold = unChangedThreshold(WatchPriority.Medium); + for (let index = 0; index < mediumPriorityThreshold; index++) { + // Transition libFile and file1 to low priority queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + } + + // Make a change to file + file1.content = "var zz30 = 100;"; + host.reloadFS(files); + + // This should detect change in the file + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + + // Callbacks: medium priority + high priority queue and scheduled program update + host.checkTimeoutQueueLengthAndRun(3); + // During this timeout the file would be detected as unchanged + let fileUnchangeDetected = 1; + const newProgram = watch(); + assert.notStrictEqual(newProgram, initialProgram); + + verifyProgram(); + const outputFile1 = changeExtension(file1.path, ".js"); + assert.isTrue(host.fileExists(outputFile1)); + assert.equal(host.readFile(outputFile1), file1.content + host.newLine); + + const newThreshold = unChangedThreshold(WatchPriority.High) + mediumPriorityThreshold; + for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { + // For low + Medium/high priority + host.checkTimeoutQueueLengthAndRun(2); + assert.deepEqual(watch(), newProgram); + } + + // Everything goes in low priority queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), newProgram); + + function verifyProgram() { + checkProgramActualFiles(watch(), files.map(f => f.path)); + checkWatchedFiles(host, []); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, [], /*recursive*/ true); + } + }); + }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 921d4674231da..6fe5bac459f58 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -36,6 +36,7 @@ interface Array {}` currentDirectory?: string; newLine?: string; useWindowsStylePaths?: boolean; + environmentVariables?: Map; } export function createWatchedSystem(fileOrFolderList: ReadonlyArray, params?: TestServerHostCreationParameters): TestServerHost { @@ -48,7 +49,8 @@ interface Array {}` params.currentDirectory || "/", fileOrFolderList, params.newLine, - params.useWindowsStylePaths); + params.useWindowsStylePaths, + params.environmentVariables); return host; } @@ -62,7 +64,8 @@ interface Array {}` params.currentDirectory || "/", fileOrFolderList, params.newLine, - params.useWindowsStylePaths); + params.useWindowsStylePaths, + params.environmentVariables); return host; } @@ -75,6 +78,7 @@ interface Array {}` interface FSEntry { path: Path; fullPath: string; + modifiedTime: Date; } interface File extends FSEntry { @@ -259,7 +263,7 @@ interface Array {}` private readonly executingFilePath: string; private readonly currentDirectory: string; - constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) { + constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean, private readonly environmentVariables?: Map) { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); this.executingFilePath = this.getHostSpecificPath(executingFilePath); @@ -307,6 +311,7 @@ interface Array {}` // Update file if (currentEntry.content !== fileOrDirectory.content) { currentEntry.content = fileOrDirectory.content; + currentEntry.modifiedTime = new Date(); if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) { this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath); } @@ -326,6 +331,7 @@ interface Array {}` } else { // Folder update: Nothing to do. + currentEntry.modifiedTime = new Date(); } } } @@ -416,6 +422,7 @@ interface Array {}` private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder, ignoreWatch?: boolean) { folder.entries.push(fileOrDirectory); + folder.modifiedTime = new Date(); this.fs.set(fileOrDirectory.path, fileOrDirectory); if (ignoreWatch) { @@ -432,6 +439,7 @@ interface Array {}` const baseFolder = this.fs.get(basePath) as Folder; if (basePath !== fileOrDirectory.path) { Debug.assert(!!baseFolder); + baseFolder.modifiedTime = new Date(); filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory); } this.fs.delete(fileOrDirectory.path); @@ -493,23 +501,26 @@ interface Array {}` } } - private toFile(fileOrDirectory: FileOrFolder): File { - const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); + private toFsEntry(path: string): FSEntry { + const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); return { path: this.toPath(fullPath), - content: fileOrDirectory.content, fullPath, - fileSize: fileOrDirectory.fileSize + modifiedTime: new Date() }; } + private toFile(fileOrDirectory: FileOrFolder): File { + const file = this.toFsEntry(fileOrDirectory.path) as File; + file.content = fileOrDirectory.content; + file.fileSize = fileOrDirectory.fileSize; + return file; + } + private toFolder(path: string): Folder { - const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); - return { - path: this.toPath(fullPath), - entries: [], - fullPath - }; + const folder = this.toFsEntry(path) as Folder; + folder.entries = []; + return folder; } fileExists(s: string) { @@ -517,6 +528,12 @@ interface Array {}` return isFile(this.fs.get(path)); } + getModifiedTime(s: string) { + const path = this.toFullPath(s); + const fsEntry = this.fs.get(path); + return fsEntry && fsEntry.modifiedTime; + } + readFile(s: string) { const fsEntry = this.fs.get(this.toFullPath(s)); return isFile(fsEntry) ? fsEntry.content : undefined; @@ -624,7 +641,7 @@ interface Array {}` this.timeoutCallbacks.invoke(timeoutId); } catch (e) { - if (e.message === this.existMessage) { + if (e.message === this.exitMessage) { return; } throw e; @@ -682,15 +699,17 @@ interface Array {}` clear(this.output); } - readonly existMessage = "System Exit"; + readonly exitMessage = "System Exit"; exitCode: number; readonly resolvePath = (s: string) => s; readonly getExecutingFilePath = () => this.executingFilePath; readonly getCurrentDirectory = () => this.currentDirectory; exit(exitCode?: number) { this.exitCode = exitCode; - throw new Error(this.existMessage); + throw new Error(this.exitMessage); + } + getEnvironmentVariable(name: string) { + return this.environmentVariables && this.environmentVariables.get(name); } - readonly getEnvironmentVariable = notImplemented; } } From 09caaa3775c132183f1737599af1695045aed576 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 16 Jan 2018 11:38:42 -0800 Subject: [PATCH 11/23] Fix the api test failures --- src/compiler/sys.ts | 1 + src/server/editorServices.ts | 4 ---- tests/baselines/reference/api/tsserverlibrary.d.ts | 8 +------- tests/baselines/reference/api/typescript.d.ts | 5 ----- 4 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 6428036a17ebb..1702911d77eac 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -24,6 +24,7 @@ namespace ts { export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; export type DirectoryWatcherCallback = (fileName: string) => void; + /*@internal*/ export interface WatchedFile { readonly fileName: string; readonly callback: FileWatcherCallback; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 43cb678d743f6..e6153d91069c6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -321,10 +321,6 @@ namespace ts.server { typesMapLocation?: string; } - export type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, pollingInterval: number | undefined, watchType: WatchType, project?: Project) => FileWatcher; - export type WatchFilePath = (host: ServerHost, file: string, cb: FilePathWatcherCallback, pollingInterval: number | undefined, path: Path, watchType: WatchType, project?: Project) => FileWatcher; - export type WatchDirectory = (host: ServerHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, watchType: WatchType, project?: Project) => FileWatcher; - function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) { return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`; } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 942ca44a78239..ba551acf3e6e4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2824,11 +2824,6 @@ declare namespace ts { } type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; type DirectoryWatcherCallback = (fileName: string) => void; - interface WatchedFile { - fileName: string; - callback: FileWatcherCallback; - mtime?: Date; - } /** * Partial interface of the System thats needed to support the caching of directory structure */ @@ -7765,7 +7760,6 @@ declare namespace ts.server { /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects; constructor(opts: ProjectServiceOptions); - private createWatcherLog(watchType, project); toPath(fileName: string): Path; private loadTypesMap(); updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse): void; @@ -7792,7 +7786,7 @@ declare namespace ts.server { private findContainingExternalProject(fileName); getFormatCodeOptions(file?: NormalizedPath): FormatCodeSettings; private updateProjectGraphs(projects); - private onSourceFileChanged(fileName, eventKind); + private onSourceFileChanged(fileName, eventKind, path); private handleDeletedFile(info); private onConfigChangedForConfiguredProject(project, eventKind); /** diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d5259f39f7fc2..efde52f5f7b6b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2824,11 +2824,6 @@ declare namespace ts { } type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; type DirectoryWatcherCallback = (fileName: string) => void; - interface WatchedFile { - fileName: string; - callback: FileWatcherCallback; - mtime?: Date; - } /** * Partial interface of the System thats needed to support the caching of directory structure */ From fa8d4cba7843b75d68c71fdc86cb490eb50156e8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 16 Jan 2018 13:15:39 -0800 Subject: [PATCH 12/23] Move the polling settings to sys instead of watch utilities --- src/compiler/sys.ts | 172 +++++++++++++--------- src/compiler/watch.ts | 10 +- src/compiler/watchUtilities.ts | 44 ++---- src/harness/unittests/tscWatchMode.ts | 12 +- src/harness/virtualFileSystemWithWatch.ts | 10 +- src/server/editorServices.ts | 8 +- src/server/project.ts | 2 +- 7 files changed, 139 insertions(+), 119 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 1702911d77eac..972918231fe7c 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -32,10 +32,10 @@ namespace ts { } /* @internal */ - export enum WatchPriority { - High, - Medium, - Low + export enum PollingInterval { + High = 2000, + Medium = 500, + Low = 250 } function getPriorityValues(highPriorityValue: number): [number, number, number] { @@ -44,36 +44,47 @@ namespace ts { return [highPriorityValue, mediumPriorityValue, lowPriorityValue]; } - const pollingIntervalsForPriority = getPriorityValues(250); - function pollingInterval(watchPriority: WatchPriority): number { + function pollingInterval(watchPriority: PollingInterval): number { return pollingIntervalsForPriority[watchPriority]; } + const pollingIntervalsForPriority = getPriorityValues(250); + /* @internal */ - export function watchFileUsingPriorityPollingInterval(host: System, fileName: string, callback: FileWatcherCallback, watchPriority: WatchPriority): FileWatcher { + export function watchFileUsingPriorityPollingInterval(host: System, fileName: string, callback: FileWatcherCallback, watchPriority: PollingInterval): FileWatcher { return host.watchFile(fileName, callback, pollingInterval(watchPriority)); } /* @internal */ - export interface DynamicPriorityPollingStatsSet { - watchFile(fileName: string, callback: FileWatcherCallback, defaultPriority: WatchPriority): FileWatcher; - } + export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval) => FileWatcher; /* @internal */ export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time - const chunkSizeOrUnchangedThresholdsForPriority = getPriorityValues(32); - function chunkSize(watchPriority: WatchPriority) { - return chunkSizeOrUnchangedThresholdsForPriority[watchPriority]; + enum ChunkSize { + Low = 32, + Medium = 64, + High = 256 + } + + function chunkSize(pollingInterval: PollingInterval) { + switch (pollingInterval) { + case PollingInterval.Low: + return ChunkSize.Low; + case PollingInterval.Medium: + return ChunkSize.Medium; + case PollingInterval.High: + return ChunkSize.High; + } } /*@internal*/ - export function unChangedThreshold(watchPriority: WatchPriority) { - return chunkSizeOrUnchangedThresholdsForPriority[watchPriority]; + export function unChangedThreshold(pollingInterval: PollingInterval) { + return chunkSize(pollingInterval); } /* @internal */ - export function createDynamicPriorityPollingStatsSet(host: System): DynamicPriorityPollingStatsSet { + export function createDynamicPriorityPollingWatchFile(host: System): HostWatchFile { if (!host.getModifiedTime || !host.setTimeout) { throw notImplemented(); } @@ -83,20 +94,20 @@ namespace ts { unchangedPolls: number; } - interface WatchPriorityQueue extends Array { - watchPriority: WatchPriority; + interface PollingIntervalQueue extends Array { + pollingInterval: PollingInterval; pollIndex: number; pollScheduled: boolean; } const watchedFiles: WatchedFile[] = []; const changedFilesInLastPoll: WatchedFile[] = []; - const priorityQueues = [createPriorityQueue(WatchPriority.High), createPriorityQueue(WatchPriority.Medium), createPriorityQueue(WatchPriority.Low)]; - return { - watchFile - }; + const lowPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Low); + const mediumPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Medium); + const highPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.High); + return watchFile; - function watchFile(fileName: string, callback: FileWatcherCallback, defaultPriority: WatchPriority): FileWatcher { + function watchFile(fileName: string, callback: FileWatcherCallback, defaultPollingInterval: PollingInterval): FileWatcher { const file: WatchedFile = { fileName, callback, @@ -105,31 +116,30 @@ namespace ts { }; watchedFiles.push(file); - addToPriorityQueue(file, defaultPriority); + addToPollingIntervalQueue(file, defaultPollingInterval); return { close: () => { file.isClosed = true; // Remove from watchedFiles unorderedRemoveItem(watchedFiles, file); - // Do not update priority queue since that will happen as part of polling + // Do not update polling interval queue since that will happen as part of polling } }; } - function createPriorityQueue(watchPriority: WatchPriority): WatchPriorityQueue { - const queue = [] as WatchPriorityQueue; - queue.watchPriority = watchPriority; + function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue { + const queue = [] as PollingIntervalQueue; + queue.pollingInterval = pollingInterval; queue.pollIndex = 0; queue.pollScheduled = false; return queue; } - function pollPriorityQueue(queue: WatchPriorityQueue) { - const priority = queue.watchPriority; - queue.pollIndex = pollQueue(queue, priority, queue.pollIndex, chunkSize(priority)); + function pollPollingIntervalQueue(queue: PollingIntervalQueue) { + queue.pollIndex = pollQueue(queue, queue.pollingInterval, queue.pollIndex, chunkSize(queue.pollingInterval)); // Set the next polling index and timeout if (queue.length) { - scheduleNextPoll(priority); + scheduleNextPoll(queue.pollingInterval); } else { Debug.assert(queue.pollIndex === 0); @@ -137,20 +147,20 @@ namespace ts { } } - function pollHighPriorityQueue(queue: WatchPriorityQueue) { + function pollLowPollingIntervalQueue(queue: PollingIntervalQueue) { // Always poll complete list of changedFilesInLastPoll - pollQueue(changedFilesInLastPoll, WatchPriority.High, /*pollIndex*/ 0, changedFilesInLastPoll.length); + pollQueue(changedFilesInLastPoll, PollingInterval.Low, /*pollIndex*/ 0, changedFilesInLastPoll.length); // Finally do the actual polling of the queue - pollPriorityQueue(queue); + pollPollingIntervalQueue(queue); // Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue - // as pollPriorityQueue wont schedule for next poll + // as pollPollingIntervalQueue wont schedule for next poll if (!queue.pollScheduled && changedFilesInLastPoll.length) { - scheduleNextPoll(WatchPriority.High); + scheduleNextPoll(PollingInterval.Low); } } - function pollQueue(queue: WatchedFile[], priority: WatchPriority, pollIndex: number, chunkSize: number) { + function pollQueue(queue: WatchedFile[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) { // Max visit would be all elements of the queue let needsVisit = queue.length; let definedValueCopyToIndex = pollIndex; @@ -175,22 +185,22 @@ namespace ts { // Changed files go to changedFilesInLastPoll queue if (queue !== changedFilesInLastPoll) { queue[pollIndex] = undefined; - addChangedFileToHighPriorityQueue(watchedFile); + addChangedFileToLowPollingIntervalQueue(watchedFile); } } - else if (watchedFile.unchangedPolls !== unChangedThreshold(priority)) { + else if (watchedFile.unchangedPolls !== unChangedThreshold(pollingInterval)) { watchedFile.unchangedPolls++; } else if (queue === changedFilesInLastPoll) { - // Restart unchangedPollCount for unchanged file and move to high priority queue + // Restart unchangedPollCount for unchanged file and move to low polling interval queue watchedFile.unchangedPolls = 1; queue[pollIndex] = undefined; - addToPriorityQueue(watchedFile, WatchPriority.High); + addToPollingIntervalQueue(watchedFile, PollingInterval.Low); } - else if (priority !== WatchPriority.Low) { + else if (pollingInterval !== PollingInterval.High) { watchedFile.unchangedPolls++; queue[pollIndex] = undefined; - addToPriorityQueue(watchedFile, priority + 1); + addToPollingIntervalQueue(watchedFile, pollingInterval === PollingInterval.Low ? PollingInterval.Medium : PollingInterval.High); } if (queue[pollIndex]) { @@ -219,24 +229,35 @@ namespace ts { } } - function addToPriorityQueue(file: WatchedFile, priority: WatchPriority) { - priorityQueues[priority].push(file); - scheduleNextPollIfNotAlreadyScheduled(priority); + function pollingIntervalQueue(pollingInterval: PollingInterval) { + switch (pollingInterval) { + case PollingInterval.Low: + return lowPollingIntervalQueue; + case PollingInterval.Medium: + return mediumPollingIntervalQueue; + case PollingInterval.High: + return highPollingIntervalQueue; + } + } + + function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).push(file); + scheduleNextPollIfNotAlreadyScheduled(pollingInterval); } - function addChangedFileToHighPriorityQueue(file: WatchedFile) { + function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) { changedFilesInLastPoll.push(file); - scheduleNextPollIfNotAlreadyScheduled(WatchPriority.High); + scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low); } - function scheduleNextPollIfNotAlreadyScheduled(priority: WatchPriority) { - if (!priorityQueues[priority].pollScheduled) { - scheduleNextPoll(priority); + function scheduleNextPollIfNotAlreadyScheduled(pollingInterval: PollingInterval) { + if (!pollingIntervalQueue(pollingInterval).pollScheduled) { + scheduleNextPoll(pollingInterval); } } - function scheduleNextPoll(priority: WatchPriority) { - priorityQueues[priority].pollScheduled = host.setTimeout(priority === WatchPriority.High ? pollHighPriorityQueue : pollPriorityQueue, pollingInterval(priority), priorityQueues[priority]); + function scheduleNextPoll(pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval)); } function getModifiedTime(fileName: string) { @@ -381,6 +402,7 @@ namespace ts { } const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; + const tscWatchOption = process.env.TSC_WATCHOPTION; const nodeSystem: System = { args: process.argv.slice(2), @@ -391,20 +413,6 @@ namespace ts { }, readFile, writeFile, - watchFile: useNonPollingWatchers ? createNonPollingWatchFile() : fsWatchFile, - watchDirectory: (directoryName, callback, recursive) => { - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - return fsWatchDirectory(directoryName, (eventName, relativeFileName) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); - } - }, recursive); - }, resolvePath: path => _path.resolve(path), fileExists, directoryExists, @@ -474,6 +482,20 @@ namespace ts { process.stdout.write("\x1Bc"); } }; + nodeSystem.watchFile = getWatchFile(); + nodeSystem.watchDirectory = (directoryName, callback, recursive) => { + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + return fsWatchDirectory(directoryName, (eventName, relativeFileName) => { + // In watchDirectory we only care about adding and removing files (when event name is + // "rename"); changes made within files are handled by corresponding fileWatchers (when + // event name is "change") + if (eventName === "rename") { + // When deleting a file, the passed baseFileName is null + callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); + } + }, recursive); + }; return nodeSystem; function isFileSystemCaseSensitive(): boolean { @@ -493,6 +515,20 @@ namespace ts { }); } + function getWatchFile(): HostWatchFile { + switch (tscWatchOption) { + case "PriorityPollingInterval": + // Use polling interval based on priority when create watch using host.watchFile + return fsWatchFile; + case "DynamicPriorityPolling": + return createDynamicPriorityPollingWatchFile(nodeSystem); + } + return useNonPollingWatchers ? + createNonPollingWatchFile() : + // Default to do not use polling interval as it is before this experiment branch + (fileName, callback) => fsWatchFile(fileName, callback); + } + function createNonPollingWatchFile() { // One file can have multiple watchers const fileWatcherCallbacks = createMultiMap(); diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index d5004ba39205b..0d9285921e943 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -257,11 +257,11 @@ namespace ts { const writeLog: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? s => { system.write(s); system.write(system.newLine); } : noop; watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; - const { watchFile, watchFilePath, watchDirectory: watchDirectoryWorker } = getWatchFactory(system, watchLogLevel, writeLog); + const { watchFile, watchFilePath, watchDirectory: watchDirectoryWorker } = getWatchFactory(watchLogLevel, writeLog); const directoryStructureHost = configFileName ? createCachedDirectoryStructureHost(system) : system; if (configFileName) { - watchFile(system, configFileName, scheduleProgramReload, WatchPriority.Low); + watchFile(system, configFileName, scheduleProgramReload, PollingInterval.High); } const getCurrentDirectory = memoize(() => directoryStructureHost.getCurrentDirectory()); @@ -414,7 +414,7 @@ namespace ts { hostSourceFile.sourceFile = sourceFile; sourceFile.version = hostSourceFile.version.toString(); if (!hostSourceFile.fileWatcher) { - hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, WatchPriority.High, path); + hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, PollingInterval.Low, path); } } else { @@ -427,7 +427,7 @@ namespace ts { let fileWatcher: FileWatcher; if (sourceFile) { sourceFile.version = "0"; - fileWatcher = watchFilePath(system, fileName, onSourceFileChange, WatchPriority.High, path); + fileWatcher = watchFilePath(system, fileName, onSourceFileChange, PollingInterval.Low, path); sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); } else { @@ -601,7 +601,7 @@ namespace ts { } function watchMissingFilePath(missingFilePath: Path) { - return watchFilePath(system, missingFilePath, onMissingFileChange, WatchPriority.Medium, missingFilePath); + return watchFilePath(system, missingFilePath, onMissingFileChange, PollingInterval.Medium, missingFilePath); } function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) { diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index f35f952834afb..75298cc87c05e 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -96,9 +96,9 @@ namespace ts { Verbose } - export type WatchFile = (host: System, file: string, callback: FileWatcherCallback, watchPriority: WatchPriority, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; + export type WatchFile = (host: System, file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; - export type WatchFilePath = (host: System, file: string, callback: FilePathWatcherCallback, watchPriority: WatchPriority, path: Path, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; + export type WatchFilePath = (host: System, file: string, callback: FilePathWatcherCallback, pollingInterval: PollingInterval, path: Path, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; export type WatchDirectory = (host: System, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; export interface WatchFactory { @@ -107,30 +107,15 @@ namespace ts { watchDirectory: WatchDirectory; } - export function getWatchFactory(host: System, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { - const value = host.getEnvironmentVariable && host.getEnvironmentVariable("TSC_WATCHFILE"); - switch (value) { - case "PriorityPollingInterval": - // Use polling interval based on priority when create watch using host.watchFile - return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFileUsingPriorityPollingInterval, watchDirectory); - case "DynamicPriorityPolling": - // Dynamically move frequently changing files to high frequency polling and non changing files to lower frequency - return getWatchFactoryWithDynamicPriorityPolling(host, watchLogLevel, log, getDetailWatchInfo); - default: - return getDefaultWatchFactory(watchLogLevel, log, getDetailWatchInfo); - } - } - - export function getDefaultWatchFactory(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { - // Current behaviour in which polling interval is always 250 ms + export function getWatchFactory(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFile, watchDirectory); } function getWatchFactoryWith(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo: GetDetailWatchInfo | undefined, - watchFile: (host: System, file: string, callback: FileWatcherCallback, watchPriority: WatchPriority) => FileWatcher, + watchFile: (host: System, file: string, callback: FileWatcherCallback, watchPriority: PollingInterval) => FileWatcher, watchDirectory: (host: System, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags) => FileWatcher): WatchFactory { - const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFile); - const createFilePathWatcher: CreateFileWatcher = watchLogLevel === WatchLogLevel.None ? watchFilePath : createFileWatcher; + const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFile); + const createFilePathWatcher: CreateFileWatcher = watchLogLevel === WatchLogLevel.None ? watchFilePath : createFileWatcher; const createDirectoryWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchDirectory); return { watchFile: (host, file, callback, pollingInterval, detailInfo1, detailInfo2) => @@ -141,22 +126,13 @@ namespace ts { createDirectoryWatcher(host, directory, callback, flags, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchDirectory, log, "DirectoryWatcher", getDetailWatchInfo) }; - function watchFilePath(host: System, file: string, callback: FilePathWatcherCallback, watchPriority: WatchPriority, path: Path): FileWatcher { - return watchFile(host, file, (fileName, eventKind) => callback(fileName, eventKind, path), watchPriority); - } - } - - function getWatchFactoryWithDynamicPriorityPolling(host: System, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { - const pollingSet = createDynamicPriorityPollingStatsSet(host); - return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFile, watchDirectory); - - function watchFile(_host: System, file: string, callback: FileWatcherCallback, watchPriority: WatchPriority): FileWatcher { - return pollingSet.watchFile(file, callback, watchPriority); + function watchFilePath(host: System, file: string, callback: FilePathWatcherCallback, pollingInterval: PollingInterval, path: Path): FileWatcher { + return watchFile(host, file, (fileName, eventKind) => callback(fileName, eventKind, path), pollingInterval); } } - function watchFile(host: System, file: string, callback: FileWatcherCallback, _watchPriority: WatchPriority): FileWatcher { - return host.watchFile(file, callback); + function watchFile(host: System, file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval): FileWatcher { + return host.watchFile(file, callback, pollingInterval); } function watchDirectory(host: System, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 60361f56c1af5..ffdf02c4eb128 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2123,15 +2123,15 @@ declare module "fs" { }; const files = [file1, libFile]; const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); + environmentVariables.set("TSC_WATCHOPTION", "DynamicPriorityPolling"); const host = createWatchedSystem(files, { environmentVariables }); const watch = createWatchModeWithoutConfigFile([file1.path], host); const initialProgram = watch(); verifyProgram(); - const mediumPriorityThreshold = unChangedThreshold(WatchPriority.Medium); - for (let index = 0; index < mediumPriorityThreshold; index++) { + const mediumPollingIntervalThreshold = unChangedThreshold(PollingInterval.Medium); + for (let index = 0; index < mediumPollingIntervalThreshold; index++) { // Transition libFile and file1 to low priority queue host.checkTimeoutQueueLengthAndRun(1); assert.deepEqual(watch(), initialProgram); @@ -2157,14 +2157,14 @@ declare module "fs" { assert.isTrue(host.fileExists(outputFile1)); assert.equal(host.readFile(outputFile1), file1.content + host.newLine); - const newThreshold = unChangedThreshold(WatchPriority.High) + mediumPriorityThreshold; + const newThreshold = unChangedThreshold(PollingInterval.Low) + mediumPollingIntervalThreshold; for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { - // For low + Medium/high priority + // For high + Medium/low polling interval host.checkTimeoutQueueLengthAndRun(2); assert.deepEqual(watch(), newProgram); } - // Everything goes in low priority queue + // Everything goes in high polling interval queue host.checkTimeoutQueueLengthAndRun(1); assert.deepEqual(watch(), newProgram); diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 6fe5bac459f58..7f732a9c67281 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -262,6 +262,7 @@ interface Array {}` readonly watchedFiles = createMultiMap(); private readonly executingFilePath: string; private readonly currentDirectory: string; + private readonly dynamicPriorityWatchFile: HostWatchFile; constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean, private readonly environmentVariables?: Map) { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); @@ -269,6 +270,9 @@ interface Array {}` this.executingFilePath = this.getHostSpecificPath(executingFilePath); this.currentDirectory = this.getHostSpecificPath(currentDirectory); this.reloadFS(fileOrFolderList); + this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHOPTION") === "DynamicPriorityPolling" ? + createDynamicPriorityPollingWatchFile(this) : + undefined; } getNewLine() { @@ -602,7 +606,11 @@ interface Array {}` return Harness.mockHash(s); } - watchFile(fileName: string, cb: FileWatcherCallback) { + watchFile(fileName: string, cb: FileWatcherCallback, pollingInterval: number) { + if (this.dynamicPriorityWatchFile) { + return this.dynamicPriorityWatchFile(fileName, cb, pollingInterval); + } + const path = this.toFullPath(fileName); const callback: TestFileWatcher = { fileName, cb }; this.watchedFiles.add(path, callback); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e6153d91069c6..929abbd001fdb 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -449,7 +449,7 @@ namespace ts.server { const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose : this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; - this.watchFactory = getDefaultWatchFactory(watchLogLevel, log, getDetailWatchInfo); + this.watchFactory = getWatchFactory(watchLogLevel, log, getDetailWatchInfo); } toPath(fileName: string) { @@ -1116,7 +1116,7 @@ namespace ts.server { this.host, configFileName, (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind), - WatchPriority.Low, + PollingInterval.High, WatchType.ConfigFileForInferredRoot ); this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); @@ -1500,7 +1500,7 @@ namespace ts.server { this.host, configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind), - WatchPriority.Low, + PollingInterval.High, WatchType.ConfigFilePath, project ); @@ -1727,7 +1727,7 @@ namespace ts.server { this.host, fileName, (fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path), - WatchPriority.Medium, + PollingInterval.Medium, info.path, WatchType.ClosedScriptInfo ); diff --git a/src/server/project.ts b/src/server/project.ts index c7ea875f072eb..f1c4dd6af8534 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -932,7 +932,7 @@ namespace ts.server { this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } }, - WatchPriority.Medium, + PollingInterval.Medium, WatchType.MissingFilePath, this ); From bcfa02f501ba104d2d12c224f5deba818631db41 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 16 Jan 2018 13:59:02 -0800 Subject: [PATCH 13/23] Add option to watch using fs.watch instead of watching through polling --- src/compiler/sys.ts | 127 +++++++++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 972918231fe7c..640a9f86259ef 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -521,7 +521,14 @@ namespace ts { // Use polling interval based on priority when create watch using host.watchFile return fsWatchFile; case "DynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval return createDynamicPriorityPollingWatchFile(nodeSystem); + case "UseFsEvents": + // Use notifications from FS to watch with falling back to fs.watchFile + return watchFileUsingFsWatch; + case "UseFsEventsWithFallbackDynamicPolling": + // Use notifications from FS to watch with falling back to dynamic watch file + return watchFileUsingDynamicWatchFile; } return useNonPollingWatchers ? createNonPollingWatchFile() : @@ -607,24 +614,58 @@ namespace ts { } } - function fsWatchDirectory(directoryName: string, callback: (eventName: string, relativeFileName: string) => void, recursive?: boolean): FileWatcher { + type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string) => void; + + function createFsWatchFileCallback(callback: FsWatchCallback): FileWatcherCallback { + return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); + } + + function createFsWatchCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback { + return eventName => { + if (eventName === "rename") { + callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); + } + else { + // Change + callback(fileName, FileWatcherEventKind.Changed); + } + }; + } + + function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind.File | FileSystemEntryKind.Directory, callback: FsWatchCallback, recursive: boolean, fallbackPollingWatchFile: HostWatchFile, pollingInterval?: number): FileWatcher { let options: any; - /** Watcher for the directory depending on whether it is missing or present */ - let watcher = !directoryExists(directoryName) ? - watchMissingDirectory() : - watchPresentDirectory(); + /** Watcher for the file system entry depending on whether it is missing or present */ + let watcher = !fileSystemEntryExists(fileOrDirectory, entryKind) ? + watchMissingFileSystemEntry() : + watchPresentFileSystemEntry(); return { close: () => { - // Close the watcher (either existing directory watcher or missing directory watcher) + // Close the watcher (either existing file system entry watcher or missing file system entry watcher) watcher.close(); + watcher = undefined; } }; /** - * Watch the directory that is currently present - * and when the watched directory is deleted, switch to missing directory watcher + * Invoke the callback with rename and update the watcher if not closed + * @param createWatcher + */ + function invokeCallbackAndUpdateWatcher(createWatcher: () => FileWatcher) { + // Call the callback for current directory + callback("rename", ""); + + // If watcher is not closed, update it + if (watcher) { + watcher.close(); + watcher = createWatcher(); + } + } + + /** + * Watch the file or directory that is currently present + * and when the watched file or directory is deleted, switch to missing file system entry watcher */ - function watchPresentDirectory(): FileWatcher { + function watchPresentFileSystemEntry(): FileWatcher { // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) if (options === undefined) { @@ -635,40 +676,62 @@ namespace ts { options = { persistent: true }; } } + try { - const dirWatcher = _fs.watch( - directoryName, - options, - callback - ); - dirWatcher.on("error", () => { - // Watch the missing directory - watcher.close(); - watcher = watchMissingDirectory(); - // Call the callback for current directory - callback("rename", ""); - }); - return dirWatcher; + const presentWatcher = _fs.watch( + fileOrDirectory, + options, + callback + ); + // Watch the missing file or directory or error + presentWatcher.on("error", () => invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry)); + return presentWatcher; + } + catch (e) { + // Catch the exception and use polling instead + // Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point + // so instead of throwing error, use fs.watchFile + return watchPresentFileSystemEntryWithFsWatchFile(); + } + } + + /** + * Watch the file or directory using fs.watchFile since fs.watch threw exception + * Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point + */ + function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher { + return fallbackPollingWatchFile(fileOrDirectory, createFsWatchFileCallback(callback), pollingInterval); } /** - * Watch the directory that is missing - * and switch to existing directory when the directory is created + * Watch the file or directory that is missing + * and switch to existing file or directory when the missing filesystem entry is created */ - function watchMissingDirectory(): FileWatcher { - return fsWatchFile(directoryName, (_fileName, eventKind) => { - if (eventKind === FileWatcherEventKind.Created && directoryExists(directoryName)) { - watcher.close(); - watcher = watchPresentDirectory(); - // Call the callback for current directory + function watchMissingFileSystemEntry(): FileWatcher { + return fallbackPollingWatchFile(fileOrDirectory, (_fileName, eventKind) => { + if (eventKind === FileWatcherEventKind.Created && fileSystemEntryExists(fileOrDirectory, entryKind)) { + // Call the callback for current file or directory // For now it could be callback for the inner directory creation, // but just return current directory, better than current no-op - callback("rename", ""); + invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry); } - }); + }, pollingInterval); } } + function watchFileUsingFsWatch(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval); + } + + function watchFileUsingDynamicWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { + const watchFile = createDynamicPriorityPollingWatchFile(nodeSystem); + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); + } + + function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher { + return fsWatch(directoryName, FileSystemEntryKind.Directory, callback, !!recursive, fsWatchFile); + } + function readFile(fileName: string, _encoding?: string): string | undefined { if (!fileExists(fileName)) { return undefined; From 787c995985b31c995e4249cc0790db592665f649 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 16 Jan 2018 16:35:34 -0800 Subject: [PATCH 14/23] Allow recursive directory watching on non supported file system --- src/compiler/core.ts | 38 +++++ src/compiler/sys.ts | 161 +++++++++++++++++++--- src/compiler/utilities.ts | 1 - src/compiler/watchUtilities.ts | 4 - src/harness/unittests/tscWatchMode.ts | 2 +- src/harness/virtualFileSystemWithWatch.ts | 2 +- src/server/project.ts | 5 +- src/server/utilities.ts | 30 ---- 8 files changed, 183 insertions(+), 60 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 75564779280ba..2df3b46fdb212 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -20,6 +20,12 @@ namespace ts { /* @internal */ namespace ts { + export const emptyArray: never[] = [] as never[]; + + export function closeFileWatcher(watcher: FileWatcher) { + watcher.close(); + } + /** Create a MapLike with good performance. */ function createDictionaryObject(): MapLike { const map = Object.create(/*prototype*/ null); // tslint:disable-line:no-null-keyword @@ -3270,4 +3276,36 @@ namespace ts { export function singleElementArray(t: T | undefined): T[] | undefined { return t === undefined ? undefined : [t]; } + + export function enumerateInsertsAndDeletes(newItems: ReadonlyArray, oldItems: ReadonlyArray, comparer: (a: T, b: U) => Comparison, inserted: (newItem: T) => void, deleted: (oldItem: U) => void, unchanged?: (oldItem: U, newItem: T) => void) { + unchanged = unchanged || noop; + let newIndex = 0; + let oldIndex = 0; + const newLen = newItems.length; + const oldLen = oldItems.length; + while (newIndex < newLen && oldIndex < oldLen) { + const newItem = newItems[newIndex]; + const oldItem = oldItems[oldIndex]; + const compareResult = comparer(newItem, oldItem); + if (compareResult === Comparison.LessThan) { + inserted(newItem); + newIndex++; + } + else if (compareResult === Comparison.GreaterThan) { + deleted(oldItem); + oldIndex++; + } + else { + unchanged(oldItem, newItem); + newIndex++; + oldIndex++; + } + } + while (newIndex < newLen) { + inserted(newItems[newIndex++]); + } + while (oldIndex < oldLen) { + deleted(oldItems[oldIndex++]); + } + } } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 640a9f86259ef..c7f378e747839 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -57,6 +57,8 @@ namespace ts { /* @internal */ export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval) => FileWatcher; + /* @internal */ + export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive?: boolean) => FileWatcher; /* @internal */ export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time @@ -286,6 +288,93 @@ namespace ts { return false; } + /*@internal*/ + export interface RecursiveDirectoryWatcherHost { + watchDirectory: HostWatchDirectory; + getAccessileSortedChildDirectories(path: string): ReadonlyArray; + filePathComparer: Comparer; + } + + /** + * Watch the directory recursively using host provided method to watch child directories + * that means if this is recursive watcher, watch the children directories as well + * (eg on OS that dont support recursive watch using fs.watch use fs.watchFile) + */ + /*@internal*/ + export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher { + type ChildWatches = ReadonlyArray; + interface DirectoryWatcher extends FileWatcher { + childWatches: ChildWatches; + dirName: string; + } + + return createDirectoryWatcher; + + /** + * Create the directory watcher for the dirPath. + */ + function createDirectoryWatcher(dirName: string, callback: DirectoryWatcherCallback): DirectoryWatcher { + const watcher = host.watchDirectory(dirName, fileName => { + // Call the actual callback + callback(fileName); + + // Iterate through existing children and update the watches if needed + updateChildWatches(result, callback); + }); + + let result: DirectoryWatcher = { + close: () => { + watcher.close(); + result.childWatches.forEach(closeFileWatcher); + result = undefined; + }, + dirName, + childWatches: emptyArray + }; + updateChildWatches(result, callback); + return result; + } + + function updateChildWatches(watcher: DirectoryWatcher, callback: DirectoryWatcherCallback) { + // Iterate through existing children and update the watches if needed + if (watcher) { + watcher.childWatches = watchChildDirectories(watcher.dirName, watcher.childWatches, callback); + } + } + + /** + * Watch the directories in the parentDir + */ + function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches { + let newChildWatches: DirectoryWatcher[] | undefined; + enumerateInsertsAndDeletes( + host.getAccessileSortedChildDirectories(parentDir), + existingChildWatches, + (child, childWatcher) => host.filePathComparer(getNormalizedAbsolutePath(child, parentDir), childWatcher.dirName), + createAndAddChildDirectoryWatcher, + closeFileWatcher, + addChildDirectoryWatcher + ); + + return newChildWatches || emptyArray; + + /** + * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list + */ + function createAndAddChildDirectoryWatcher(childName: string) { + const result = createDirectoryWatcher(getNormalizedAbsolutePath(childName, parentDir), callback); + addChildDirectoryWatcher(result); + } + + /** + * Add child directory watcher to the new ChildDirectoryWatcher list + */ + function addChildDirectoryWatcher(childWatcher: DirectoryWatcher) { + (newChildWatches || (newChildWatches = [])).push(childWatcher); + } + } + } + /** * Partial interface of the System thats needed to support the caching of directory structure */ @@ -402,7 +491,8 @@ namespace ts { } const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; - const tscWatchOption = process.env.TSC_WATCHOPTION; + const tscWatchFile = process.env.TSC_WATCHFILE; + const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY; const nodeSystem: System = { args: process.argv.slice(2), @@ -483,19 +573,7 @@ namespace ts { } }; nodeSystem.watchFile = getWatchFile(); - nodeSystem.watchDirectory = (directoryName, callback, recursive) => { - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - return fsWatchDirectory(directoryName, (eventName, relativeFileName) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); - } - }, recursive); - }; + nodeSystem.watchDirectory = getWatchDirectory(); return nodeSystem; function isFileSystemCaseSensitive(): boolean { @@ -516,7 +594,7 @@ namespace ts { } function getWatchFile(): HostWatchFile { - switch (tscWatchOption) { + switch (tscWatchFile) { case "PriorityPollingInterval": // Use polling interval based on priority when create watch using host.watchFile return fsWatchFile; @@ -536,6 +614,29 @@ namespace ts { (fileName, callback) => fsWatchFile(fileName, callback); } + function getWatchDirectory(): HostWatchDirectory { + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + const fsSupportsRecursive = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); + if (fsSupportsRecursive) { + return watchDirectoryUsingFsWatch; + } + + const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? watchDirectoryUsingFsWatchFile : watchDirectoryUsingFsWatch; + const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ + filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, + watchDirectory + }); + + return (directoryName, callback, recursive) => { + if (recursive) { + return watchDirectoryRecursively(directoryName, callback); + } + watchDirectory(directoryName, callback); + }; + } + function createNonPollingWatchFile() { // One file can have multiple watchers const fileWatcherCallbacks = createMultiMap(); @@ -616,11 +717,11 @@ namespace ts { type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string) => void; - function createFsWatchFileCallback(callback: FsWatchCallback): FileWatcherCallback { + function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); } - function createFsWatchCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback { + function createFsWatchCallbackForFileWatcherCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback { return eventName => { if (eventName === "rename") { callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); @@ -632,6 +733,18 @@ namespace ts { }; } + function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback { + return (eventName, relativeFileName) => { + // In watchDirectory we only care about adding and removing files (when event name is + // "rename"); changes made within files are handled by corresponding fileWatchers (when + // event name is "change") + if (eventName === "rename") { + // When deleting a file, the passed baseFileName is null + callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName))); + } + }; + } + function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind.File | FileSystemEntryKind.Directory, callback: FsWatchCallback, recursive: boolean, fallbackPollingWatchFile: HostWatchFile, pollingInterval?: number): FileWatcher { let options: any; /** Watcher for the file system entry depending on whether it is missing or present */ @@ -700,7 +813,7 @@ namespace ts { * Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point */ function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher { - return fallbackPollingWatchFile(fileOrDirectory, createFsWatchFileCallback(callback), pollingInterval); + return fallbackPollingWatchFile(fileOrDirectory, createFileWatcherCallback(callback), pollingInterval); } /** @@ -720,18 +833,26 @@ namespace ts { } function watchFileUsingFsWatch(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { - return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval); + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval); } function watchFileUsingDynamicWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { const watchFile = createDynamicPriorityPollingWatchFile(nodeSystem); - return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); } function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher { return fsWatch(directoryName, FileSystemEntryKind.Directory, callback, !!recursive, fsWatchFile); } + function watchDirectoryUsingFsWatch(directoryName: string, callback: DirectoryWatcherCallback, recursive?: boolean) { + return fsWatchDirectory(directoryName, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive); + } + + function watchDirectoryUsingFsWatchFile(directoryName: string, callback: DirectoryWatcherCallback) { + return fsWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium); + } + function readFile(fileName: string, _encoding?: string): string | undefined { if (!fileExists(fileName)) { return undefined; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index ffcdf42c67e43..75083e159c5c1 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2,7 +2,6 @@ /* @internal */ namespace ts { - export const emptyArray: never[] = [] as never[]; export const resolvingEmptyArray: never[] = [] as never[]; export const emptyMap: ReadonlyMap = createMap(); export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap = emptyMap as ReadonlyUnderscoreEscapedMap; diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 75298cc87c05e..56dd68bb25ec7 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -181,10 +181,6 @@ namespace ts { return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : ""}`; } - export function closeFileWatcher(watcher: FileWatcher) { - watcher.close(); - } - export function closeFileWatcherOf(objWithWatcher: T) { objWithWatcher.watcher.close(); } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index ffdf02c4eb128..81f4d2ed19efe 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2123,7 +2123,7 @@ declare module "fs" { }; const files = [file1, libFile]; const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHOPTION", "DynamicPriorityPolling"); + environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); const host = createWatchedSystem(files, { environmentVariables }); const watch = createWatchModeWithoutConfigFile([file1.path], host); diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 7f732a9c67281..6c9855fde0e53 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -270,7 +270,7 @@ interface Array {}` this.executingFilePath = this.getHostSpecificPath(executingFilePath); this.currentDirectory = this.getHostSpecificPath(currentDirectory); this.reloadFS(fileOrFolderList); - this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHOPTION") === "DynamicPriorityPolling" ? + this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ? createDynamicPriorityPollingWatchFile(this) : undefined; } diff --git a/src/server/project.ts b/src/server/project.ts index f1c4dd6af8534..c7bff91ff7c1d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -891,7 +891,7 @@ namespace ts.server { const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; this.externalFiles = this.getExternalFiles(); - enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, + enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, compareStringsCaseSensitive, // Ensure a ScriptInfo is created for new external files. This is performed indirectly // by the LSHost for files in the program when the program is retrieved above but // the program doesn't contain external files so this must be done explicitly. @@ -899,8 +899,7 @@ namespace ts.server { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost); scriptInfo.attachToProject(this); }, - removed => this.detachScriptInfoFromProject(removed), - compareStringsCaseSensitive + removed => this.detachScriptInfoFromProject(removed) ); const elapsed = timestamp() - start; this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index c44419f8cf30a..262489dad5ba1 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -276,36 +276,6 @@ namespace ts.server { return index === 0 || value !== array[index - 1]; } - export function enumerateInsertsAndDeletes(newItems: SortedReadonlyArray, oldItems: SortedReadonlyArray, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, comparer: Comparer) { - let newIndex = 0; - let oldIndex = 0; - const newLen = newItems.length; - const oldLen = oldItems.length; - while (newIndex < newLen && oldIndex < oldLen) { - const newItem = newItems[newIndex]; - const oldItem = oldItems[oldIndex]; - const compareResult = comparer(newItem, oldItem); - if (compareResult === Comparison.LessThan) { - inserted(newItem); - newIndex++; - } - else if (compareResult === Comparison.GreaterThan) { - deleted(oldItem); - oldIndex++; - } - else { - newIndex++; - oldIndex++; - } - } - while (newIndex < newLen) { - inserted(newItems[newIndex++]); - } - while (oldIndex < oldLen) { - deleted(oldItems[oldIndex++]); - } - } - /* @internal */ export function indent(str: string): string { return "\n " + str; From 0c04fb18035cc23d3cee12ad934d98d5a74d8d87 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 21 Nov 2017 10:57:57 -0800 Subject: [PATCH 15/23] Add support to test when watching directories through watchFile --- src/harness/virtualFileSystemWithWatch.ts | 33 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 6c9855fde0e53..2f0525945f8c7 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -263,6 +263,7 @@ interface Array {}` private readonly executingFilePath: string; private readonly currentDirectory: string; private readonly dynamicPriorityWatchFile: HostWatchFile; + private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined; constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean, private readonly environmentVariables?: Map) { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); @@ -273,6 +274,23 @@ interface Array {}` this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ? createDynamicPriorityPollingWatchFile(this) : undefined; + const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY"); + if (tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile") { + const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium); + this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + getAccessileSortedChildDirectories: path => this.getDirectories(path), + filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + watchDirectory + }); + } + else if (tscWatchDirectory === "RecursiveDirectoryUsingNonRecursiveWatchDirectory") { + const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false); + this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + getAccessileSortedChildDirectories: path => this.getDirectories(path), + filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + watchDirectory + }); + } } getNewLine() { @@ -432,9 +450,7 @@ interface Array {}` if (ignoreWatch) { return; } - if (isFile(fileOrDirectory)) { - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); - } + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); } @@ -448,10 +464,8 @@ interface Array {}` } this.fs.delete(fileOrDirectory.path); - if (isFile(fileOrDirectory)) { - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); - } - else { + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); + if (!isFile(fileOrDirectory)) { Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); const relativePath = this.getRelativePathToDirectory(fileOrDirectory.fullPath, fileOrDirectory.fullPath); // Invoke directory and recursive directory watcher for the folder @@ -485,6 +499,8 @@ interface Array {}` */ private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); + // Folder is changed when the directory watcher is invoked + invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(folderFullPath)), ({ cb, fileName }) => cb(fileName, FileWatcherEventKind.Changed)); invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); } @@ -590,6 +606,9 @@ interface Array {}` } watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { + if (recursive && this.customRecursiveWatchDirectory) { + return this.customRecursiveWatchDirectory(directoryName, cb, true); + } const path = this.toFullPath(directoryName); const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; const callback: TestDirectoryWatcher = { From c3db9fadb0e5c5668801ed4c2080af38a8b8dc1b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 16 Jan 2018 17:06:51 -0800 Subject: [PATCH 16/23] Add tests for #19989, #20023 --- src/compiler/sys.ts | 4 +- src/harness/unittests/tscWatchMode.ts | 51 +++++++++++++- .../unittests/tsserverProjectSystem.ts | 69 +++++++++++++++++-- src/harness/virtualFileSystemWithWatch.ts | 12 ++-- 4 files changed, 122 insertions(+), 14 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index c7f378e747839..91faaa620fec1 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -292,6 +292,7 @@ namespace ts { export interface RecursiveDirectoryWatcherHost { watchDirectory: HostWatchDirectory; getAccessileSortedChildDirectories(path: string): ReadonlyArray; + directoryExists(dir: string): boolean; filePathComparer: Comparer; } @@ -348,7 +349,7 @@ namespace ts { function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches { let newChildWatches: DirectoryWatcher[] | undefined; enumerateInsertsAndDeletes( - host.getAccessileSortedChildDirectories(parentDir), + host.directoryExists(parentDir) ? host.getAccessileSortedChildDirectories(parentDir) : emptyArray, existingChildWatches, (child, childWatcher) => host.filePathComparer(getNormalizedAbsolutePath(child, parentDir), childWatcher.dirName), createAndAddChildDirectoryWatcher, @@ -625,6 +626,7 @@ namespace ts { const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? watchDirectoryUsingFsWatchFile : watchDirectoryUsingFsWatch; const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + directoryExists, getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, watchDirectory }); diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 81f4d2ed19efe..afc000f8ed9e8 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -7,7 +7,7 @@ namespace ts.tscWatch { import WatchedSystem = ts.TestFSWithWatch.TestServerHost; type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; import createWatchedSystem = ts.TestFSWithWatch.createWatchedSystem; - import checkFileNames = ts.TestFSWithWatch.checkFileNames; + import checkArray = ts.TestFSWithWatch.checkArray; import libFile = ts.TestFSWithWatch.libFile; import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; @@ -15,11 +15,11 @@ namespace ts.tscWatch { import checkOutputDoesNotContain = ts.TestFSWithWatch.checkOutputDoesNotContain; export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { - checkFileNames(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); + checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); } export function checkProgramRootFiles(program: Program, expectedFiles: string[]) { - checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); + checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); } function createWatchingSystemHost(system: WatchedSystem) { @@ -2175,5 +2175,50 @@ declare module "fs" { checkWatchedDirectories(host, [], /*recursive*/ true); } }); + + it("watchingDirectories with watchFile", () => { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: FileOrFolder = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const file: FileOrFolder = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + const programFiles = [file, libFile]; + const files = [file, configFile, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", "RecursiveDirectoryUsingFsWatchFile"); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchModeWithConfigFile(configFile.path, host); + // Watching config file, file, lib file and directories + const expectedWatchedFiles = files.map(f => f.path).concat(projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`); + + verifyProgram(checkOutputErrorsInitial); + + // Rename the file: + file.path = file.path.replace("file1.ts", "file2.ts"); + expectedWatchedFiles[0] = file.path; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProgram(checkOutputErrorsIncremental); + + function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + // Watching config file, file, lib file and directories + checkWatchedFiles(host, expectedWatchedFiles); + + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrors(host, emptyArray); + + const outputFile = changeExtension(file.path, ".js"); + assert(host.fileExists(outputFile)); + assert.equal(host.readFile(outputFile), file.content); + } + }); }); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 68fbc1ecba070..dc1dca302fbbf 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -10,7 +10,7 @@ namespace ts.projectSystem { export import TestServerHost = ts.TestFSWithWatch.TestServerHost; export type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; export import createServerHost = ts.TestFSWithWatch.createServerHost; - export import checkFileNames = ts.TestFSWithWatch.checkFileNames; + export import checkArray = ts.TestFSWithWatch.checkArray; export import libFile = ts.TestFSWithWatch.libFile; export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; @@ -330,11 +330,11 @@ namespace ts.projectSystem { } export function checkProjectActualFiles(project: server.Project, expectedFiles: string[]) { - checkFileNames(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); + checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); } function checkProjectRootFiles(project: server.Project, expectedFiles: string[]) { - checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); + checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { @@ -367,7 +367,7 @@ namespace ts.projectSystem { } function checkOpenFiles(projectService: server.ProjectService, expectedFiles: FileOrFolder[]) { - checkFileNames("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path)); + checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path)); } /** @@ -506,7 +506,7 @@ namespace ts.projectSystem { const project = projectService.inferredProjects[0]; - checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); + checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); @@ -6584,4 +6584,63 @@ namespace ts.projectSystem { checkProjectActualFiles(project, [file.path]); }); }); + + describe("WatchingDirectories with polling", () => { + it("when file is added to subfolder, completion list has new file", () => { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: FileOrFolder = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const index: FileOrFolder = { + path: `${projectSrcFolder}/index.ts`, + content: `import {} from "./"` + }; + const file1: FileOrFolder = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + + const files = [index, file1, configFile, libFile]; + const fileNames = files.map(file => file.path); + // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder + const expectedWatchedFiles = fileNames.slice(1).concat(projectFolder, projectSrcFolder, `${projectFolder}/${nodeModulesAtTypes}`); + const expectedCompletions = ["file1"]; + const completionPosition = index.content.lastIndexOf('"'); + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", "RecursiveDirectoryUsingFsWatchFile"); + const host = createServerHost(files, { environmentVariables }); + const projectService = createProjectService(host); + projectService.openClientFile(index.path); + + const project = projectService.configuredProjects.get(configFile.path); + assert.isDefined(project); + verifyProjectAndCompletions(); + + // Add file2 + const file2: FileOrFolder = { + path: `${projectSrcFolder}/file2.ts`, + content: "" + }; + files.push(file2); + fileNames.push(file2.path); + expectedWatchedFiles.push(file2.path); + expectedCompletions.push("file2"); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.equal(projectService.configuredProjects.get(configFile.path), project); + verifyProjectAndCompletions(); + + function verifyProjectAndCompletions() { + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkWatchedFiles(host, expectedWatchedFiles); + checkProjectActualFiles(project, fileNames); + + const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); + } + }); + }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 2f0525945f8c7..58f6371be84e8 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -147,10 +147,10 @@ interface Array {}` } } - export function checkFileNames(caption: string, actualFileNames: ReadonlyArray, expectedFileNames: string[]) { - assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected:\r\n${expectedFileNames.join("\r\n")}\r\ngot: ${actualFileNames.join("\r\n")}`); - for (const f of expectedFileNames) { - assert.equal(true, contains(actualFileNames, f), `${caption}: expected to find ${f} in ${actualFileNames}`); + export function checkArray(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { + assert.equal(actual.length, expected.length, `${caption}: incorrect actual number of files, expected:\r\n${expected.join("\r\n")}\r\ngot: ${actual.join("\r\n")}`); + for (const f of expected) { + assert.equal(true, contains(actual, f), `${caption}: expected to find ${f} in ${actual}`); } } @@ -278,6 +278,7 @@ interface Array {}` if (tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile") { const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + directoryExists: path => this.directoryExists(path), getAccessileSortedChildDirectories: path => this.getDirectories(path), filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, watchDirectory @@ -286,6 +287,7 @@ interface Array {}` else if (tscWatchDirectory === "RecursiveDirectoryUsingNonRecursiveWatchDirectory") { const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + directoryExists: path => this.directoryExists(path), getAccessileSortedChildDirectories: path => this.getDirectories(path), filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, watchDirectory @@ -607,7 +609,7 @@ interface Array {}` watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { if (recursive && this.customRecursiveWatchDirectory) { - return this.customRecursiveWatchDirectory(directoryName, cb, true); + return this.customRecursiveWatchDirectory(directoryName, cb, /*recursive*/ true); } const path = this.toFullPath(directoryName); const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; From ab17600e8610e81bc093bc8c544d1b9c17bf9501 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 30 Nov 2017 10:35:28 -0800 Subject: [PATCH 17/23] Improve test to verify the count of callbacks for the watched directories through watchFile --- src/harness/unittests/tscWatchMode.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 20 +++++++++---------- src/harness/virtualFileSystemWithWatch.ts | 12 +++++++++++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index afc000f8ed9e8..7b64e043eb6ae 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2210,7 +2210,7 @@ declare module "fs" { checkWatchedDirectories(host, emptyArray, /*recursive*/ true); // Watching config file, file, lib file and directories - checkWatchedFiles(host, expectedWatchedFiles); + ts.TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedFiles", host.watchedFiles, expectedWatchedFiles, 1); checkProgramActualFiles(watch(), programFiles.map(f => f.path)); checkOutputErrors(host, emptyArray); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index dc1dca302fbbf..fdaebc4b054b1 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -5322,16 +5322,11 @@ namespace ts.projectSystem { } function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { - const calledMap = calledMaps[callback]; - ts.TestFSWithWatch.verifyMapSize(callback, calledMap, arrayFrom(expectedKeys.keys())); - expectedKeys.forEach((called, name) => { - assert.isTrue(calledMap.has(name), `${callback} is expected to contain ${name}, actual keys: ${arrayFrom(calledMap.keys())}`); - assert.equal(calledMap.get(name).length, called, `${callback} is expected to be called ${called} times with ${name}. Actual entry: ${calledMap.get(name)}`); - }); + ts.TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys); } function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: string[], nTimes: number) { - return verifyCalledOnEachEntry(callback, zipToMap(expectedKeys, expectedKeys.map(() => nTimes))); + ts.TestFSWithWatch.checkMultiMapEachKeyWithCount(callback, calledMaps[callback], expectedKeys, nTimes); } function verifyNoHostCalls() { @@ -6605,7 +6600,11 @@ namespace ts.projectSystem { const files = [index, file1, configFile, libFile]; const fileNames = files.map(file => file.path); // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder - const expectedWatchedFiles = fileNames.slice(1).concat(projectFolder, projectSrcFolder, `${projectFolder}/${nodeModulesAtTypes}`); + const expectedWatchedFiles = arrayToMap(fileNames.slice(1).concat(`${projectFolder}/${nodeModulesAtTypes}`), s => s, () => 1); + // For failed resolution lookup and tsconfig files + expectedWatchedFiles.set(projectFolder, 2); + // Through above recursive watches + expectedWatchedFiles.set(projectSrcFolder, 2); const expectedCompletions = ["file1"]; const completionPosition = index.content.lastIndexOf('"'); const environmentVariables = createMap(); @@ -6625,7 +6624,7 @@ namespace ts.projectSystem { }; files.push(file2); fileNames.push(file2.path); - expectedWatchedFiles.push(file2.path); + expectedWatchedFiles.set(file2.path, 1); expectedCompletions.push("file2"); host.reloadFS(files); host.runQueuedTimeoutCallbacks(); @@ -6635,7 +6634,8 @@ namespace ts.projectSystem { function verifyProjectAndCompletions() { checkWatchedDirectories(host, emptyArray, /*recursive*/ false); checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - checkWatchedFiles(host, expectedWatchedFiles); + + ts.TestFSWithWatch.checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedWatchedFiles); checkProjectActualFiles(project, fileNames); const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 58f6371be84e8..bb7fa128c794d 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -147,6 +147,18 @@ interface Array {}` } } + export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeys: Map) { + verifyMapSize(caption, actual, arrayFrom(expectedKeys.keys())); + expectedKeys.forEach((count, name) => { + assert.isTrue(actual.has(name), `${caption}: expected to contain ${name}, actual keys: ${arrayFrom(actual.keys())}`); + assert.equal(actual.get(name).length, count, `${caption}: Expected to be have ${count} entries for ${name}. Actual entry: ${JSON.stringify(actual.get(name))}`); + }); + } + + export function checkMultiMapEachKeyWithCount(caption: string, actual: MultiMap, expectedKeys: ReadonlyArray, count: number) { + return checkMultiMapKeyCount(caption, actual, arrayToMap(expectedKeys, s => s, () => count)); + } + export function checkArray(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { assert.equal(actual.length, expected.length, `${caption}: incorrect actual number of files, expected:\r\n${expected.join("\r\n")}\r\ngot: ${actual.join("\r\n")}`); for (const f of expected) { From f4954d0529079150b043e45ee1c7c1d73d830c95 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 30 Nov 2017 11:34:04 -0800 Subject: [PATCH 18/23] Test case for watching using fs.watch recursively --- src/harness/unittests/tscWatchMode.ts | 87 +++++++++++-------- .../unittests/tsserverProjectSystem.ts | 26 ++++-- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 7b64e043eb6ae..82bfb7f4b8925 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2176,49 +2176,64 @@ declare module "fs" { } }); - it("watchingDirectories with watchFile", () => { - const projectFolder = "/a/username/project"; - const projectSrcFolder = `${projectFolder}/src`; - const configFile: FileOrFolder = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const file: FileOrFolder = { - path: `${projectSrcFolder}/file1.ts`, - content: "" - }; - const programFiles = [file, libFile]; - const files = [file, configFile, libFile]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", "RecursiveDirectoryUsingFsWatchFile"); - const host = createWatchedSystem(files, { environmentVariables }); - const watch = createWatchModeWithConfigFile(configFile.path, host); - // Watching config file, file, lib file and directories - const expectedWatchedFiles = files.map(f => f.path).concat(projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`); + describe("tsc-watch when watchDirectories implementation", () => { + function verifyRenamingFileInSubFolder(usesWatchFile: boolean) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: FileOrFolder = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const file: FileOrFolder = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + const programFiles = [file, libFile]; + const files = [file, configFile, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", usesWatchFile ? "RecursiveDirectoryUsingFsWatchFile" : "RecursiveDirectoryUsingNonRecursiveWatchDirectory"); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchModeWithConfigFile(configFile.path, host); + const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; + // Watching files config file, file, lib file + const expectedWatchedFiles = files.map(f => f.path); + const expectedWatchedDirectories = usesWatchFile ? [] : projectFolders; + if (usesWatchFile) { + expectedWatchedFiles.push(...projectFolders); + } - verifyProgram(checkOutputErrorsInitial); + verifyProgram(checkOutputErrorsInitial); - // Rename the file: - file.path = file.path.replace("file1.ts", "file2.ts"); - expectedWatchedFiles[0] = file.path; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyProgram(checkOutputErrorsIncremental); + // Rename the file: + file.path = file.path.replace("file1.ts", "file2.ts"); + expectedWatchedFiles[0] = file.path; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProgram(checkOutputErrorsIncremental); - function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - // Watching config file, file, lib file and directories - ts.TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedFiles", host.watchedFiles, expectedWatchedFiles, 1); + // Watching config file, file, lib file and directories + ts.TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedFiles", host.watchedFiles, expectedWatchedFiles, 1); + ts.TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories, 1); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrors(host, emptyArray); - const outputFile = changeExtension(file.path, ".js"); - assert(host.fileExists(outputFile)); - assert.equal(host.readFile(outputFile), file.content); + const outputFile = changeExtension(file.path, ".js"); + assert(host.fileExists(outputFile)); + assert.equal(host.readFile(outputFile), file.content); + } } + + it("uses watchFile when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(/*usesWatchFile*/ true); + }); + + it("uses non recursive watchDirectory when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(/*usesWatchFile*/ false); + }); }); }); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index fdaebc4b054b1..b343a269c20eb 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -6580,8 +6580,8 @@ namespace ts.projectSystem { }); }); - describe("WatchingDirectories with polling", () => { - it("when file is added to subfolder, completion list has new file", () => { + describe("watchDirectories implementation", () => { + function verifyCompletionListWithNewFileInSubFolder(usesWatchFile: boolean) { const projectFolder = "/a/username/project"; const projectSrcFolder = `${projectFolder}/src`; const configFile: FileOrFolder = { @@ -6600,15 +6600,19 @@ namespace ts.projectSystem { const files = [index, file1, configFile, libFile]; const fileNames = files.map(file => file.path); // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder - const expectedWatchedFiles = arrayToMap(fileNames.slice(1).concat(`${projectFolder}/${nodeModulesAtTypes}`), s => s, () => 1); + const expectedWatchedFiles = arrayToMap(fileNames.slice(1), s => s, () => 1); + const expectedWatchedDirectories = createMap(); + const mapOfDirectories = usesWatchFile ? expectedWatchedFiles : expectedWatchedDirectories; // For failed resolution lookup and tsconfig files - expectedWatchedFiles.set(projectFolder, 2); + mapOfDirectories.set(projectFolder, 2); // Through above recursive watches - expectedWatchedFiles.set(projectSrcFolder, 2); + mapOfDirectories.set(projectSrcFolder, 2); + // node_modules/@types folder + mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1); const expectedCompletions = ["file1"]; const completionPosition = index.content.lastIndexOf('"'); const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", "RecursiveDirectoryUsingFsWatchFile"); + environmentVariables.set("TSC_WATCHDIRECTORY", usesWatchFile ? "RecursiveDirectoryUsingFsWatchFile" : "RecursiveDirectoryUsingNonRecursiveWatchDirectory"); const host = createServerHost(files, { environmentVariables }); const projectService = createProjectService(host); projectService.openClientFile(index.path); @@ -6632,15 +6636,23 @@ namespace ts.projectSystem { verifyProjectAndCompletions(); function verifyProjectAndCompletions() { - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); checkWatchedDirectories(host, emptyArray, /*recursive*/ true); ts.TestFSWithWatch.checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedWatchedFiles); + ts.TestFSWithWatch.checkMultiMapKeyCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories); checkProjectActualFiles(project, fileNames); const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); } + } + + it("uses watchFile when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(/*usesWatchFile*/ true); + }); + + it("uses non recursive watchDirectory when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(/*usesWatchFile*/ false); }); }); } From 56d754cf0faa657dc9cf943d0769efea317597f6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 17 Jan 2018 12:56:33 -0800 Subject: [PATCH 19/23] Add watchDirectory to be using dynamic polling --- src/compiler/sys.ts | 20 ++++++----- src/harness/unittests/tscWatchMode.ts | 33 ++++++++++++------- .../unittests/tsserverProjectSystem.ts | 24 +++++++++----- src/harness/virtualFileSystemWithWatch.ts | 23 +++++++++++-- 4 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 91faaa620fec1..a12fee09cdc22 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -494,7 +494,7 @@ namespace ts { const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; const tscWatchFile = process.env.TSC_WATCHFILE; const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY; - + let dynamicPollingWatchFile: HostWatchFile | undefined; const nodeSystem: System = { args: process.argv.slice(2), newLine: _os.EOL, @@ -607,7 +607,8 @@ namespace ts { return watchFileUsingFsWatch; case "UseFsEventsWithFallbackDynamicPolling": // Use notifications from FS to watch with falling back to dynamic watch file - return watchFileUsingDynamicWatchFile; + dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile(nodeSystem); + return createWatchFileUsingDynamicWatchFile(dynamicPollingWatchFile); } return useNonPollingWatchers ? createNonPollingWatchFile() : @@ -623,7 +624,11 @@ namespace ts { return watchDirectoryUsingFsWatch; } - const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? watchDirectoryUsingFsWatchFile : watchDirectoryUsingFsWatch; + const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? + createWatchDirectoryUsing(fsWatchFile) : + tscWatchDirectory === "RecursiveDirectoryUsingDynamicPriorityPolling" ? + createWatchDirectoryUsing(dynamicPollingWatchFile || createDynamicPriorityPollingWatchFile(nodeSystem)) : + watchDirectoryUsingFsWatch; const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, directoryExists, @@ -838,9 +843,8 @@ namespace ts { return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval); } - function watchFileUsingDynamicWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { - const watchFile = createDynamicPriorityPollingWatchFile(nodeSystem); - return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); + function createWatchFileUsingDynamicWatchFile(watchFile: HostWatchFile): HostWatchFile { + return (fileName, callback, pollingInterval) => fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); } function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher { @@ -851,8 +855,8 @@ namespace ts { return fsWatchDirectory(directoryName, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive); } - function watchDirectoryUsingFsWatchFile(directoryName: string, callback: DirectoryWatcherCallback) { - return fsWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium); + function createWatchDirectoryUsing(fsWatchFile: HostWatchFile): HostWatchDirectory { + return (directoryName, callback) => fsWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium); } function readFile(fileName: string, _encoding?: string): string | undefined { diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 82bfb7f4b8925..75db67bee71d1 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2177,7 +2177,7 @@ declare module "fs" { }); describe("tsc-watch when watchDirectories implementation", () => { - function verifyRenamingFileInSubFolder(usesWatchFile: boolean) { + function verifyRenamingFileInSubFolder(tscWatchDirectory: TestFSWithWatch.Tsc_WatchDirectory) { const projectFolder = "/a/username/project"; const projectSrcFolder = `${projectFolder}/src`; const configFile: FileOrFolder = { @@ -2191,14 +2191,14 @@ declare module "fs" { const programFiles = [file, libFile]; const files = [file, configFile, libFile]; const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", usesWatchFile ? "RecursiveDirectoryUsingFsWatchFile" : "RecursiveDirectoryUsingNonRecursiveWatchDirectory"); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); const host = createWatchedSystem(files, { environmentVariables }); const watch = createWatchModeWithConfigFile(configFile.path, host); const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; // Watching files config file, file, lib file const expectedWatchedFiles = files.map(f => f.path); - const expectedWatchedDirectories = usesWatchFile ? [] : projectFolders; - if (usesWatchFile) { + const expectedWatchedDirectories = tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; + if (tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.WatchFile) { expectedWatchedFiles.push(...projectFolders); } @@ -2208,31 +2208,40 @@ declare module "fs" { file.path = file.path.replace("file1.ts", "file2.ts"); expectedWatchedFiles[0] = file.path; host.reloadFS(files); + if (tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling) { + // With dynamic polling the fs change would be detected only by running timeouts + host.runQueuedTimeoutCallbacks(); + } + // Delayed update program host.runQueuedTimeoutCallbacks(); verifyProgram(checkOutputErrorsIncremental); function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - - // Watching config file, file, lib file and directories - ts.TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedFiles", host.watchedFiles, expectedWatchedFiles, 1); - ts.TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories, 1); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); checkOutputErrors(host, emptyArray); const outputFile = changeExtension(file.path, ".js"); assert(host.fileExists(outputFile)); assert.equal(host.readFile(outputFile), file.content); + + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + // Watching config file, file, lib file and directories + ts.TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedFiles", host.watchedFiles, expectedWatchedFiles, 1); + ts.TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories, 1); } } it("uses watchFile when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(/*usesWatchFile*/ true); + verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.WatchFile); }); it("uses non recursive watchDirectory when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(/*usesWatchFile*/ false); + verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses non recursive dynamic polling when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling); }); }); }); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index b343a269c20eb..9b4705c9e347b 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -6581,7 +6581,7 @@ namespace ts.projectSystem { }); describe("watchDirectories implementation", () => { - function verifyCompletionListWithNewFileInSubFolder(usesWatchFile: boolean) { + function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: TestFSWithWatch.Tsc_WatchDirectory) { const projectFolder = "/a/username/project"; const projectSrcFolder = `${projectFolder}/src`; const configFile: FileOrFolder = { @@ -6602,7 +6602,11 @@ namespace ts.projectSystem { // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder const expectedWatchedFiles = arrayToMap(fileNames.slice(1), s => s, () => 1); const expectedWatchedDirectories = createMap(); - const mapOfDirectories = usesWatchFile ? expectedWatchedFiles : expectedWatchedDirectories; + const mapOfDirectories = tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory ? + expectedWatchedDirectories : + tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.WatchFile ? + expectedWatchedFiles : + createMap(); // For failed resolution lookup and tsconfig files mapOfDirectories.set(projectFolder, 2); // Through above recursive watches @@ -6612,7 +6616,7 @@ namespace ts.projectSystem { const expectedCompletions = ["file1"]; const completionPosition = index.content.lastIndexOf('"'); const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", usesWatchFile ? "RecursiveDirectoryUsingFsWatchFile" : "RecursiveDirectoryUsingNonRecursiveWatchDirectory"); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); const host = createServerHost(files, { environmentVariables }); const projectService = createProjectService(host); projectService.openClientFile(index.path); @@ -6636,23 +6640,27 @@ namespace ts.projectSystem { verifyProjectAndCompletions(); function verifyProjectAndCompletions() { + const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); ts.TestFSWithWatch.checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedWatchedFiles); ts.TestFSWithWatch.checkMultiMapKeyCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories); checkProjectActualFiles(project, fileNames); - - const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); - checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); } } it("uses watchFile when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(/*usesWatchFile*/ true); + verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.WatchFile); }); it("uses non recursive watchDirectory when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(/*usesWatchFile*/ false); + verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses dynamic polling when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling); }); }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index bb7fa128c794d..9bb106fe6be08 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -257,6 +257,12 @@ interface Array {}` ignoreWatchInvokedWithTriggerAsFileCreate: boolean; } + export enum Tsc_WatchDirectory { + WatchFile = "RecursiveDirectoryUsingFsWatchFile", + NonRecursiveWatchDirectory = "RecursiveDirectoryUsingNonRecursiveWatchDirectory", + DynamicPolling = "RecursiveDirectoryUsingDynamicPriorityPolling" + } + export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, ModuleResolutionHost { args: string[] = []; @@ -286,8 +292,8 @@ interface Array {}` this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ? createDynamicPriorityPollingWatchFile(this) : undefined; - const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY"); - if (tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile") { + 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); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ directoryExists: path => this.directoryExists(path), @@ -296,7 +302,7 @@ interface Array {}` watchDirectory }); } - else if (tscWatchDirectory === "RecursiveDirectoryUsingNonRecursiveWatchDirectory") { + else if (tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory) { const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ directoryExists: path => this.directoryExists(path), @@ -305,6 +311,16 @@ interface Array {}` watchDirectory }); } + else if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { + const watchFile = createDynamicPriorityPollingWatchFile(this); + const watchDirectory: HostWatchDirectory = (directory, cb) => watchFile(directory, () => cb(directory), PollingInterval.Medium); + this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + directoryExists: path => this.directoryExists(path), + getAccessileSortedChildDirectories: path => this.getDirectories(path), + filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + watchDirectory + }); + } } getNewLine() { @@ -348,6 +364,7 @@ interface Array {}` if (currentEntry.content !== fileOrDirectory.content) { currentEntry.content = fileOrDirectory.content; currentEntry.modifiedTime = new Date(); + this.fs.get(getDirectoryPath(currentEntry.path)).modifiedTime = new Date(); if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) { this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath); } From 95b2630fb805c120b17e21aa0f3a8a0be7e8ca65 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 25 Jan 2018 09:30:20 -0800 Subject: [PATCH 20/23] Add watchFile option to use fsEvents on parent directory --- src/compiler/sys.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 2097475758bc9..67beed1936351 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -608,6 +608,9 @@ namespace ts { // Use notifications from FS to watch with falling back to dynamic watch file dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile(nodeSystem); return createWatchFileUsingDynamicWatchFile(dynamicPollingWatchFile); + case "UseFsEventsOnParentDirectory": + // Use notifications from FS to watch with falling back to fs.watchFile + return createNonPollingWatchFile(); } return useNonPollingWatchers ? createNonPollingWatchFile() : From b7f69102ce13e39adc0e4fae5ad069a4faa36036 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 5 Feb 2018 14:50:07 -0800 Subject: [PATCH 21/23] Make the PollingInterval, PollingChunkSize, PollingUnchangedThresholds customisable --- src/compiler/sys.ts | 85 +++++++++++++++++++++------ src/harness/unittests/tscWatchMode.ts | 4 +- 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 9c2a711474015..82a6bcde4e29e 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -63,26 +63,73 @@ namespace ts { /* @internal */ export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time - enum ChunkSize { - Low = 32, - Medium = 64, - High = 256 + interface Levels { + Low: number; + Medium: number; + High: number; } - function chunkSize(pollingInterval: PollingInterval) { - switch (pollingInterval) { - case PollingInterval.Low: - return ChunkSize.Low; - case PollingInterval.Medium: - return ChunkSize.Medium; - case PollingInterval.High: - return ChunkSize.High; - } + function createPollingIntervalBasedLevels(levels: Levels) { + return { + [PollingInterval.Low]: levels.Low, + [PollingInterval.Medium]: levels.Medium, + [PollingInterval.High]: levels.High + }; } - /*@internal*/ - export function unChangedThreshold(pollingInterval: PollingInterval) { - return chunkSize(pollingInterval); + const defaultChunkLevels: Levels = { Low: 32, Medium: 64, High: 256 }; + let pollingChunkSize = createPollingIntervalBasedLevels(defaultChunkLevels); + /* @internal */ + export let unchangedPollThresholds = createPollingIntervalBasedLevels(defaultChunkLevels); + + /* @internal */ + export function setCustomPollingValues(system: System) { + if (!system.getEnvironmentVariable) { + return; + } + const pollingIntervalChanged = setCustomLevels("TSC_WATCH_POLLINGINTERVAL", PollingInterval); + pollingChunkSize = getCustomPollingBasedLevels("TSC_WATCH_POLLINGCHUNKSIZE", defaultChunkLevels) || pollingChunkSize; + unchangedPollThresholds = getCustomPollingBasedLevels("TSC_WATCH_UNCHANGEDPOLLTHRESHOLDS", defaultChunkLevels) || unchangedPollThresholds; + + function getLevel(envVar: string, level: keyof Levels) { + return system.getEnvironmentVariable(`${envVar}_${level.toUpperCase()}`); + } + + function getCustomLevels(baseVariable: string) { + let customLevels: Partial | undefined; + setCustomLevel("Low"); + setCustomLevel("Medium"); + setCustomLevel("High"); + return customLevels; + + function setCustomLevel(level: keyof Levels) { + const customLevel = getLevel(baseVariable, level); + if (customLevel) { + (customLevels || (customLevels || {}))[level] = Number(customLevel); + } + } + } + + function setCustomLevels(baseVariable: string, levels: Levels) { + const customLevels = getCustomLevels(baseVariable); + if (customLevels) { + setLevel("Low"); + setLevel("Medium"); + setLevel("High"); + return true; + } + return false; + + function setLevel(level: keyof Levels) { + levels[level] = customLevels[level] || levels[level]; + } + } + + function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { + let customLevels = getCustomLevels(baseVariable); + return (pollingIntervalChanged || customLevels) && + createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); + } } /* @internal */ @@ -138,7 +185,7 @@ namespace ts { } function pollPollingIntervalQueue(queue: PollingIntervalQueue) { - queue.pollIndex = pollQueue(queue, queue.pollingInterval, queue.pollIndex, chunkSize(queue.pollingInterval)); + queue.pollIndex = pollQueue(queue, queue.pollingInterval, queue.pollIndex, pollingChunkSize[queue.pollingInterval]); // Set the next polling index and timeout if (queue.length) { scheduleNextPoll(queue.pollingInterval); @@ -190,7 +237,7 @@ namespace ts { addChangedFileToLowPollingIntervalQueue(watchedFile); } } - else if (watchedFile.unchangedPolls !== unChangedThreshold(pollingInterval)) { + else if (watchedFile.unchangedPolls !== unchangedPollThresholds[pollingInterval]) { watchedFile.unchangedPolls++; } else if (queue === changedFilesInLastPoll) { @@ -575,6 +622,7 @@ namespace ts { process.stdout.write("\x1Bc"); } }; + nodeSystem.watchFile = getWatchFile(); nodeSystem.watchDirectory = getWatchDirectory(); return nodeSystem; @@ -1063,6 +1111,7 @@ namespace ts { })(); if (sys && sys.getEnvironmentVariable) { + setCustomPollingValues(sys); Debug.currentAssertionLevel = /^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) ? AssertionLevel.Normal : AssertionLevel.None; diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 0794516a30124..4b749f9763f62 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2155,7 +2155,7 @@ declare module "fs" { const initialProgram = watch(); verifyProgram(); - const mediumPollingIntervalThreshold = unChangedThreshold(PollingInterval.Medium); + const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; for (let index = 0; index < mediumPollingIntervalThreshold; index++) { // Transition libFile and file1 to low priority queue host.checkTimeoutQueueLengthAndRun(1); @@ -2182,7 +2182,7 @@ declare module "fs" { assert.isTrue(host.fileExists(outputFile1)); assert.equal(host.readFile(outputFile1), file1.content + host.newLine); - const newThreshold = unChangedThreshold(PollingInterval.Low) + mediumPollingIntervalThreshold; + const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { // For high + Medium/low polling interval host.checkTimeoutQueueLengthAndRun(2); From c5b21d4a6d796ca0fb189142d457d0361f474f85 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 6 Feb 2018 14:13:13 -0800 Subject: [PATCH 22/23] Fix the lint error --- src/compiler/sys.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 82a6bcde4e29e..d9c355a12e8c4 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -126,7 +126,7 @@ namespace ts { } function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { - let customLevels = getCustomLevels(baseVariable); + const customLevels = getCustomLevels(baseVariable); return (pollingIntervalChanged || customLevels) && createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); } From 8378f692c7bcef0a111ec4ddc45d8526f394563e Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 8 Mar 2018 12:18:04 -0800 Subject: [PATCH 23/23] Directly assign values for watchFile and watchDirectory in node System --- src/compiler/sys.ts | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 6fc305a417fd2..4f62cfdfe2d91 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -133,11 +133,7 @@ namespace ts { } /* @internal */ - export function createDynamicPriorityPollingWatchFile(host: System): HostWatchFile { - if (!host.getModifiedTime || !host.setTimeout) { - throw notImplemented(); - } - + export function createDynamicPriorityPollingWatchFile(host: { getModifiedTime: System["getModifiedTime"]; setTimeout: System["setTimeout"]; }): HostWatchFile { interface WatchedFile extends ts.WatchedFile { isClosed?: boolean; unchangedPolls: number; @@ -555,6 +551,8 @@ namespace ts { }, readFile, writeFile, + watchFile: getWatchFile(), + watchDirectory: getWatchDirectory(), resolvePath: path => _path.resolve(path), fileExists, directoryExists, @@ -574,14 +572,7 @@ namespace ts { return process.env[name] || ""; }, readDirectory, - getModifiedTime(path) { - try { - return _fs.statSync(path).mtime; - } - catch (e) { - return undefined; - } - }, + getModifiedTime, createHash: _crypto ? createMD5HashUsingNativeCrypto : generateDjb2Hash, getMemoryUsage() { if (global.gc) { @@ -625,9 +616,6 @@ namespace ts { process.stdout.write("\x1Bc"); } }; - - nodeSystem.watchFile = getWatchFile(); - nodeSystem.watchDirectory = getWatchDirectory(); return nodeSystem; function isFileSystemCaseSensitive(): boolean { @@ -654,13 +642,13 @@ namespace ts { return fsWatchFile; case "DynamicPriorityPolling": // Use polling interval but change the interval depending on file changes and their default polling interval - return createDynamicPriorityPollingWatchFile(nodeSystem); + return createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); case "UseFsEvents": // Use notifications from FS to watch with falling back to fs.watchFile return watchFileUsingFsWatch; case "UseFsEventsWithFallbackDynamicPolling": // Use notifications from FS to watch with falling back to dynamic watch file - dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile(nodeSystem); + dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); return createWatchFileUsingDynamicWatchFile(dynamicPollingWatchFile); case "UseFsEventsOnParentDirectory": // Use notifications from FS to watch with falling back to fs.watchFile @@ -683,7 +671,7 @@ namespace ts { const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? createWatchDirectoryUsing(fsWatchFile) : tscWatchDirectory === "RecursiveDirectoryUsingDynamicPriorityPolling" ? - createWatchDirectoryUsing(dynamicPollingWatchFile || createDynamicPriorityPollingWatchFile(nodeSystem)) : + createWatchDirectoryUsing(dynamicPollingWatchFile || createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })) : watchDirectoryUsingFsWatch; const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, @@ -1027,6 +1015,15 @@ namespace ts { return filter(_fs.readdirSync(path), dir => fileSystemEntryExists(combinePaths(path, dir), FileSystemEntryKind.Directory)); } + function getModifiedTime(path: string) { + try { + return _fs.statSync(path).mtime; + } + catch (e) { + return undefined; + } + } + /** * djb2 hashing algorithm * http://www.cse.yorku.ca/~oz/hash.html