Skip to content

Commit c3b9904

Browse files
committed
Add test to verify timeout queues
1 parent 976f330 commit c3b9904

4 files changed

Lines changed: 130 additions & 32 deletions

File tree

src/compiler/sys.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ namespace ts {
6060

6161
/* @internal */
6262
export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time
63+
64+
const chunkSizeOrUnchangedThresholdsForPriority = getPriorityValues(32);
65+
function chunkSize(watchPriority: WatchPriority) {
66+
return chunkSizeOrUnchangedThresholdsForPriority[watchPriority];
67+
}
68+
69+
/*@internal*/
70+
export function unChangedThreshold(watchPriority: WatchPriority) {
71+
return chunkSizeOrUnchangedThresholdsForPriority[watchPriority];
72+
}
73+
6374
/* @internal */
6475
export function createDynamicPriorityPollingStatsSet(host: System): DynamicPriorityPollingStatsSet {
6576
if (!host.getModifiedTime || !host.setTimeout) {
@@ -74,10 +85,9 @@ namespace ts {
7485
interface WatchPriorityQueue extends Array<WatchedFile> {
7586
watchPriority: WatchPriority;
7687
pollIndex: number;
88+
pollScheduled: boolean;
7789
}
7890

79-
const chunkSizes = getPriorityValues(32);
80-
const unChangedThresholds = getPriorityValues(32);
8191
const watchedFiles: WatchedFile[] = [];
8292
const changedFilesInLastPoll: WatchedFile[] = [];
8393
const priorityQueues = [createPriorityQueue(WatchPriority.High), createPriorityQueue(WatchPriority.Medium), createPriorityQueue(WatchPriority.Low)];
@@ -109,18 +119,20 @@ namespace ts {
109119
const queue = [] as WatchPriorityQueue;
110120
queue.watchPriority = watchPriority;
111121
queue.pollIndex = 0;
122+
queue.pollScheduled = false;
112123
return queue;
113124
}
114125

115126
function pollPriorityQueue(queue: WatchPriorityQueue) {
116127
const priority = queue.watchPriority;
117-
queue.pollIndex = pollQueue(queue, priority, queue.pollIndex, chunkSizes[priority]);
128+
queue.pollIndex = pollQueue(queue, priority, queue.pollIndex, chunkSize(priority));
118129
// Set the next polling index and timeout
119130
if (queue.length) {
120131
scheduleNextPoll(priority);
121132
}
122133
else {
123134
Debug.assert(queue.pollIndex === 0);
135+
queue.pollScheduled = false;
124136
}
125137
}
126138

@@ -132,7 +144,7 @@ namespace ts {
132144
pollPriorityQueue(queue);
133145
// Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue
134146
// as pollPriorityQueue wont schedule for next poll
135-
if (!queue.length && changedFilesInLastPoll.length) {
147+
if (!queue.pollScheduled && changedFilesInLastPoll.length) {
136148
scheduleNextPoll(WatchPriority.High);
137149
}
138150
}
@@ -141,7 +153,6 @@ namespace ts {
141153
// Max visit would be all elements of the queue
142154
let needsVisit = queue.length;
143155
let definedValueCopyToIndex = pollIndex;
144-
const unChangedThreshold = unChangedThresholds[priority];
145156
for (let polled = 0; polled < chunkSize && needsVisit > 0; nextPollIndex(), needsVisit--) {
146157
const watchedFile = queue[pollIndex];
147158
if (!watchedFile) {
@@ -161,17 +172,18 @@ namespace ts {
161172
else if (fileChanged) {
162173
watchedFile.unchangedPolls = 0;
163174
// Changed files go to changedFilesInLastPoll queue
164-
if (queue !== changedFilesInLastPoll && priority !== WatchPriority.High) {
175+
if (queue !== changedFilesInLastPoll) {
165176
queue[pollIndex] = undefined;
166177
addChangedFileToHighPriorityQueue(watchedFile);
167178
}
168179
}
169-
else if (watchedFile.unchangedPolls !== unChangedThreshold) {
180+
else if (watchedFile.unchangedPolls !== unChangedThreshold(priority)) {
170181
watchedFile.unchangedPolls++;
171182
}
172183
else if (queue === changedFilesInLastPoll) {
173184
// Restart unchangedPollCount for unchanged file and move to high priority queue
174-
watchedFile.unchangedPolls = 0;
185+
watchedFile.unchangedPolls = 1;
186+
queue[pollIndex] = undefined;
175187
addToPriorityQueue(watchedFile, WatchPriority.High);
176188
}
177189
else if (priority !== WatchPriority.Low) {
@@ -207,19 +219,23 @@ namespace ts {
207219
}
208220

209221
function addToPriorityQueue(file: WatchedFile, priority: WatchPriority) {
210-
if (priorityQueues[priority].push(file) === 1) {
211-
scheduleNextPoll(priority);
212-
}
222+
priorityQueues[priority].push(file);
223+
scheduleNextPollIfNotAlreadyScheduled(priority);
213224
}
214225

215226
function addChangedFileToHighPriorityQueue(file: WatchedFile) {
216-
if (changedFilesInLastPoll.push(file) === 1 && !priorityQueues[WatchPriority.High].length) {
217-
scheduleNextPoll(WatchPriority.High);
227+
changedFilesInLastPoll.push(file);
228+
scheduleNextPollIfNotAlreadyScheduled(WatchPriority.High);
229+
}
230+
231+
function scheduleNextPollIfNotAlreadyScheduled(priority: WatchPriority) {
232+
if (!priorityQueues[priority].pollScheduled) {
233+
scheduleNextPoll(priority);
218234
}
219235
}
220236

221237
function scheduleNextPoll(priority: WatchPriority) {
222-
host.setTimeout(priority === WatchPriority.High ? pollHighPriorityQueue : pollPriorityQueue, pollingInterval(priority), priorityQueues[priority]);
238+
priorityQueues[priority].pollScheduled = host.setTimeout(priority === WatchPriority.High ? pollHighPriorityQueue : pollPriorityQueue, pollingInterval(priority), priorityQueues[priority]);
223239
}
224240

225241
function getModifiedTime(fileName: string) {

src/compiler/watchUtilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ namespace ts {
108108
}
109109

110110
export function getWatchFactory<X = undefined, Y = undefined>(host: System, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo<X, Y>): WatchFactory<X, Y> {
111-
const value = host.getEnvironmentVariable("TSC_WATCHFILE");
111+
const value = host.getEnvironmentVariable && host.getEnvironmentVariable("TSC_WATCHFILE");
112112
switch (value) {
113113
case "PriorityPollingInterval":
114114
// Use polling interval based on priority when create watch using host.watchFile

src/harness/unittests/tscWatchMode.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,4 +2113,67 @@ declare module "fs" {
21132113
host.checkScreenClears(2);
21142114
});
21152115
});
2116+
2117+
describe("tsc-watch with different polling/non polling options", () => {
2118+
it("watchFile using dynamic priority polling", () => {
2119+
const projectFolder = "/a/username/project";
2120+
const file1: FileOrFolder = {
2121+
path: `${projectFolder}/typescript.ts`,
2122+
content: "var z = 10;"
2123+
};
2124+
const files = [file1, libFile];
2125+
const environmentVariables = createMap<string>();
2126+
environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling");
2127+
const host = createWatchedSystem(files, { environmentVariables });
2128+
const watch = createWatchModeWithoutConfigFile([file1.path], host);
2129+
2130+
const initialProgram = watch();
2131+
verifyProgram();
2132+
2133+
const mediumPriorityThreshold = unChangedThreshold(WatchPriority.Medium);
2134+
for (let index = 0; index < mediumPriorityThreshold; index++) {
2135+
// Transition libFile and file1 to low priority queue
2136+
host.checkTimeoutQueueLengthAndRun(1);
2137+
assert.deepEqual(watch(), initialProgram);
2138+
}
2139+
2140+
// Make a change to file
2141+
file1.content = "var zz30 = 100;";
2142+
host.reloadFS(files);
2143+
2144+
// This should detect change in the file
2145+
host.checkTimeoutQueueLengthAndRun(1);
2146+
assert.deepEqual(watch(), initialProgram);
2147+
2148+
// Callbacks: medium priority + high priority queue and scheduled program update
2149+
host.checkTimeoutQueueLengthAndRun(3);
2150+
// During this timeout the file would be detected as unchanged
2151+
let fileUnchangeDetected = 1;
2152+
const newProgram = watch();
2153+
assert.notStrictEqual(newProgram, initialProgram);
2154+
2155+
verifyProgram();
2156+
const outputFile1 = changeExtension(file1.path, ".js");
2157+
assert.isTrue(host.fileExists(outputFile1));
2158+
assert.equal(host.readFile(outputFile1), file1.content + host.newLine);
2159+
2160+
const newThreshold = unChangedThreshold(WatchPriority.High) + mediumPriorityThreshold;
2161+
for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) {
2162+
// For low + Medium/high priority
2163+
host.checkTimeoutQueueLengthAndRun(2);
2164+
assert.deepEqual(watch(), newProgram);
2165+
}
2166+
2167+
// Everything goes in low priority queue
2168+
host.checkTimeoutQueueLengthAndRun(1);
2169+
assert.deepEqual(watch(), newProgram);
2170+
2171+
function verifyProgram() {
2172+
checkProgramActualFiles(watch(), files.map(f => f.path));
2173+
checkWatchedFiles(host, []);
2174+
checkWatchedDirectories(host, [], /*recursive*/ false);
2175+
checkWatchedDirectories(host, [], /*recursive*/ true);
2176+
}
2177+
});
2178+
});
21162179
}

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface Array<T> {}`
3636
currentDirectory?: string;
3737
newLine?: string;
3838
useWindowsStylePaths?: boolean;
39+
environmentVariables?: Map<string>;
3940
}
4041

4142
export function createWatchedSystem(fileOrFolderList: ReadonlyArray<FileOrFolder>, params?: TestServerHostCreationParameters): TestServerHost {
@@ -48,7 +49,8 @@ interface Array<T> {}`
4849
params.currentDirectory || "/",
4950
fileOrFolderList,
5051
params.newLine,
51-
params.useWindowsStylePaths);
52+
params.useWindowsStylePaths,
53+
params.environmentVariables);
5254
return host;
5355
}
5456

@@ -62,7 +64,8 @@ interface Array<T> {}`
6264
params.currentDirectory || "/",
6365
fileOrFolderList,
6466
params.newLine,
65-
params.useWindowsStylePaths);
67+
params.useWindowsStylePaths,
68+
params.environmentVariables);
6669
return host;
6770
}
6871

@@ -75,6 +78,7 @@ interface Array<T> {}`
7578
interface FSEntry {
7679
path: Path;
7780
fullPath: string;
81+
modifiedTime: Date;
7882
}
7983

8084
interface File extends FSEntry {
@@ -259,7 +263,7 @@ interface Array<T> {}`
259263
private readonly executingFilePath: string;
260264
private readonly currentDirectory: string;
261265

262-
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray<FileOrFolder>, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) {
266+
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray<FileOrFolder>, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean, private readonly environmentVariables?: Map<string>) {
263267
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
264268
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
265269
this.executingFilePath = this.getHostSpecificPath(executingFilePath);
@@ -307,6 +311,7 @@ interface Array<T> {}`
307311
// Update file
308312
if (currentEntry.content !== fileOrDirectory.content) {
309313
currentEntry.content = fileOrDirectory.content;
314+
currentEntry.modifiedTime = new Date();
310315
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
311316
this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath);
312317
}
@@ -326,6 +331,7 @@ interface Array<T> {}`
326331
}
327332
else {
328333
// Folder update: Nothing to do.
334+
currentEntry.modifiedTime = new Date();
329335
}
330336
}
331337
}
@@ -416,6 +422,7 @@ interface Array<T> {}`
416422

417423
private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder, ignoreWatch?: boolean) {
418424
folder.entries.push(fileOrDirectory);
425+
folder.modifiedTime = new Date();
419426
this.fs.set(fileOrDirectory.path, fileOrDirectory);
420427

421428
if (ignoreWatch) {
@@ -432,6 +439,7 @@ interface Array<T> {}`
432439
const baseFolder = this.fs.get(basePath) as Folder;
433440
if (basePath !== fileOrDirectory.path) {
434441
Debug.assert(!!baseFolder);
442+
baseFolder.modifiedTime = new Date();
435443
filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory);
436444
}
437445
this.fs.delete(fileOrDirectory.path);
@@ -493,30 +501,39 @@ interface Array<T> {}`
493501
}
494502
}
495503

496-
private toFile(fileOrDirectory: FileOrFolder): File {
497-
const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory);
504+
private toFsEntry(path: string): FSEntry {
505+
const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
498506
return {
499507
path: this.toPath(fullPath),
500-
content: fileOrDirectory.content,
501508
fullPath,
502-
fileSize: fileOrDirectory.fileSize
509+
modifiedTime: new Date()
503510
};
504511
}
505512

513+
private toFile(fileOrDirectory: FileOrFolder): File {
514+
const file = this.toFsEntry(fileOrDirectory.path) as File;
515+
file.content = fileOrDirectory.content;
516+
file.fileSize = fileOrDirectory.fileSize;
517+
return file;
518+
}
519+
506520
private toFolder(path: string): Folder {
507-
const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
508-
return {
509-
path: this.toPath(fullPath),
510-
entries: [],
511-
fullPath
512-
};
521+
const folder = this.toFsEntry(path) as Folder;
522+
folder.entries = [];
523+
return folder;
513524
}
514525

515526
fileExists(s: string) {
516527
const path = this.toFullPath(s);
517528
return isFile(this.fs.get(path));
518529
}
519530

531+
getModifiedTime(s: string) {
532+
const path = this.toFullPath(s);
533+
const fsEntry = this.fs.get(path);
534+
return fsEntry && fsEntry.modifiedTime;
535+
}
536+
520537
readFile(s: string) {
521538
const fsEntry = this.fs.get(this.toFullPath(s));
522539
return isFile(fsEntry) ? fsEntry.content : undefined;
@@ -624,7 +641,7 @@ interface Array<T> {}`
624641
this.timeoutCallbacks.invoke(timeoutId);
625642
}
626643
catch (e) {
627-
if (e.message === this.existMessage) {
644+
if (e.message === this.exitMessage) {
628645
return;
629646
}
630647
throw e;
@@ -682,15 +699,17 @@ interface Array<T> {}`
682699
clear(this.output);
683700
}
684701

685-
readonly existMessage = "System Exit";
702+
readonly exitMessage = "System Exit";
686703
exitCode: number;
687704
readonly resolvePath = (s: string) => s;
688705
readonly getExecutingFilePath = () => this.executingFilePath;
689706
readonly getCurrentDirectory = () => this.currentDirectory;
690707
exit(exitCode?: number) {
691708
this.exitCode = exitCode;
692-
throw new Error(this.existMessage);
709+
throw new Error(this.exitMessage);
710+
}
711+
getEnvironmentVariable(name: string) {
712+
return this.environmentVariables && this.environmentVariables.get(name);
693713
}
694-
readonly getEnvironmentVariable = notImplemented;
695714
}
696715
}

0 commit comments

Comments
 (0)