From 68bc7ecbb8d1c8b9719b6800723221144483f308 Mon Sep 17 00:00:00 2001 From: Alex Kondratyuk Date: Wed, 26 Oct 2022 10:39:49 +0300 Subject: [PATCH] Support percent values in maxProcesses --- README.md | 3 ++- bin/concurrently.ts | 5 +++-- src/concurrently.spec.ts | 32 ++++++++++++++++++++++++++++++++ src/concurrently.ts | 15 +++++++++++++-- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a6106751..b75346e1 100644 --- a/README.md +++ b/README.md @@ -146,8 +146,9 @@ concurrently [options] General -m, --max-processes How many processes should run at once. + Could be an exact number or a percent of CPUs available. New processes only spawn after all restart tries - of a process. [number] + of a process. [string] -n, --names List of custom names to be used in prefix template. Example names: "main,browser,server" [string] diff --git a/bin/concurrently.ts b/bin/concurrently.ts index db84ca0e..e9ee89f3 100755 --- a/bin/concurrently.ts +++ b/bin/concurrently.ts @@ -30,8 +30,9 @@ const args = yargs(argsBeforeSep) alias: 'm', describe: 'How many processes should run at once.\n' + - 'New processes only spawn after all restart tries of a process.', - type: 'number', + 'New processes only spawn after all restart tries of a process.\n' + + 'Could be an exact number or a percent of CPUs available (for example "50%")', + type: 'string', }, names: { alias: 'n', diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index 1edef399..dd44e760 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -1,4 +1,5 @@ import { createMockInstance } from 'jest-create-mock-instance'; +import os from 'os'; import { Writable } from 'stream'; import { ChildProcess, KillProcess, SpawnCommand } from './command'; @@ -16,6 +17,8 @@ const create = (commands: ConcurrentlyCommandInput[], options: Partial { + jest.resetAllMocks(); + processes = []; spawn = jest.fn(() => { const process = createFakeProcess(processes.length); @@ -79,6 +82,35 @@ it('spawns commands up to configured limit at once', () => { expect(spawn).toHaveBeenCalledTimes(4); }); +it('spawns commands up to percent based limit at once', () => { + const cpusSpy = jest.spyOn(os, 'cpus'); + cpusSpy.mockReturnValue( + new Array(4).fill({ + model: 'Intel', + speed: 0, + times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 }, + }) + ); + + create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: '50%' }); + expect(spawn).toHaveBeenCalledTimes(2); + expect(spawn).toHaveBeenCalledWith('foo', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('bar', expect.objectContaining({})); + + // Test out of order completion picking up new processes in-order + processes[1].emit('close', 1, null); + expect(spawn).toHaveBeenCalledTimes(3); + expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({})); + + processes[0].emit('close', null, 'SIGINT'); + expect(spawn).toHaveBeenCalledTimes(4); + expect(spawn).toHaveBeenCalledWith('qux', expect.objectContaining({})); + + // Shouldn't attempt to spawn anything else. + processes[2].emit('close', 1, null); + expect(spawn).toHaveBeenCalledTimes(4); +}); + it('runs controllers with the commands', () => { create(['echo', '"echo wrapped"']); diff --git a/src/concurrently.ts b/src/concurrently.ts index a1d8ee38..3dffe9e9 100644 --- a/src/concurrently.ts +++ b/src/concurrently.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import _ from 'lodash'; +import { cpus } from 'os'; import spawn from 'spawn-command'; import { Writable } from 'stream'; import treeKill from 'tree-kill'; @@ -67,11 +68,12 @@ export type ConcurrentlyOptions = { /** * Maximum number of commands to run at once. + * Could be an exact number or a percent of CPUs available. * * If undefined, then all processes will start in parallel. * Setting this value to 1 will achieve sequential running. */ - maxProcesses?: number; + maxProcesses?: number | string; /** * Whether commands should be spawned in raw mode. @@ -187,7 +189,12 @@ export function concurrently( } const commandsLeft = commands.slice(); - const maxProcesses = Math.max(1, Number(options.maxProcesses) || commandsLeft.length); + const maxProcesses = Math.max( + 1, + (typeof options.maxProcesses === 'string' && options.maxProcesses.endsWith('%') + ? Math.round(cpus().length * percentToNumber(options.maxProcesses)) + : Number(options.maxProcesses)) || commandsLeft.length + ); for (let i = 0; i < maxProcesses; i++) { maybeRunMore(commandsLeft); } @@ -245,3 +252,7 @@ function maybeRunMore(commandsLeft: Command[]) { maybeRunMore(commandsLeft); }); } + +function percentToNumber(percent: string): number { + return Number(percent.replace('%', '')) / 100; +}