feat(language-service): add perf tracing to LanguageService (#41319)

Adds perf tracing for the public methods in LanguageService. If the log level is verbose or higher,
trace performance results to the tsServer logger. This logger is implemented on the extension side
in angular/vscode-ng-language-service.

PR Close #41319
This commit is contained in:
Zach Arend 2021-03-19 07:56:13 -07:00 committed by Alex Rickabaugh
parent a371646a37
commit 90f85da2de
4 changed files with 240 additions and 138 deletions

View File

@ -110,6 +110,40 @@ export enum PerfPhase {
*/ */
LsReferencesAndRenames, LsReferencesAndRenames,
/**
* Time spent by the Angular Language Service calculating a "quick info" operation.
*/
LsQuickInfo,
/**
* Time spent by the Angular Language Service calculating a "get type definition" or "get
* definition" operation.
*/
LsDefinition,
/**
* Time spent by the Angular Language Service calculating a "get completions" (AKA autocomplete)
* operation.
*/
LsCompletions,
/**
* Time spent by the Angular Language Service calculating a "view template typecheck block"
* operation.
*/
LsTcb,
/**
* Time spent by the Angular Language Service calculating diagnostics.
*/
LsDiagnostics,
/**
* Time spent by the Angular Language Service calculating a "get component locations for template"
* operation.
*/
LsComponentLocations,
/** /**
* Tracks the number of `PerfPhase`s, and must appear at the end of the list. * Tracks the number of `PerfPhase`s, and must appear at the end of the list.
*/ */

View File

@ -11,6 +11,7 @@ import {CompilerOptions, ConfigurationHost, readConfiguration} from '@angular/co
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics'; import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf';
import {isNamedClassDeclaration} from '@angular/compiler-cli/src/ngtsc/reflection'; import {isNamedClassDeclaration} from '@angular/compiler-cli/src/ngtsc/reflection';
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck'; import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
@ -63,51 +64,54 @@ export class LanguageService {
} }
getSemanticDiagnostics(fileName: string): ts.Diagnostic[] { getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
const compiler = this.compilerFactory.getOrCreate(); return this.withCompilerAndPerfTracing(PerfPhase.LsDiagnostics, (compiler) => {
const ttc = compiler.getTemplateTypeChecker(); const ttc = compiler.getTemplateTypeChecker();
const diagnostics: ts.Diagnostic[] = []; const diagnostics: ts.Diagnostic[] = [];
if (isTypeScriptFile(fileName)) { if (isTypeScriptFile(fileName)) {
const program = compiler.getNextProgram(); const program = compiler.getNextProgram();
const sourceFile = program.getSourceFile(fileName); const sourceFile = program.getSourceFile(fileName);
if (sourceFile) { if (sourceFile) {
const ngDiagnostics = compiler.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile); const ngDiagnostics = compiler.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile);
// There are several kinds of diagnostics returned by `NgCompiler` for a source file: // There are several kinds of diagnostics returned by `NgCompiler` for a source file:
// //
// 1. Angular-related non-template diagnostics from decorated classes within that file. // 1. Angular-related non-template diagnostics from decorated classes within that
// 2. Template diagnostics for components with direct inline templates (a string literal). // file.
// 3. Template diagnostics for components with indirect inline templates (templates computed // 2. Template diagnostics for components with direct inline templates (a string
// by expression). // literal).
// 4. Template diagnostics for components with external templates. // 3. Template diagnostics for components with indirect inline templates (templates
// // computed
// When showing diagnostics for a TS source file, we want to only include kinds 1 and 2 - // by expression).
// those diagnostics which are reported at a location within the TS file itself. Diagnostics // 4. Template diagnostics for components with external templates.
// for external templates will be shown when editing that template file (the `else` block) //
// below. // When showing diagnostics for a TS source file, we want to only include kinds 1 and
// // 2 - those diagnostics which are reported at a location within the TS file itself.
// Currently, indirect inline template diagnostics (kind 3) are not shown at all by the // Diagnostics for external templates will be shown when editing that template file
// Language Service, because there is no sensible location in the user's code for them. Such // (the `else` block) below.
// templates are an edge case, though, and should not be common. //
// // Currently, indirect inline template diagnostics (kind 3) are not shown at all by
// TODO(alxhub): figure out a good user experience for indirect template diagnostics and // the Language Service, because there is no sensible location in the user's code for
// show them from within the Language Service. // them. Such templates are an edge case, though, and should not be common.
diagnostics.push(...ngDiagnostics.filter( //
diag => diag.file !== undefined && diag.file.fileName === sourceFile.fileName)); // TODO(alxhub): figure out a good user experience for indirect template diagnostics
} // and show them from within the Language Service.
} else { diagnostics.push(...ngDiagnostics.filter(
const components = compiler.getComponentsWithTemplateFile(fileName); diag => diag.file !== undefined && diag.file.fileName === sourceFile.fileName));
for (const component of components) { }
if (ts.isClassDeclaration(component)) { } else {
diagnostics.push(...ttc.getDiagnosticsForComponent(component)); const components = compiler.getComponentsWithTemplateFile(fileName);
for (const component of components) {
if (ts.isClassDeclaration(component)) {
diagnostics.push(...ttc.getDiagnosticsForComponent(component));
}
} }
} }
} return diagnostics;
this.compilerFactory.registerLastKnownProgram(); });
return diagnostics;
} }
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan
|undefined { |undefined {
return this.withCompiler((compiler) => { return this.withCompilerAndPerfTracing(PerfPhase.LsDefinition, (compiler) => {
if (!isInAngularContext(compiler.getNextProgram(), fileName, position)) { if (!isInAngularContext(compiler.getNextProgram(), fileName, position)) {
return undefined; return undefined;
} }
@ -118,7 +122,7 @@ export class LanguageService {
getTypeDefinitionAtPosition(fileName: string, position: number): getTypeDefinitionAtPosition(fileName: string, position: number):
readonly ts.DefinitionInfo[]|undefined { readonly ts.DefinitionInfo[]|undefined {
return this.withCompiler((compiler) => { return this.withCompilerAndPerfTracing(PerfPhase.LsDefinition, (compiler) => {
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
return undefined; return undefined;
} }
@ -128,64 +132,70 @@ export class LanguageService {
} }
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined { getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined {
return this.withCompiler((compiler) => { return this.withCompilerAndPerfTracing(PerfPhase.LsQuickInfo, (compiler) => {
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { return this.getQuickInfoAtPositionImpl(fileName, position, compiler);
return undefined;
}
const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler);
if (templateInfo === undefined) {
return undefined;
}
const positionDetails = getTargetAtPosition(templateInfo.template, position);
if (positionDetails === null) {
return undefined;
}
// Because we can only show 1 quick info, just use the bound attribute if the target is a two
// way binding. We may consider concatenating additional display parts from the other target
// nodes or representing the two way binding in some other manner in the future.
const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
positionDetails.context.nodes[0] :
positionDetails.context.node;
return new QuickInfoBuilder(this.tsLS, compiler, templateInfo.component, node).get();
}); });
} }
private getQuickInfoAtPositionImpl(
fileName: string,
position: number,
compiler: NgCompiler,
): ts.QuickInfo|undefined {
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
return undefined;
}
const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler);
if (templateInfo === undefined) {
return undefined;
}
const positionDetails = getTargetAtPosition(templateInfo.template, position);
if (positionDetails === null) {
return undefined;
}
// Because we can only show 1 quick info, just use the bound attribute if the target is a two
// way binding. We may consider concatenating additional display parts from the other target
// nodes or representing the two way binding in some other manner in the future.
const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
positionDetails.context.nodes[0] :
positionDetails.context.node;
return new QuickInfoBuilder(this.tsLS, compiler, templateInfo.component, node).get();
}
getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[]|undefined { getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[]|undefined {
const compiler = this.compilerFactory.getOrCreate(); return this.withCompilerAndPerfTracing(PerfPhase.LsReferencesAndRenames, (compiler) => {
const results = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler) return new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler)
.getReferencesAtPosition(fileName, position); .getReferencesAtPosition(fileName, position);
this.compilerFactory.registerLastKnownProgram(); });
return results;
} }
getRenameInfo(fileName: string, position: number): ts.RenameInfo { getRenameInfo(fileName: string, position: number): ts.RenameInfo {
const compiler = this.compilerFactory.getOrCreate(); return this.withCompilerAndPerfTracing(PerfPhase.LsReferencesAndRenames, (compiler) => {
const renameInfo = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler) const renameInfo = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler)
.getRenameInfo(absoluteFrom(fileName), position); .getRenameInfo(absoluteFrom(fileName), position);
if (!renameInfo.canRename) { if (!renameInfo.canRename) {
return renameInfo; return renameInfo;
} }
const quickInfo = this.getQuickInfoAtPosition(fileName, position) ?? const quickInfo = this.getQuickInfoAtPositionImpl(fileName, position, compiler) ??
this.tsLS.getQuickInfoAtPosition(fileName, position); this.tsLS.getQuickInfoAtPosition(fileName, position);
const kind = quickInfo?.kind ?? ts.ScriptElementKind.unknown; const kind = quickInfo?.kind ?? ts.ScriptElementKind.unknown;
const kindModifiers = quickInfo?.kindModifiers ?? ts.ScriptElementKind.unknown; const kindModifiers = quickInfo?.kindModifiers ?? ts.ScriptElementKind.unknown;
return {...renameInfo, kind, kindModifiers}; return {...renameInfo, kind, kindModifiers};
});
} }
findRenameLocations(fileName: string, position: number): readonly ts.RenameLocation[]|undefined { findRenameLocations(fileName: string, position: number): readonly ts.RenameLocation[]|undefined {
const compiler = this.compilerFactory.getOrCreate(); return this.withCompilerAndPerfTracing(PerfPhase.LsReferencesAndRenames, (compiler) => {
const results = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler) return new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler)
.findRenameLocations(fileName, position); .findRenameLocations(fileName, position);
this.compilerFactory.registerLastKnownProgram(); });
return results;
} }
private getCompletionBuilder(fileName: string, position: number): private getCompletionBuilder(fileName: string, position: number, compiler: NgCompiler):
CompletionBuilder<TmplAstNode|AST>|null { CompletionBuilder<TmplAstNode|AST>|null {
const compiler = this.compilerFactory.getOrCreate();
const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler); const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler);
if (templateInfo === undefined) { if (templateInfo === undefined) {
return null; return null;
@ -208,29 +218,35 @@ export class LanguageService {
getCompletionsAtPosition( getCompletionsAtPosition(
fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions|undefined): fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions|undefined):
ts.WithMetadata<ts.CompletionInfo>|undefined { ts.WithMetadata<ts.CompletionInfo>|undefined {
return this.withCompiler((compiler) => { return this.withCompilerAndPerfTracing(PerfPhase.LsCompletions, (compiler) => {
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { return this.getCompletionsAtPositionImpl(fileName, position, options, compiler);
return undefined;
}
const builder = this.getCompletionBuilder(fileName, position);
if (builder === null) {
return undefined;
}
return builder.getCompletionsAtPosition(options);
}); });
} }
private getCompletionsAtPositionImpl(
fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions|undefined,
compiler: NgCompiler): ts.WithMetadata<ts.CompletionInfo>|undefined {
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
return undefined;
}
const builder = this.getCompletionBuilder(fileName, position, compiler);
if (builder === null) {
return undefined;
}
return builder.getCompletionsAtPosition(options);
}
getCompletionEntryDetails( getCompletionEntryDetails(
fileName: string, position: number, entryName: string, fileName: string, position: number, entryName: string,
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined, formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined { preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
return this.withCompiler((compiler) => { return this.withCompilerAndPerfTracing(PerfPhase.LsCompletions, (compiler) => {
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
return undefined; return undefined;
} }
const builder = this.getCompletionBuilder(fileName, position); const builder = this.getCompletionBuilder(fileName, position, compiler);
if (builder === null) { if (builder === null) {
return undefined; return undefined;
} }
@ -240,12 +256,12 @@ export class LanguageService {
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): ts.Symbol getCompletionEntrySymbol(fileName: string, position: number, entryName: string): ts.Symbol
|undefined { |undefined {
return this.withCompiler((compiler) => { return this.withCompilerAndPerfTracing(PerfPhase.LsCompletions, (compiler) => {
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) { if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
return undefined; return undefined;
} }
const builder = this.getCompletionBuilder(fileName, position); const builder = this.getCompletionBuilder(fileName, position, compiler);
if (builder === null) { if (builder === null) {
return undefined; return undefined;
} }
@ -256,30 +272,31 @@ export class LanguageService {
} }
getComponentLocationsForTemplate(fileName: string): GetComponentLocationsForTemplateResponse { getComponentLocationsForTemplate(fileName: string): GetComponentLocationsForTemplateResponse {
return this.withCompiler<GetComponentLocationsForTemplateResponse>((compiler) => { return this.withCompilerAndPerfTracing<GetComponentLocationsForTemplateResponse>(
const components = compiler.getComponentsWithTemplateFile(fileName); PerfPhase.LsComponentLocations, (compiler) => {
const componentDeclarationLocations: ts.DocumentSpan[] = const components = compiler.getComponentsWithTemplateFile(fileName);
Array.from(components.values()).map(c => { const componentDeclarationLocations: ts.DocumentSpan[] =
let contextSpan: ts.TextSpan|undefined = undefined; Array.from(components.values()).map(c => {
let textSpan: ts.TextSpan; let contextSpan: ts.TextSpan|undefined = undefined;
if (isNamedClassDeclaration(c)) { let textSpan: ts.TextSpan;
textSpan = ts.createTextSpanFromBounds(c.name.getStart(), c.name.getEnd()); if (isNamedClassDeclaration(c)) {
contextSpan = ts.createTextSpanFromBounds(c.getStart(), c.getEnd()); textSpan = ts.createTextSpanFromBounds(c.name.getStart(), c.name.getEnd());
} else { contextSpan = ts.createTextSpanFromBounds(c.getStart(), c.getEnd());
textSpan = ts.createTextSpanFromBounds(c.getStart(), c.getEnd()); } else {
} textSpan = ts.createTextSpanFromBounds(c.getStart(), c.getEnd());
return { }
fileName: c.getSourceFile().fileName, return {
textSpan, fileName: c.getSourceFile().fileName,
contextSpan, textSpan,
}; contextSpan,
}); };
return componentDeclarationLocations; });
}); return componentDeclarationLocations;
});
} }
getTcb(fileName: string, position: number): GetTcbResponse|undefined { getTcb(fileName: string, position: number): GetTcbResponse|undefined {
return this.withCompiler<GetTcbResponse|undefined>(compiler => { return this.withCompilerAndPerfTracing<GetTcbResponse|undefined>(PerfPhase.LsTcb, compiler => {
const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler); const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler);
if (templateInfo === undefined) { if (templateInfo === undefined) {
return undefined; return undefined;
@ -323,10 +340,34 @@ export class LanguageService {
}); });
} }
private withCompiler<T>(p: (compiler: NgCompiler) => T): T { /**
* Provides an instance of the `NgCompiler` and traces perf results. Perf results are logged only
* if the log level is verbose or higher. This method is intended to be called once per public
* method call.
*
* Here is an example of the log output.
*
* Perf 245 [16:16:39.353] LanguageService#getQuickInfoAtPosition(): {"events":{},"phases":{
* "Unaccounted":379,"TtcSymbol":4},"memory":{}}
*
* Passing name of caller instead of using `arguments.caller` because 'caller', 'callee', and
* 'arguments' properties may not be accessed in strict mode.
*
* @param phase the `PerfPhase` to execute the `p` callback in
* @param p callback to be run synchronously with an instance of the `NgCompiler` as argument
* @return the result of running the `p` callback
*/
private withCompilerAndPerfTracing<T>(phase: PerfPhase, p: (compiler: NgCompiler) => T): T {
const compiler = this.compilerFactory.getOrCreate(); const compiler = this.compilerFactory.getOrCreate();
const result = p(compiler); const result = compiler.perfRecorder.inPhase(phase, () => p(compiler));
this.compilerFactory.registerLastKnownProgram(); this.compilerFactory.registerLastKnownProgram();
const logger = this.project.projectService.logger;
if (logger.hasLevel(ts.server.LogLevel.verbose)) {
logger.perftrc(`LanguageService#${PerfPhase[phase]}: ${
JSON.stringify(compiler.perfRecorder.finalize())}`);
}
return result; return result;
} }
@ -336,26 +377,27 @@ export class LanguageService {
return []; return [];
} }
const diagnostics: ts.Diagnostic[] = []; return this.withCompilerAndPerfTracing(PerfPhase.LsDiagnostics, (compiler) => {
const configSourceFile = ts.readJsonConfigFile( const diagnostics: ts.Diagnostic[] = [];
project.getConfigFilePath(), (path: string) => project.readFile(path)); const configSourceFile = ts.readJsonConfigFile(
project.getConfigFilePath(), (path: string) => project.readFile(path));
if (!this.options.strictTemplates && !this.options.fullTemplateTypeCheck) { if (!this.options.strictTemplates && !this.options.fullTemplateTypeCheck) {
diagnostics.push({ diagnostics.push({
messageText: 'Some language features are not available. ' + messageText: 'Some language features are not available. ' +
'To access all features, enable `strictTemplates` in `angularCompilerOptions`.', 'To access all features, enable `strictTemplates` in `angularCompilerOptions`.',
category: ts.DiagnosticCategory.Suggestion, category: ts.DiagnosticCategory.Suggestion,
code: ngErrorCode(ErrorCode.SUGGEST_STRICT_TEMPLATES), code: ngErrorCode(ErrorCode.SUGGEST_STRICT_TEMPLATES),
file: configSourceFile, file: configSourceFile,
start: undefined, start: undefined,
length: undefined, length: undefined,
}); });
} }
const compiler = this.compilerFactory.getOrCreate(); diagnostics.push(...compiler.getOptionDiagnostics());
diagnostics.push(...compiler.getOptionDiagnostics());
return diagnostics; return diagnostics;
});
} }
private watchConfigFile(project: ts.server.Project) { private watchConfigFile(project: ts.server.Project) {

View File

@ -275,6 +275,28 @@ describe('getSemanticDiagnostics', () => {
expect(diag.category).toBe(ts.DiagnosticCategory.Suggestion); expect(diag.category).toBe(ts.DiagnosticCategory.Suggestion);
expect(getTextOfDiagnostic(diag)).toBe('user'); expect(getTextOfDiagnostic(diag)).toBe('user');
}); });
it('logs perf tracing', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({ template: '' })
export class MyComponent {}
`
};
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const logger = project.getLogger();
spyOn(logger, 'hasLevel').and.returnValue(true);
spyOn(logger, 'perftrc').and.callFake(() => {});
const diags = project.getDiagnosticsForFile('app.ts');
expect(diags.length).toEqual(0);
expect(logger.perftrc)
.toHaveBeenCalledWith(jasmine.stringMatching(
/LanguageService\#LsDiagnostics\:.*\"LsDiagnostics\":\s*\d+.*/g));
});
}); });
function getTextOfDiagnostic(diag: ts.Diagnostic): string { function getTextOfDiagnostic(diag: ts.Diagnostic): string {

View File

@ -179,6 +179,10 @@ export class Project {
getTemplateTypeChecker(): TemplateTypeChecker { getTemplateTypeChecker(): TemplateTypeChecker {
return this.ngLS.compilerFactory.getOrCreate().getTemplateTypeChecker(); return this.ngLS.compilerFactory.getOrCreate().getTemplateTypeChecker();
} }
getLogger(): ts.server.Logger {
return this.tsProject.projectService.logger;
}
} }
function getClassOrError(sf: ts.SourceFile, name: string): ts.ClassDeclaration { function getClassOrError(sf: ts.SourceFile, name: string): ts.ClassDeclaration {