Add test to verify timeout queues
This commit is contained in:
parent
976f330044
commit
c3b9904190
4 changed files with 135 additions and 37 deletions
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue