Skip to content

Commit 26cb7ab

Browse files
crisbetopkozlowski-opensource
authored andcommitted
perf(migrations): speed up language service lookups (#49010)
For the module pruning and bootstrap API migration steps we depend heavily upon the TypeScript `LanguageService` which ends up being slow on a large project. E.g. in some large internal projects single-file lookups were taking around 30s. These changes introduce a wrapper around the `LanguageService` that we can use to trick it into not traversing the entire project every time. PR Close #49010
1 parent 642cc1c commit 26cb7ab

File tree

4 files changed

+165
-118
lines changed

4 files changed

+165
-118
lines changed

packages/core/schematics/ng-generate/standalone-migration/index.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ function standaloneMigration(
7474
skipLibCheck: true,
7575
skipDefaultLibCheck: true,
7676
});
77+
const referenceLookupExcludedFiles = /node_modules|\.ngtypecheck\.ts/;
7778
const program = createProgram({rootNames, host, options}) as NgtscProgram;
7879
const printer = ts.createPrinter();
7980

@@ -82,10 +83,9 @@ function standaloneMigration(
8283
pathToMigrate} has to be a directory. Cannot run the standalone migration.`);
8384
}
8485

85-
const sourceFiles = program.getTsProgram().getSourceFiles().filter(sourceFile => {
86-
return sourceFile.fileName.startsWith(pathToMigrate) &&
87-
canMigrateFile(basePath, sourceFile, program.getTsProgram());
88-
});
86+
const sourceFiles = program.getTsProgram().getSourceFiles().filter(
87+
sourceFile => sourceFile.fileName.startsWith(pathToMigrate) &&
88+
canMigrateFile(basePath, sourceFile, program.getTsProgram()));
8989

9090
if (sourceFiles.length === 0) {
9191
return 0;
@@ -95,14 +95,17 @@ function standaloneMigration(
9595
let filesToRemove: Set<ts.SourceFile>|null = null;
9696

9797
if (schematicOptions.mode === MigrationMode.pruneModules) {
98-
const result = pruneNgModules(program, host, basePath, rootNames, sourceFiles, printer);
98+
const result = pruneNgModules(
99+
program, host, basePath, rootNames, sourceFiles, printer, undefined,
100+
referenceLookupExcludedFiles);
99101
pendingChanges = result.pendingChanges;
100102
filesToRemove = result.filesToRemove;
101103
} else if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
102-
pendingChanges =
103-
toStandaloneBootstrap(program, host, basePath, rootNames, sourceFiles, printer);
104+
pendingChanges = toStandaloneBootstrap(
105+
program, host, basePath, rootNames, sourceFiles, printer, undefined,
106+
referenceLookupExcludedFiles);
104107
} else {
105-
/** MigrationMode.toStandalone */
108+
// This shouldn't happen, but default to `MigrationMode.toStandalone` just in case.
106109
pendingChanges = toStandalone(sourceFiles, program, printer);
107110
}
108111

packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ import ts from 'typescript';
1212
import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators';
1313
import {closestNode} from '../../utils/typescript/nodes';
1414

15-
import {ChangeTracker, createLanguageService, findClassDeclaration, findLiteralProperty, getNodeLookup, ImportRemapper, offsetsToNodes, UniqueItemTracker} from './util';
16-
17-
/** Mapping between a file name and spans for node references inside of it. */
18-
type ReferencesByFile = Map<string, [start: number, end: number][]>;
15+
import {ChangeTracker, findClassDeclaration, findLiteralProperty, getNodeLookup, ImportRemapper, offsetsToNodes, ReferenceResolver, UniqueItemTracker} from './util';
1916

2017
/** Keeps track of the places from which we need to remove AST nodes. */
2118
interface RemovalLocations {
@@ -28,11 +25,13 @@ interface RemovalLocations {
2825

2926
export function pruneNgModules(
3027
program: NgtscProgram, host: ts.CompilerHost, basePath: string, rootFileNames: string[],
31-
sourceFiles: ts.SourceFile[], printer: ts.Printer, importRemapper?: ImportRemapper) {
28+
sourceFiles: ts.SourceFile[], printer: ts.Printer, importRemapper?: ImportRemapper,
29+
referenceLookupExcludedFiles?: RegExp) {
3230
const filesToRemove = new Set<ts.SourceFile>();
3331
const tracker = new ChangeTracker(printer, importRemapper);
3432
const typeChecker = program.getTsProgram().getTypeChecker();
35-
const languageService = createLanguageService(program, host, rootFileNames, basePath);
33+
const referenceResolver =
34+
new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
3635
const removalLocations: RemovalLocations = {
3736
arrays: new UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>(),
3837
imports: new UniqueItemTracker<ts.NamedImports, ts.Node>(),
@@ -43,7 +42,7 @@ export function pruneNgModules(
4342

4443
sourceFiles.forEach(function walk(node: ts.Node) {
4544
if (ts.isClassDeclaration(node) && canRemoveClass(node, typeChecker)) {
46-
collectRemovalLocations(node, removalLocations, languageService, program);
45+
collectRemovalLocations(node, removalLocations, referenceResolver, program);
4746
removalLocations.classes.add(node);
4847
}
4948
node.forEachChild(walk);
@@ -73,13 +72,13 @@ export function pruneNgModules(
7372
* Collects all the nodes that a module needs to be removed from.
7473
* @param ngModule Module being removed.
7574
* @param removalLocations
76-
* @param languageService
75+
* @param referenceResolver
7776
* @param program
7877
*/
7978
function collectRemovalLocations(
8079
ngModule: ts.ClassDeclaration, removalLocations: RemovalLocations,
81-
languageService: ts.LanguageService, program: NgtscProgram) {
82-
const refsByFile = extractReferences(ngModule, languageService);
80+
referenceResolver: ReferenceResolver, program: NgtscProgram) {
81+
const refsByFile = referenceResolver.findReferencesInProject(ngModule.name!);
8382
const tsProgram = program.getTsProgram();
8483
const nodes = new Set<ts.Node>();
8584

@@ -295,34 +294,6 @@ function canRemoveFile(sourceFile: ts.SourceFile, classesToBeRemoved: Set<ts.Cla
295294
return true;
296295
}
297296

298-
299-
/**
300-
* Finds all the locations in a file where a node is referenced.
301-
* @param node Node that is being looked up.
302-
* @param languageService Language service used to find the references.
303-
*/
304-
function extractReferences(
305-
node: ts.ClassDeclaration, languageService: ts.LanguageService): ReferencesByFile {
306-
const result: ReferencesByFile = new Map();
307-
const referencedSymbols =
308-
languageService.findReferences(node.getSourceFile().fileName, node.name!.getStart()) || [];
309-
310-
for (const symbol of referencedSymbols) {
311-
for (const ref of symbol.references) {
312-
if (!ref.isDefinition || symbol.definition.kind === ts.ScriptElementKind.alias) {
313-
if (!result.has(ref.fileName)) {
314-
result.set(ref.fileName, []);
315-
}
316-
317-
result.get(ref.fileName)!.push(
318-
[ref.textSpan.start, ref.textSpan.start + ref.textSpan.length]);
319-
}
320-
}
321-
}
322-
323-
return result;
324-
}
325-
326297
/**
327298
* Gets whether an AST node contains another AST node.
328299
* @param parent Parent node that may contain the child.

packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {getAngularDecorators} from '../../utils/ng_decorators';
1616
import {closestNode} from '../../utils/typescript/nodes';
1717

1818
import {convertNgModuleDeclarationToStandalone, extractDeclarationsFromModule, findTestObjectsToMigrate, migrateTestDeclarations} from './to-standalone';
19-
import {ChangeTracker, createLanguageService, findClassDeclaration, findLiteralProperty, getNodeLookup, getRelativeImportPath, ImportRemapper, NamedClassDeclaration, NodeLookup, offsetsToNodes, UniqueItemTracker} from './util';
19+
import {ChangeTracker, findClassDeclaration, findLiteralProperty, getNodeLookup, getRelativeImportPath, ImportRemapper, NamedClassDeclaration, NodeLookup, offsetsToNodes, ReferenceResolver, UniqueItemTracker} from './util';
2020

2121
/** Information extracted from a `bootstrapModule` call necessary to migrate it. */
2222
interface BootstrapCallAnalysis {
@@ -34,11 +34,13 @@ interface BootstrapCallAnalysis {
3434

3535
export function toStandaloneBootstrap(
3636
program: NgtscProgram, host: ts.CompilerHost, basePath: string, rootFileNames: string[],
37-
sourceFiles: ts.SourceFile[], printer: ts.Printer, importRemapper?: ImportRemapper) {
37+
sourceFiles: ts.SourceFile[], printer: ts.Printer, importRemapper?: ImportRemapper,
38+
referenceLookupExcludedFiles?: RegExp) {
3839
const tracker = new ChangeTracker(printer, importRemapper);
3940
const typeChecker = program.getTsProgram().getTypeChecker();
4041
const templateTypeChecker = program.compiler.getTemplateTypeChecker();
41-
const languageService = createLanguageService(program, host, rootFileNames, basePath);
42+
const referenceResolver =
43+
new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
4244
const bootstrapCalls: BootstrapCallAnalysis[] = [];
4345
const testObjects: ts.ObjectLiteralExpression[] = [];
4446
const allDeclarations: Reference<ts.ClassDeclaration>[] = [];
@@ -62,7 +64,7 @@ export function toStandaloneBootstrap(
6264

6365
for (const call of bootstrapCalls) {
6466
allDeclarations.push(...call.declarations);
65-
migrateBootstrapCall(call, tracker, languageService, typeChecker, printer);
67+
migrateBootstrapCall(call, tracker, referenceResolver, typeChecker, printer);
6668
}
6769

6870
// The previous migrations explicitly skip over bootstrapped
@@ -133,12 +135,12 @@ function analyzeBootstrapCall(
133135
* Converts a `bootstrapModule` call to `bootstrapApplication`.
134136
* @param analysis Analysis result of the call.
135137
* @param tracker Tracker in which to register the changes.
136-
* @param languageService
138+
* @param referenceResolver
137139
* @param typeChecker
138140
* @param printer
139141
*/
140142
function migrateBootstrapCall(
141-
analysis: BootstrapCallAnalysis, tracker: ChangeTracker, languageService: ts.LanguageService,
143+
analysis: BootstrapCallAnalysis, tracker: ChangeTracker, referenceResolver: ReferenceResolver,
142144
typeChecker: ts.TypeChecker, printer: ts.Printer) {
143145
const sourceFile = analysis.call.getSourceFile();
144146
const moduleSourceFile = analysis.metadata.getSourceFile();
@@ -168,14 +170,14 @@ function migrateBootstrapCall(
168170
providersInNewCall.push(ts.factory.createSpreadElement(providers.initializer));
169171
}
170172

171-
addNodesToCopy(sourceFile, providers, nodeLookup, tracker, nodesToCopy, languageService);
173+
addNodesToCopy(sourceFile, providers, nodeLookup, tracker, nodesToCopy, referenceResolver);
172174
}
173175

174176
if (imports && ts.isPropertyAssignment(imports)) {
175177
nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
176178
migrateImportsForBootstrapCall(
177179
sourceFile, imports, nodeLookup, moduleImportsInNewCall, providersInNewCall, tracker,
178-
nodesToCopy, languageService, typeChecker);
180+
nodesToCopy, referenceResolver, typeChecker);
179181
}
180182

181183
if (nodesToCopy.size > 0) {
@@ -253,13 +255,13 @@ function replaceBootstrapCallExpression(
253255
* @param providersInNewCall Array keeping track of the providers in the new call.
254256
* @param tracker Tracker in which changes to files are being stored.
255257
* @param nodesToCopy Nodes that should be copied to the new file.
256-
* @param languageService
258+
* @param referenceResolver
257259
* @param typeChecker
258260
*/
259261
function migrateImportsForBootstrapCall(
260262
sourceFile: ts.SourceFile, imports: ts.PropertyAssignment, nodeLookup: NodeLookup,
261263
importsForNewCall: ts.Expression[], providersInNewCall: ts.Expression[], tracker: ChangeTracker,
262-
nodesToCopy: Set<ts.Node>, languageService: ts.LanguageService,
264+
nodesToCopy: Set<ts.Node>, referenceResolver: ReferenceResolver,
263265
typeChecker: ts.TypeChecker): void {
264266
if (!ts.isArrayLiteralExpression(imports.initializer)) {
265267
importsForNewCall.push(imports.initializer);
@@ -282,9 +284,9 @@ function migrateImportsForBootstrapCall(
282284
tracker.addImport(sourceFile, 'provideRouter', '@angular/router'), [],
283285
[element.arguments[0], ...features]));
284286
addNodesToCopy(
285-
sourceFile, element.arguments[0], nodeLookup, tracker, nodesToCopy, languageService);
287+
sourceFile, element.arguments[0], nodeLookup, tracker, nodesToCopy, referenceResolver);
286288
if (options) {
287-
addNodesToCopy(sourceFile, options, nodeLookup, tracker, nodesToCopy, languageService);
289+
addNodesToCopy(sourceFile, options, nodeLookup, tracker, nodesToCopy, referenceResolver);
288290
}
289291
continue;
290292
}
@@ -326,7 +328,7 @@ function migrateImportsForBootstrapCall(
326328
decorators.every(
327329
({name}) => name !== 'Directive' && name !== 'Component' && name !== 'Pipe')) {
328330
importsForNewCall.push(element);
329-
addNodesToCopy(sourceFile, element, nodeLookup, tracker, nodesToCopy, languageService);
331+
addNodesToCopy(sourceFile, element, nodeLookup, tracker, nodesToCopy, referenceResolver);
330332
}
331333
}
332334
}
@@ -444,12 +446,12 @@ function getRouterModuleForRootFeatures(
444446
* @param nodeLookup Map used to look up nodes based on their positions in a file.
445447
* @param tracker Tracker in which changes to files are stored.
446448
* @param nodesToCopy Set that keeps track of the nodes being copied.
447-
* @param languageService
449+
* @param referenceResolver
448450
*/
449451
function addNodesToCopy(
450452
targetFile: ts.SourceFile, rootNode: ts.Node, nodeLookup: NodeLookup, tracker: ChangeTracker,
451-
nodesToCopy: Set<ts.Node>, languageService: ts.LanguageService): void {
452-
const refs = findAllSameFileReferences(rootNode, nodeLookup, languageService);
453+
nodesToCopy: Set<ts.Node>, referenceResolver: ReferenceResolver): void {
454+
const refs = findAllSameFileReferences(rootNode, nodeLookup, referenceResolver);
453455

454456
for (const ref of refs) {
455457
const importSpecifier = closestOrSelf(ref, ts.isImportSpecifier);
@@ -504,10 +506,10 @@ function addNodesToCopy(
504506
* Finds all the nodes referenced within the root node in the same file.
505507
* @param rootNode Node from which to start looking for references.
506508
* @param nodeLookup Map used to look up nodes based on their positions in a file.
507-
* @param languageService
509+
* @param referenceResolver
508510
*/
509511
function findAllSameFileReferences(
510-
rootNode: ts.Node, nodeLookup: NodeLookup, languageService: ts.LanguageService): Set<ts.Node> {
512+
rootNode: ts.Node, nodeLookup: NodeLookup, referenceResolver: ReferenceResolver): Set<ts.Node> {
511513
const results = new Set<ts.Node>();
512514
const excludeStart = rootNode.getStart();
513515
const excludeEnd = rootNode.getEnd();
@@ -518,8 +520,8 @@ function findAllSameFileReferences(
518520
return;
519521
}
520522

521-
const refs =
522-
referencesToNodeWithinSameFile(node, nodeLookup, excludeStart, excludeEnd, languageService);
523+
const refs = referencesToNodeWithinSameFile(
524+
node, nodeLookup, excludeStart, excludeEnd, referenceResolver);
523525

524526
if (refs === null) {
525527
return;
@@ -551,38 +553,20 @@ function findAllSameFileReferences(
551553
* @param nodeLookup Map used to look up nodes based on their positions in a file.
552554
* @param excludeStart Start of a range that should be excluded from the results.
553555
* @param excludeEnd End of a range that should be excluded from the results.
554-
* @param languageService
556+
* @param referenceResolver
555557
*/
556558
function referencesToNodeWithinSameFile(
557559
node: ts.Identifier, nodeLookup: NodeLookup, excludeStart: number, excludeEnd: number,
558-
languageService: ts.LanguageService): Set<ts.Node>|null {
559-
const sourceFile = node.getSourceFile();
560-
const fileName = sourceFile.fileName;
561-
const highlights = languageService.getDocumentHighlights(fileName, node.getStart(), [fileName]);
562-
563-
if (highlights) {
564-
const offsets: [start: number, end: number][] = [];
565-
566-
for (const file of highlights) {
567-
// We are pretty much guaranteed to only have one match from the current file since it is
568-
// the only one being passed in `getDocumentHighlight`, but we check here just in case.
569-
if (file.fileName === fileName) {
570-
for (const {textSpan: {start, length}, kind} of file.highlightSpans) {
571-
const end = start + length;
572-
if (kind !== ts.HighlightSpanKind.none &&
573-
isOutsideRange(excludeStart, excludeEnd, start, end)) {
574-
offsets.push([start, end]);
575-
}
576-
}
577-
}
578-
}
560+
referenceResolver: ReferenceResolver): Set<ts.Node>|null {
561+
const offsets =
562+
referenceResolver.findSameFileReferences(node, node.getSourceFile().fileName)
563+
.filter(([start, end]) => isOutsideRange(excludeStart, excludeEnd, start, end));
579564

580-
if (offsets.length > 0) {
581-
const nodes = offsetsToNodes(nodeLookup, offsets, new Set());
565+
if (offsets.length > 0) {
566+
const nodes = offsetsToNodes(nodeLookup, offsets, new Set());
582567

583-
if (nodes.size > 0) {
584-
return nodes;
585-
}
568+
if (nodes.size > 0) {
569+
return nodes;
586570
}
587571
}
588572

0 commit comments

Comments
 (0)