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
3 changes: 1 addition & 2 deletions packages/angular/build/src/builders/dev-server/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,7 @@ export async function* serveWithVite(
// Always enable JIT linking to support applications built with and without AOT.
// In a development environment the additional scope information does not
// have a negative effect unlike production where final output size is relevant.
{ sourcemap: true, jit: true, thirdPartySourcemaps },
1,
{ sourcemap: true, jit: true, thirdPartySourcemaps, maxConcurrency: 1 },
);

// The index HTML path will be updated from the build results if provided by the builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ import type {
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import * as path from 'node:path';
import {
hasCustomMaxWorkers,
maxWorkers,
useTypeChecking,
} from '../../../utils/environment-options';
import { maxTransformWorkers, useTypeChecking } from '../../../utils/environment-options';
import { calculateHash, initializeHash } from '../../../utils/hash';
import { AngularHostOptions } from '../../angular/angular-host';
import { AngularCompilation, DiagnosticModes } from '../../angular/compilation';
Expand Down Expand Up @@ -102,18 +98,14 @@ export function createCompilerPlugin(
});
}
}
// During bundling, esbuild runs its own multi-threaded Go process across all available cores.
// Unless explicitly configured via NG_BUILD_MAX_WORKERS, cap transformation concurrency to at
// most 4 to prevent CPU contention during bundling.
const maxTransformWorkers = hasCustomMaxWorkers ? maxWorkers : Math.min(4, maxWorkers);
const javascriptTransformer = new JavaScriptTransformer(
{
sourcemap: !!pluginOptions.sourcemap,
thirdPartySourcemaps: pluginOptions.thirdPartySourcemaps,
advancedOptimizations: pluginOptions.advancedOptimizations,
jit: pluginOptions.jit || pluginOptions.includeTestMetadata,
maxConcurrency: maxTransformWorkers,
},
maxTransformWorkers,
cacheStore?.createCache('jstransformer'),
);

Expand Down
43 changes: 32 additions & 11 deletions packages/angular/build/src/tools/esbuild/javascript-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ export interface JavaScriptTransformerOptions {
thirdPartySourcemaps?: boolean;
advancedOptimizations?: boolean;
jit?: boolean;

/**
* The maximum number of concurrent transformation operations.
* When omitted, concurrency defaults to the available worker pool threads.
*/
maxConcurrency?: number;
}

/**
Expand Down Expand Up @@ -146,7 +152,7 @@ export interface TransformOptions {
*/
export class JavaScriptTransformer {
#workerPool: WorkerPool | undefined;
#commonOptions: Required<JavaScriptTransformerOptions>;
#commonOptions: Required<Omit<JavaScriptTransformerOptions, 'maxConcurrency'>>;
#fileCacheKeyBase: Uint8Array;

/** Queue of pending transformation tasks waiting for an active concurrency slot. */
Expand All @@ -155,16 +161,21 @@ export class JavaScriptTransformer {
/** Current count of actively executing transformation tasks. */
#activeTasks = 0;

/** Maximum number of transformation tasks allowed to execute concurrently. */
#maxConcurrent: number;
get #maxConcurrency(): number {
return this.options.maxConcurrency ?? (this.#workerPool?.maxThreads || 1);
}

constructor(
options: JavaScriptTransformerOptions,
readonly maxThreads: number,
private readonly options: JavaScriptTransformerOptions,
private readonly cache?: Cache<Uint8Array>,
) {
// Maintain 2 active tasks per worker thread to keep transformation pipelines fully saturated
this.#maxConcurrent = Math.max(1, maxThreads * 2);
if (
options.maxConcurrency !== undefined &&
(!Number.isInteger(options.maxConcurrency) || options.maxConcurrency < 1)
) {
throw new RangeError('options.maxConcurrency must be an integer greater than or equal to 1.');
}

// Extract options to ensure only the named options are serialized and sent to the worker
const {
sourcemap,
Expand All @@ -189,7 +200,7 @@ export class JavaScriptTransformer {
* @returns A promise resolving to the transformation result.
*/
async #runWithThrottle<T>(action: () => Promise<T>): Promise<T> {
if (this.#activeTasks >= this.#maxConcurrent) {
if (this.#activeTasks >= this.#maxConcurrency) {
await new Promise<void>((resolve, reject) => {
this.#pendingTasks.push({ resolve, reject });
});
Expand All @@ -216,9 +227,11 @@ export class JavaScriptTransformer {

const workerPoolOptions: WorkerPoolOptions = {
filename: require.resolve('./javascript-transformer-worker'),
maxThreads: this.maxThreads,
minThreads: this.maxThreads,
workerData: this.#commonOptions,
...(this.options.maxConcurrency !== undefined && {
minThreads: this.options.maxConcurrency,
maxThreads: this.options.maxConcurrency,
}),
};

// Prevent passing SSR `--import` (loader-hooks) from parent to child worker.
Expand All @@ -243,7 +256,7 @@ export class JavaScriptTransformer {
return this.#runWithThrottle(async () => {
const data = await readFile(filename);

return this.transformData(filename, data, options);
return this.#transform(filename, data, options);
});
}

Expand All @@ -259,6 +272,14 @@ export class JavaScriptTransformer {
filename: string,
data: string | Uint8Array,
options?: TransformOptions,
): Promise<Uint8Array> {
return this.#runWithThrottle(() => this.#transform(filename, data, options));
}

async #transform(
filename: string,
data: string | Uint8Array,
options?: TransformOptions,
): Promise<Uint8Array> {
let resolvedSideEffects: boolean | undefined;
let sideEffectsQueried = false;
Expand Down
Loading