Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@
import type * as ng from '@angular/compiler-cli';
import type { PartialMessage } from 'esbuild';
import type ts from 'typescript';
import { isMainThread } from 'node:worker_threads';
import { convertTypeScriptDiagnostic } from '../../esbuild/angular/diagnostics';
import { profileAsync, profileSync } from '../../esbuild/profiling';
import {
logCumulativeDurations,
profileAsync,
profileSync,
resetCumulativeDurations,
} from '../../esbuild/profiling';
Comment thread
arturovt marked this conversation as resolved.
import type { AngularHostOptions } from '../angular-host';

export interface EmitFileResult {
Expand Down Expand Up @@ -94,20 +100,39 @@ export abstract class AngularCompilation {
// This allows for avoiding the load of typescript in the main thread when using the parallel compilation.
const typescript = await AngularCompilation.loadTypescript();

await profileAsync('NG_DIAGNOSTICS_TOTAL', async () => {
for (const diagnostic of await this.collectDiagnostics(modes)) {
const message = convertTypeScriptDiagnostic(typescript, diagnostic);
if (diagnostic.category === typescript.DiagnosticCategory.Error) {
(result.errors ??= []).push(message);
} else {
(result.warnings ??= []).push(message);
// When running inside a worker (ParallelCompilation), the cumulative duration Map lives in
// this module's memory — the main thread never sees it. So we own the full reset→log
// lifecycle here rather than relying on the main thread to do it.
// On the main thread we skip this to avoid clearing/printing timings accumulated elsewhere.
const flushTimings = this.shouldFlushPerformanceTimings();
if (flushTimings) {
resetCumulativeDurations();
}

try {
await profileAsync('NG_DIAGNOSTICS_TOTAL', async () => {
for (const diagnostic of await this.collectDiagnostics(modes)) {
const message = convertTypeScriptDiagnostic(typescript, diagnostic);
if (diagnostic.category === typescript.DiagnosticCategory.Error) {
(result.errors ??= []).push(message);
} else {
(result.warnings ??= []).push(message);
}
}
});
} finally {
if (flushTimings) {
logCumulativeDurations();
}
});
}

return result;
}
Comment thread
arturovt marked this conversation as resolved.

protected shouldFlushPerformanceTimings(): boolean {
return !isMainThread;
}

update?(files: Set<string>): Promise<void>;

close?(): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* @license
* Copyright Google LLC 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.dev/license
*/

import type * as ng from '@angular/compiler-cli';
import type { PartialMessage } from 'esbuild';
import type ts from 'typescript';
import * as diagnosticsModule from '../../esbuild/angular/diagnostics';
import * as profilingModule from '../../esbuild/profiling';
import type { AngularHostOptions } from '../angular-host';
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';

/**
* Minimal stub for the TypeScript module: only DiagnosticCategory is accessed in diagnoseFiles.
*/
const MOCK_TYPESCRIPT = {
DiagnosticCategory: { Error: 1, Warning: 0, Message: 2, Suggestion: 3 },
} as unknown as typeof ts;

/** Concrete subclass used to control collectDiagnostics behaviour in tests. */
class ConcreteCompilation extends AngularCompilation {
private diagnostics_: ts.Diagnostic[] = [];
private throwError_: Error | undefined;

setDiagnostics(diagnostics: ts.Diagnostic[]): void {
this.diagnostics_ = diagnostics;
}

setThrowError(error: Error): void {
this.throwError_ = error;
}

override async initialize(
_tsconfig: string,
_hostOptions: AngularHostOptions,
_compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
): Promise<{
affectedFiles: ReadonlySet<ts.SourceFile>;
compilerOptions: ng.CompilerOptions;
referencedFiles: readonly string[];
}> {
return {
affectedFiles: new Set(),
compilerOptions: {} as ng.CompilerOptions,
referencedFiles: [],
};
}

override emitAffectedFiles(): Iterable<EmitFileResult> {
return [];
}

protected override *collectDiagnostics(_modes: DiagnosticModes): Iterable<ts.Diagnostic> {
if (this.throwError_) {
throw this.throwError_;
}
yield* this.diagnostics_;
}
Comment thread
arturovt marked this conversation as resolved.

protected override shouldFlushPerformanceTimings(): boolean {
return true;
}
}

describe('AngularCompilation.diagnoseFiles', () => {
let compilation: ConcreteCompilation;
let resetSpy: jasmine.Spy;
let logSpy: jasmine.Spy;
let profileAsyncSpy: jasmine.Spy;

beforeEach(() => {
compilation = new ConcreteCompilation();

resetSpy = spyOn(profilingModule, 'resetCumulativeDurations');
logSpy = spyOn(profilingModule, 'logCumulativeDurations');
// Default: transparent passthrough so the real loop still runs.
profileAsyncSpy = spyOn(profilingModule, 'profileAsync').and.callFake(
<T>(_name: string, action: () => Promise<T>): Promise<T> => action(),
);
spyOn(AngularCompilation, 'loadTypescript').and.resolveTo(MOCK_TYPESCRIPT);
});

it('calls resetCumulativeDurations once before profileAsync', async () => {
const callOrder: string[] = [];
resetSpy.and.callFake(() => {
callOrder.push('reset');
});
profileAsyncSpy.and.callFake(async (_name: string, action: () => Promise<unknown>) => {
callOrder.push('profileAsync');
return action();
});

await compilation.diagnoseFiles();

expect(resetSpy).toHaveBeenCalledTimes(1);
expect(callOrder).toEqual(['reset', 'profileAsync']);
});

it('calls logCumulativeDurations once after profileAsync completes, even with no diagnostics', async () => {
const callOrder: string[] = [];
profileAsyncSpy.and.callFake(async (_name: string, action: () => Promise<unknown>) => {
const result = await action();
callOrder.push('profileAsync-done');
return result;
});
logSpy.and.callFake(() => {
callOrder.push('log');
});

await compilation.diagnoseFiles();

expect(logSpy).toHaveBeenCalledTimes(1);
expect(callOrder).toEqual(['profileAsync-done', 'log']);
});

it('returns correct errors and warnings, unaffected by profiling calls', async () => {
const errorDiagnostic = { category: 1 /* Error */ } as ts.Diagnostic;
const warningDiagnostic = { category: 0 /* Warning */ } as ts.Diagnostic;
compilation.setDiagnostics([errorDiagnostic, warningDiagnostic]);

spyOn(diagnosticsModule, 'convertTypeScriptDiagnostic').and.callFake(
(_ts: typeof ts, diagnostic: ts.Diagnostic): PartialMessage => ({
text: diagnostic.category === 1 ? 'error message' : 'warning message',
}),
);

const result = await compilation.diagnoseFiles();

expect(result.errors).toEqual([{ text: 'error message' }]);
expect(result.warnings).toEqual([{ text: 'warning message' }]);
// Profiling hooks ran but did not affect the diagnostic output.
expect(resetSpy).toHaveBeenCalledTimes(1);
expect(logSpy).toHaveBeenCalledTimes(1);
});

it('calls logCumulativeDurations even when collectDiagnostics throws, and re-throws the error', async () => {
// logCumulativeDurations sits in a finally block, so it always runs regardless of errors.
compilation.setThrowError(new Error('diagnostics failure'));

await expectAsync(compilation.diagnoseFiles()).toBeRejectedWithError('diagnostics failure');

expect(logSpy).toHaveBeenCalledTimes(1);
});
});
Loading