Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
180 changes: 168 additions & 12 deletions src/compiler/resolutionCache.ts

Large diffs are not rendered by default.

93 changes: 70 additions & 23 deletions src/compiler/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getDirectoryPath,
getFallbackOptions,
getNormalizedAbsolutePath,
getRelativePathFromDirectory,
getRelativePathToDirectoryOrUrl,
getRootLength,
getStringComparer,
Expand Down Expand Up @@ -607,15 +608,17 @@ function createDirectoryWatcherSupportingRecursive({
watcher: FileWatcher;
childWatches: ChildWatches;
refCount: number;
targetWatcher: ChildDirectoryWatcher | undefined;
links: Set<string> | undefined;
}

const cache = new Map<string, HostDirectoryWatcher>();
const cache = new Map<Path, HostDirectoryWatcher>();
const callbackCache = createMultiMap<Path, { dirName: string; callback: DirectoryWatcherCallback; }>();
const cacheToUpdateChildWatches = new Map<Path, { dirName: string; options: WatchOptions | undefined; fileNames: string[]; }>();
let timerToUpdateChildWatches: any;

const filePathComparer = getStringComparer(!useCaseSensitiveFileNames);
const toCanonicalFilePath = createGetCanonicalFileName(useCaseSensitiveFileNames);
const toCanonicalFilePath = createGetCanonicalFileName(useCaseSensitiveFileNames) as (fileName: string) => Path;

return (dirName, callback, recursive, options) =>
recursive ?
Expand All @@ -625,8 +628,13 @@ function createDirectoryWatcherSupportingRecursive({
/**
* Create the directory watcher for the dirPath.
*/
function createDirectoryWatcher(dirName: string, options: WatchOptions | undefined, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher {
const dirPath = toCanonicalFilePath(dirName) as Path;
function createDirectoryWatcher(
dirName: string,
options: WatchOptions | undefined,
callback?: DirectoryWatcherCallback,
link?: string,
): ChildDirectoryWatcher {
const dirPath = toCanonicalFilePath(dirName);
let directoryWatcher = cache.get(dirPath);
if (directoryWatcher) {
directoryWatcher.refCount++;
Expand All @@ -640,7 +648,7 @@ function createDirectoryWatcherSupportingRecursive({

if (options?.synchronousWatchDirectory) {
// Call the actual callback
invokeCallbacks(dirPath, fileName);
if (!cache.get(dirPath)?.targetWatcher) invokeCallbacks(dirName, dirPath, fileName);

// Iterate through existing children and update the watches if needed
updateChildWatches(dirName, dirPath, options);
Expand All @@ -654,11 +662,15 @@ function createDirectoryWatcherSupportingRecursive({
),
refCount: 1,
childWatches: emptyArray,
targetWatcher: undefined,
links: undefined,
};
cache.set(dirPath, directoryWatcher);
updateChildWatches(dirName, dirPath, options);
}

if (link) (directoryWatcher.links ??= new Set()).add(link);

const callbackToAdd = callback && { dirName, callback };
if (callbackToAdd) {
callbackCache.add(dirPath, callbackToAdd);
Expand All @@ -669,21 +681,24 @@ function createDirectoryWatcherSupportingRecursive({
close: () => {
const directoryWatcher = Debug.checkDefined(cache.get(dirPath));
if (callbackToAdd) callbackCache.remove(dirPath, callbackToAdd);
if (link) directoryWatcher.links?.delete(link);
directoryWatcher.refCount--;

if (directoryWatcher.refCount) return;

cache.delete(dirPath);
directoryWatcher.links = undefined;
closeFileWatcherOf(directoryWatcher);
closeTargetWatcher(directoryWatcher);
directoryWatcher.childWatches.forEach(closeFileWatcher);
},
};
}

type InvokeMap = Map<Path, string[] | true>;
function invokeCallbacks(dirPath: Path, fileName: string): void;
function invokeCallbacks(dirPath: Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void;
function invokeCallbacks(dirPath: Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) {
function invokeCallbacks(dirName: string, dirPath: Path, fileName: string): void;
function invokeCallbacks(dirName: string, dirPath: Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void;
function invokeCallbacks(dirName: string, dirPath: Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) {
let fileName: string | undefined;
let invokeMap: InvokeMap | undefined;
if (isString(fileNameOrInvokeMap)) {
Expand Down Expand Up @@ -715,6 +730,15 @@ function createDirectoryWatcherSupportingRecursive({
}
}
});
cache.get(dirPath)?.links?.forEach(link => {
const toPathInLink = (fileName: string) => combinePaths(link, getRelativePathFromDirectory(dirName, fileName, toCanonicalFilePath));
if (invokeMap) {
invokeCallbacks(link, toCanonicalFilePath(link), invokeMap, fileNames?.map(toPathInLink));
}
else {
invokeCallbacks(link, toCanonicalFilePath(link), toPathInLink(fileName!));
}
});
}

function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) {
Expand All @@ -727,7 +751,8 @@ function createDirectoryWatcherSupportingRecursive({
}

// Call the actual callbacks and remove child watches
invokeCallbacks(dirPath, fileName);
invokeCallbacks(dirName, dirPath, fileName);
closeTargetWatcher(parentWatcher);
removeChildWatches(parentWatcher);
}

Expand Down Expand Up @@ -760,7 +785,7 @@ function createDirectoryWatcherSupportingRecursive({
// Because the child refresh is fresh, we would need to invalidate whole root directory being watched
// to ensure that all the changes are reflected at this time
const hasChanges = updateChildWatches(dirName, dirPath, options);
invokeCallbacks(dirPath, invokeMap, hasChanges ? undefined : fileNames);
if (!cache.get(dirPath)?.targetWatcher) invokeCallbacks(dirName, dirPath, invokeMap, hasChanges ? undefined : fileNames);
}

sysLog(`sysLog:: invokingWatchers:: Elapsed:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`);
Expand Down Expand Up @@ -792,24 +817,46 @@ function createDirectoryWatcherSupportingRecursive({
}
}

function closeTargetWatcher(watcher: HostDirectoryWatcher | undefined) {
if (watcher?.targetWatcher) {
watcher.targetWatcher.close();
watcher.targetWatcher = undefined;
}
}

function updateChildWatches(parentDir: string, parentDirPath: Path, options: WatchOptions | undefined) {
// Iterate through existing children and update the watches if needed
const parentWatcher = cache.get(parentDirPath);
if (!parentWatcher) return false;
const target = normalizePath(realpath(parentDir));
let hasChanges;
let newChildWatches: ChildDirectoryWatcher[] | undefined;
const hasChanges = enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
fileSystemEntryExists(parentDir, FileSystemEntryKind.Directory) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => {
const childFullName = getNormalizedAbsolutePath(child, parentDir);
// Filter our the symbolic link directories since those arent included in recursive watch
// which is same behaviour when recursive: true is passed to fs.watch
return !isIgnoredPath(childFullName, options) && filePathComparer(childFullName, normalizePath(realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
}) : emptyArray,
parentWatcher.childWatches,
(child, childWatcher) => filePathComparer(child, childWatcher.dirName),
createAndAddChildDirectoryWatcher,
closeFileWatcher,
addChildDirectoryWatcher,
);
if (filePathComparer(target, parentDir) === Comparison.EqualTo) {
// if (parentWatcher.target) closeFileWatcher
hasChanges = enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
fileSystemEntryExists(parentDir, FileSystemEntryKind.Directory) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => {
const childFullName = getNormalizedAbsolutePath(child, parentDir);
// Filter our the symbolic link directories since those arent included in recursive watch
// which is same behaviour when recursive: true is passed to fs.watch
return !isIgnoredPath(childFullName, options) && filePathComparer(childFullName, normalizePath(realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
}) : emptyArray,
parentWatcher.childWatches,
(child, childWatcher) => filePathComparer(child, childWatcher.dirName),
createAndAddChildDirectoryWatcher,
closeFileWatcher,
addChildDirectoryWatcher,
);
}
else if (parentWatcher.targetWatcher && filePathComparer(target, parentWatcher.targetWatcher.dirName) === Comparison.EqualTo) {
hasChanges = false;
Debug.assert(parentWatcher.childWatches === emptyArray);
}
else {
closeTargetWatcher(parentWatcher);
parentWatcher.targetWatcher = createDirectoryWatcher(target, options, /*callback*/ undefined, parentDir);
parentWatcher.childWatches.forEach(closeFileWatcher);
hasChanges = true;
}
parentWatcher.childWatches = newChildWatches || emptyArray;
return hasChanges;

Expand Down
52 changes: 47 additions & 5 deletions src/harness/incrementalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ export function verifyResolutionCache(
verifyResolutionSet(expected.resolutionsWithOnlyAffectingLocations, actual.resolutionsWithOnlyAffectingLocations, `resolutionsWithOnlyAffectingLocations`);
verifyDirectoryWatchesOfFailedLookups(expected.directoryWatchesOfFailedLookups, actual.directoryWatchesOfFailedLookups);
verifyFileWatchesOfAffectingLocations(expected.fileWatchesOfAffectingLocations, actual.fileWatchesOfAffectingLocations);
verifyPackageDirWatchers(expected.packageDirWatchers, actual.packageDirWatchers);
verifyDirPathToSymlinkPackageRefCount(expected.dirPathToSymlinkPackageRefCount, actual.dirPathToSymlinkPackageRefCount);

// Stop watching resolutions to verify everything gets closed.
expected.startCachingPerDirectoryResolution();
Expand Down Expand Up @@ -368,11 +370,17 @@ export function verifyResolutionCache(
}

function verifyDirectoryWatchesOfFailedLookups(expected: Map<string, ts.DirectoryWatchesOfFailedLookup>, actual: Map<string, ts.DirectoryWatchesOfFailedLookup>) {
verifyMap(expected, actual, (expected, actual, caption) => {
ts.Debug.assert(expected?.refCount === actual?.refCount, `${projectName}:: ${caption}:: refCount`);
ts.Debug.assert(!!expected?.refCount, `${projectName}:: ${caption}:: expected refCount to be non zero`);
ts.Debug.assert(expected?.nonRecursive === actual?.nonRecursive, `${projectName}:: ${caption}:: nonRecursive`);
}, "directoryWatchesOfFailedLookups");
verifyMap(expected, actual, verifyDirectoryWatchesOfFailedLookup, "directoryWatchesOfFailedLookups");
}

function verifyDirectoryWatchesOfFailedLookup(
expected: ts.DirectoryWatchesOfFailedLookup | undefined,
actual: ts.DirectoryWatchesOfFailedLookup | undefined,
caption: string,
) {
ts.Debug.assert(expected?.refCount === actual?.refCount, `${projectName}:: ${caption}:: refCount`);
ts.Debug.assert(!!expected?.refCount, `${projectName}:: ${caption}:: expected refCount to be non zero`);
ts.Debug.assert(expected?.nonRecursive === actual?.nonRecursive, `${projectName}:: ${caption}:: nonRecursive`);
}

function verifyFileWatchesOfAffectingLocations(
Expand All @@ -391,6 +399,40 @@ export function verifyResolutionCache(
ts.Debug.assert(expected?.files === actual?.files, `${projectName}:: ${caption}:: files`);
verifySet(expected?.symlinks, actual?.symlinks, `${projectName}:: ${caption}:: symlinks`);
}

function verifyPackageDirWatchers(
expected: Map<ts.Path, ts.PackageDirWatcher>,
actual: Map<ts.Path, ts.PackageDirWatcher>,
) {
verifyMap(expected, actual, verifyPackageDirWatcher, "packageDirWatchers");
}

function verifyPackageDirWatcher(
expected: ts.PackageDirWatcher | undefined,
actual: ts.PackageDirWatcher | undefined,
caption: string,
) {
ts.Debug.assert(expected?.isSymlink === actual?.isSymlink, `${projectName}:: ${caption}:: isSymlink`);
verifyMap(expected?.dirPathToWatcher, actual?.dirPathToWatcher, verfiyDirPathToWatcherOfPackageDirWatcher, `${projectName}:: ${caption}:: dirPathToWatcher`);
}

function verfiyDirPathToWatcherOfPackageDirWatcher(
expected: ts.DirPathToWatcherOfPackageDirWatcher | undefined,
actual: ts.DirPathToWatcherOfPackageDirWatcher | undefined,
caption: string,
) {
ts.Debug.assert(expected?.refCount === actual?.refCount, `${projectName}:: ${caption}:: refCount`);
verifyDirectoryWatchesOfFailedLookup(expected?.watcher, actual?.watcher, `${projectName}:: ${caption}:: directoryWatchesOfFailedLookup`);
}

function verifyDirPathToSymlinkPackageRefCount(
expected: Map<ts.Path, number>,
actual: Map<ts.Path, number>,
) {
verifyMap(expected, actual, (expected, actual, caption) => {
ts.Debug.assert(expected === actual, `${projectName}:: ${caption}`);
}, "dirPathToSymlinkPackageRefCount");
}
}

function verifyMap<Key extends string, Expected, Actual>(
Expand Down
Loading