Skip to content

Add a switch for reporting expensive statements #37785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
78 changes: 78 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ namespace ts {
const Signature = objectAllocator.getSignatureConstructor();

let typeCount = 0;
let literalTypeCount = 0;
let symbolCount = 0;
let enumCount = 0;
let totalInstantiationCount = 0;
Expand All @@ -333,6 +334,10 @@ namespace ts {
const keyofStringsOnly = !!compilerOptions.keyofStringsOnly;
const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ObjectFlags.FreshLiteral;

let checkingDtsFile: boolean;
const maxExpensiveStatementCount = compilerOptions.expensiveStatements ?? 0;
const expensiveStatements: ExpensiveStatement[] = [];

const emitResolver = createResolver();
const nodeBuilder = createNodeBuilder();

Expand Down Expand Up @@ -368,6 +373,7 @@ namespace ts {
subtype: subtypeRelation.size,
strictSubtype: strictSubtypeRelation.size,
}),
getExpensiveStatements: () => expensiveStatements,
isUndefinedSymbol: symbol => symbol === undefinedSymbol,
isArgumentsSymbol: symbol => symbol === argumentsSymbol,
isUnknownSymbol: symbol => symbol === unknownSymbol,
Expand Down Expand Up @@ -3629,6 +3635,7 @@ namespace ts {
function createType(flags: TypeFlags): Type {
const result = new Type(checker, flags);
typeCount++;
literalTypeCount += (flags & TypeFlags.Literal) ? 1 : 0;
result.id = typeCount;
return result;
}
Expand Down Expand Up @@ -30164,13 +30171,17 @@ namespace ts {

function checkExpression(node: Expression | QualifiedName, checkMode?: CheckMode, forceTuple?: boolean): Type {
const saveCurrentNode = currentNode;
const oldCounts = getExpensiveStatementCounts();

currentNode = node;
instantiationCount = 0;
const uninstantiatedType = checkExpressionWorker(node, checkMode, forceTuple);
const type = instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode);
if (isConstEnumObjectType(type)) {
checkConstEnumAccess(node, type);
}

insertExpensiveStatement(node, oldCounts);
currentNode = saveCurrentNode;
return type;
}
Expand Down Expand Up @@ -35656,11 +35667,76 @@ namespace ts {
const saveCurrentNode = currentNode;
currentNode = node;
instantiationCount = 0;

const oldCounts = getExpensiveStatementCounts();

checkSourceElementWorker(node);

insertExpensiveStatement(node, oldCounts);

currentNode = saveCurrentNode;
}
}

interface Counts {
typeCount: number;
literalTypeCount: number;
symbolCount: number;
}

function getExpensiveStatementCounts(): Counts {
return {
typeCount,
literalTypeCount,
symbolCount,
};
}

function insertExpensiveStatement(node: Node, oldCounts: Counts): void {
// Never report expensive statements in .d.ts files
if (checkingDtsFile || maxExpensiveStatementCount <= 0) {
return;
}

const newCounts = getExpensiveStatementCounts();
const typeDelta = (newCounts.typeCount - newCounts.literalTypeCount) -
(oldCounts.typeCount - oldCounts.literalTypeCount);
const symbolDelta = newCounts.symbolCount - oldCounts.symbolCount;

if (typeDelta === 0) {
return;
}

let i = 0;
for (const existing of expensiveStatements) {
// The = is important - we need to insert record before its
// descendants for the de-duping logic to work correctly.
if (existing.typeDelta <= typeDelta) {
break;
}
i++;
}

if (i < maxExpensiveStatementCount) {
let hasExpensiveDescendent = false;
// Search forward since descendants cannot be more expensive
for (let j = i; j < expensiveStatements.length; j++) {
const candidate = expensiveStatements[j];
if (isNodeDescendantOf(candidate.node, node)) {
// If a single descendant makes up the majority of the cost, this node isn't interesting
hasExpensiveDescendent = (typeDelta / candidate.typeDelta) < 2;
break; // Stop looking since all subsequent nodes are less expensive
}
}
if (!hasExpensiveDescendent) {
expensiveStatements.splice(i, 0, { node, typeDelta, symbolDelta });
if (expensiveStatements.length > maxExpensiveStatementCount) {
expensiveStatements.pop();
}
}
}
}

function checkSourceElementWorker(node: Node): void {
if (isInJSFile(node)) {
forEach((node as JSDocContainer).jsDoc, ({ tags }) => forEach(tags, checkSourceElement));
Expand Down Expand Up @@ -35974,10 +36050,12 @@ namespace ts {
}

function checkSourceFile(node: SourceFile) {
checkingDtsFile = fileExtensionIs(node.path, Extension.Dts);
performance.mark("beforeCheck");
checkSourceFileWorker(node);
performance.mark("afterCheck");
performance.measure("Check", "beforeCheck", "afterCheck");
checkingDtsFile = false;
}

function unusedIsError(kind: UnusedKind, isAmbient: boolean): boolean {
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ namespace ts {
category: Diagnostics.Advanced_Options,
description: Diagnostics.The_locale_used_when_displaying_messages_to_the_user_e_g_en_us
},
{
name: "expensiveStatements",
type: "number",
category: Diagnostics.Advanced_Options,
description: Diagnostics.Heuristically_reports_statements_that_appear_to_contribute_disproportionately_to_check_time
},
];

/* @internal */
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4482,6 +4482,10 @@
"category": "Error",
"code": 6236
},
"Heuristically reports statements that appear to contribute disproportionately to check time.": {
"category": "Message",
"code": 6237
},

"Projects to reference": {
"category": "Message",
Expand Down Expand Up @@ -5984,5 +5988,10 @@
"Invalid value for 'jsxFragmentFactory'. '{0}' is not a valid identifier or qualified-name.": {
"category": "Error",
"code": 18035
},

"Checking this statement may result in the creation of as many as {0} types and {1} symbols.": {
"category": "Warning",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first Warning. It feels pretty distinct to me, but it doesn't really matter one way or the other.

"code": 19000
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, since this felt like a new kind of diagnostic, I added a new code range.

}
}
10 changes: 9 additions & 1 deletion src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,14 @@ namespace ts {
}

missingFilePaths = arrayFrom(mapDefinedIterator(filesByName.entries(), ([path, file]) => file === undefined ? path as Path : undefined));
files = stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(processingOtherFiles);

const dtsFiles: SourceFile[] = [];
const otherFiles: SourceFile[] = [];
for (const file of processingOtherFiles) {
(fileExtensionIs(file.path, Extension.Dts) ? dtsFiles : otherFiles).push(file);
}

files = stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(dtsFiles).concat(otherFiles);
processingDefaultLibFiles = undefined;
processingOtherFiles = undefined;
}
Expand Down Expand Up @@ -954,6 +961,7 @@ namespace ts {
getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(),
getInstantiationCount: () => getDiagnosticsProducingTypeChecker().getInstantiationCount(),
getRelationCacheSizes: () => getDiagnosticsProducingTypeChecker().getRelationCacheSizes(),
getExpensiveStatements: () => getDiagnosticsProducingTypeChecker().getExpensiveStatements(),
getFileProcessingDiagnostics: () => fileProcessingDiagnostics,
getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives,
isSourceFileFromExternalLibrary,
Expand Down
10 changes: 10 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3737,6 +3737,7 @@ namespace ts {
getTypeCount(): number;
getInstantiationCount(): number;
getRelationCacheSizes(): { assignable: number, identity: number, subtype: number, strictSubtype: number };
/* @internal */ getExpensiveStatements(): readonly ExpensiveStatement[];

/* @internal */ getFileProcessingDiagnostics(): DiagnosticCollection;
/* @internal */ getResolvedTypeReferenceDirectives(): ESMap<string, ResolvedTypeReferenceDirective | undefined>;
Expand Down Expand Up @@ -4070,6 +4071,7 @@ namespace ts {
/* @internal */ getTypeCount(): number;
/* @internal */ getInstantiationCount(): number;
/* @internal */ getRelationCacheSizes(): { assignable: number, identity: number, subtype: number, strictSubtype: number };
/* @internal */ getExpensiveStatements(): readonly ExpensiveStatement[];

/* @internal */ isArrayType(type: Type): boolean;
/* @internal */ isTupleType(type: Type): boolean;
Expand Down Expand Up @@ -5675,6 +5677,7 @@ namespace ts {
downlevelIteration?: boolean;
emitBOM?: boolean;
emitDecoratorMetadata?: boolean;
/*@internal*/ expensiveStatements?: number;
experimentalDecorators?: boolean;
forceConsistentCasingInFileNames?: boolean;
/*@internal*/generateCpuProfile?: string;
Expand Down Expand Up @@ -8067,4 +8070,11 @@ namespace ts {
negative: boolean;
base10Value: string;
}

/* @internal */
export interface ExpensiveStatement {
node: Node;
typeDelta: number;
symbolDelta: number;
}
}
32 changes: 23 additions & 9 deletions src/executeCommandLine/executeCommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ namespace ts {
createBuilderStatusReporter(sys, shouldBePretty(sys, buildOptions)),
createWatchStatusReporter(sys, buildOptions)
);
updateSolutionBuilderHost(sys, cb, buildHost);
updateSolutionBuilderHost(sys, reportDiagnostic, cb, buildHost);
const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions, watchOptions);
builder.build();
return builder;
Expand All @@ -453,7 +453,7 @@ namespace ts {
createBuilderStatusReporter(sys, shouldBePretty(sys, buildOptions)),
createReportErrorSummary(sys, buildOptions)
);
updateSolutionBuilderHost(sys, cb, buildHost);
updateSolutionBuilderHost(sys, reportDiagnostic, cb, buildHost);
const builder = createSolutionBuilder(buildHost, projects, buildOptions);
const exitStatus = buildOptions.clean ? builder.clean() : builder.build();
return sys.exit(exitStatus);
Expand Down Expand Up @@ -492,7 +492,7 @@ namespace ts {
s => sys.write(s + sys.newLine),
createReportErrorSummary(sys, options)
);
reportStatistics(sys, program);
reportStatistics(sys, program, reportDiagnostic);
cb(program);
return sys.exit(exitStatus);
}
Expand All @@ -516,7 +516,7 @@ namespace ts {
reportDiagnostic,
reportErrorSummary: createReportErrorSummary(sys, options),
afterProgramEmitAndDiagnostics: builderProgram => {
reportStatistics(sys, builderProgram.getProgram());
reportStatistics(sys, builderProgram.getProgram(), reportDiagnostic);
cb(builderProgram);
}
});
Expand All @@ -525,12 +525,13 @@ namespace ts {

function updateSolutionBuilderHost(
sys: System,
reportDiagnostic: DiagnosticReporter,
cb: ExecuteCommandLineCallbacks,
buildHost: SolutionBuilderHostBase<EmitAndSemanticDiagnosticsBuilderProgram>
) {
updateCreateProgram(sys, buildHost);
buildHost.afterProgramEmitAndDiagnostics = program => {
reportStatistics(sys, program.getProgram());
reportStatistics(sys, program.getProgram(), reportDiagnostic);
cb(program);
};
buildHost.afterEmitBundle = cb;
Expand All @@ -549,14 +550,15 @@ namespace ts {

function updateWatchCompilationHost(
sys: System,
reportDiagnostic: DiagnosticReporter,
cb: ExecuteCommandLineCallbacks,
watchCompilerHost: WatchCompilerHost<EmitAndSemanticDiagnosticsBuilderProgram>,
) {
updateCreateProgram(sys, watchCompilerHost);
const emitFilesUsingBuilder = watchCompilerHost.afterProgramCreate!; // TODO: GH#18217
watchCompilerHost.afterProgramCreate = builderProgram => {
emitFilesUsingBuilder(builderProgram);
reportStatistics(sys, builderProgram.getProgram());
reportStatistics(sys, builderProgram.getProgram(), reportDiagnostic);
cb(builderProgram);
};
}
Expand All @@ -581,7 +583,7 @@ namespace ts {
reportDiagnostic,
reportWatchStatus: createWatchStatusReporter(system, configParseResult.options)
});
updateWatchCompilationHost(system, cb, watchCompilerHost);
updateWatchCompilationHost(system, reportDiagnostic, cb, watchCompilerHost);
watchCompilerHost.configFileParsingResult = configParseResult;
return createWatchProgram(watchCompilerHost);
}
Expand All @@ -602,7 +604,7 @@ namespace ts {
reportDiagnostic,
reportWatchStatus: createWatchStatusReporter(system, options)
});
updateWatchCompilationHost(system, cb, watchCompilerHost);
updateWatchCompilationHost(system, reportDiagnostic, cb, watchCompilerHost);
return createWatchProgram(watchCompilerHost);
}

Expand All @@ -616,9 +618,21 @@ namespace ts {
}
}

function reportStatistics(sys: System, program: Program) {
function reportStatistics(sys: System, program: Program, reportDiagnostic: DiagnosticReporter) {
let statistics: Statistic[];
const compilerOptions = program.getCompilerOptions();

if (compilerOptions.expensiveStatements) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're really only diagnostics for the pretty-printing. Otherwise, they seem like they belong with the statistics.

for (const expensiveStatement of program.getExpensiveStatements()) {
reportDiagnostic(
createDiagnosticForNode(
expensiveStatement.node,
Diagnostics.Checking_this_statement_may_result_in_the_creation_of_as_many_as_0_types_and_1_symbols,
expensiveStatement.typeDelta,
expensiveStatement.symbolDelta));
}
}

if (canReportDiagnostics(sys, compilerOptions)) {
statistics = [];
const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1;
Expand Down
Loading