Skip to content

Commit 11bc4bc

Browse files
MathiasWPclaude
andauthored
perf: micro-optimizations in hot paths (#2960)
* perf: micro-optimizations in hot paths for faster completions and diagnostics Replace O(n²) flatten() with Array.prototype.flat(), remove lodash flatten dependency from PluginHost, use Object.create instead of object spread in svelteNodeAt AST walking, cache getExistingImports per document, use RegExp.test() over .match(), replace regex with charCode check in possiblyComponent, use cached line offsets for lineCount, and optimize iteration in SnapshotManager. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: prettier formatting in PluginHost.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Create big-clocks-care.md * fix: prettier formatting in changeset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * address pr feedback: remove flatten util, use charCodeAt, fix code style - Replace all flatten() call sites with .flat()/.flatMap() and remove the flatten utility function entirely - Use charCodeAt(0) directly in possiblyComponent instead of char comparison - Use block syntax for single-line for loops in SnapshotManager - Remove ineffective existingImportsCache from CompletionProvider Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: prettier formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 95452d7 commit 11bc4bc

File tree

14 files changed

+136
-125
lines changed

14 files changed

+136
-125
lines changed

.changeset/big-clocks-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-language-server': patch
3+
---
4+
5+
perf: micro-optimizations in hot paths

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export abstract class ReadableDocument implements TextDocument {
7373
}
7474

7575
get lineCount(): number {
76-
return this.getText().split(/\r?\n/).length;
76+
return this.getLineOffsets().length;
7777
}
7878

7979
abstract languageId: string;

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

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { flatten } from 'lodash';
21
import { performance } from 'perf_hooks';
32
import {
43
CallHierarchyIncomingCall,
@@ -100,14 +99,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
10099
return [];
101100
}
102101

103-
return flatten(
102+
return (
104103
await this.execute<Diagnostic[]>(
105104
'getDiagnostics',
106105
[document, cancellationToken],
107106
ExecuteMode.Collect,
108107
'high'
109108
)
110-
);
109+
).flat();
111110
}
112111

113112
async doHover(textDocument: TextDocumentIdentifier, position: Position): Promise<Hover | null> {
@@ -190,9 +189,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
190189
}
191190
}
192191

193-
let flattenedCompletions = flatten(
194-
completions.map((completion) => completion.result.items)
195-
);
192+
let flattenedCompletions = completions.map((completion) => completion.result.items).flat();
196193
const isIncomplete = completions.reduce(
197194
(incomplete, completion) => incomplete || completion.result.isIncomplete,
198195
false as boolean
@@ -242,14 +239,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
242239
): Promise<TextEdit[]> {
243240
const document = this.getDocument(textDocument.uri);
244241

245-
return flatten(
242+
return (
246243
await this.execute<TextEdit[]>(
247244
'formatDocument',
248245
[document, options],
249246
ExecuteMode.Collect,
250247
'high'
251248
)
252-
);
249+
).flat();
253250
}
254251

255252
async doTagComplete(
@@ -269,14 +266,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
269266
async getDocumentColors(textDocument: TextDocumentIdentifier): Promise<ColorInformation[]> {
270267
const document = this.getDocument(textDocument.uri);
271268

272-
return flatten(
269+
return (
273270
await this.execute<ColorInformation[]>(
274271
'getDocumentColors',
275272
[document],
276273
ExecuteMode.Collect,
277274
'low'
278275
)
279-
);
276+
).flat();
280277
}
281278

282279
async getColorPresentations(
@@ -286,14 +283,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
286283
): Promise<ColorPresentation[]> {
287284
const document = this.getDocument(textDocument.uri);
288285

289-
return flatten(
286+
return (
290287
await this.execute<ColorPresentation[]>(
291288
'getColorPresentations',
292289
[document, range, color],
293290
ExecuteMode.Collect,
294291
'high'
295292
)
296-
);
293+
).flat();
297294
}
298295

299296
async getDocumentSymbols(
@@ -309,14 +306,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
309306
return [];
310307
}
311308

312-
return flatten(
309+
return (
313310
await this.execute<SymbolInformation[]>(
314311
'getDocumentSymbols',
315312
[document, cancellationToken],
316313
ExecuteMode.Collect,
317314
'high'
318315
)
319-
);
316+
).flat();
320317
}
321318

