Skip to content

Commit ec7be4b

Browse files
authored
perf: use failed path to invalidate module cache (#2853)
* perf: use failed path to invalidate module cache * Don't emit both document events in open This makes the test do extra work after everything is done and creates weird race conditions, which I have already tried to fix many times * changeset * remove unnecessary Array.from
1 parent dec37ea commit ec7be4b

File tree

10 files changed

+94
-54
lines changed

10 files changed

+94
-54
lines changed

.changeset/dark-falcons-pump.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'svelte-language-server': patch
3+
'svelte-check': patch
4+
---
5+
6+
perf: more precise module cache invalidation

packages/language-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"./bin/server.js": "./bin/server.js"
1111
},
1212
"scripts": {
13-
"test": "cross-env TS_NODE_TRANSPILE_ONLY=true mocha --require ts-node/register \"test/**/*.test.ts\"",
13+
"test": "cross-env TS_NODE_TRANSPILE_ONLY=true mocha --require ts-node/register \"test/**/*.test.ts\" --no-experimental-strip-types",
1414
"build": "tsc",
1515
"prepublishOnly": "npm run build",
1616
"watch": "tsc -w"

packages/language-server/src/lib/documents/DocumentManager.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,14 @@ export class DocumentManager {
5050
// open state should only be updated when the document is closed
5151
document.openedByClient ||= openedByClient;
5252
document.setText(textDocument.text);
53+
this.notify('documentChange', document);
5354
} else {
5455
document = this.createDocument(textDocument);
5556
document.openedByClient = openedByClient;
5657
this.documents.set(textDocument.uri, document);
5758
this.notify('documentOpen', document);
5859
}
5960

60-
this.notify('documentChange', document);
61-
6261
return document;
6362
}
6463

packages/language-server/src/plugins/css/CSSPlugin.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,11 @@ export class CSSPlugin
9999
this.updateConfigs();
100100
});
101101

102-
docManager.on('documentChange', (document) =>
103-
this.cssDocuments.set(document, new CSSDocument(document, this.cssLanguageServices))
104-
);
102+
const sync = (document: Document) => {
103+
this.cssDocuments.set(document, new CSSDocument(document, this.cssLanguageServices));
104+
};
105+
docManager.on('documentChange', sync);
106+
docManager.on('documentOpen', sync);
105107
docManager.on('documentClose', (document) => this.cssDocuments.delete(document));
106108
}
107109

packages/language-server/src/plugins/html/HTMLPlugin.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ export class HTMLPlugin
7676
configManager.onChange(() =>
7777
this.lang.setDataProviders(false, this.getCustomDataProviders())
7878
);
79-
docManager.on('documentChange', (document) => {
79+
const sync = (document: Document) => {
8080
this.documents.set(document, document.html);
81-
});
81+
};
82+
docManager.on('documentChange', sync);
83+
docManager.on('documentOpen', sync);
8284
}
8385

