Skip to content

Commit 67f9533

Browse files
committed
Limit the resolution invalidation to max number of files as invalidation for larger cache might take more time than to just recalculate resolutions
1 parent b179cd1 commit 67f9533

File tree

6 files changed

+211
-112
lines changed

6 files changed

+211
-112
lines changed

src/compiler/resolutionCache.ts

Lines changed: 104 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace ts {
1212
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
1313

1414
invalidateResolutionOfFile(filePath: Path): void;
15+
removeResolutionsOfFile(filePath: Path): void;
1516
createHasInvalidatedResolution(): HasInvalidatedResolution;
1617

1718
startCachingPerDirectoryResolution(): void;
@@ -25,7 +26,7 @@ namespace ts {
2526
clear(): void;
2627
}
2728

28-
interface NameResolutionWithFailedLookupLocations {
29+
interface ResolutionWithFailedLookupLocations {
2930
readonly failedLookupLocations: ReadonlyArray<string>;
3031
isInvalidated?: boolean;
3132
}
@@ -45,6 +46,7 @@ namespace ts {
4546
projectName?: string;
4647
getGlobalCache?(): string | undefined;
4748
writeLog(s: string): void;
49+
maxNumberOfFilesToIterateForInvalidation?: number;
4850
}
4951

5052
interface DirectoryWatchesOfFailedLookup {
@@ -54,9 +56,17 @@ namespace ts {
5456
refCount: number;
5557
}
5658

59+
/*@internal*/
60+
export const maxNumberOfFilesToIterateForInvalidation = 256;
61+
62+
interface GetResolutionWithResolvedFileName<T extends ResolutionWithFailedLookupLocations = ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName = ResolutionWithResolvedFileName> {
63+
(resolution: T): R;
64+
}
65+
5766
export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache {
5867
let filesWithChangedSetOfUnresolvedImports: Path[] | undefined;
5968
let filesWithInvalidatedResolutions: Map<true> | undefined;
69+
let allFilesHaveInvalidatedResolution = false;
6070

6171
// The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file.
6272
// The key in the map is source file's path.
@@ -86,6 +96,7 @@ namespace ts {
8696
finishCachingPerDirectoryResolution,
8797
resolveModuleNames,
8898
resolveTypeReferenceDirectives,
99+
removeResolutionsOfFile,
89100
invalidateResolutionOfFile,
90101
createHasInvalidatedResolution,
91102
setRootDirectory,
@@ -121,6 +132,7 @@ namespace ts {
121132
closeTypeRootsWatch();
122133
resolvedModuleNames.clear();
123134
resolvedTypeReferenceDirectives.clear();
135+
allFilesHaveInvalidatedResolution = false;
124136
Debug.assert(perDirectoryResolvedModuleNames.size === 0 && perDirectoryResolvedTypeReferenceDirectives.size === 0);
125137
}
126138

@@ -135,6 +147,11 @@ namespace ts {
135147
}
136148

137149
function createHasInvalidatedResolution(): HasInvalidatedResolution {
150+
if (allFilesHaveInvalidatedResolution) {
151+
// Any file asked would have invalidated resolution
152+
filesWithInvalidatedResolutions = undefined;
153+
return returnTrue;
154+
}
138155
const collected = filesWithInvalidatedResolutions;
139156
filesWithInvalidatedResolutions = undefined;
140157
return path => collected && collected.has(path);
@@ -145,6 +162,7 @@ namespace ts {
145162
}
146163

147164
function finishCachingPerDirectoryResolution() {
165+
allFilesHaveInvalidatedResolution = false;
148166
directoryWatchesOfFailedLookups.forEach((watcher, path) => {
149167
if (watcher.refCount === 0) {
150168
directoryWatchesOfFailedLookups.delete(path);
@@ -178,13 +196,13 @@ namespace ts {
178196
return primaryResult;
179197
}
180198

181-
function resolveNamesWithLocalCache<T extends NameResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>(
199+
function resolveNamesWithLocalCache<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>(
182200
names: string[],
183201
containingFile: string,
184202
cache: Map<Map<T>>,
185203
perDirectoryCache: Map<Map<T>>,
186204
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
187-
getResolutionFromNameResolutionWithFailedLookupLocations: (s: T) => R,
205+
getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>,
188206
logChanges: boolean): R[] {
189207

190208
const path = resolutionHost.toPath(containingFile);
@@ -203,7 +221,7 @@ namespace ts {
203221
for (const name of names) {
204222
let resolution = resolutionsInFile.get(name);
205223
// Resolution is valid if it is present and not invalidated
206-
if (!resolution || resolution.isInvalidated) {
224+
if (allFilesHaveInvalidatedResolution || !resolution || resolution.isInvalidated) {
207225
const existingResolution = resolution;
208226
const resolutionInDirectory = perDirectoryResolution.get(name);
209227
if (resolutionInDirectory) {
@@ -225,7 +243,7 @@ namespace ts {
225243
}
226244
Debug.assert(resolution !== undefined && !resolution.isInvalidated);
227245
seenNamesInFile.set(name, true);
228-
resolvedModules.push(getResolutionFromNameResolutionWithFailedLookupLocations(resolution));
246+
resolvedModules.push(getResolutionWithResolvedFileName(resolution));
229247
}
230248

231249
// Stop watching and remove the unused name
@@ -245,8 +263,8 @@ namespace ts {
245263
if (!oldResolution || !newResolution || oldResolution.isInvalidated) {
246264
return false;
247265
}
248-
const oldResult = getResolutionFromNameResolutionWithFailedLookupLocations(oldResolution);
249-
const newResult = getResolutionFromNameResolutionWithFailedLookupLocations(newResolution);
266+
const oldResult = getResolutionWithResolvedFileName(oldResolution);
267+
const newResult = getResolutionWithResolvedFileName(newResolution);
250268
if (oldResult === newResult) {
251269
return true;
252270
}
@@ -321,9 +339,7 @@ namespace ts {
321339
return fileExtensionIsOneOf(path, failedLookupDefaultExtensions);
322340
}
323341

324-
function watchFailedLookupLocationOfResolution<T extends NameResolutionWithFailedLookupLocations>(
325-
resolution: T, startIndex?: number
326-
) {
342+
function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations, startIndex?: number) {
327343
if (resolution && resolution.failedLookupLocations) {
328344
for (let i = startIndex || 0; i < resolution.failedLookupLocations.length; i++) {
329345
const failedLookupLocation = resolution.failedLookupLocations[i];
@@ -346,15 +362,11 @@ namespace ts {
346362
}
347363
}
348364

349-
function stopWatchFailedLookupLocationOfResolution<T extends NameResolutionWithFailedLookupLocations>(
350-
resolution: T
351-
) {
365+
function stopWatchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) {
352366
stopWatchFailedLookupLocationOfResolutionFrom(resolution, 0);
353367
}
354368

355-
function stopWatchFailedLookupLocationOfResolutionFrom<T extends NameResolutionWithFailedLookupLocations>(
356-
resolution: T, startIndex: number
357-
) {
369+
function stopWatchFailedLookupLocationOfResolutionFrom(resolution: ResolutionWithFailedLookupLocations, startIndex: number) {
358370
if (resolution && resolution.failedLookupLocations) {
359371
for (let i = startIndex; i < resolution.failedLookupLocations.length; i++) {
360372
const failedLookupLocation = resolution.failedLookupLocations[i];
@@ -387,107 +399,106 @@ namespace ts {
387399
// If the files are added to project root or node_modules directory, always run through the invalidation process
388400
// Otherwise run through invalidation only if adding to the immediate directory
389401
if (dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrFolderPath) === dirPath) {
390-
let isChangedFailedLookupLocation: (location: string) => boolean;
391-
if (dirPath === fileOrFolderPath) {
392-
// Watching directory is created
393-
// Invalidate any resolution has failed lookup in this directory
394-
isChangedFailedLookupLocation = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location));
395-
}
396-
else {
397-
// Some file or folder in the watching directory is created
398-
// Return early if it does not have any of the watching extension or not the custom failed lookup path
399-
if (!isPathWithDefaultFailedLookupExtension(fileOrFolderPath) && !customFailedLookupPaths.has(fileOrFolderPath)) {
400-
return;
401-
}
402-
// Resolution need to be invalidated if failed lookup location is same as the file or folder getting created
403-
isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrFolderPath;
404-
}
405-
const hasChangedFailedLookupLocation = (resolution: NameResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation);
406-
if (invalidateResolutionOfFailedLookupLocation(hasChangedFailedLookupLocation)) {
402+
if (invalidateResolutionOfFailedLookupLocation(fileOrFolderPath, dirPath === fileOrFolderPath)) {
407403
resolutionHost.onInvalidatedResolution();
408404
}
409405
}
410406
}, WatchDirectoryFlags.Recursive);
411407
}
412408

413-
function invalidateResolutionCache<T extends NameResolutionWithFailedLookupLocations>(
409+
function removeResolutionsOfFileFromCache(cache: Map<Map<ResolutionWithFailedLookupLocations>>, filePath: Path) {
410+
// Deleted file, stop watching failed lookups for all the resolutions in the file
411+
const resolutions = cache.get(filePath);
412+
if (resolutions) {
413+
resolutions.forEach(stopWatchFailedLookupLocationOfResolution);
414+
cache.delete(filePath);
415+
}
416+
}
417+
418+
function removeResolutionsOfFile(filePath: Path) {
419+
removeResolutionsOfFileFromCache(resolvedModuleNames, filePath);
420+
removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath);
421+
}
422+
423+
function invalidateResolutionCache<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>(
414424
cache: Map<Map<T>>,
415-
ignoreFile: (resolutions: Map<T>, containingFilePath: Path) => boolean,
416-
isInvalidatedResolution: (resolution: T) => boolean
425+
isInvalidatedResolution: (resolution: T, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>) => boolean,
426+
getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>
417427
) {
418428
const seen = createMap<Map<true>>();
419429
cache.forEach((resolutions, containingFilePath) => {
420-
if (!ignoreFile(resolutions, containingFilePath as Path) && resolutions) {
421-
const dirPath = getDirectoryPath(containingFilePath);
422-
let seenInDir = seen.get(dirPath);
423-
if (!seenInDir) {
424-
seenInDir = createMap<true>();
425-
seen.set(dirPath, seenInDir);
426-
}
427-
resolutions.forEach((resolution, name) => {
428-
if (seenInDir.has(name)) {
429-
return;
430-
}
431-
seenInDir.set(name, true);
432-
if (!resolution.isInvalidated && isInvalidatedResolution(resolution)) {
433-
// Mark the file as needing re-evaluation of module resolution instead of using it blindly.
434-
resolution.isInvalidated = true;
435-
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(containingFilePath, true);
436-
}
437-
});
430+
const dirPath = getDirectoryPath(containingFilePath);
431+
let seenInDir = seen.get(dirPath);
432+
if (!seenInDir) {
433+
seenInDir = createMap<true>();
434+
seen.set(dirPath, seenInDir);
438435
}
436+
resolutions.forEach((resolution, name) => {
437+
if (seenInDir.has(name)) {
438+
return;
439+
}
440+
seenInDir.set(name, true);
441+
if (!resolution.isInvalidated && isInvalidatedResolution(resolution, getResolutionWithResolvedFileName)) {
442+
// Mark the file as needing re-evaluation of module resolution instead of using it blindly.
443+
resolution.isInvalidated = true;
444+
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(containingFilePath, true);
445+
}
446+
});
439447
});
440448
}
441449

442-
function invalidateResolutionCacheOfDeletedFile<T extends NameResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>(
443-
deletedFilePath: Path,
444-
cache: Map<Map<T>>,
445-
getResolutionFromNameResolutionWithFailedLookupLocations: (s: T) => R,
450+
function hasReachedResolutionIterationLimit() {
451+
const maxSize = resolutionHost.maxNumberOfFilesToIterateForInvalidation || maxNumberOfFilesToIterateForInvalidation;
452+
return resolvedModuleNames.size > maxSize || resolvedTypeReferenceDirectives.size > maxSize;
453+
}
454+
455+
function invalidateResolutions(
456+
isInvalidatedResolution: (resolution: ResolutionWithFailedLookupLocations, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) => boolean,
446457
) {
447-
invalidateResolutionCache(
448-
cache,
449-
// Ignore file thats same as deleted file path, and handle it here
450-
(resolutions, containingFilePath) => {
451-
if (containingFilePath !== deletedFilePath) {
452-
return false;
453-
}
458+
// If more than maxNumberOfFilesToIterateForInvalidation present,
459+
// just invalidated all files and recalculate the resolutions for files instead
460+
if (hasReachedResolutionIterationLimit()) {
461+
allFilesHaveInvalidatedResolution = true;
462+
return;
463+
}
464+
invalidateResolutionCache(resolvedModuleNames, isInvalidatedResolution, getResolvedModule);
465+
invalidateResolutionCache(resolvedTypeReferenceDirectives, isInvalidatedResolution, getResolvedTypeReferenceDirective);
466+
}
454467

455-
// Deleted file, stop watching failed lookups for all the resolutions in the file
456-
cache.delete(containingFilePath);
457-
resolutions.forEach(stopWatchFailedLookupLocationOfResolution);
458-
return true;
459-
},
468+
function invalidateResolutionOfFile(filePath: Path) {
469+
removeResolutionsOfFile(filePath);
470+
invalidateResolutions(
460471
// Resolution is invalidated if the resulting file name is same as the deleted file path
461-
resolution => {
462-
const result = getResolutionFromNameResolutionWithFailedLookupLocations(resolution);
463-
return result && resolutionHost.toPath(result.resolvedFileName) === deletedFilePath;
472+
(resolution, getResolutionWithResolvedFileName) => {
473+
const result = getResolutionWithResolvedFileName(resolution);
474+
return result && resolutionHost.toPath(result.resolvedFileName) === filePath;
464475
}
465476
);
466477
}
467478

468-
function invalidateResolutionOfFile(filePath: Path) {
469-
invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, getResolvedModule);
470-
invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, getResolvedTypeReferenceDirective);
471-
}
472-
473-
function invalidateResolutionCacheOfFailedLookupLocation<T extends NameResolutionWithFailedLookupLocations>(
474-
cache: Map<Map<T>>,
475-
hasChangedFailedLookupLocation: (resolution: T) => boolean
476-
) {
477-
invalidateResolutionCache(
478-
cache,
479-
// Do not ignore any file
480-
returnFalse,
479+
function invalidateResolutionOfFailedLookupLocation(fileOrFolderPath: Path, isCreatingWatchedDirectory: boolean) {
480+
let isChangedFailedLookupLocation: (location: string) => boolean;
481+
if (isCreatingWatchedDirectory) {
482+
// Watching directory is created
483+
// Invalidate any resolution has failed lookup in this directory
484+
isChangedFailedLookupLocation = location => isInDirectoryPath(fileOrFolderPath, resolutionHost.toPath(location));
485+
}
486+
else {
487+
// Some file or folder in the watching directory is created
488+
// Return early if it does not have any of the watching extension or not the custom failed lookup path
489+
if (!isPathWithDefaultFailedLookupExtension(fileOrFolderPath) && !customFailedLookupPaths.has(fileOrFolderPath)) {
490+
return false;
491+
}
492+
// Resolution need to be invalidated if failed lookup location is same as the file or folder getting created
493+
isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrFolderPath;
494+
}
495+
const hasChangedFailedLookupLocation = (resolution: ResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation);
496+
const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size;
497+
invalidateResolutions(
481498
// Resolution is invalidated if the resulting file name is same as the deleted file path
482499
hasChangedFailedLookupLocation
483500
);
484-
}
485-
486-
function invalidateResolutionOfFailedLookupLocation(hasChangedFailedLookupLocation: (resolution: NameResolutionWithFailedLookupLocations) => boolean) {
487-
const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size;
488-
invalidateResolutionCacheOfFailedLookupLocation(resolvedModuleNames, hasChangedFailedLookupLocation);
489-
invalidateResolutionCacheOfFailedLookupLocation(resolvedTypeReferenceDirectives, hasChangedFailedLookupLocation);
490-
return filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount;
501+
return allFilesHaveInvalidatedResolution || filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount;
491502
}
492503

493504
function closeTypeRootsWatch() {

src/compiler/watchedProgram.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ namespace ts {
501501
}
502502
else if (hostSourceFileInfo.sourceFile === oldSourceFile) {
503503
sourceFilesCache.delete(oldSourceFile.path);
504-
resolutionCache.invalidateResolutionOfFile(oldSourceFile.path);
504+
resolutionCache.removeResolutionsOfFile(oldSourceFile.path);
505505
}
506506
}
507507
}

0 commit comments

Comments
 (0)