Skip to content

Commit 2ff3a7c

Browse files
authored
perf: cancel diagnostics (#2216)
Another part of #2179 is that TypeScript took a long time to type-check the file. When the file is ts or check-js, it'll be getSemanticDiagnostics, and for non-checked js, it's getSuggestionDiagnostics. There is probably not much we can do about it. But we can instead cancel the check when the file is updated. The cancellation can only be checked if there is an async operation. I added a small delay in the typescript check so that the update notification queue might run in that time frame. And we could return early for the svelte and typescript diagnostic. CSS and HTML are pure sync code and generally not that heavy, so it's probably not worth the overhead.
1 parent 675bfc4 commit 2ff3a7c

File tree

7 files changed

+98
-23
lines changed

7 files changed

+98
-23
lines changed

packages/language-server/src/lib/DiagnosticsManager.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import { _Connection, TextDocumentIdentifier, Diagnostic } from 'vscode-languageserver';
1+
import {
2+
Connection,
3+
TextDocumentIdentifier,
4+
Diagnostic,
5+
CancellationTokenSource,
6+
CancellationToken
7+
} from 'vscode-languageserver';
28
import { DocumentManager, Document } from './documents';
39
import { debounceThrottle } from '../utils';
410

5-
export type SendDiagnostics = _Connection['sendDiagnostics'];
6-
export type GetDiagnostics = (doc: TextDocumentIdentifier) => Thenable<Diagnostic[]>;
11+
export type SendDiagnostics = Connection['sendDiagnostics'];
12+
export type GetDiagnostics = (
13+
doc: TextDocumentIdentifier,
14+
cancellationToken?: CancellationToken
15+
) => Thenable<Diagnostic[]>;
716

817
export class DiagnosticsManager {
918
constructor(
@@ -13,6 +22,7 @@ export class DiagnosticsManager {
1322
) {}
1423

1524
private pendingUpdates = new Set<Document>();
25+
private cancellationTokens = new Map<string, { cancel: () => void }>();
1626

1727
private updateAll() {
1828
this.docManager.getAllOpenedByClient().forEach((doc) => {
@@ -21,14 +31,43 @@ export class DiagnosticsManager {
2131
this.pendingUpdates.clear();
2232
}
2333

24-
scheduleUpdateAll = debounceThrottle(() => this.updateAll(), 1000);
34+
scheduleUpdateAll() {
35+
this.cancellationTokens.forEach((token) => token.cancel());
36+
this.cancellationTokens.clear();
37+
this.pendingUpdates.clear();
38+
this.debouncedUpdateAll();
39+
}
40+
41+
private debouncedUpdateAll = debounceThrottle(() => this.updateAll(), 1000);
2542

2643
private async update(document: Document) {
27-
const diagnostics = await this.getDiagnostics({ uri: document.getURL() });
44+
const uri = document.getURL();
45+
this.cancelStarted(uri);
46+
47+
const tokenSource = new CancellationTokenSource();
48+
this.cancellationTokens.set(uri, tokenSource);
49+
50+
const diagnostics = await this.getDiagnostics(
51+
{ uri: document.getURL() },
52+
tokenSource.token
53+
);
2854
this.sendDiagnostics({
2955
uri: document.getURL(),
3056
diagnostics
3157
});
58+
59+
tokenSource.dispose();
60+
61+
if (this.cancellationTokens.get(uri) === tokenSource) {
62+
this.cancellationTokens.delete(uri);
63+
}
64+
}
65+
66+
cancelStarted(uri: string) {
67+
const started = this.cancellationTokens.get(uri);
68+
if (started) {
69+
started.cancel();
70+
}
3271
}
3372

3473
removeDiagnostics(document: Document) {
@@ -44,6 +83,7 @@ export class DiagnosticsManager {
4483
return;
4584
}
4685

86+
this.cancelStarted(document.getURL());
4787
this.pendingUpdates.add(document);
4888
this.scheduleBatchUpdate();
4989
}
@@ -53,5 +93,5 @@ export class DiagnosticsManager {
5393
this.update(doc);
5494
});
5595
this.pendingUpdates.clear();
56-
}, 750);
96+
}, 700);
5797
}

