Skip to content

Commit 1da18c6

Browse files
authored
Refactor export map cache to not store transient symbols (#44816)
* Add some failing tests around transient symbols * Working, but slower * A class is much faster, apparently * This is probably best? * Back to multimap * Go back to single symbol cache * Revert now-unnecessary generics * Rename and reorganize * Fix weird compound condition * Clean up
1 parent fc5c765 commit 1da18c6

File tree

11 files changed

+641
-491
lines changed

11 files changed

+641
-491
lines changed

src/server/moduleSpecifierCache.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*@internal*/
2+
namespace ts.server {
3+
export interface ModuleSpecifierResolutionCacheHost {
4+
watchNodeModulesForPackageJsonChanges(directoryPath: string): FileWatcher;
5+
}
6+
7+
export function createModuleSpecifierCache(host: ModuleSpecifierResolutionCacheHost): ModuleSpecifierCache {
8+
let containedNodeModulesWatchers: ESMap<string, FileWatcher> | undefined;
9+
let cache: ESMap<Path, ResolvedModuleSpecifierInfo> | undefined;
10+
let currentKey: string | undefined;
11+
const result: ModuleSpecifierCache = {
12+
get(fromFileName, toFileName, preferences) {
13+
if (!cache || currentKey !== key(fromFileName, preferences)) return undefined;
14+
return cache.get(toFileName);
15+
},
16+
set(fromFileName, toFileName, preferences, modulePaths, moduleSpecifiers) {
17+
ensureCache(fromFileName, preferences).set(toFileName, createInfo(modulePaths, moduleSpecifiers, /*isAutoImportable*/ true));
18+
19+
// If any module specifiers were generated based off paths in node_modules,
20+
// a package.json file in that package was read and is an input to the cached.
21+
// Instead of watching each individual package.json file, set up a wildcard
22+
// directory watcher for any node_modules referenced and clear the cache when
23+
// it sees any changes.
24+
if (moduleSpecifiers) {
25+
for (const p of modulePaths) {
26+
if (p.isInNodeModules) {
27+
// No trailing slash
28+
const nodeModulesPath = p.path.substring(0, p.path.indexOf(nodeModulesPathPart) + nodeModulesPathPart.length - 1);
29+
if (!containedNodeModulesWatchers?.has(nodeModulesPath)) {
30+
(containedNodeModulesWatchers ||= new Map()).set(
31+
nodeModulesPath,
32+
host.watchNodeModulesForPackageJsonChanges(nodeModulesPath),
33+
);
34+
}
35+
}
36+
}
37+
}
38+
},
39+
setModulePaths(fromFileName, toFileName, preferences, modulePaths) {
40+
const cache = ensureCache(fromFileName, preferences);
41+
const info = cache.get(toFileName);
42+
if (info) {
43+
info.modulePaths = modulePaths;
44+
}
45+
else {
46+
cache.set(toFileName, createInfo(modulePaths, /*moduleSpecifiers*/ undefined, /*isAutoImportable*/ undefined));
47+
}
48+
},
49+
setIsAutoImportable(fromFileName, toFileName, preferences, isAutoImportable) {
50+
const cache = ensureCache(fromFileName, preferences);
51+
const info = cache.get(toFileName);
52+
if (info) {
53+
info.isAutoImportable = isAutoImportable;
54+
}
55+
else {
56+
cache.set(toFileName, createInfo(/*modulePaths*/ undefined, /*moduleSpecifiers*/ undefined, isAutoImportable));
57+
}
58+
},
59+
clear() {
60+
containedNodeModulesWatchers?.forEach(watcher => watcher.close());
61+
cache?.clear();
62+
containedNodeModulesWatchers?.clear();
63+
currentKey = undefined;
64+
},
65+
count() {
66+
return cache ? cache.size : 0;
67+
}
68+
};
69+
if (Debug.isDebugging) {
70+
Object.defineProperty(result, "__cache", { get: () => cache });
71+
}
72+
return result;
73+
74+
function ensureCache(fromFileName: Path, preferences: UserPreferences) {
75+
const newKey = key(fromFileName, preferences);
76+
if (cache && (currentKey !== newKey)) {
77+
result.clear();
78+
}
79+
currentKey = newKey;
80+
return cache ||= new Map();
81+
}
82+
83+
function key(fromFileName: Path, preferences: UserPreferences) {
84+
return `${fromFileName},${preferences.importModuleSpecifierEnding},${preferences.importModuleSpecifierPreference}`;
85+
}
86+
87+
function createInfo(
88+
modulePaths: readonly ModulePath[] | undefined,
89+
moduleSpecifiers: readonly string[] | undefined,
90+
isAutoImportable: boolean | undefined,
91+
): ResolvedModuleSpecifierInfo {
92+
return { modulePaths, moduleSpecifiers, isAutoImportable };
93+
}
94+
}
95+
}

src/server/project.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ namespace ts.server {
251251
public readonly getCanonicalFileName: GetCanonicalFileName;
252252

253253
/*@internal*/
254-
private exportMapCache = createExportMapCache();
254+
private exportMapCache: ExportInfoMap | undefined;
255255
/*@internal*/
256256
private changedFilesForExportMapCache: Set<Path> | undefined;
257257
/*@internal*/
@@ -793,6 +793,7 @@ namespace ts.server {
793793
this.cachedUnresolvedImportsPerFile = undefined!;
794794
this.moduleSpecifierCache = undefined!;
795795
this.directoryStructureHost = undefined!;
796+
this.exportMapCache = undefined;
796797
this.projectErrors = undefined;
797798

798799
// Clean up file watchers waiting for missing files
@@ -990,7 +991,7 @@ namespace ts.server {
990991
/*@internal*/
991992
markFileAsDirty(changedFile: Path) {
992993
this.markAsDirty();
993-
if (!this.exportMapCache.isEmpty()) {
994+
if (this.exportMapCache && !this.exportMapCache.isEmpty()) {
994995
(this.changedFilesForExportMapCache ||= new Set()).add(changedFile);
995996
}
996997
}
@@ -1192,7 +1193,8 @@ namespace ts.server {
11921193
}
11931194
}
11941195

1195-
if (!this.exportMapCache.isEmpty()) {
1196+
if (this.exportMapCache && !this.exportMapCache.isEmpty()) {
1197+
this.exportMapCache.releaseSymbols();
11961198
if (this.hasAddedorRemovedFiles || oldProgram && !this.program!.structureIsReused) {
11971199
this.exportMapCache.clear();
11981200
}
@@ -1201,10 +1203,10 @@ namespace ts.server {
12011203
const oldSourceFile = oldProgram.getSourceFileByPath(fileName);
12021204
const sourceFile = this.program!.getSourceFileByPath(fileName);
12031205
if (!oldSourceFile || !sourceFile) {
1204-
this.exportMapCache.clear();
1206+
this.exportMapCache!.clear();
12051207
return true;
12061208
}
1207-
return this.exportMapCache.onFileChanged(oldSourceFile, sourceFile, !!this.getTypeAcquisition().enable);
1209+
return this.exportMapCache!.onFileChanged(oldSourceFile, sourceFile, !!this.getTypeAcquisition().enable);
12081210
});
12091211
}
12101212
}
@@ -1666,8 +1668,13 @@ namespace ts.server {
16661668
}
16671669

16681670
/*@internal*/
1669-
getExportMapCache() {
1670-
return this.exportMapCache;
1671+
getCachedExportInfoMap() {
1672+
return this.exportMapCache ||= createCacheableExportInfoMap(this);
1673+
}
1674+
1675+
/*@internal*/
1676+
clearCachedExportInfoMap() {
1677+
this.exportMapCache?.clear();
16711678
}
16721679

16731680
/*@internal*/
@@ -2017,7 +2024,7 @@ namespace ts.server {
20172024
const oldProgram = this.getCurrentProgram();
20182025
const hasSameSetOfFiles = super.updateGraph();
20192026
if (oldProgram && oldProgram !== this.getCurrentProgram()) {
2020-
this.hostProject.getExportMapCache().clear();
2027+
this.hostProject.clearCachedExportInfoMap();
20212028
}
20222029
return hasSameSetOfFiles;
20232030
}

src/server/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"typingsCache.ts",
2424
"project.ts",
2525
"editorServices.ts",
26+
"moduleSpecifierCache.ts",
2627
"packageJsonCache.ts",
2728
"session.ts",
2829
"scriptVersionCache.ts"

0 commit comments

Comments
 (0)