Skip to content

Commit

Permalink
feat(ivy): performance trace mechanism for ngtsc (angular#29380)
Browse files Browse the repository at this point in the history
This commit adds a `tracePerformance` option for tsconfig.json. When
specified, it causes a JSON file with timing information from the ngtsc
compiler to be emitted at the specified path.

This tracing system is used to instrument the analysis/emit phases of
compilation, and will be useful in debugging future integration work with
@angular/cli.

See ngtsc/perf/README.md for more details.

PR Close angular#29380
  • Loading branch information
alxhub authored and wKoza committed Apr 17, 2019
1 parent 0c7bcbb commit 5a7c5cd
Show file tree
Hide file tree
Showing 14 changed files with 289 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/compiler-cli/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/scope",
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/ngcc/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/transform",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool} from '@angular/compiler';
import {NOOP_PERF_RECORDER} from '@angular/compiler-cli/src/ngtsc/perf';
import * as path from 'canonical-path';
import * as fs from 'fs';
import * as ts from 'typescript';
Expand Down Expand Up @@ -90,7 +91,8 @@ export class DecorationAnalyzer {
this.reflectionHost, this.evaluator, this.scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
this.isCore),
new InjectableDecoratorHandler(
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, /* strictCtorDeps */ false),
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore,
/* strictCtorDeps */ false),
new NgModuleDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry,
this.isCore, /* routeAnalyzer */ null, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER),
Expand Down
15 changes: 15 additions & 0 deletions packages/compiler-cli/src/ngtsc/perf/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "perf",
srcs = ["index.ts"] + glob([
"src/*.ts",
]),
deps = [
"//packages:types",
"@npm//@types/node",
"@npm//typescript",
],
)
21 changes: 21 additions & 0 deletions packages/compiler-cli/src/ngtsc/perf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# What is the `perf` package?

This package contains utilities for collecting performance information from around the compiler and for producing ngtsc performance traces.

This feature is currently undocumented and not exposed to users as the trace file format is still unstable.

# What is a performance trace?

A performance trace is a JSON file which logs events within ngtsc with microsecond precision. It tracks the phase of compilation, the file and possibly symbol being compiled, and other metadata explaining what the compiler was doing during the event.

Traces track two specific kinds of events: marks and spans. A mark is a single event with a timestamp, while a span is two events (start and end) that cover a section of work in the compiler. Analyzing a file is a span event, while a decision (such as deciding not to emit a particular file) is a mark event.

# Enabling performance traces

Performance traces are enabled via the undocumented `tracePerformance` option in `angularCompilerOptions` inside the tsconfig.json file. This option takes a string path relative to the current directory, which will be used to write the trace JSON file.

## In-Memory TS Host Tracing

By default, the trace file will be written with the `fs` package directly. However, ngtsc supports in-memory compilation using a `ts.CompilerHost` for all operations. In the event that tracing is required when using an in-memory filesystem, a `ts:` prefix can be added to the value of `tracePerformance`, which will cause the trace JSON file to be written with the TS host's `writeFile` method instead.

This is not done by default as `@angular/cli` does not allow writing arbitrary JSON files via its host.
11 changes: 11 additions & 0 deletions packages/compiler-cli/src/ngtsc/perf/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export {PerfRecorder} from './src/api';
export {NOOP_PERF_RECORDER} from './src/noop';
export {PerfTracker} from './src/tracking';
18 changes: 18 additions & 0 deletions packages/compiler-cli/src/ngtsc/perf/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';

export interface PerfRecorder {
readonly enabled: boolean;

mark(name: string, node?: ts.SourceFile|ts.Declaration, category?: string, detail?: string): void;
start(name: string, node?: ts.SourceFile|ts.Declaration, category?: string, detail?: string):
number;
stop(span: number): void;
}
21 changes: 21 additions & 0 deletions packages/compiler-cli/src/ngtsc/perf/src/clock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

// This file uses 'process'
/// <reference types="node" />

export type HrTime = [number, number];

export function mark(): HrTime {
return process.hrtime();
}

