Skip to content
Merged
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 @@ -20,7 +20,11 @@ import type {
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import * as path from 'node:path';
import { maxWorkers, useTypeChecking } from '../../../utils/environment-options';
import {
hasCustomMaxWorkers,
maxWorkers,
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 @@ -97,14 +101,18 @@ 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,
},
maxWorkers,
maxTransformWorkers,
cacheStore?.createCache('jstransformer'),
);

Expand Down
26 changes: 15 additions & 11 deletions packages/angular/build/src/utils/environment-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,28 @@ export const allowMinify = debugOptimize.minify;
*/
export const useRolldownChunks = parseTristate(process.env['NG_BUILD_CHUNKS_ROLLDOWN']) ?? true;

const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS'];

let customMaxWorkers: number | undefined;
if (isPresent(maxWorkersVariable)) {
const parsed = +maxWorkersVariable;
if (Number.isInteger(parsed) && parsed >= 1) {
customMaxWorkers = parsed;
}
}

/**
* Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available.
* This cause `Error: Call retries were exceeded` errors when trying to use them.
*
* @see https://github.com/nodejs/node/issues/28762
* @see https://github.com/webpack-contrib/terser-webpack-plugin/issues/143
* @see https://ithub.com/angular/angular-cli/issues/16860#issuecomment-588828079
*
* Whether the maximum number of workers was explicitly configured via the
* `NG_BUILD_MAX_WORKERS` environment variable.
*/
const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS'];
export const hasCustomMaxWorkers = customMaxWorkers !== undefined;

/**
* The maximum number of workers to use for parallel processing.
* This can be controlled by the `NG_BUILD_MAX_WORKERS` environment variable.
* When not set, defaults to available parallelism minus one to ensure the main thread is not starved.
*/
export const maxWorkers = isPresent(maxWorkersVariable)
? +maxWorkersVariable
: Math.min(4, Math.max(availableParallelism() - 1, 1));
export const maxWorkers = customMaxWorkers ?? Math.max(availableParallelism() - 1, 1);

/**
* When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled.
Expand Down
116 changes: 116 additions & 0 deletions packages/angular/build/src/utils/environment-options_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* @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 { availableParallelism } from 'node:os';

describe('environment options - maxWorkers', () => {
const originalEnvValue = process.env['NG_BUILD_MAX_WORKERS'];

function loadEnvironmentOptions(): typeof import('./environment-options') {
delete require.cache[require.resolve('./environment-options')];

return require('./environment-options');
}

afterEach(() => {
if (originalEnvValue !== undefined) {
process.env['NG_BUILD_MAX_WORKERS'] = originalEnvValue;
} else {
delete process.env['NG_BUILD_MAX_WORKERS'];
}
delete require.cache[require.resolve('./environment-options')];
});

it('defaults maxWorkers to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is unset', () => {
delete process.env['NG_BUILD_MAX_WORKERS'];
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeFalse();
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
});

it('uses configured positive integer when NG_BUILD_MAX_WORKERS is set', () => {
process.env['NG_BUILD_MAX_WORKERS'] = '8';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeTrue();
expect(maxWorkers).toBe(8);
});

it('allows maxWorkers greater than 4 when explicitly configured', () => {
process.env['NG_BUILD_MAX_WORKERS'] = '32';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeTrue();
expect(maxWorkers).toBe(32);
});

it('supports maxWorkers set to 1', () => {
process.env['NG_BUILD_MAX_WORKERS'] = '1';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeTrue();
expect(maxWorkers).toBe(1);
});

it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is 0', () => {
process.env['NG_BUILD_MAX_WORKERS'] = '0';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeFalse();
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
});

it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is negative', () => {
process.env['NG_BUILD_MAX_WORKERS'] = '-4';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeFalse();
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
});

it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is not a number', () => {
process.env['NG_BUILD_MAX_WORKERS'] = 'invalid';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeFalse();
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
});

it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is a float', () => {
process.env['NG_BUILD_MAX_WORKERS'] = '2.5';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeFalse();
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
});

it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is an empty string', () => {
process.env['NG_BUILD_MAX_WORKERS'] = '';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeFalse();
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
});

it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is whitespace only', () => {
process.env['NG_BUILD_MAX_WORKERS'] = ' ';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeFalse();
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
});

it('parses positive integers with surrounding whitespace', () => {
process.env['NG_BUILD_MAX_WORKERS'] = ' 8 ';
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();

expect(hasCustomMaxWorkers).toBeTrue();
expect(maxWorkers).toBe(8);
});
});