Skip to content

Commit 7760021

Browse files
authored
(perf) add priority queue (#1144)
Give certain requests priority. If a request doesn't have priority, we first wait 1 second to 1. let higher priority requests get through first 2. wait for possible document changes, which make the request wait again This hopefully makes perceived responsiveness of the language server better #1098, #1130, #1139
1 parent 75b9f3e commit 7760021

File tree

2 files changed

+109
-26
lines changed

2 files changed

+109
-26
lines changed

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

Lines changed: 105 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
5353
filterIncompleteCompletions: true,
5454
definitionLinkSupport: false
5555
};
56+
private deferredRequests: Record<string, [number, Promise<any>]> = {};
5657

5758
constructor(private documentsManager: DocumentManager) {}
5859

@@ -64,14 +65,23 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
6465
this.plugins.push(plugin);
6566
}
6667

68+
didUpdateDocument() {
69+
this.deferredRequests = {};
70+
}
71+
6772
async getDiagnostics(textDocument: TextDocumentIdentifier): Promise<Diagnostic[]> {
6873
const document = this.getDocument(textDocument.uri);
6974
if (!document) {
7075
throw new Error('Cannot call methods on an unopened document');
7176
}
7277

7378
return flatten(
74-
await this.execute<Diagnostic[]>('getDiagnostics', [document], ExecuteMode.Collect)
79+
await this.execute<Diagnostic[]>(
80+
'getDiagnostics',
81+
[document],
82+
ExecuteMode.Collect,
83+
'high'
84+
)
7585
);
7686
}
7787

@@ -81,7 +91,12 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
8191
throw new Error('Cannot call methods on an unopened document');
8292
}
8393

84-
return this.execute<Hover>('doHover', [document, position], ExecuteMode.FirstNonNull);
94+
return this.execute<Hover>(
95+
'doHover',
96+
[document, position],
97+
ExecuteMode.FirstNonNull,
98+
'high'
99+
);
85100
}
86101

87102
async getCompletions(
@@ -99,7 +114,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
99114
await this.execute<CompletionList>(
100115
'getCompletions',
101116
[document, position, completionContext, cancellationToken],
102-
ExecuteMode.Collect
117+
ExecuteMode.Collect,
118+
'high'
103119
)
104120
).filter((completion) => completion != null);
105121

@@ -141,7 +157,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
141157
const result = await this.execute<CompletionItem>(
142158
'resolveCompletion',
143159
[document, completionItem, cancellationToken],
144-
ExecuteMode.FirstNonNull
160+
ExecuteMode.FirstNonNull,
161+
'high'
145162
);
146163

147164
return result ?? completionItem;
@@ -160,7 +177,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
160177
await this.execute<TextEdit[]>(
161178
'formatDocument',
162179
[document, options],
163-
ExecuteMode.Collect
180+
ExecuteMode.Collect,
181+
'high'
164182
)
165183
);
166184
}
@@ -177,7 +195,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
177195
return this.execute<string | null>(
178196
'doTagComplete',
179197
[document, position],
180-
ExecuteMode.FirstNonNull
198+
ExecuteMode.FirstNonNull,
199+
'high'
181200
);
182201
}
183202

@@ -191,7 +210,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
191210
await this.execute<ColorInformation[]>(
192211
'getDocumentColors',
193212
[document],
194-
ExecuteMode.Collect
213+
ExecuteMode.Collect,
214+
'low'
195215
)
196216
);
197217
}
@@ -210,7 +230,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
210230
await this.execute<ColorPresentation[]>(
211231
'getColorPresentations',
212232
[document, range, color],
213-
ExecuteMode.Collect
233+
ExecuteMode.Collect,
234+
'low'
214235
)
215236
);
216237
}
@@ -228,7 +249,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
228249
await this.execute<SymbolInformation[]>(
229250
'getDocumentSymbols',
230251
[document, cancellationToken],
231-
ExecuteMode.Collect
252+
ExecuteMode.Collect,
253+
'low'
232254
)
233255
);
234256
}
@@ -246,7 +268,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
246268
await this.execute<DefinitionLink[]>(
247269
'getDefinitions',
248270
[document, position],
249-
ExecuteMode.Collect
271+
ExecuteMode.Collect,
272+
'high'
250273
)
251274
);
252275

@@ -274,7 +297,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
274297
await this.execute<CodeAction[]>(
275298
'getCodeActions',
276299
[document, range, context, cancellationToken],
277-
ExecuteMode.Collect
300+
ExecuteMode.Collect,
301+
'high'
278302
)
279303
);
280304
}
@@ -292,15 +316,17 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
292316
return await this.execute<WorkspaceEdit>(
293317
'executeCommand',
294318
[document, command, args],
295-
ExecuteMode.FirstNonNull
319+
ExecuteMode.FirstNonNull,
320+
'high'
296321
);
297322
}
298323

299324
async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
300325
return await this.execute<WorkspaceEdit>(
301326
'updateImports',
302327
[fileRename],
303-
ExecuteMode.FirstNonNull
328+
ExecuteMode.FirstNonNull,
329+
'high'
304330
);
305331
}
306332