export function timeSinceInMicros(mark: HrTime): number {
const delta = process.hrtime(mark);
return (delta[0] * 1000000) + Math.floor(delta[1] / 1000);
}
20 changes: 20 additions & 0 deletions packages/compiler-cli/src/ngtsc/perf/src/noop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';

import {PerfRecorder} from './api';

export const NOOP_PERF_RECORDER: PerfRecorder = {
enabled: false,
mark: (name: string, node: ts.SourceFile | ts.Declaration, category?: string, detail?: string):
void => {},
start: (name: string, node: ts.SourceFile | ts.Declaration, category?: string, detail?: string):
number => { return 0;},
stop: (span: number | false): void => {},
};
110 changes: 110 additions & 0 deletions packages/compiler-cli/src/ngtsc/perf/src/tracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/// <reference types="node" />
import * as fs from 'fs';
import * as path from 'path';

import * as ts from 'typescript';

import {PerfRecorder} from './api';
import {HrTime, mark, timeSinceInMicros} from './clock';

export class PerfTracker implements PerfRecorder {
private nextSpanId = 1;
private log: PerfLogEvent[] = [];

readonly enabled = true;

private constructor(private zeroTime: HrTime) {}

static zeroedToNow(): PerfTracker { return new PerfTracker(mark()); }

mark(name: string, node?: ts.SourceFile|ts.Declaration, category?: string, detail?: string):
void {
const msg = this.makeLogMessage(PerfLogEventType.MARK, name, node, category, detail, undefined);
this.log.push(msg);
}

start(name: string, node?: ts.SourceFile|ts.Declaration, category?: string, detail?: string):
number {
const span = this.nextSpanId++;
const msg = this.makeLogMessage(PerfLogEventType.SPAN_OPEN, name, node, category, detail, span);
this.log.push(msg);
return span;
}

stop(span: number): void {
this.log.push({
type: PerfLogEventType.SPAN_CLOSE,
span,
stamp: timeSinceInMicros(this.zeroTime),
});
}

private makeLogMessage(
type: PerfLogEventType, name: string, node: ts.SourceFile|ts.Declaration|undefined,
category: string|undefined, detail: string|undefined, span: number|undefined): PerfLogEvent {
const msg: PerfLogEvent = {
type,
name,
stamp: timeSinceInMicros(this.zeroTime),
};
if (category !== undefined) {
msg.category = category;
}
if (detail !== undefined) {
msg.detail = detail;
}
if (span !== undefined) {
msg.span = span;
}
if (node !== undefined) {
msg.file = node.getSourceFile().fileName;
if (!ts.isSourceFile(node)) {
const name = ts.getNameOfDeclaration(node);
if (name !== undefined && ts.isIdentifier(name)) {
msg.declaration = name.text;
}
}
}
return msg;
}

asJson(): unknown { return this.log; }

serializeToFile(target: string, host: ts.CompilerHost): void {
const json = JSON.stringify(this.log, null, 2);

if (target.startsWith('ts:')) {
target = target.substr('ts:'.length);
const outFile = path.posix.resolve(host.getCurrentDirectory(), target);
host.writeFile(outFile, json, false);
} else {
const outFile = path.posix.resolve(host.getCurrentDirectory(), target);
fs.writeFileSync(outFile, json);
}
}
}

export interface PerfLogEvent {
name?: string;
span?: number;
file?: string;
declaration?: string;
type: PerfLogEventType;
category?: string;
detail?: string;
stamp: number;
}