8486
doHover(document: Document, position: Position): Hover | null {

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

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,27 @@ import {
77
ensureRealSvelteFilePath,
88
getExtensionFromScriptKind,
99
isSvelteFilePath,
10-
isVirtualSvelteFilePath
10+
isVirtualSvelteFilePath,
11+
toVirtualSvelteFilePath
1112
} from './utils';
1213

1314
const CACHE_KEY_SEPARATOR = ':::';
1415
/**
1516
* Caches resolved modules.
1617
*/
1718
class ModuleResolutionCache {
18-
private cache = new FileMap<ts.ResolvedModule | undefined>();
19+
private cache = new FileMap<ts.ResolvedModuleWithFailedLookupLocations>();
1920
private pendingInvalidations = new FileSet();
2021
private getCanonicalFileName = createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames);
2122

2223
/**
2324
* Tries to get a cached module.
2425
* Careful: `undefined` can mean either there's no match found, or that the result resolved to `undefined`.
2526
*/
26-
get(moduleName: string, containingFile: string): ts.ResolvedModule | undefined {
27+
get(
28+
moduleName: string,
29+
containingFile: string
30+
): ts.ResolvedModuleWithFailedLookupLocations | undefined {
2731
return this.cache.get(this.getKey(moduleName, containingFile));
2832
}
2933

@@ -37,7 +41,11 @@ class ModuleResolutionCache {
3741
/**
3842
* Caches resolved module (or undefined).
3943
*/
40-
set(moduleName: string, containingFile: string, resolvedModule: ts.ResolvedModule | undefined) {
44+
set(
45+
moduleName: string,
46+
containingFile: string,
47+
resolvedModule: ts.ResolvedModuleWithFailedLookupLocations
48+
) {
4149
this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
4250
}
4351

@@ -48,7 +56,11 @@ class ModuleResolutionCache {
4856
delete(resolvedModuleName: string): void {
4957
resolvedModuleName = this.getCanonicalFileName(resolvedModuleName);
5058
this.cache.forEach((val, key) => {
51-
if (val && this.getCanonicalFileName(val.resolvedFileName) === resolvedModuleName) {
59+
if (
60+
val.resolvedModule &&
61+
this.getCanonicalFileName(val.resolvedModule.resolvedFileName) ===
62+
resolvedModuleName
63+
) {
5264
this.cache.delete(key);
5365
this.pendingInvalidations.add(key.split(CACHE_KEY_SEPARATOR).shift() || '');
5466
}
@@ -59,17 +71,10 @@ class ModuleResolutionCache {
5971
* Deletes everything from cache that resolved to `undefined`
6072
* and which might match the path.
6173
*/
62-
deleteUnresolvedResolutionsFromCache(path: string): void {
63-
const fileNameWithoutEnding =
64-
getLastPartOfPath(this.getCanonicalFileName(path)).split('.').shift() || '';
74+
deleteByValues(list: ts.ResolvedModuleWithFailedLookupLocations[]): void {
6575
this.cache.forEach((val, key) => {
66-
if (val) {
67-
return;
68-
}
69-
const [containingFile, moduleName = ''] = key.split(CACHE_KEY_SEPARATOR);
70-
if (moduleName.includes(fileNameWithoutEnding)) {
76+
if (list.includes(val)) {
7177
this.cache.delete(key);
72-
this.pendingInvalidations.add(containingFile);
7378
}
7479
});
7580
}
@@ -181,13 +186,13 @@ export function createSvelteModuleLoader(
181186
svelteSys.deleteFromCache(path);
182187
moduleCache.delete(path);
183188
},
184-
deleteUnresolvedResolutionsFromCache: (path: string) => {
189+
scheduleResolutionFailedLocationCheck: (path: string) => {
185190
svelteSys.deleteFromCache(path);
186-
moduleCache.deleteUnresolvedResolutionsFromCache(path);
187-
pendingFailedLocationCheck.add(path);
188-
189-
tsModuleCache.clear();
190-
typeReferenceCache.clear();
191+
if (isSvelteFilePath(path)) {
192+
pendingFailedLocationCheck.add(toVirtualSvelteFilePath(path));
193+
} else {
194+
pendingFailedLocationCheck.add(path);
195+
}
191196
},
192197
resolveModuleNames,
193198
resolveTypeReferenceDirectiveReferences,
@@ -206,8 +211,9 @@ export function createSvelteModuleLoader(
206211
containingSourceFile?: ts.SourceFile | undefined
207212
): Array<ts.ResolvedModule | undefined> {
208213
return moduleNames.map((moduleName, index) => {
209-
if (moduleCache.has(moduleName, containingFile)) {
210-
return moduleCache.get(moduleName, containingFile);
214+
const cached = moduleCache.get(moduleName, containingFile);
215+
if (cached) {
216+
return cached.resolvedModule;
211217
}
212218

213219
const resolvedModule = resolveModuleName(
@@ -221,7 +227,7 @@ export function createSvelteModuleLoader(
221227

222228
cacheResolutionWithFailedLookup(resolvedModule, containingFile);
223229

224-
moduleCache.set(moduleName, containingFile, resolvedModule?.resolvedModule);
230+
moduleCache.set(moduleName, containingFile, resolvedModule);
225231
return resolvedModule?.resolvedModule;
226232
});
227233
}
@@ -347,17 +353,15 @@ export function createSvelteModuleLoader(
347353
}
348354

349355
function invalidateFailedLocationResolution() {
356+
const toRemoves: ts.ResolvedModuleWithFailedLookupLocations[] = [];
350357
resolutionWithFailedLookup.forEach((resolvedModule) => {
351-
if (
352-
!resolvedModule.resolvedModule ||
353-
!resolvedModule.files ||
354-
!resolvedModule.failedLookupLocations
355-
) {
358+
if (!resolvedModule.failedLookupLocations) {
356359
return;
357360
}
361+
358362
for (const location of resolvedModule.failedLookupLocations) {
359363
if (pendingFailedLocationCheck.has(location)) {
360-
moduleCache.delete(resolvedModule.resolvedModule.resolvedFileName);
364+
toRemoves.push(resolvedModule);
361365
resolvedModule.files?.forEach((file) => {
362366
failedLocationInvalidated.add(file);
363367
});
@@ -366,6 +370,16 @@ export function createSvelteModuleLoader(
366370
}
367371
});
368372

373+
if (toRemoves.length) {
374+
moduleCache.deleteByValues(toRemoves);
375+
resolutionWithFailedLookup.forEach((r) => {
376+
if (toRemoves.includes(r)) {
377+
resolutionWithFailedLookup.delete(r);
378+
}
379+
});
380+
tsModuleCache.clear();
381+
tsTypeReferenceDirectiveCache.clear();
382+
}
369383
pendingFailedLocationCheck.clear();
370384
}
371385
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ async function createLanguageService(
531531
for (const filePath of filePaths) {
532532
const normalizedPath = normalizePath(filePath);
533533
svelteModuleLoader.deleteFromModuleCache(normalizedPath);
534-
svelteModuleLoader.deleteUnresolvedResolutionsFromCache(normalizedPath);
534+
svelteModuleLoader.scheduleResolutionFailedLocationCheck(normalizedPath);
535535

536536
scheduleUpdate(normalizedPath);
537537
}
@@ -559,7 +559,7 @@ async function createLanguageService(
559559
const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig);
560560

561561
if (!prevSnapshot) {
562-
svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath);
562+
svelteModuleLoader.scheduleResolutionFailedLocationCheck(filePath);
563563
if (configFileForOpenFiles.get(filePath) === '' && services.size > 1) {
564564
configFileForOpenFiles.delete(filePath);
565565
}
@@ -580,6 +580,7 @@ async function createLanguageService(
580580
return prevSnapshot;
581581
}
582582

583+
svelteModuleLoader.scheduleResolutionFailedLocationCheck(filePath);
583584
return createSnapshot(filePath);
584585
}
585586

@@ -600,6 +601,8 @@ async function createLanguageService(
600601
return undefined;
601602
}
602603

604+
// don't invalidate the module cache here
605+
// this only get called if we already know the file exists
603606
return createSnapshot(
604607
svelteModuleLoader.svelteFileExists(fileName) ? svelteFileName : fileName
605608
);
@@ -613,11 +616,12 @@ async function createLanguageService(
613616
return doc;
614617
}
615618

619+
// don't invalidate the module cache here
620+
// this only get called if we already know the file exists
616621
return createSnapshot(fileName);
617622
}
618623

619624
function createSnapshot(fileName: string) {
620-
svelteModuleLoader.deleteUnresolvedResolutionsFromCache(fileName);
621625
const doc = DocumentSnapshot.fromFilePath(
622626
fileName,
623627
docContext.createDocument,
@@ -730,7 +734,7 @@ async function createLanguageService(
730734

731735
function updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void {
732736
if (!snapshotManager.has(fileName)) {
733-
svelteModuleLoader.deleteUnresolvedResolutionsFromCache(fileName);
737+
svelteModuleLoader.scheduleResolutionFailedLocationCheck(fileName);
734738
}
735739
snapshotManager.updateTsOrJsFile(fileName, changes);
736740
}

packages/language-server/test/lib/documents/DocumentManager.test.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,6 @@ describe('Document Manager', () => {
6868
assert.throws(() => manager.updateDocument(textDocument, []));
6969
});
7070

71-
it('emits a document change event on open and update', () => {
72-
const manager = new DocumentManager(createTextDocument);
73-
const cb = sinon.spy();
74-
75-
manager.on('documentChange', cb);
76-
77-
manager.openClientDocument(textDocument);
78-
sinon.assert.calledOnce(cb);
79-
80-
manager.updateDocument(textDocument, []);
81-
sinon.assert.calledTwice(cb);
82-
});
83-
8471
it('update document in case-insensitive fs with different casing', () => {
8572
const textDocument: TextDocumentItem = {
8673
uri: 'file:///hello2.svelte',

packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,28 @@ describe('DiagnosticsProvider', function () {
9292
}
9393
}).timeout(this.timeout() * 2.5);
9494

95+
it('notices changes of module resolution because of new svelte file', async () => {
96+
const { plugin, document, lsAndTsDocResolver } = setup('unresolvedimport2.svelte');
97+
98+
const diagnostics1 = await plugin.getDiagnostics(document);
99+
assert.deepStrictEqual(diagnostics1.length, 1);
100+
101+
// back-and-forth-conversion normalizes slashes
102+
const newSvelteFilePath = normalizePath(path.join(testDir, 'doesntexistyet.svelte')) || '';
103+
104+
writeFileSync(newSvelteFilePath, '<script lang="ts"></script>');
105+
assert.ok(existsSync(newSvelteFilePath));
106+
await lsAndTsDocResolver.invalidateModuleCache([newSvelteFilePath]);
107+
108+
try {
109+
const diagnostics2 = await plugin.getDiagnostics(document);
110+
assert.deepStrictEqual(diagnostics2.length, 0);
111+
await lsAndTsDocResolver.deleteSnapshot(newSvelteFilePath);
112+
} finally {
113+
unlinkSync(newSvelteFilePath);
114+
}
115+
}).timeout(this.timeout() * 2.5);
116+
95117
it('notices update of imported module', async () => {
96118
const { plugin, document, lsAndTsDocResolver } = setup(
97119
'diagnostics-imported-js-update.svelte'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<script lang="ts">
2+
import foo from './doesntexistyet.svelte';
3+
foo;
4+
</script>

0 commit comments

Comments
 (0)