diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 7edfd5dd503..6428036a17e 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) { + priorityQueues[priority].push(file); + scheduleNextPollIfNotAlreadyScheduled(priority); + } + + function addChangedFileToHighPriorityQueue(file: WatchedFile) { + changedFilesInLastPoll.push(file); + scheduleNextPollIfNotAlreadyScheduled(WatchPriority.High); + } + + function scheduleNextPollIfNotAlreadyScheduled(priority: WatchPriority) { + if (!priorityQueues[priority].pollScheduled) { 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]); + 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 9ddaa8f2d67..f35f952834a 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 8abc23f1ac7..60361f56c1a 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 921d4674231..6fe5bac459f 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,30 +501,39 @@ interface Array {}` } } - private toFile(fileOrDirectory: FileOrFolder): File { - const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); - return { - path: this.toPath(fullPath), - content: fileOrDirectory.content, - fullPath, - fileSize: fileOrDirectory.fileSize - }; - } - - private toFolder(path: string): Folder { + private toFsEntry(path: string): FSEntry { const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); return { path: this.toPath(fullPath), - entries: [], - fullPath + fullPath, + 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 folder = this.toFsEntry(path) as Folder; + folder.entries = []; + return folder; + } + fileExists(s: string) { const path = this.toFullPath(s); 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; } }