Move fixed chunk size polling as a watch option and move it out of server (#42542)

* Move fixed chunk size polling as a watch option and move it out of server
Fixes #41549

* Feedback
This commit is contained in:
Sheetal Nandi 2021-03-02 16:45:53 -08:00 committed by GitHub
parent 5dbb110497
commit 0cf834ceec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 572 additions and 179 deletions

View File

@ -101,11 +101,12 @@ namespace ts {
fixedpollinginterval: WatchFileKind.FixedPollingInterval,
prioritypollinginterval: WatchFileKind.PriorityPollingInterval,
dynamicprioritypolling: WatchFileKind.DynamicPriorityPolling,
fixedchunksizepolling: WatchFileKind.FixedChunkSizePolling,
usefsevents: WatchFileKind.UseFsEvents,
usefseventsonparentdirectory: WatchFileKind.UseFsEventsOnParentDirectory,
})),
category: Diagnostics.Advanced_Options,
description: Diagnostics.Specify_strategy_for_watching_file_Colon_FixedPollingInterval_default_PriorityPollingInterval_DynamicPriorityPolling_UseFsEvents_UseFsEventsOnParentDirectory,
description: Diagnostics.Specify_strategy_for_watching_file_Colon_FixedPollingInterval_default_PriorityPollingInterval_DynamicPriorityPolling_FixedChunkSizePolling_UseFsEvents_UseFsEventsOnParentDirectory,
},
{
name: "watchDirectory",
@ -113,9 +114,10 @@ namespace ts {
usefsevents: WatchDirectoryKind.UseFsEvents,
fixedpollinginterval: WatchDirectoryKind.FixedPollingInterval,
dynamicprioritypolling: WatchDirectoryKind.DynamicPriorityPolling,
fixedchunksizepolling: WatchDirectoryKind.FixedChunkSizePolling,
})),
category: Diagnostics.Advanced_Options,
description: Diagnostics.Specify_strategy_for_watching_directory_on_platforms_that_don_t_support_recursive_watching_natively_Colon_UseFsEvents_default_FixedPollingInterval_DynamicPriorityPolling,
description: Diagnostics.Specify_strategy_for_watching_directory_on_platforms_that_don_t_support_recursive_watching_natively_Colon_UseFsEvents_default_FixedPollingInterval_DynamicPriorityPolling_FixedChunkSizePolling,
},
{
name: "fallbackPolling",
@ -123,9 +125,10 @@ namespace ts {
fixedinterval: PollingWatchKind.FixedInterval,
priorityinterval: PollingWatchKind.PriorityInterval,
dynamicpriority: PollingWatchKind.DynamicPriority,
fixedchunksize: PollingWatchKind.FixedChunkSize,
})),
category: Diagnostics.Advanced_Options,
description: Diagnostics.Specify_strategy_for_creating_a_polling_watch_when_it_fails_to_create_using_file_system_events_Colon_FixedInterval_default_PriorityInterval_DynamicPriority,
description: Diagnostics.Specify_strategy_for_creating_a_polling_watch_when_it_fails_to_create_using_file_system_events_Colon_FixedInterval_default_PriorityInterval_DynamicPriority_FixedChunkSize,
},
{
name: "synchronousWatchDirectory",

View File

@ -4701,15 +4701,15 @@
"category": "Message",
"code": 6224
},
"Specify strategy for watching file: 'FixedPollingInterval' (default), 'PriorityPollingInterval', 'DynamicPriorityPolling', 'UseFsEvents', 'UseFsEventsOnParentDirectory'.": {
"Specify strategy for watching file: 'FixedPollingInterval' (default), 'PriorityPollingInterval', 'DynamicPriorityPolling', 'FixedChunkSizePolling', 'UseFsEvents', 'UseFsEventsOnParentDirectory'.": {
"category": "Message",
"code": 6225
},
"Specify strategy for watching directory on platforms that don't support recursive watching natively: 'UseFsEvents' (default), 'FixedPollingInterval', 'DynamicPriorityPolling'.": {
"Specify strategy for watching directory on platforms that don't support recursive watching natively: 'UseFsEvents' (default), 'FixedPollingInterval', 'DynamicPriorityPolling', 'FixedChunkSizePolling'.": {
"category": "Message",
"code": 6226
},
"Specify strategy for creating a polling watch when it fails to create using file system events: 'FixedInterval' (default), 'PriorityInterval', 'DynamicPriority'.": {
"Specify strategy for creating a polling watch when it fails to create using file system events: 'FixedInterval' (default), 'PriorityInterval', 'DynamicPriority', 'FixedChunkSize'.": {
"category": "Message",
"code": 6227
},

View File

@ -57,6 +57,11 @@ namespace ts {
/* @internal */
export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time
/* @internal */
export function getModifiedTime(host: { getModifiedTime: NonNullable<System["getModifiedTime"]>; }, fileName: string) {
return host.getModifiedTime(fileName) || missingFileModifiedTime;
}
interface Levels {
Low: number;
Medium: number;
@ -126,6 +131,64 @@ namespace ts {
}
}
interface WatchedFileWithIsClosed extends WatchedFile {
isClosed?: boolean;
}
function pollWatchedFileQueue<T extends WatchedFileWithIsClosed>(
host: { getModifiedTime: NonNullable<System["getModifiedTime"]>; },
queue: (T | undefined)[],
pollIndex: number, chunkSize: number,
callbackOnWatchFileStat?: (watchedFile: T, pollIndex: number, fileChanged: boolean) => void
) {
let definedValueCopyToIndex = pollIndex;
// Max visit would be all elements of the queue
for (let canVisit = queue.length; chunkSize && canVisit; nextPollIndex(), canVisit--) {
const watchedFile = queue[pollIndex];
if (!watchedFile) {
continue;
}
else if (watchedFile.isClosed) {
queue[pollIndex] = undefined;
continue;
}
// Only files polled count towards chunkSize
chunkSize--;
const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(host, watchedFile.fileName));
if (watchedFile.isClosed) {
// Closed watcher as part of callback
queue[pollIndex] = undefined;
continue;
}
callbackOnWatchFileStat?.(watchedFile, pollIndex, fileChanged);
// Defragment the queue while we are at it
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 definedValueCopyToIndex to end of queue, change queue size
queue.length = definedValueCopyToIndex;
}
pollIndex = 0;
definedValueCopyToIndex = 0;
}
}
}
/* @internal */
export function createDynamicPriorityPollingWatchFile(host: {
getModifiedTime: NonNullable<System["getModifiedTime"]>;
@ -154,7 +217,7 @@ namespace ts {
fileName,
callback,
unchangedPolls: 0,
mtime: getModifiedTime(fileName)
mtime: getModifiedTime(host, fileName)
};
watchedFiles.push(file);
@ -203,26 +266,16 @@ namespace ts {
}
function pollQueue(queue: (WatchedFile | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) {
// Max visit would be all elements of the queue
let needsVisit = queue.length;
let definedValueCopyToIndex = pollIndex;
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;
}
return pollWatchedFileQueue(
host,
queue,
pollIndex,
chunkSize,
onWatchFileStat
);
polled++;
const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(watchedFile.fileName));
if (watchedFile.isClosed) {
// Closed watcher as part of callback
queue[pollIndex] = undefined;
}
else if (fileChanged) {
function onWatchFileStat(watchedFile: WatchedFile, pollIndex: number, fileChanged: boolean) {
if (fileChanged) {
watchedFile.unchangedPolls = 0;
// Changed files go to changedFilesInLastPoll queue
if (queue !== changedFilesInLastPoll) {
@ -244,30 +297,6 @@ namespace ts {
queue[pollIndex] = undefined;
addToPollingIntervalQueue(watchedFile, pollingInterval === PollingInterval.Low ? PollingInterval.Medium : PollingInterval.High);
}
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;
}
}
}
@ -301,10 +330,6 @@ namespace ts {
function scheduleNextPoll(pollingInterval: PollingInterval) {
pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval));
}
function getModifiedTime(fileName: string) {
return host.getModifiedTime(fileName) || missingFileModifiedTime;
}
}
function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile {
@ -361,6 +386,43 @@ namespace ts {
}
}
function createFixedChunkSizePollingWatchFile(host: {
getModifiedTime: NonNullable<System["getModifiedTime"]>;
setTimeout: NonNullable<System["setTimeout"]>;
}): HostWatchFile {
const watchedFiles: (WatchedFileWithIsClosed | undefined)[] = [];
let pollIndex = 0;
let pollScheduled: any;
return watchFile;
function watchFile(fileName: string, callback: FileWatcherCallback): FileWatcher {
const file: WatchedFileWithIsClosed = {
fileName,
callback,
mtime: getModifiedTime(host, fileName)
};
watchedFiles.push(file);
scheduleNextPoll();
return {
close: () => {
file.isClosed = true;
unorderedRemoveItem(watchedFiles, file);
}
};
}
function pollQueue() {
pollScheduled = undefined;
pollIndex = pollWatchedFileQueue(host, watchedFiles, pollIndex, pollingChunkSize[PollingInterval.Low]);
scheduleNextPoll();
}
function scheduleNextPoll() {
if (!watchedFiles.length || pollScheduled) return;
pollScheduled = host.setTimeout(pollQueue, PollingInterval.High);
}
}
/* @internal */
export function createSingleFileWatcherPerName(
watchFile: HostWatchFile,
@ -795,6 +857,7 @@ namespace ts {
tscWatchFile: string | undefined;
useNonPollingWatchers?: boolean;
tscWatchDirectory: string | undefined;
defaultWatchFileKind: System["defaultWatchFileKind"];
}
/*@internal*/
@ -814,8 +877,10 @@ namespace ts {
tscWatchFile,
useNonPollingWatchers,
tscWatchDirectory,
defaultWatchFileKind,
}: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } {
let dynamicPollingWatchFile: HostWatchFile | undefined;
let fixedChunkSizePollingWatchFile: HostWatchFile | undefined;
let nonPollingWatchFile: HostWatchFile | undefined;
let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined;
return {
@ -833,6 +898,8 @@ namespace ts {
return pollingWatchFile(fileName, callback, pollingInterval, /*options*/ undefined);
case WatchFileKind.DynamicPriorityPolling:
return ensureDynamicPollingWatchFile()(fileName, callback, pollingInterval, /*options*/ undefined);
case WatchFileKind.FixedChunkSizePolling:
return ensureFixedChunkSizePollingWatchFile()(fileName, callback, /* pollingInterval */ undefined!, /*options*/ undefined);
case WatchFileKind.UseFsEvents:
return fsWatch(
fileName,
@ -853,8 +920,11 @@ namespace ts {
}
function ensureDynamicPollingWatchFile() {
return dynamicPollingWatchFile ||
(dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }));
return dynamicPollingWatchFile ||= createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout });
}
function ensureFixedChunkSizePollingWatchFile() {
return fixedChunkSizePollingWatchFile ||= createFixedChunkSizePollingWatchFile({ getModifiedTime, setTimeout });
}
function updateOptionsForWatchFile(options: WatchOptions | undefined, useNonPollingWatchers?: boolean): WatchOptions {
@ -880,7 +950,7 @@ namespace ts {
// Use notifications from FS to watch with falling back to fs.watchFile
generateWatchFileOptions(WatchFileKind.UseFsEventsOnParentDirectory, PollingWatchKind.PriorityInterval, options) :
// Default to do not use fixed polling interval
{ watchFile: WatchFileKind.FixedPollingInterval };
{ watchFile: defaultWatchFileKind?.() || WatchFileKind.FixedPollingInterval };
}
}
@ -944,6 +1014,13 @@ namespace ts {
PollingInterval.Medium,
/*options*/ undefined
);
case WatchDirectoryKind.FixedChunkSizePolling:
return ensureFixedChunkSizePollingWatchFile()(
directoryName,
() => callback(directoryName),
/* pollingInterval */ undefined!,
/*options*/ undefined
);
case WatchDirectoryKind.UseFsEvents:
return fsWatch(
directoryName,
@ -1131,6 +1208,7 @@ namespace ts {
// For testing
/*@internal*/ now?(): Date;
/*@internal*/ require?(baseDir: string, moduleName: string): RequireResult;
/*@internal*/ defaultWatchFileKind?(): WatchFileKind | undefined;
}
export interface FileWatcher {
@ -1219,6 +1297,7 @@ namespace ts {
tscWatchFile: process.env.TSC_WATCHFILE,
useNonPollingWatchers: process.env.TSC_NONPOLLING_WATCHER,
tscWatchDirectory: process.env.TSC_WATCHDIRECTORY,
defaultWatchFileKind: () => sys!.defaultWatchFileKind?.(),
});
const nodeSystem: System = {
args: process.argv.slice(2),

View File

@ -1334,7 +1334,7 @@ namespace ts {
function checkConfigFileUpToDateStatus(state: SolutionBuilderState, configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined {
// Check tsconfig time
const tsconfigTime = state.host.getModifiedTime(configFile) || missingFileModifiedTime;
const tsconfigTime = getModifiedTime(state.host, configFile);
if (oldestOutputFileTime < tsconfigTime) {
return {
type: UpToDateStatusType.OutOfDateWithSelf,
@ -1357,7 +1357,7 @@ namespace ts {
};
}
const inputTime = host.getModifiedTime(inputFile) || missingFileModifiedTime;
const inputTime = getModifiedTime(host, inputFile); host.getModifiedTime(inputFile);
if (inputTime > newestInputFileTime) {
newestInputFileName = inputFile;
newestInputFileTime = inputTime;
@ -1390,7 +1390,7 @@ namespace ts {
break;
}
const outputTime = host.getModifiedTime(output) || missingFileModifiedTime;
const outputTime = getModifiedTime(host, output);
if (outputTime < oldestOutputFileTime) {
oldestOutputFileTime = outputTime;
oldestOutputFileName = output;
@ -1413,7 +1413,7 @@ namespace ts {
// had its file touched but not had its contents changed - this allows us
// to skip a downstream typecheck
if (isDeclarationFile(output)) {
const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime;
const outputModifiedTime = getModifiedTime(host, output);
newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime);
}
}
@ -1571,7 +1571,7 @@ namespace ts {
}
if (isDeclarationFile(file)) {
priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime);
priorNewestUpdateTime = newer(priorNewestUpdateTime, getModifiedTime(host, file));
}
host.setModifiedTime(file, now);

View File

@ -5795,6 +5795,7 @@ namespace ts {
FixedPollingInterval,
PriorityPollingInterval,
DynamicPriorityPolling,
FixedChunkSizePolling,
UseFsEvents,
UseFsEventsOnParentDirectory,
}
@ -5803,12 +5804,14 @@ namespace ts {
UseFsEvents,
FixedPollingInterval,
DynamicPriorityPolling,
FixedChunkSizePolling,
}
export enum PollingWatchKind {
FixedInterval,
PriorityInterval,
DynamicPriority,
FixedChunkSize,
}
export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginImport[] | ProjectReference[] | null | undefined;

View File

@ -395,6 +395,7 @@ interface Array<T> { length: number; [n: number]: T; }`
private readonly executingFilePath: string;
private readonly currentDirectory: string;
public require: ((initialPath: string, moduleName: string) => RequireResult) | undefined;
public defaultWatchFileKind?: () => WatchFileKind | undefined;
watchFile: HostWatchFile;
watchDirectory: HostWatchDirectory;
constructor(
@ -439,7 +440,8 @@ interface Array<T> { length: number; [n: number]: T; }`
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
realpath: this.realpath.bind(this),
tscWatchFile,
tscWatchDirectory
tscWatchDirectory,
defaultWatchFileKind: () => this.defaultWatchFileKind?.(),
});
this.watchFile = watchFile;
this.watchDirectory = watchDirectory;

View File

@ -1538,6 +1538,7 @@ namespace ts.server.protocol {
FixedPollingInterval = "FixedPollingInterval",
PriorityPollingInterval = "PriorityPollingInterval",
DynamicPriorityPolling = "DynamicPriorityPolling",
FixedChunkSizePolling = "FixedChunkSizePolling",
UseFsEvents = "UseFsEvents",
UseFsEventsOnParentDirectory = "UseFsEventsOnParentDirectory",
}
@ -1546,12 +1547,14 @@ namespace ts.server.protocol {
UseFsEvents = "UseFsEvents",
FixedPollingInterval = "FixedPollingInterval",
DynamicPriorityPolling = "DynamicPriorityPolling",
FixedChunkSizePolling = "FixedChunkSizePolling",
}
export const enum PollingWatchKind {
FixedInterval = "FixedInterval",
PriorityInterval = "PriorityInterval",
DynamicPriority = "DynamicPriority",
FixedChunkSize = "FixedChunkSize",
}
export interface WatchOptions {

View File

@ -650,7 +650,7 @@ namespace ts {
length: undefined
},
{
messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority'.",
messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority', 'fixedchunksize'.",
category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category,
code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code,
file: undefined,
@ -957,7 +957,7 @@ namespace ts {
length: undefined
},
{
messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority'.",
messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority', 'fixedchunksize'.",
category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category,
code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code,
file: undefined,

View File

@ -66,6 +66,90 @@ namespace ts.tscWatch {
]
});
verifyTscWatch({
scenario,
subScenario: "watchFile/using fixed chunk size polling",
commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"],
sys: () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({
watchOptions: {
watchFile: "FixedChunkSizePolling"
}
})
};
const files = [libFile, commonFile1, commonFile2, configFile];
return createWatchedSystem(files);
},
changes: [
{
caption: "The timeout is to check the status of all files",
change: noop,
timeouts: (sys, programs) => {
// On each timeout file does not change
const initialProgram = programs[0][0];
for (let index = 0; index < 4; index++) {
sys.checkTimeoutQueueLengthAndRun(1);
assert.deepEqual(programs[0][0], initialProgram);
}
},
},
{
caption: "Make change to file but should detect as changed and schedule program update",
// Make a change to file
change: sys => sys.writeFile(commonFile1.path, "var zz30 = 100;"),
timeouts: checkSingleTimeoutQueueLengthAndRun,
},
{
caption: "Callbacks: queue and scheduled program update",
change: noop,
// Callbacks: scheduled program update and queue for the polling
timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2),
},
{
caption: "The timeout is to check the status of all files",
change: noop,
timeouts: (sys, programs) => {
// On each timeout file does not change
const initialProgram = programs[0][0];
sys.checkTimeoutQueueLengthAndRun(1);
assert.deepEqual(programs[0][0], initialProgram);
},
},
]
});
verifyTscWatch({
scenario,
subScenario: "watchFile/setting default as fixed chunk size watch file works",
commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"],
sys: () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
content: "{}"
};
const files = [libFile, commonFile1, commonFile2, configFile];
const sys = createWatchedSystem(files);
sys.defaultWatchFileKind = () => WatchFileKind.FixedChunkSizePolling;
return sys;
},
changes: [
{
caption: "Make change to file but should detect as changed and schedule program update",
// Make a change to file
change: sys => sys.writeFile(commonFile1.path, "var zz30 = 100;"),
timeouts: checkSingleTimeoutQueueLengthAndRun,
},
{
caption: "Callbacks: queue and scheduled program update",
change: noop,
// Callbacks: scheduled program update and queue for the polling
timeouts: sys => sys.checkTimeoutQueueLengthAndRun(2),
},
]
});
describe("tsc-watch when watchDirectories implementation", () => {
function verifyRenamingFileInSubFolder(subScenario: string, tscWatchDirectory: Tsc_WatchDirectory) {
const projectFolder = "/a/username/project";

View File

@ -180,21 +180,6 @@ namespace ts.server {
const originalWatchDirectory: ServerHost["watchDirectory"] = sys.watchDirectory.bind(sys);
const logger = createLogger();
// REVIEW: for now this implementation uses polling.
// The advantage of polling is that it works reliably
// on all os and with network mounted files.
// For 90 referenced files, the average time to detect
// changes is 2*msInterval (by default 5 seconds).
// The overhead of this is .04 percent (1/2500) with
// average pause of < 1 millisecond (and max
// pause less than 1.5 milliseconds); question is
// do we anticipate reference sets in the 100s and
// do we care about waiting 10-20 seconds to detect
// changes for large reference sets? If so, do we want
// to increase the chunk size or decrease the interval
// time dynamically to match the large reference set?
const pollingWatchedFileSet = createPollingWatchedFileSet();
const pending: Buffer[] = [];
let canWrite = true;
@ -248,12 +233,20 @@ namespace ts.server {
// Override sys.write because fs.writeSync is not reliable on Node 4
sys.write = (s: string) => writeMessage(sys.bufferFrom!(s, "utf8") as globalThis.Buffer);
sys.watchFile = (fileName, callback) => {
const watchedFile = pollingWatchedFileSet.addFile(fileName, callback);
return {
close: () => pollingWatchedFileSet.removeFile(watchedFile)
};
};
// REVIEW: for now this implementation uses polling.
// The advantage of polling is that it works reliably
// on all os and with network mounted files.
// For 90 referenced files, the average time to detect
// changes is 2*msInterval (by default 5 seconds).
// The overhead of this is .04 percent (1/2500) with
// average pause of < 1 millisecond (and max
// pause less than 1.5 milliseconds); question is
// do we anticipate reference sets in the 100s and
// do we care about waiting 10-20 seconds to detect
// changes for large reference sets? If so, do we want
// to increase the chunk size or decrease the interval
// time dynamically to match the large reference set?
sys.defaultWatchFileKind = () => WatchFileKind.FixedChunkSizePolling;
/* eslint-disable no-restricted-globals */
sys.setTimeout = setTimeout;
@ -324,89 +317,6 @@ namespace ts.server {
const logVerbosity = cmdLineVerbosity || envLogOptions.detailLevel;
return new Logger(substitutedLogFileName!, envLogOptions.traceToConsole!, logVerbosity!); // TODO: GH#18217
}
// This places log file in the directory containing editorServices.js
// TODO: check that this location is writable
// average async stat takes about 30 microseconds
// set chunk size to do 30 files in < 1 millisecond
function createPollingWatchedFileSet(interval = 2500, chunkSize = 30) {
const watchedFiles: WatchedFile[] = [];
let nextFileToCheck = 0;
return { getModifiedTime, poll, startWatchTimer, addFile, removeFile };
function getModifiedTime(fileName: string): Date {
// Caller guarantees that `fileName` exists, so there'd be no benefit from throwIfNoEntry
return fs.statSync(fileName).mtime;
}
function poll(checkedIndex: number) {
const watchedFile = watchedFiles[checkedIndex];
if (!watchedFile) {
return;
}
fs.stat(watchedFile.fileName, (err, stats) => {
if (err) {
if (err.code === "ENOENT") {
if (watchedFile.mtime.getTime() !== 0) {
watchedFile.mtime = missingFileModifiedTime;
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Deleted);
}
}
else {
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Changed);
}
}
else {
onWatchedFileStat(watchedFile, stats.mtime);
}
});
}
// this implementation uses polling and
// stat due to inconsistencies of fs.watch
// and efficiency of stat on modern filesystems
function startWatchTimer() {
// eslint-disable-next-line no-restricted-globals
setInterval(() => {
let count = 0;
let nextToCheck = nextFileToCheck;
let firstCheck = -1;
while ((count < chunkSize) && (nextToCheck !== firstCheck)) {
poll(nextToCheck);
if (firstCheck < 0) {
firstCheck = nextToCheck;
}
nextToCheck++;
if (nextToCheck === watchedFiles.length) {
nextToCheck = 0;
}
count++;
}
nextFileToCheck = nextToCheck;
}, interval);
}
function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile {
const file: WatchedFile = {
fileName,
callback,
mtime: sys.fileExists(fileName)
? getModifiedTime(fileName)
: missingFileModifiedTime // Any subsequent modification will occur after this time
};
watchedFiles.push(file);
if (watchedFiles.length === 1) {
startWatchTimer();
}
return file;
}
function removeFile(file: WatchedFile) {
unorderedRemoveItem(watchedFiles, file);
}
}
function writeMessage(buf: Buffer) {
if (!canWrite) {

View File

@ -2775,18 +2775,21 @@ declare namespace ts {
FixedPollingInterval = 0,
PriorityPollingInterval = 1,
DynamicPriorityPolling = 2,
UseFsEvents = 3,
UseFsEventsOnParentDirectory = 4
FixedChunkSizePolling = 3,
UseFsEvents = 4,
UseFsEventsOnParentDirectory = 5
}
export enum WatchDirectoryKind {
UseFsEvents = 0,
FixedPollingInterval = 1,
DynamicPriorityPolling = 2
DynamicPriorityPolling = 2,
FixedChunkSizePolling = 3
}
export enum PollingWatchKind {
FixedInterval = 0,
PriorityInterval = 1,
DynamicPriority = 2
DynamicPriority = 2,
FixedChunkSize = 3
}
export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginImport[] | ProjectReference[] | null | undefined;
export interface CompilerOptions {
@ -7685,18 +7688,21 @@ declare namespace ts.server.protocol {
FixedPollingInterval = "FixedPollingInterval",
PriorityPollingInterval = "PriorityPollingInterval",
DynamicPriorityPolling = "DynamicPriorityPolling",
FixedChunkSizePolling = "FixedChunkSizePolling",
UseFsEvents = "UseFsEvents",
UseFsEventsOnParentDirectory = "UseFsEventsOnParentDirectory"
}
enum WatchDirectoryKind {
UseFsEvents = "UseFsEvents",
FixedPollingInterval = "FixedPollingInterval",
DynamicPriorityPolling = "DynamicPriorityPolling"
DynamicPriorityPolling = "DynamicPriorityPolling",
FixedChunkSizePolling = "FixedChunkSizePolling"
}
enum PollingWatchKind {
FixedInterval = "FixedInterval",
PriorityInterval = "PriorityInterval",
DynamicPriority = "DynamicPriority"
DynamicPriority = "DynamicPriority",
FixedChunkSize = "FixedChunkSize"
}
interface WatchOptions {
watchFile?: WatchFileKind | ts.WatchFileKind;

View File

@ -2775,18 +2775,21 @@ declare namespace ts {
FixedPollingInterval = 0,
PriorityPollingInterval = 1,
DynamicPriorityPolling = 2,
UseFsEvents = 3,
UseFsEventsOnParentDirectory = 4
FixedChunkSizePolling = 3,
UseFsEvents = 4,
UseFsEventsOnParentDirectory = 5
}
export enum WatchDirectoryKind {
UseFsEvents = 0,
FixedPollingInterval = 1,
DynamicPriorityPolling = 2
DynamicPriorityPolling = 2,
FixedChunkSizePolling = 3
}
export enum PollingWatchKind {
FixedInterval = 0,
PriorityInterval = 1,
DynamicPriority = 2
DynamicPriority = 2,
FixedChunkSize = 3
}
export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginImport[] | ProjectReference[] | null | undefined;
export interface CompilerOptions {

View File

@ -0,0 +1,131 @@
Input::
//// [/a/lib/lib.d.ts]
/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }
//// [/a/b/commonFile1.ts]
let x = 1
//// [/a/b/commonFile2.ts]
let y = 1
//// [/a/b/tsconfig.json]
{}
/a/lib/tsc.js -w -p /a/b/tsconfig.json
Output::
>> Screen clear
[12:00:17 AM] Starting compilation in watch mode...
[12:00:22 AM] Found 0 errors. Watching for file changes.
Program root files: ["/a/b/commonFile1.ts","/a/b/commonFile2.ts"]
Program options: {"watch":true,"project":"/a/b/tsconfig.json","configFilePath":"/a/b/tsconfig.json"}
Program structureReused: Not
Program files::
/a/lib/lib.d.ts
/a/b/commonFile1.ts
/a/b/commonFile2.ts
Semantic diagnostics in builder refreshed for::
/a/lib/lib.d.ts
/a/b/commonFile1.ts
/a/b/commonFile2.ts
WatchedFiles::
FsWatches::
FsWatchesRecursive::
/a/b/node_modules/@types:
{"directoryName":"/a/b/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
/a/b:
{"directoryName":"/a/b","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
exitCode:: ExitStatus.undefined
//// [/a/b/commonFile1.js]
var x = 1;
//// [/a/b/commonFile2.js]
var y = 1;
Change:: Make change to file but should detect as changed and schedule program update
Input::
//// [/a/b/commonFile1.ts]
var zz30 = 100;
Output::
WatchedFiles::
FsWatches::
FsWatchesRecursive::
/a/b/node_modules/@types:
{"directoryName":"/a/b/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
/a/b:
{"directoryName":"/a/b","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
exitCode:: ExitStatus.undefined
Change:: Callbacks: queue and scheduled program update
Input::
Output::
>> Screen clear
[12:00:26 AM] File change detected. Starting incremental compilation...
[12:00:33 AM] Found 0 errors. Watching for file changes.
Program root files: ["/a/b/commonFile1.ts","/a/b/commonFile2.ts"]
Program options: {"watch":true,"project":"/a/b/tsconfig.json","configFilePath":"/a/b/tsconfig.json"}
Program structureReused: Completely
Program files::
/a/lib/lib.d.ts
/a/b/commonFile1.ts
/a/b/commonFile2.ts
Semantic diagnostics in builder refreshed for::
/a/lib/lib.d.ts
/a/b/commonFile1.ts
/a/b/commonFile2.ts
WatchedFiles::
FsWatches::
FsWatchesRecursive::
/a/b/node_modules/@types:
{"directoryName":"/a/b/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
/a/b:
{"directoryName":"/a/b","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
exitCode:: ExitStatus.undefined
//// [/a/b/commonFile1.js]
var zz30 = 100;
//// [/a/b/commonFile2.js] file written with same contents

View File

@ -0,0 +1,169 @@
Input::
//// [/a/lib/lib.d.ts]
/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }
//// [/a/b/commonFile1.ts]
let x = 1
//// [/a/b/commonFile2.ts]
let y = 1
//// [/a/b/tsconfig.json]
{"watchOptions":{"watchFile":"FixedChunkSizePolling"}}
/a/lib/tsc.js -w -p /a/b/tsconfig.json
Output::
>> Screen clear
[12:00:17 AM] Starting compilation in watch mode...
[12:00:22 AM] Found 0 errors. Watching for file changes.
Program root files: ["/a/b/commonFile1.ts","/a/b/commonFile2.ts"]
Program options: {"watch":true,"project":"/a/b/tsconfig.json","configFilePath":"/a/b/tsconfig.json"}
Program structureReused: Not
Program files::
/a/lib/lib.d.ts
/a/b/commonFile1.ts
/a/b/commonFile2.ts
Semantic diagnostics in builder refreshed for::
/a/lib/lib.d.ts
/a/b/commonFile1.ts
/a/b/commonFile2.ts
WatchedFiles::
FsWatches::
FsWatchesRecursive::
/a/b/node_modules/@types:
{"directoryName":"/a/b/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
/a/b:
{"directoryName":"/a/b","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
exitCode:: ExitStatus.undefined
//// [/a/b/commonFile1.js]
var x = 1;
//// [/a/b/commonFile2.js]
var y = 1;
Change:: The timeout is to check the status of all files
Input::
Output::
WatchedFiles::
FsWatches::
FsWatchesRecursive::
/a/b/node_modules/@types:
{"directoryName":"/a/b/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
/a/b:
{"directoryName":"/a/b","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
exitCode:: ExitStatus.undefined
Change:: Make change to file but should detect as changed and schedule program update
Input::
//// [/a/b/commonFile1.ts]
var zz30 = 100;
Output::
WatchedFiles::
FsWatches::
FsWatchesRecursive::
/a/b/node_modules/@types:
{"directoryName":"/a/b/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
/a/b:
{"directoryName":"/a/b","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
exitCode:: ExitStatus.undefined
Change:: Callbacks: queue and scheduled program update
Input::
Output::
>> Screen clear
[12:00:26 AM] File change detected. Starting incremental compilation...
[12:00:33 AM] Found 0 errors. Watching for file changes.
Program root files: ["/a/b/commonFile1.ts","/a/b/commonFile2.ts"]
Program options: {"watch":true,"project":"/a/b/tsconfig.json","configFilePath":"/a/b/tsconfig.json"}
Program structureReused: Completely
Program files::
/a/lib/lib.d.ts
/a/b/commonFile1.ts
/a/b/commonFile2.ts
Semantic diagnostics in builder refreshed for::
/a/lib/lib.d.ts
/a/b/commonFile1.ts
/a/b/commonFile2.ts
WatchedFiles::
FsWatches::
FsWatchesRecursive::
/a/b/node_modules/@types:
{"directoryName":"/a/b/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
/a/b:
{"directoryName":"/a/b","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
exitCode:: ExitStatus.undefined
//// [/a/b/commonFile1.js]
var zz30 = 100;
//// [/a/b/commonFile2.js] file written with same contents
Change:: The timeout is to check the status of all files
Input::
Output::
WatchedFiles::
FsWatches::
FsWatchesRecursive::
/a/b/node_modules/@types:
{"directoryName":"/a/b/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
/a/b:
{"directoryName":"/a/b","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}}
exitCode:: ExitStatus.undefined