From 4a5b437174af377c876374a51fc8d6eee8aa9cb4 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:05:41 -0400 Subject: [PATCH] perf(@angular/cli): implement semaphore backpressure throttling in PackageManager Execute package manager subprocess invocations using a semaphore-based backpressure throttle. When callers perform concurrent registry lookups (such as running Promise.all over candidate update packages), this prevents unbounded child subprocess spawning. Clamping active concurrent CLI commands to a fixed limit protects the operating system against process table exhaustion, V8 heap saturation, and upstream registry rate-limiting while maintaining fast execution. --- .../src/package-managers/package-manager.ts | 88 ++++++++++++------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/packages/angular/cli/src/package-managers/package-manager.ts b/packages/angular/cli/src/package-managers/package-manager.ts index a5ebfad62553..afa2aa6b4c57 100644 --- a/packages/angular/cli/src/package-managers/package-manager.ts +++ b/packages/angular/cli/src/package-managers/package-manager.ts @@ -93,6 +93,9 @@ export class PackageManager { readonly #initializationError?: Error; #dependencyCache: Map | null = null; #version: string | undefined; + #activeTasks = 0; + readonly #pendingTasks: (() => void)[] = []; + readonly #maxConcurrent = 5; /** * Creates a new `PackageManager` instance. @@ -159,49 +162,72 @@ export class PackageManager { * @param options Options for the child process. * @returns A promise that resolves with the standard output and standard error of the command. */ + async #runWithThrottle(action: () => Promise): Promise { + if (this.#activeTasks >= this.#maxConcurrent) { + await new Promise((resolve) => { + this.#pendingTasks.push(resolve); + }); + } else { + this.#activeTasks++; + } + + try { + return await action(); + } finally { + const next = this.#pendingTasks.shift(); + if (next) { + next(); + } else { + this.#activeTasks--; + } + } + } + async #run( args: readonly string[], options: { timeout?: number; registry?: string; cwd?: string } = {}, ): Promise<{ stdout: string; stderr: string }> { - this.ensureInstalled(); + return this.#runWithThrottle(async () => { + this.ensureInstalled(); + + const { registry, cwd, ...runOptions } = options; + const finalArgs = [...args]; + let finalEnv: Record | undefined; + + if (registry) { + const registryOptions = this.descriptor.getRegistryOptions?.(registry); + if (!registryOptions) { + throw new Error( + `The configured package manager, '${this.descriptor.binary}', does not support a custom registry.`, + ); + } - const { registry, cwd, ...runOptions } = options; - const finalArgs = [...args]; - let finalEnv: Record | undefined; + if (registryOptions.args) { + finalArgs.push(...registryOptions.args); + } + if (registryOptions.env) { + finalEnv = registryOptions.env; + } + } - if (registry) { - const registryOptions = this.descriptor.getRegistryOptions?.(registry); - if (!registryOptions) { - throw new Error( - `The configured package manager, '${this.descriptor.binary}', does not support a custom registry.`, + const executionDirectory = cwd ?? this.cwd; + if (this.options.dryRun) { + this.options.logger?.info( + `[DRY RUN] Would execute in [${executionDirectory}]: ${this.descriptor.binary} ${finalArgs.join(' ')}`, ); - } - if (registryOptions.args) { - finalArgs.push(...registryOptions.args); - } - if (registryOptions.env) { - finalEnv = registryOptions.env; + return { stdout: '', stderr: '' }; } - } - const executionDirectory = cwd ?? this.cwd; - if (this.options.dryRun) { - this.options.logger?.info( - `[DRY RUN] Would execute in [${executionDirectory}]: ${this.descriptor.binary} ${finalArgs.join(' ')}`, - ); + const commandResult = await this.host.runCommand(this.descriptor.binary, finalArgs, { + ...runOptions, + cwd: executionDirectory, + stdio: 'pipe', + env: finalEnv, + }); - return { stdout: '', stderr: '' }; - } - - const commandResult = await this.host.runCommand(this.descriptor.binary, finalArgs, { - ...runOptions, - cwd: executionDirectory, - stdio: 'pipe', - env: finalEnv, + return { stdout: commandResult.stdout.trim(), stderr: commandResult.stderr.trim() }; }); - - return { stdout: commandResult.stdout.trim(), stderr: commandResult.stderr.trim() }; } /**