From 68bc7ecbb8d1c8b9719b6800723221144483f308 Mon Sep 17 00:00:00 2001 From: Alex Kondratyuk Date: Wed, 26 Oct 2022 10:39:49 +0300 Subject: [PATCH 1/3] 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; +} From 6f4e129d0324baeb3a53eeb63f5ee15e56931070 Mon Sep 17 00:00:00 2001 From: Alex Kondratyuk Date: Wed, 26 Oct 2022 11:45:55 +0300 Subject: [PATCH 2/3] Apply suggested changes --- README.md | 2 +- bin/concurrently.ts | 2 +- src/concurrently.spec.ts | 14 +++++++------- src/concurrently.ts | 8 ++------ 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b75346e1..4ad549b6 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ concurrently [options] General -m, --max-processes How many processes should run at once. - Could be an exact number or a percent of CPUs available. + Exact number or a percent of CPUs available (for example "50%"). New processes only spawn after all restart tries of a process. [string] -n, --names List of custom names to be used in prefix diff --git a/bin/concurrently.ts b/bin/concurrently.ts index e9ee89f3..08f5f6a6 100755 --- a/bin/concurrently.ts +++ b/bin/concurrently.ts @@ -31,7 +31,7 @@ const args = yargs(argsBeforeSep) describe: 'How many processes should run at once.\n' + '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%")', + 'Exact number or a percent of CPUs available (for example "50%")', type: 'string', }, names: { diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index dd44e760..55fcf9eb 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -83,6 +83,7 @@ it('spawns commands up to configured limit at once', () => { }); it('spawns commands up to percent based limit at once', () => { + // Mock architecture with 4 cores const cpusSpy = jest.spyOn(os, 'cpus'); cpusSpy.mockReturnValue( new Array(4).fill({ @@ -93,22 +94,21 @@ it('spawns commands up to percent based limit at once', () => { ); create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: '50%' }); + + // Max parallel processes should be 50% of 4 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); + // Close first process + processes[0].emit('close', 1, null); expect(spawn).toHaveBeenCalledTimes(3); expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({})); - processes[0].emit('close', null, 'SIGINT'); + // Close second process + processes[1].emit('close', 1, null); 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', () => { diff --git a/src/concurrently.ts b/src/concurrently.ts index 3dffe9e9..98863bee 100644 --- a/src/concurrently.ts +++ b/src/concurrently.ts @@ -68,7 +68,7 @@ export type ConcurrentlyOptions = { /** * Maximum number of commands to run at once. - * Could be an exact number or a percent of CPUs available. + * Exact number or a percent of CPUs available (for example "50%"). * * If undefined, then all processes will start in parallel. * Setting this value to 1 will achieve sequential running. @@ -192,7 +192,7 @@ export function concurrently( const maxProcesses = Math.max( 1, (typeof options.maxProcesses === 'string' && options.maxProcesses.endsWith('%') - ? Math.round(cpus().length * percentToNumber(options.maxProcesses)) + ? Math.round((cpus().length * Number(options.maxProcesses.slice(0, -1))) / 100) : Number(options.maxProcesses)) || commandsLeft.length ); for (let i = 0; i < maxProcesses; i++) { @@ -252,7 +252,3 @@ function maybeRunMore(commandsLeft: Command[]) { maybeRunMore(commandsLeft); }); } - -function percentToNumber(percent: string): number { - return Number(percent.replace('%', '')) / 100; -} From 47a918577d3cb79cbc10d309e0b6d6d55bcc5317 Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Wed, 26 Oct 2022 10:56:08 +0200 Subject: [PATCH 3/3] Apply suggestions from code review --- src/concurrently.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index 55fcf9eb..726f9af3 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -95,17 +95,17 @@ it('spawns commands up to percent based limit at once', () => { create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: '50%' }); - // Max parallel processes should be 50% of 4 + // Max parallel processes should be 2 (50% of 4 cores) expect(spawn).toHaveBeenCalledTimes(2); expect(spawn).toHaveBeenCalledWith('foo', expect.objectContaining({})); expect(spawn).toHaveBeenCalledWith('bar', expect.objectContaining({})); - // Close first process + // Close first process and expect third to be spawned processes[0].emit('close', 1, null); expect(spawn).toHaveBeenCalledTimes(3); expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({})); - // Close second process + // Close second process and expect fourth to be spawned processes[1].emit('close', 1, null); expect(spawn).toHaveBeenCalledTimes(4); expect(spawn).toHaveBeenCalledWith('qux', expect.objectContaining({}));