Add test to verify timeout queues

This commit is contained in:
Sheetal Nandi 2017-12-22 17:01:33 -08:00
parent 976f330044
commit c3b9904190
4 changed files with 135 additions and 37 deletions

View file

@ -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<WatchedFile> {
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) {

View file

@ -108,7 +108,7 @@ namespace ts {
}
export function getWatchFactory<X = undefined, Y = undefined>(host: System, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo<X, Y>): WatchFactory<X, Y> {
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

View file

@ -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<string>();
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);
}
});
});
}

View file

@ -36,6 +36,7 @@ interface Array<T> {}`
currentDirectory?: string;
newLine?: string;
useWindowsStylePaths?: boolean;
environmentVariables?: Map<string>;
}
export function createWatchedSystem(fileOrFolderList: ReadonlyArray<FileOrFolder>, params?: TestServerHostCreationParameters): TestServerHost {
@ -48,7 +49,8 @@ interface Array<T> {}`
params.currentDirectory || "/",
fileOrFolderList,
params.newLine,
params.useWindowsStylePaths);
params.useWindowsStylePaths,
params.environmentVariables);
return host;
}
@ -62,7 +64,8 @@ interface Array<T> {}`
params.currentDirectory || "/",
fileOrFolderList,
params.newLine,
params.useWindowsStylePaths);
params.useWindowsStylePaths,
params.environmentVariables);
return host;
}
@ -75,6 +78,7 @@ interface Array<T> {}`
interface FSEntry {
path: Path;
fullPath: string;
modifiedTime: Date;
}
interface File extends FSEntry {
@ -259,7 +263,7 @@ interface Array<T> {}`
private readonly executingFilePath: string;
private readonly currentDirectory: string;
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray<FileOrFolder>, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) {
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>) {
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
this.executingFilePath = this.getHostSpecificPath(executingFilePath);
@ -307,6 +311,7 @@ interface Array<T> {}`
// 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<T> {}`
}
else {
// Folder update: Nothing to do.
currentEntry.modifiedTime = new Date();
}
}
}
@ -416,6 +422,7 @@ interface Array<T> {}`
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<T> {}`
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<T> {}`
}
}
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<T> {}`
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<T> {}`
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;
}
}