Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make useFsEvents as default strategy for the watching #50366

Merged
merged 10 commits into from
Sep 6, 2022
  •  
  •  
  •  
160 changes: 86 additions & 74 deletions src/compiler/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ namespace ts {

export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, modifiedTime?: Date) => void;
export type DirectoryWatcherCallback = (fileName: string) => void;
/*@internal*/
export interface WatchedFile {
interface WatchedFile {
readonly fileName: string;
readonly callback: FileWatcherCallback;
mtime: Date;
Expand Down Expand Up @@ -81,8 +80,7 @@ namespace ts {
/* @internal */
export let unchangedPollThresholds = createPollingIntervalBasedLevels(defaultChunkLevels);

/* @internal */
export function setCustomPollingValues(system: System) {
function setCustomPollingValues(system: System) {
if (!system.getEnvironmentVariable) {
return;
}
Expand Down Expand Up @@ -189,31 +187,28 @@ namespace ts {
}
}

/* @internal */
export function createDynamicPriorityPollingWatchFile(host: {
interface WatchedFileWithUnchangedPolls extends WatchedFileWithIsClosed {
unchangedPolls: number;
}
function createDynamicPriorityPollingWatchFile(host: {
getModifiedTime: NonNullable<System["getModifiedTime"]>;
setTimeout: NonNullable<System["setTimeout"]>;
}): HostWatchFile {
interface WatchedFile extends ts.WatchedFile {
isClosed?: boolean;
unchangedPolls: number;
}

interface PollingIntervalQueue extends Array<WatchedFile> {
interface PollingIntervalQueue extends Array<WatchedFileWithUnchangedPolls> {
pollingInterval: PollingInterval;
pollIndex: number;
pollScheduled: boolean;
}

const watchedFiles: WatchedFile[] = [];
const changedFilesInLastPoll: WatchedFile[] = [];
const watchedFiles: WatchedFileWithUnchangedPolls[] = [];
const changedFilesInLastPoll: WatchedFileWithUnchangedPolls[] = [];
const lowPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Low);
const mediumPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Medium);
const highPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.High);
return watchFile;

function watchFile(fileName: string, callback: FileWatcherCallback, defaultPollingInterval: PollingInterval): FileWatcher {
const file: WatchedFile = {
const file: WatchedFileWithUnchangedPolls = {
fileName,
callback,
unchangedPolls: 0,
Expand All @@ -233,7 +228,7 @@ namespace ts {
}

function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue {
const queue = [] as WatchedFile[] as PollingIntervalQueue;
const queue = [] as WatchedFileWithUnchangedPolls[] as PollingIntervalQueue;
queue.pollingInterval = pollingInterval;
queue.pollIndex = 0;
queue.pollScheduled = false;
Expand Down Expand Up @@ -265,7 +260,7 @@ namespace ts {
}
}

function pollQueue(queue: (WatchedFile | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) {
function pollQueue(queue: (WatchedFileWithUnchangedPolls | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) {
return pollWatchedFileQueue(
host,
queue,
Expand All @@ -274,7 +269,7 @@ namespace ts {
onWatchFileStat
);

function onWatchFileStat(watchedFile: WatchedFile, pollIndex: number, fileChanged: boolean) {
function onWatchFileStat(watchedFile: WatchedFileWithUnchangedPolls, pollIndex: number, fileChanged: boolean) {
if (fileChanged) {
watchedFile.unchangedPolls = 0;
// Changed files go to changedFilesInLastPoll queue
Expand Down Expand Up @@ -311,12 +306,12 @@ namespace ts {
}
}

function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) {
function addToPollingIntervalQueue(file: WatchedFileWithUnchangedPolls, pollingInterval: PollingInterval) {
pollingIntervalQueue(pollingInterval).push(file);
scheduleNextPollIfNotAlreadyScheduled(pollingInterval);
}

function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) {
function addChangedFileToLowPollingIntervalQueue(file: WatchedFileWithUnchangedPolls) {
changedFilesInLastPoll.push(file);
scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low);
}
Expand Down Expand Up @@ -423,59 +418,50 @@ namespace ts {
}
}

/* @internal */
export function createSingleFileWatcherPerName(
watchFile: HostWatchFile,
useCaseSensitiveFileNames: boolean
): HostWatchFile {
interface SingleFileWatcher {
watcher: FileWatcher;
refCount: number;
}
const cache = new Map<string, SingleFileWatcher>();
const callbacksCache = createMultiMap<FileWatcherCallback>();
interface SingleFileWatcher<T extends FileWatcherCallback | FsWatchCallback>{
watcher: FileWatcher;
callbacks: T[];
}
function createSingleWatcherPerName<T extends FileWatcherCallback | FsWatchCallback>(
cache: Map<SingleFileWatcher<T>>,
useCaseSensitiveFileNames: boolean,
name: string,
callback: T,
createWatcher: (callback: T) => FileWatcher,
): FileWatcher {
const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
const path = toCanonicalFileName(name);
const existing = cache.get(path);
if (existing) {
existing.callbacks.push(callback);
}
else {
cache.set(path, {
watcher: createWatcher((
// Cant infer types correctly so lets satisfy checker
(param1: any, param2: never, param3: any) => cache.get(path)?.callbacks.slice().forEach(cb => cb(param1, param2, param3))
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
) as T),
callbacks: [callback]
});
}

return (fileName, callback, pollingInterval, options) => {
const path = toCanonicalFileName(fileName);
const existing = cache.get(path);
if (existing) {
existing.refCount++;
}
else {
cache.set(path, {
watcher: watchFile(
fileName,
(fileName, eventKind, modifiedTime) => forEach(
callbacksCache.get(path),
cb => cb(fileName, eventKind, modifiedTime)
),
pollingInterval,
options
),
refCount: 1
});
return {
close: () => {
const watcher = cache.get(path);
// Watcher is not expected to be undefined, but if it is normally its because
// exception was thrown somewhere else and watch state is not what it should be
if (!watcher) return;
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
if (!orderedRemoveItem(watcher.callbacks, callback) || watcher.callbacks.length) return;
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
cache.delete(path);
closeFileWatcherOf(watcher);
}
callbacksCache.add(path, callback);

return {
close: () => {
const watcher = Debug.checkDefined(cache.get(path));
callbacksCache.remove(path, callback);
watcher.refCount--;
if (watcher.refCount) return;
cache.delete(path);
closeFileWatcherOf(watcher);
}
};
};
}

/**
* Returns true if file status changed
*/
/*@internal*/
export function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean {
function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean {
const oldTime = watchedFile.mtime.getTime();
const newTime = modifiedTime.getTime();
if (oldTime !== newTime) {
Expand Down Expand Up @@ -512,8 +498,7 @@ namespace ts {
curSysLog = logger;
}

/*@internal*/
export interface RecursiveDirectoryWatcherHost {
interface RecursiveDirectoryWatcherHost {
watchDirectory: HostWatchDirectory;
useCaseSensitiveFileNames: boolean;
getCurrentDirectory: System["getCurrentDirectory"];
Expand All @@ -529,8 +514,7 @@ namespace ts {
* that means if this is recursive watcher, watch the children directories as well
* (eg on OS that dont support recursive watch using fs.watch use fs.watchFile)
*/
/*@internal*/
export function createDirectoryWatcherSupportingRecursive({
function createDirectoryWatcherSupportingRecursive({
watchDirectory,
useCaseSensitiveFileNames,
getCurrentDirectory,
Expand Down Expand Up @@ -792,8 +776,7 @@ namespace ts {
Directory,
}

/*@internal*/
export function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback {
function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback {
return (_fileName, eventKind, modifiedTime) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", "", modifiedTime);
}

Expand Down Expand Up @@ -854,7 +837,7 @@ namespace ts {
/*@internal*/
export interface CreateSystemWatchFunctions {
// Polling watch file
pollingWatchFile: HostWatchFile;
pollingWatchFileWorker: HostWatchFile;
// For dynamic polling watch file
getModifiedTime: NonNullable<System["getModifiedTime"]>;
setTimeout: NonNullable<System["setTimeout"]>;
Expand All @@ -878,7 +861,7 @@ namespace ts {

/*@internal*/
export function createSystemWatchFunctions({
pollingWatchFile,
pollingWatchFileWorker,
getModifiedTime,
setTimeout,
clearTimeout,
Expand All @@ -896,6 +879,9 @@ namespace ts {
inodeWatching,
sysLog,
}: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } {
const pollingWatches = new Map<string, SingleFileWatcher<FileWatcherCallback>>();
const fsWatches = new Map<string, SingleFileWatcher<FsWatchCallback>>();
const fsWatchesRecursive = new Map<string, SingleFileWatcher<FsWatchCallback>>();
let dynamicPollingWatchFile: HostWatchFile | undefined;
let fixedChunkSizePollingWatchFile: HostWatchFile | undefined;
let nonPollingWatchFile: HostWatchFile | undefined;
Expand Down Expand Up @@ -968,7 +954,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: defaultWatchFileKind?.() || WatchFileKind.FixedPollingInterval };
{ watchFile: defaultWatchFileKind?.() || WatchFileKind.UseFsEvents };
}
}

Expand Down Expand Up @@ -1073,13 +1059,39 @@ namespace ts {
}
}

function pollingWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) {
return createSingleWatcherPerName(
pollingWatches,
useCaseSensitiveFileNames,
fileName,
callback,
cb => pollingWatchFileWorker(fileName, cb, pollingInterval, options),
);
}
function fsWatch(
fileOrDirectory: string,
entryKind: FileSystemEntryKind,
callback: FsWatchCallback,
recursive: boolean,
fallbackPollingInterval: PollingInterval,
fallbackOptions: WatchOptions | undefined
): FileWatcher {
return createSingleWatcherPerName(
recursive ? fsWatchesRecursive : fsWatches,
useCaseSensitiveFileNames,
fileOrDirectory,
callback,
cb => fsWatchHandlingExistenceOnHost(fileOrDirectory, entryKind, cb, recursive, fallbackPollingInterval, fallbackOptions),
);
}

function fsWatchHandlingExistenceOnHost(
fileOrDirectory: string,
entryKind: FileSystemEntryKind,
callback: FsWatchCallback,
recursive: boolean,
fallbackPollingInterval: PollingInterval,
fallbackOptions: WatchOptions | undefined
): FileWatcher {
let lastDirectoryPartWithDirectorySeparator: string | undefined;
let lastDirectoryPart: string | undefined;
Expand Down Expand Up @@ -1445,7 +1457,7 @@ namespace ts {
const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin");
const getCurrentDirectory = memoize(() => process.cwd());
const { watchFile, watchDirectory } = createSystemWatchFunctions({
pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames),
pollingWatchFileWorker: fsWatchFileWorker,
getModifiedTime,
setTimeout,
clearTimeout,
Expand Down
Loading