packages/language-server/src/plugins/PluginHost.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
7676
this.deferredRequests = {};
7777
}
7878

79-
async getDiagnostics(textDocument: TextDocumentIdentifier): Promise<Diagnostic[]> {
79+
async getDiagnostics(
80+
textDocument: TextDocumentIdentifier,
81+
cancellationToken?: CancellationToken
82+
): Promise<Diagnostic[]> {
8083
const document = this.getDocument(textDocument.uri);
8184

8285
if (
@@ -96,7 +99,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
9699
return flatten(
97100
await this.execute<Diagnostic[]>(
98101
'getDiagnostics',
99-
[document],
102+
[document, cancellationToken],
100103
ExecuteMode.Collect,
101104
'high'
102105
)

packages/language-server/src/plugins/svelte/SveltePlugin.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,19 @@ export class SveltePlugin
4949

5050
constructor(private configManager: LSConfigManager) {}
5151

52-
async getDiagnostics(document: Document): Promise<Diagnostic[]> {
52+
async getDiagnostics(
53+
document: Document,
54+
cancellationToken?: CancellationToken
55+
): Promise<Diagnostic[]> {
5356
if (!this.featureEnabled('diagnostics') || !this.configManager.getIsTrusted()) {
5457
return [];
5558
}
5659

5760
return getDiagnostics(
5861
document,
5962
await this.getSvelteDoc(document),
60-
this.configManager.getConfig().svelte.compilerWarnings
63+
this.configManager.getConfig().svelte.compilerWarnings,
64+
cancellationToken
6165
);
6266
}
6367

packages/language-server/src/plugins/svelte/features/getDiagnostics.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
// @ts-ignore
22
import { Warning } from 'svelte/types/compiler/interfaces';
3-
import { Diagnostic, DiagnosticSeverity, Position, Range } from 'vscode-languageserver';
3+
import {
4+
CancellationToken,
5+
Diagnostic,
6+
DiagnosticSeverity,
7+
Position,
8+
Range
9+
} from 'vscode-languageserver';
410
import {
511
Document,
612
isInTag,
@@ -19,15 +25,20 @@ import { SvelteDocument, TranspileErrorSource } from '../SvelteDocument';
1925
export async function getDiagnostics(
2026
document: Document,
2127
svelteDoc: SvelteDocument,
22-
settings: CompilerWarningsSettings
28+
settings: CompilerWarningsSettings,
29+
cancellationToken?: CancellationToken
2330
): Promise<Diagnostic[]> {
2431
const config = await svelteDoc.config;
2532
if (config?.loadConfigError) {
2633
return getConfigLoadErrorDiagnostics(config.loadConfigError);
2734
}
2835

36+
if (cancellationToken?.isCancellationRequested) {
37+
return [];
38+
}
39+
2940
try {
30-
return await tryGetDiagnostics(document, svelteDoc, settings);
41+
return await tryGetDiagnostics(document, svelteDoc, settings, cancellationToken);
3142
} catch (error) {
3243
return getPreprocessErrorDiagnostics(document, error);
3344
}
@@ -39,12 +50,19 @@ export async function getDiagnostics(
3950
async function tryGetDiagnostics(
4051
document: Document,
4152
svelteDoc: SvelteDocument,
42-
settings: CompilerWarningsSettings
53+
settings: CompilerWarningsSettings,
54+
cancellationToken: CancellationToken | undefined
4355
): Promise<Diagnostic[]> {
4456
const transpiled = await svelteDoc.getTranspiled();
57+
if (cancellationToken?.isCancellationRequested) {
58+
return [];
59+
}
4560

4661
try {
4762
const res = await svelteDoc.getCompiled();
63+
if (cancellationToken?.isCancellationRequested) {
64+
return [];
65+
}
4866
return (((res.stats as any)?.warnings || res.warnings || []) as Warning[])
4967
.filter((warning) => settings[warning.code] !== 'ignore')
5068
.map((warning) => {
@@ -65,7 +83,7 @@ async function tryGetDiagnostics(
6583
.map((diag) => adjustMappings(diag, document))
6684
.filter((diag) => isNoFalsePositive(diag, document));
6785
} catch (err) {
68-
return (await createParserErrorDiagnostic(err, document))
86+
return createParserErrorDiagnostic(err, document)
6987
.map((diag) => mapObjWithRangeToOriginal(transpiled, diag))
7088
.map((diag) => adjustMappings(diag, document));
7189
}
@@ -74,7 +92,7 @@ async function tryGetDiagnostics(
7492
/**
7593
* Try to infer a nice diagnostic error message from the compilation error.
7694
*/
77-
async function createParserErrorDiagnostic(error: any, document: Document) {
95+
function createParserErrorDiagnostic(error: any, document: Document) {
7896
const start = error.start || { line: 1, column: 0 };
7997
const end = error.end || start;
8098
const diagnostic: Diagnostic = {

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,20 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
8080
];
8181
}
8282

83-
let diagnostics: ts.Diagnostic[] = [
84-
...lang.getSyntacticDiagnostics(tsDoc.filePath),
85-
...lang.getSuggestionDiagnostics(tsDoc.filePath),
86-
...lang.getSemanticDiagnostics(tsDoc.filePath)
87-
];
83+
let diagnostics: ts.Diagnostic[] = lang.getSyntacticDiagnostics(tsDoc.filePath);
84+
const checkers = [lang.getSuggestionDiagnostics, lang.getSemanticDiagnostics];
85+
86+
for (const checker of checkers) {
87+
if (cancellationToken) {
88+
// wait a bit so the event loop can check for cancellation
89+
// or let completion go first
90+
await new Promise((resolve) => setTimeout(resolve, 10));
91+
if (cancellationToken.isCancellationRequested) {
92+
return [];
93+
}
94+
}
95+
diagnostics.push(...checker.call(lang, tsDoc.filePath));
96+
}
8897

8998
const additionalStoreDiagnostics: ts.Diagnostic[] = [];
9099
const notGenerated = isNotGenerated(tsDoc.getFullText());

packages/language-server/src/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ export function startServer(options?: LSOptions) {
353353

354354
connection.onDidCloseTextDocument((evt) => docManager.closeDocument(evt.textDocument.uri));
355355
connection.onDidChangeTextDocument((evt) => {
356+
diagnosticsManager.cancelStarted(evt.textDocument.uri);
356357
docManager.updateDocument(evt.textDocument, evt.contentChanges);
357358
pluginHost.didUpdateDocument();
358359
});
@@ -468,7 +469,7 @@ export function startServer(options?: LSOptions) {
468469
refreshCrossFilesSemanticFeatures();
469470
}
470471

471-
connection.onDidSaveTextDocument(diagnosticsManager.scheduleUpdateAll);
472+
connection.onDidSaveTextDocument(diagnosticsManager.scheduleUpdateAll.bind(diagnosticsManager));
472473
connection.onNotification('$/onDidChangeTsOrJsFile', async (e: any) => {
473474
const path = urlToPath(e.uri);
474475
if (path) {

packages/language-server/test/plugins/PluginHost.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('PluginHost', () => {
5252
await pluginHost.getDiagnostics(textDocument);
5353

5454
sinon.assert.calledOnce(plugin.getDiagnostics);
55-
sinon.assert.calledWithExactly(plugin.getDiagnostics, document);
55+
sinon.assert.calledWithExactly(plugin.getDiagnostics, document, undefined);
5656
});
5757

5858
it('executes doHover on plugins', async () => {

0 commit comments

Comments
 (0)