322319
private comparePosition(pos1: Position, pos2: Position) {
@@ -382,14 +379,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
382379
): Promise<DefinitionLink[] | Location[]> {
383380
const document = this.getDocument(textDocument.uri);
384381

385-
const definitions = flatten(
382+
const definitions = (
386383
await this.execute<DefinitionLink[]>(
387384
'getDefinitions',
388385
[document, position],
389386
ExecuteMode.Collect,
390387
'high'
391388
)
392-
);
389+
).flat();
393390

394391
if (this.pluginHostConfig.definitionLinkSupport) {
395392
return definitions;
@@ -408,14 +405,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
408405
): Promise<CodeAction[]> {
409406
const document = this.getDocument(textDocument.uri);
410407

411-
const actions = flatten(
408+
const actions = (
412409
await this.execute<CodeAction[]>(
413410
'getCodeActions',
414411
[document, range, context, cancellationToken],
415412
ExecuteMode.Collect,
416413
'high'
417414
)
418-
);
415+
).flat();
419416
// Sort Svelte actions below other actions as they are often less relevant
420417
actions.sort((a, b) => {
421418
const aPrio = a.title.startsWith('(svelte)') ? 1 : 0;
@@ -700,20 +697,20 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
700697
ExecuteMode.Collect,
701698
'smart'
702699
);
703-
return flatten(result.filter(Boolean));
700+
return result.filter(Boolean).flat();
704701
}
705702

706703
async getFoldingRanges(textDocument: TextDocumentIdentifier): Promise<FoldingRange[]> {
707704
const document = this.getDocument(textDocument.uri);
708705

709-
const result = flatten(
706+
const result = (
710707
await this.execute<FoldingRange[]>(
711708
'getFoldingRanges',
712709
[document],
713710
ExecuteMode.Collect,
714711
'high'
715712
)
716-
);
713+
).flat();
717714

718715
return result;
719716
}

packages/language-server/src/plugins/css/global-vars.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FSWatcher, watch } from 'chokidar';
22
import { readFile } from 'fs';
33
import globrex from 'globrex';
44
import { join } from 'path';
5-
import { flatten, isNotNullOrUndefined, normalizePath } from '../../utils';
5+
import { isNotNullOrUndefined, normalizePath } from '../../utils';
66

77
const varRegex = /^\s*(--\w+.*?):\s*?([^;]*)/;
88

@@ -95,6 +95,6 @@ export class GlobalVars {
9595
}
9696

9797
getGlobalVars(): GlobalVar[] {
98-
return flatten([...this.globalVars.values()]);
98+
return [...this.globalVars.values()].flat();
9999
}
100100
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Hover, Position } from 'vscode-languageserver';
22
import { SvelteDocument } from '../SvelteDocument';
33
import { documentation, SvelteTag, getLatestOpeningTag } from './SvelteTags';
4-
import { flatten } from '../../../utils';
54
import { Document } from '../../../lib/documents';
65
import { AttributeContext, getAttributeContextAtPosition } from '../../../lib/documents/parseHtml';
76
import { attributeCanHaveEventModifier, inStyleOrScript } from './utils';
@@ -117,7 +116,7 @@ const tagPossibilities: Array<{ tag: SvelteTag | ':else'; values: string[] }> =
117116
];
118117

