Skip to content

Commit 01ba3a4

Browse files
authored
Tune FAR aggregation (#49581)
* Tune FAR aggregation In making the work queue management more intelligible, we centralized the redundancy check at dequeue time. As a result, the queue tends to get very large (~1.6M items for `SyntaxKind` in this repo) and dequeuing via `shift` is too slow to do that many times. This change makes a few tweaks: 1. Use `Project` identity for de-duping and only maintain a set of keys for `loadAncestorProjectTree` 2. Attempt to filter prior to insertion 3. Use `splice` if many consecutive work queue items will be discarded. On my box, this cuts FAR for `SyntaxKind` in parser.ts from 38 minutes to 20 seconds (we could do better, but effectively decided not to optimize this worst case scenario).
1 parent 734b982 commit 01ba3a4

File tree

1 file changed

+26
-16
lines changed

1 file changed

+26
-16
lines changed

src/server/session.ts

+26-16
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ namespace ts.server {
380380
// correct results to all other projects.
381381

382382
const defaultProjectResults = perProjectResults.get(defaultProject);
383-
if (defaultProjectResults?.[0].references[0]?.isDefinition === undefined) {
383+
if (defaultProjectResults?.[0]?.references[0]?.isDefinition === undefined) {
384384
// Clear all isDefinition properties
385385
perProjectResults.forEach(projectResults => {
386386
for (const referencedSymbol of projectResults) {
@@ -531,26 +531,35 @@ namespace ts.server {
531531
defaultDefinition :
532532
defaultProject.getLanguageService().getSourceMapper().tryGetSourcePosition(defaultDefinition!));
533533

534-
// Track which projects we have already searched so that we don't repeat searches.
535-
// We store the project key, rather than the project, because that's what `loadAncestorProjectTree` wants.
536-
// (For that same reason, we don't use `resultsMap` for this check.)
537-
const searchedProjects = new Set<string>();
534+
// The keys of resultsMap allow us to check which projects have already been searched, but we also
535+
// maintain a set of strings because that's what `loadAncestorProjectTree` wants.
536+
const searchedProjectKeys = new Set<string>();
538537

539538
onCancellation:
540539
while (queue.length) {
541540
while (queue.length) {
542541
if (cancellationToken.isCancellationRequested()) break onCancellation;
543542

543+
let skipCount = 0;
544+
for (; skipCount < queue.length && resultsMap.has(queue[skipCount].project); skipCount++);
545+
546+
if (skipCount === queue.length) {
547+
queue.length = 0;
548+
break;
549+
}
550+
551+
if (skipCount > 0) {
552+
queue.splice(0, skipCount);
553+
}
554+
555+
// NB: we may still skip if it's a project reference redirect
544556
const { project, location } = queue.shift()!;
545557

546558
if (isLocationProjectReferenceRedirect(project, location)) continue;
547559

548-
if (!tryAddToSet(searchedProjects, getProjectKey(project))) continue;
549-
550560
const projectResults = searchPosition(project, location);
551-
if (projectResults) {
552-
resultsMap.set(project, projectResults);
553-
}
561+
resultsMap.set(project, projectResults ?? emptyArray);
562+
searchedProjectKeys.add(getProjectKey(project));
554563
}
555564

556565
// At this point, we know about all projects passed in as arguments and any projects in which
@@ -559,10 +568,10 @@ namespace ts.server {
559568
// containing `initialLocation`.
560569
if (defaultDefinition) {
561570
// This seems to mean "load all projects downstream from any member of `seenProjects`".
562-
projectService.loadAncestorProjectTree(searchedProjects);
571+
projectService.loadAncestorProjectTree(searchedProjectKeys);
563572
projectService.forEachEnabledProject(project => {
564573
if (cancellationToken.isCancellationRequested()) return; // There's no mechanism for skipping the remaining projects
565-
if (searchedProjects.has(getProjectKey(project))) return; // Can loop forever without this (enqueue here, dequeue above, repeat)
574+
if (resultsMap.has(project)) return; // Can loop forever without this (enqueue here, dequeue above, repeat)
566575
const location = mapDefinitionInProject(defaultDefinition, project, getGeneratedDefinition, getSourceDefinition);
567576
if (location) {
568577
queue.push({ project, location });
@@ -573,9 +582,10 @@ namespace ts.server {
573582

574583
// In the common case where there's only one project, return a simpler result to make
575584
// it easier for the caller to skip post-processing.
576-
if (searchedProjects.size === 1) {
585+
if (resultsMap.size === 1) {
577586
const it = resultsMap.values().next();
578-
return it.done ? emptyArray : it.value; // There may not be any results at all
587+
Debug.assert(!it.done);
588+
return it.value;
579589
}
580590

581591
return resultsMap;
@@ -593,7 +603,7 @@ namespace ts.server {
593603
const originalScriptInfo = projectService.getScriptInfo(originalLocation.fileName)!;
594604

595605
for (const project of originalScriptInfo.containingProjects) {
596-
if (!project.isOrphan()) {
606+
if (!project.isOrphan() && !resultsMap.has(project)) { // Optimization: don't enqueue if will be discarded
597607
queue.push({ project, location: originalLocation });
598608
}
599609
}
@@ -602,7 +612,7 @@ namespace ts.server {
602612
if (symlinkedProjectsMap) {
603613
symlinkedProjectsMap.forEach((symlinkedProjects, symlinkedPath) => {
604614
for (const symlinkedProject of symlinkedProjects) {
605-
if (!symlinkedProject.isOrphan()) {
615+
if (!symlinkedProject.isOrphan() && !resultsMap.has(symlinkedProject)) { // Optimization: don't enqueue if will be discarded
606616
queue.push({ project: symlinkedProject, location: { fileName: symlinkedPath as string, pos: originalLocation.pos } });
607617
}
608618
}

0 commit comments

Comments
 (0)