Skip to content

Commit d213200

Browse files
authored
perf: prevent multiple recompiles during update import (#2071)
- add more caches to speed up things - remove getSnapshot from readFile but cache package.json calls. readFile rarely called, and if then it's for package.json mostly. Sometimes after updateImports when reading not-yet-discovered files through .d.ts.map and corresponding .js which is why getSnapshot is bad in that situation, as that increments the projectVersion so TS keeps on rebuilding the program while updating imports, causing the slowdown. #2065
1 parent afd38ea commit d213200

File tree

9 files changed

+244
-77
lines changed

9 files changed

+244
-77
lines changed

packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface LSAndTSDocResolverOptions {
3535
tsconfigPath?: string;
3636

3737
onProjectReloaded?: () => void;
38-
watchTsConfig?: boolean;
38+
watch?: boolean;
3939
tsSystem?: ts.System;
4040
}
4141

@@ -86,7 +86,10 @@ export class LSAndTSDocResolver {
8686
return document;
8787
};
8888

89-
private globalSnapshotsManager = new GlobalSnapshotsManager(this.lsDocumentContext.tsSystem);
89+
private globalSnapshotsManager = new GlobalSnapshotsManager(
90+
this.lsDocumentContext.tsSystem,
91+
/* watchPackageJson */ !!this.options?.watch
92+
);
9093
private extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>();
9194
private getCanonicalFileName: GetCanonicalFileName;
9295

@@ -99,7 +102,7 @@ export class LSAndTSDocResolver {
99102
notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit,
100103
extendedConfigCache: this.extendedConfigCache,
101104
onProjectReloaded: this.options?.onProjectReloaded,
102-
watchTsConfig: !!this.options?.watchTsConfig,
105+
watchTsConfig: !!this.options?.watch,
103106
tsSystem: this.options?.tsSystem ?? ts.sys
104107
};
105108
}
@@ -122,7 +125,7 @@ export class LSAndTSDocResolver {
122125

123126
/**
124127
* Retrieves and updates the snapshot for the given document or path from
125-
* the ts service it primarely belongs into.
128+
* the ts service it primarily belongs into.
126129
* The update is mirrored in all other services, too.
127130
*/
128131
async getSnapshot(document: Document): Promise<SvelteDocumentSnapshot>;

packages/language-server/src/plugins/typescript/SnapshotManager.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
55
import { createGetCanonicalFileName, GetCanonicalFileName, normalizePath } from '../../utils';
66
import { EventEmitter } from 'events';
77
import { FileMap } from '../../lib/documents/fileCollection';
8+
import { dirname } from 'path';
89

910
type SnapshotChangeHandler = (fileName: string, newDocument: DocumentSnapshot | undefined) => void;
1011

@@ -17,10 +18,17 @@ export class GlobalSnapshotsManager {
1718
private emitter = new EventEmitter();
1819
private documents: FileMap<DocumentSnapshot>;
1920
private getCanonicalFileName: GetCanonicalFileName;
21+
private packageJsonCache: PackageJsonCache;
2022

21-
constructor(private readonly tsSystem: ts.System) {
23+
constructor(private readonly tsSystem: ts.System, watchPackageJson = false) {
2224
this.documents = new FileMap(tsSystem.useCaseSensitiveFileNames);
2325
this.getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames);
26+
this.packageJsonCache = new PackageJsonCache(
27+
tsSystem,
28+
watchPackageJson,
29+
this.getCanonicalFileName,
30+
this.updateSnapshotsInDirectory.bind(this)
31+
);
2432
}
2533

2634
get(fileName: string) {
@@ -83,6 +91,16 @@ export class GlobalSnapshotsManager {
8391
removeChangeListener(listener: SnapshotChangeHandler) {
8492
this.emitter.off('change', listener);
8593
}
94+
95+
getPackageJson(path: string) {
96+
return this.packageJsonCache.getPackageJson(path);
97+
}
98+
99+
private updateSnapshotsInDirectory(dir: string) {
100+
this.getByPrefix(dir).forEach((snapshot) => {
101+
this.updateTsOrJsFile(snapshot.filePath);
102+
});
103+
}
86104
}
87105

88106
export interface TsFilesSpec {
@@ -244,3 +262,76 @@ export class SnapshotManager {
244262
}
245263

246264
export const ignoredBuildDirectories = ['__sapper__', '.svelte-kit'];
265+
266+
class PackageJsonCache {
267+
constructor(
268+
private readonly tsSystem: ts.System,
269+
private readonly watchPackageJson: boolean,
270+
private readonly getCanonicalFileName: GetCanonicalFileName,
271+
private readonly updateSnapshotsInDirectory: (directory: string) => void
272+
) {
273+
this.watchers = new FileMap(tsSystem.useCaseSensitiveFileNames);
274+
}
275+
276+
private readonly watchers: FileMap<ts.FileWatcher>;
277+
278+
private packageJsonCache = new FileMap<
279+
{ text: string; modifiedTime: number | undefined } | undefined
280+
>();
281+
282+
getPackageJson(path: string) {
283+
if (!this.packageJsonCache.has(path)) {
284+
this.packageJsonCache.set(path, this.initWatcherAndRead(path));
285+
}
286+
287+
return this.packageJsonCache.get(path);
288+
}
289+
290+
private initWatcherAndRead(path: string) {
291+
if (this.watchPackageJson) {
292+
this.tsSystem.watchFile?.(path, this.onPackageJsonWatchChange.bind(this), 3_000);
293+
}
294+
const exist = this.tsSystem.fileExists(path);
295+
296+
if (!exist) {
297+
return undefined;
298+
}
299+
300+
return this.readPackageJson(path);
301+
}
302+
303+
private readPackageJson(path: string) {
304+
return {
305+
text: this.tsSystem.readFile(path) ?? '',
306+
modifiedTime: this.tsSystem.getModifiedTime?.(path)?.valueOf()
307+
};
308+
}
309+
310+
private onPackageJsonWatchChange(path: string, onWatchChange: ts.FileWatcherEventKind) {
311+
const dir = dirname(path);
312+
313+
if (onWatchChange === ts.FileWatcherEventKind.Deleted) {
314+
this.packageJsonCache.delete(path);
315+
this.watchers.get(path)?.close();
316+
this.watchers.delete(path);
317+
} else {
318+
this.packageJsonCache.set(path, this.readPackageJson(path));
319+
}
320+
321+
if (!path.includes('node_modules')) {
322+
return;
323+
}
324+
325+
setTimeout(() => {
326+
this.updateSnapshotsInDirectory(dir);
327+
const realPath =
328+
this.tsSystem.realpath &&
329+
this.getCanonicalFileName(normalizePath(this.tsSystem.realpath?.(dir)));
330+
331+
// pnpm
332+
if (realPath && realPath !== dir) {
333+
this.updateSnapshotsInDirectory(realPath);
334+
}
335+
}, 500);
336+
}
337+
}

packages/language-server/src/plugins/typescript/module-loader.ts

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ts from 'typescript';
22
import { FileMap } from '../../lib/documents/fileCollection';
3-
import { createGetCanonicalFileName, getLastPartOfPath } from '../../utils';
3+
import { createGetCanonicalFileName, getLastPartOfPath, toFileNameLowerCase } from '../../utils';
44
import { DocumentSnapshot } from './DocumentSnapshot';
55
import { createSvelteSys } from './svelte-sys';
66
import {
@@ -89,26 +89,48 @@ class ImpliedNodeFormatResolver {
8989

9090
let mode = undefined;
9191
if (sourceFile) {
92-
if (!sourceFile.impliedNodeFormat && isSvelteFilePath(sourceFile.fileName)) {
93-
// impliedNodeFormat is not set for Svelte files, because the TS function which
94-
// calculates this works with a fixed set of file extensions,
95-
// which .svelte is obv not part of. Make it work by faking a TS file.
96-
if (!this.alreadyResolved.has(sourceFile.fileName)) {
97-
sourceFile.impliedNodeFormat = ts.getImpliedNodeFormatForFile(
98-
toVirtualSvelteFilePath(sourceFile.fileName) as any,
99-
undefined,
100-
ts.sys,
101-
compilerOptions
102-
);
103-
this.alreadyResolved.set(sourceFile.fileName, sourceFile.impliedNodeFormat);
104-
} else {
105-
sourceFile.impliedNodeFormat = this.alreadyResolved.get(sourceFile.fileName);
106-
}
107-
}
92+
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
10893
mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile);
10994
}
11095
return mode;
11196
}
97+
98+
private cacheImpliedNodeFormat(sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions) {
99+
if (!sourceFile.impliedNodeFormat && isSvelteFilePath(sourceFile.fileName)) {
100+
// impliedNodeFormat is not set for Svelte files, because the TS function which
101+
// calculates this works with a fixed set of file extensions,
102+
// which .svelte is obv not part of. Make it work by faking a TS file.
103+
if (!this.alreadyResolved.has(sourceFile.fileName)) {
104+
sourceFile.impliedNodeFormat = ts.getImpliedNodeFormatForFile(
105+
toVirtualSvelteFilePath(sourceFile.fileName) as any,
106+
undefined,
107+
ts.sys,
108+
compilerOptions
109+
);
110+
this.alreadyResolved.set(sourceFile.fileName, sourceFile.impliedNodeFormat);
111+
} else {
112+
sourceFile.impliedNodeFormat = this.alreadyResolved.get(sourceFile.fileName);
113+
}
114+
}
115+
}
116+
117+
resolveForTypeReference(
118+
entry: string | ts.FileReference,
119+
sourceFile: ts.SourceFile | undefined,
120+
compilerOptions: ts.CompilerOptions
121+
) {
122+
let mode = undefined;
123+
if (sourceFile) {
124+
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
125+
mode = ts.getModeForFileReference(entry, sourceFile?.impliedNodeFormat);
126+
}
127+
return mode;
128+
}
129+
}
130+
131+
// https://github.com/microsoft/TypeScript/blob/dddd0667f012c51582c2ac92c08b8e57f2456587/src/compiler/program.ts#L989
132+
function getTypeReferenceResolutionName<T extends ts.FileReference | string>(entry: T) {
133+
return typeof entry !== 'string' ? toFileNameLowerCase(entry.fileName) : entry;
112134
}
113135

114136
/**
@@ -127,10 +149,27 @@ export function createSvelteModuleLoader(
127149
getSnapshot: (fileName: string) => DocumentSnapshot,
128150
compilerOptions: ts.CompilerOptions,
129151
tsSystem: ts.System,
130-
tsResolveModuleName: typeof ts.resolveModuleName
152+
tsModule: typeof ts
131153
) {
132-
const svelteSys = createSvelteSys(getSnapshot, tsSystem);
154+
const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames);
155+
const svelteSys = createSvelteSys(tsSystem);
156+
// tsModuleCache caches package.json parsing and module resolution for directory
157+
const tsModuleCache = tsModule.createModuleResolutionCache(
158+
tsSystem.getCurrentDirectory(),
159+
createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames)
160+
);
161+
const tsTypeReferenceDirectiveCache = tsModule.createTypeReferenceDirectiveResolutionCache(
162+
tsSystem.getCurrentDirectory(),
163+
getCanonicalFileName,
164+
undefined,
165+
tsModuleCache.getPackageJsonInfoCache()
166+
);
133167
const moduleCache = new ModuleResolutionCache();
168+
const typeReferenceCache = new Map<
169+
string,
170+
ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations
171+
>();
172+
134173
const impliedNodeFormatResolver = new ImpliedNodeFormatResolver();
135174

136175
return {
@@ -145,7 +184,8 @@ export function createSvelteModuleLoader(
145184
svelteSys.deleteFromCache(path);
146185
moduleCache.deleteUnresolvedResolutionsFromCache(path);
147186
},
148-
resolveModuleNames
187+
resolveModuleNames,
188+
resolveTypeReferenceDirectiveReferences
149189
};
150190

151191
function resolveModuleNames(
@@ -187,20 +227,20 @@ export function createSvelteModuleLoader(
187227
// Delegate to the TS resolver first.
188228
// If that does not bring up anything, try the Svelte Module loader
189229
// which is able to deal with .svelte files.
190-
const tsResolvedModule = tsResolveModuleName(
230+
const tsResolvedModule = tsModule.resolveModuleName(
191231
name,
192232
containingFile,
193233
compilerOptions,
194234
ts.sys,
195-
undefined,
235+
tsModuleCache,
196236
undefined,
197237
mode
198238
).resolvedModule;
199239
if (tsResolvedModule && !isVirtualSvelteFilePath(tsResolvedModule.resolvedFileName)) {
200240
return tsResolvedModule;
201241
}
202242

203-
const svelteResolvedModule = tsResolveModuleName(
243+
const svelteResolvedModule = tsModule.resolveModuleName(
204244
name,
205245
containingFile,
206246
compilerOptions,
@@ -226,4 +266,41 @@ export function createSvelteModuleLoader(
226266
};
227267
return resolvedSvelteModule;
228268
}
269+
270+
function resolveTypeReferenceDirectiveReferences<T extends ts.FileReference | string>(
271+
typeDirectiveNames: readonly T[],
272+
containingFile: string,
273+
redirectedReference: ts.ResolvedProjectReference | undefined,
274+
options: ts.CompilerOptions,
275+
containingSourceFile: ts.SourceFile | undefined
276+
): readonly ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations[] {
277+
return typeDirectiveNames.map((typeDirectiveName) => {
278+
const entry = getTypeReferenceResolutionName(typeDirectiveName);
279+
const mode = impliedNodeFormatResolver.resolveForTypeReference(
280+
entry,
281+
containingSourceFile,
282+
options
283+
);
284+
285+
const key = `${entry}|${mode}`;
286+
let result = typeReferenceCache.get(key);
287+
if (!result) {
288+
result = ts.resolveTypeReferenceDirective(
289+
entry,
290+
containingFile,
291+
options,
292+
{
293+
...tsSystem
294+
},
295+
redirectedReference,
296+
tsTypeReferenceDirectiveCache,
297+
mode
298+
);
299+
300+
typeReferenceCache.set(key, result);
301+
}
302+
303+
return result;
304+
});
305+
}
229306
}

packages/language-server/src/plugins/typescript/service.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { dirname, resolve } from 'path';
1+
import { basename, dirname, resolve } from 'path';
22
import ts from 'typescript';
33
import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol';
44
import { getPackageInfo, importSvelte } from '../../importPackage';
@@ -185,12 +185,27 @@ async function createLanguageService(
185185
// Load all configs within the tsconfig scope and the one above so that they are all loaded
186186
// by the time they need to be accessed synchronously by DocumentSnapshots.
187187
await configLoader.loadConfigs(workspacePath);
188+
const tsSystemWithPackageJsonCache = {
189+
...tsSystem,
190+
/**
191+
* While TypeScript doesn't cache package.json in the tsserver, they do cache the
192+
* information they get from it within other internal APIs. We'll somewhat do the same
193+
* by caching the text of the package.json file here.
194+
*/
195+
readFile: (path: string, encoding?: string | undefined) => {
196+
if (basename(path) === 'package.json') {
197+
return docContext.globalSnapshotsManager.getPackageJson(path)?.text;
198+
}
199+
200+
return tsSystem.readFile(path, encoding);
201+
}
202+
};
188203

189204
const svelteModuleLoader = createSvelteModuleLoader(
190205
getSnapshot,
191206
compilerOptions,
192-
tsSystem,
193-
ts.resolveModuleName
207+
tsSystemWithPackageJsonCache,
208+
ts
194209
);
195210

196211
let svelteTsPath: string;
@@ -229,7 +244,9 @@ async function createLanguageService(
229244
useCaseSensitiveFileNames: () => tsSystem.useCaseSensitiveFileNames,
230245
getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind,
231246
getProjectVersion: () => projectVersion.toString(),
232-
getNewLine: () => tsSystem.newLine
247+
getNewLine: () => tsSystem.newLine,
248+
resolveTypeReferenceDirectiveReferences:
249+
svelteModuleLoader.resolveTypeReferenceDirectiveReferences
233250
};
234251

235252
let languageService = ts.createLanguageService(host);

0 commit comments

Comments
 (0)