export enum PerfLogEventType {
SPAN_OPEN,
SPAN_CLOSE,
MARK,
}
47 changes: 44 additions & 3 deletions packages/compiler-cli/src/ngtsc/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatInde
import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {PartialEvaluator} from './partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from './path';
import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
import {TypeScriptReflectionHost} from './reflection';
import {HostResourceLoader} from './resource_loader';
import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
Expand Down Expand Up @@ -57,10 +58,17 @@ export class NgtscProgram implements api.Program {
private refEmitter: ReferenceEmitter|null = null;
private fileToModuleHost: FileToModuleHost|null = null;
private defaultImportTracker: DefaultImportTracker;
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER;
private perfTracker: PerfTracker|null = null;

constructor(
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
host: api.CompilerHost, oldProgram?: api.Program) {
if (shouldEnablePerfTracing(options)) {
this.perfTracker = PerfTracker.zeroedToNow();
this.perfRecorder = this.perfTracker;
}

this.rootDirs = getRootDirs(host, options);
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
this.resourceManager = new HostResourceLoader(host, options);
Expand Down Expand Up @@ -187,10 +195,23 @@ export class NgtscProgram implements api.Program {
if (this.compilation === undefined) {
this.compilation = this.makeCompilation();
}
const analyzeSpan = this.perfRecorder.start('analyze');
await Promise.all(this.tsProgram.getSourceFiles()
.filter(file => !file.fileName.endsWith('.d.ts'))
.map(file => this.compilation !.analyzeAsync(file))
.map(file => {

const analyzeFileSpan = this.perfRecorder.start('analyzeFile', file);
let analysisPromise = this.compilation !.analyzeAsync(file);
if (analysisPromise === undefined) {
this.perfRecorder.stop(analyzeFileSpan);
} else if (this.perfRecorder.enabled) {
analysisPromise = analysisPromise.then(
() => this.perfRecorder.stop(analyzeFileSpan));
}
return analysisPromise;
})
.filter((result): result is Promise<void> => result !== undefined));
this.perfRecorder.stop(analyzeSpan);
this.compilation.resolve();
}

Expand Down Expand Up @@ -245,10 +266,16 @@ export class NgtscProgram implements api.Program {

private ensureAnalyzed(): IvyCompilation {
if (this.compilation === undefined) {
const analyzeSpan = this.perfRecorder.start('analyze');
this.compilation = this.makeCompilation();
this.tsProgram.getSourceFiles()
.filter(file => !file.fileName.endsWith('.d.ts'))
.forEach(file => this.compilation !.analyzeSync(file));
.forEach(file => {
const analyzeFileSpan = this.perfRecorder.start('analyzeFile', file);
this.compilation !.analyzeSync(file);
this.perfRecorder.stop(analyzeFileSpan);
});
this.perfRecorder.stop(analyzeSpan);
this.compilation.resolve();
}
return this.compilation;
Expand Down Expand Up @@ -298,12 +325,14 @@ export class NgtscProgram implements api.Program {
beforeTransforms.push(...customTransforms.beforeTs);
}

const emitSpan = this.perfRecorder.start('emit');
const emitResults: ts.EmitResult[] = [];
for (const targetSourceFile of this.tsProgram.getSourceFiles()) {
if (targetSourceFile.isDeclarationFile) {
continue;
}

const fileEmitSpan = this.perfRecorder.start('emitFile', targetSourceFile);
emitResults.push(emitCallback({
targetSourceFile,
program: this.tsProgram,
Expand All @@ -316,6 +345,12 @@ export class NgtscProgram implements api.Program {
afterDeclarations: afterDeclarationsTransforms,
},
}));
this.perfRecorder.stop(fileEmitSpan);
}
this.perfRecorder.stop(emitSpan);

if (this.perfTracker !== null && this.options.tracePerformance !== undefined) {
this.perfTracker.serializeToFile(this.options.tracePerformance, this.host);
}

// Run the emit, including a custom transformer that will downlevel the Ivy decorators in code.
Expand Down Expand Up @@ -405,7 +440,8 @@ export class NgtscProgram implements api.Program {
];

return new IvyCompilation(
handlers, checker, this.reflector, this.importRewriter, this.sourceToFactorySymbols);
handlers, checker, this.reflector, this.importRewriter, this.perfRecorder,
this.sourceToFactorySymbols);
}

private get reflector(): TypeScriptReflectionHost {
Expand Down Expand Up @@ -455,6 +491,7 @@ function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
emitSkipped = emitSkipped || er.emitSkipped;
emittedFiles.push(...(er.emittedFiles || []));
}

return {diagnostics, emitSkipped, emittedFiles};
}

Expand Down Expand Up @@ -519,3 +556,7 @@ export class ReferenceGraphAdapter implements ReferencesRegistry {
}
}
}

function shouldEnablePerfTracing(options: api.CompilerOptions): boolean {
return options.tracePerformance !== undefined;
}
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/transform/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ts_library(
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck",
Expand Down

0 comments on commit 5a7c5cd

Please sign in to comment.