@@ -316,7 +342,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
316342
return await this.execute<any>(
317343
'prepareRename',
318344
[document, position],
319-
ExecuteMode.FirstNonNull
345+
ExecuteMode.FirstNonNull,
346+
'high'
320347
);
321348
}
322349

@@ -333,7 +360,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
333360
return await this.execute<any>(
334361
'rename',
335362
[document, position, newName],
336-
ExecuteMode.FirstNonNull
363+
ExecuteMode.FirstNonNull,
364+
'high'
337365
);
338366
}
339367

@@ -350,7 +378,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
350378
return await this.execute<any>(
351379
'findReferences',
352380
[document, position, context],
353-
ExecuteMode.FirstNonNull
381+
ExecuteMode.FirstNonNull,
382+
'high'
354383
);
355384
}
356385

@@ -368,7 +397,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
368397
return await this.execute<any>(
369398
'getSignatureHelp',
370399
[document, position, context, cancellationToken],
371-
ExecuteMode.FirstNonNull
400+
ExecuteMode.FirstNonNull,
401+
'high'
372402
);
373403
}
374404

@@ -424,7 +454,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
424454
return await this.execute<SemanticTokens>(
425455
'getSemanticTokens',
426456
[document, range, cancellationToken],
427-
ExecuteMode.FirstNonNull
457+
ExecuteMode.FirstNonNull,
458+
'low'
428459
);
429460
}
430461

@@ -440,7 +471,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
440471
return await this.execute<LinkedEditingRanges>(
441472
'getLinkedEditingRanges',
442473
[document, position],
443-
ExecuteMode.FirstNonNull
474+
ExecuteMode.FirstNonNull,
475+
'high'
444476
);
445477
}
446478

@@ -463,21 +495,71 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
463495
private execute<T>(
464496
name: keyof LSProvider,
465497
args: any[],
466-
mode: ExecuteMode.FirstNonNull
498+
mode: ExecuteMode.FirstNonNull,
499+
priority: 'low' | 'high'
467500
): Promise<T | null>;
468501
private execute<T>(
469502
name: keyof LSProvider,
470503
args: any[],
471-
mode: ExecuteMode.Collect
504+
mode: ExecuteMode.Collect,
505+
priority: 'low' | 'high'
472506
): Promise<T[]>;
473-
private execute(name: keyof LSProvider, args: any[], mode: ExecuteMode.None): Promise<void>;
507+
private execute(
508+
name: keyof LSProvider,
509+
args: any[],
510+
mode: ExecuteMode.None,
511+
priority: 'low' | 'high'
512+
): Promise<void>;
474513
private async execute<T>(
475514
name: keyof LSProvider,
476515
args: any[],
477-
mode: ExecuteMode
516+
mode: ExecuteMode,
517+
priority: 'low' | 'high'
478518
): Promise<(T | null) | T[] | void> {
479519
const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function');
480520

521+
if (priority === 'low') {
522+
// If a request doesn't have priority, we first wait 1 second to
523+
// 1. let higher priority requests get through first
524+
// 2. wait for possible document changes, which make the request wait again
525+
// Due to waiting, low priority items should preferrably be those who do not
526+
// rely on positions or ranges and rather on the whole document only.
527+
const debounce = async (): Promise<boolean> => {
528+
const id = Math.random();
529+
this.deferredRequests[name] = [
530+
id,
531+
new Promise<void>((resolve, reject) => {
532+
setTimeout(() => {
533+
if (
534+
!this.deferredRequests[name] ||
535+
this.deferredRequests[name][0] === id
536+
) {
537+
resolve();
538+
} else {
539+
// We should not get into this case. According to the spec,
540+
// the language client // does not send another request
541+
// of the same type until the previous one is answered.
542+
reject();
543+
}
544+
}, 1000);
545+
})
546+
];
547+
try {
548+
await this.deferredRequests[name][1];
549+
if (!this.deferredRequests[name]) {
550+
return debounce();
551+
}
552+
return true;
553+
} catch (e) {
554+
return false;
555+
}
556+
};
557+
const shouldContinue = await debounce();
558+
if (!shouldContinue) {
559+
return;
560+
}
561+
}
562+
481563
switch (mode) {
482564
case ExecuteMode.FirstNonNull:
483565
for (const plugin of plugins) {

packages/language-server/src/server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,10 @@ export function startServer(options?: LSOptions) {
262262
});
263263

264264
connection.onDidCloseTextDocument((evt) => docManager.closeDocument(evt.textDocument.uri));
265-
connection.onDidChangeTextDocument((evt) =>
266-
docManager.updateDocument(evt.textDocument, evt.contentChanges)
267-
);
265+
connection.onDidChangeTextDocument((evt) => {
266+
docManager.updateDocument(evt.textDocument, evt.contentChanges);
267+
pluginHost.didUpdateDocument();
268+
});
268269
connection.onHover((evt) => pluginHost.doHover(evt.textDocument, evt.position));
269270
connection.onCompletion((evt, cancellationToken) =>
270271
pluginHost.getCompletions(evt.textDocument, evt.position, evt.context, cancellationToken)

0 commit comments

Comments
 (0)