119118
const tagRegexp = new RegExp(
120-
`[\\s\\S]*{\\s*(${flatten(tagPossibilities.map((p) => p.values)).join('|')})(\\s|})`
119+
`[\\s\\S]*{\\s*(${tagPossibilities.flatMap((p) => p.values).join('|')})(\\s|})`
121120
);
122121
function getEventModifierHoverInfo(
123122
attributeContext: AttributeContext,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,11 +399,11 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
399399
return;
400400
}
401401
const parent = foundNode;
402-
// Spread so the "parent" property isn't added to the original ast,
403-
// causing an infinite loop
404-
foundNode = { ...node };
402+
// Use Object.create so the "parent" property isn't added to the original ast,
403+
// causing an infinite loop. This is cheaper than spreading all properties.
404+
foundNode = Object.create(node);
405405
if (parent) {
406-
foundNode.parent = parent;
406+
foundNode!.parent = parent;
407407
}
408408
}
409409
});

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

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ export class GlobalSnapshotsManager {
3030

3131
getByPrefix(path: string) {
3232
path = this.getCanonicalFileName(normalizePath(path));
33-
return Array.from(this.documents.entries())
34-
.filter((doc) => doc[0].startsWith(path))
35-
.map((doc) => doc[1]);
33+
const results: DocumentSnapshot[] = [];
34+
for (const [key, value] of this.documents.entries()) {
35+
if (key.startsWith(path)) {
36+
results.push(value);
37+
}
38+
}
39+
return results;
3640
}
3741

3842
set(fileName: string, document: DocumentSnapshot) {
@@ -273,19 +277,31 @@ export class SnapshotManager {
273277
if (date.getTime() - this.lastLogged.getTime() > 60_000) {
274278
this.lastLogged = date;
275279

276-
const allFiles = Array.from(
277-
new Set([...this.projectFileToOriginalCasing.keys(), ...this.documents.keys()])
278-
);
280+
const allFiles = new Set<string>();
281+
for (const key of this.projectFileToOriginalCasing.keys()) {
282+
allFiles.add(key);
283+
}
284+
for (const key of this.documents.keys()) {
285+
allFiles.add(key);
286+
}
287+
288+
let svelteCount = 0;
289+
let nodeModulesCount = 0;
290+
for (const name of allFiles) {
291+
if (name.endsWith('.svelte')) {
292+
svelteCount++;
293+
}
294+
if (name.includes('node_modules')) {
295+
nodeModulesCount++;
296+
}
297+
}
298+
279299
Logger.log(
280300
'SnapshotManager File Statistics:\n' +
281301
`Project files: ${this.projectFileToOriginalCasing.size}\n` +
282-
`Svelte files: ${
283-
allFiles.filter((name) => name.endsWith('.svelte')).length
284-
}\n` +
285-
`From node_modules: ${
286-
allFiles.filter((name) => name.includes('node_modules')).length
287-
}\n` +
288-
`Total: ${allFiles.length}`
302+
`Svelte files: ${svelteCount}\n` +
303+
`From node_modules: ${nodeModulesCount}\n` +
304+
`Total: ${allFiles.size}`
289305
);
290306
}
291307
}

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

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
import { LSConfigManager } from '../../../ls-config';
2727
import {
2828
createGetCanonicalFileName,
29-
flatten,
3029
getIndent,
3130
isNotNullOrUndefined,
3231
isPositionEqual,
@@ -1255,7 +1254,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
12551254
fixAllDescription: FIX_IMPORT_FIX_DESCRIPTION
12561255
})) ?? [];
12571256

1258-
return flatten(completion.entries.filter((c) => c.name === storeIdentifier).map(toFix));
1257+
return completion.entries.filter((c) => c.name === storeIdentifier).flatMap(toFix);
12591258
}
12601259

12611260
private getAddLangTSCodeAction(
@@ -1449,43 +1448,41 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
14491448
originalRange: Range,
14501449
textRange: { pos: number; end: number }
14511450
) {
1452-
return flatten(
1453-
applicableRefactors.map((applicableRefactor) => {
1454-
if (applicableRefactor.inlineable === false) {
1455-
return [
1456-
CodeAction.create(applicableRefactor.description, {
1457-
title: applicableRefactor.description,
1458-
command: applicableRefactor.name,
1459-
arguments: [
1460-
document.uri,
1461-
<RefactorArgs>{
1462-
type: 'refactor',
1463-
textRange,
1464-
originalRange,
1465-
refactorName: 'Extract Symbol'
1466-
}
1467-
]
1468-
})
1469-
];
1470-
}
1471-
1472-
return applicableRefactor.actions.map((action) => {
1473-
return CodeAction.create(action.description, {
1474-
title: action.description,
1475-
command: action.name,
1451+
return applicableRefactors.flatMap((applicableRefactor) => {
1452+
if (applicableRefactor.inlineable === false) {
1453+
return [
1454+
CodeAction.create(applicableRefactor.description, {
1455+
title: applicableRefactor.description,
1456+
command: applicableRefactor.name,
14761457
arguments: [
14771458
document.uri,
14781459
<RefactorArgs>{
14791460
type: 'refactor',
14801461
textRange,
14811462
originalRange,
1482-
refactorName: applicableRefactor.name
1463+
refactorName: 'Extract Symbol'
14831464
}
14841465
]
1485-
});
1466+
})
1467+
];
1468+
}
1469+
1470+
return applicableRefactor.actions.map((action) => {
1471+
return CodeAction.create(action.description, {
1472+
title: action.description,
1473+
command: action.name,
1474+
arguments: [
1475+
document.uri,
1476+
<RefactorArgs>{
1477+
type: 'refactor',
1478+
textRange,
1479+
originalRange,
1480+
refactorName: applicableRefactor.name
1481+
}
1482+
]
14861483
});
1487-
})
1488-
);
1484+
});
1485+
});
14891486
}
14901487

14911488
async executeCommand(

0 commit comments

Comments
 (0)