How many tasks at once? Count the cores you are given
Inside a container, the CPU count a program sees is the host’s. A task runner that trusts it starts more work than the kernel will run. Here is how vx picks its worker count.
Performance
Performance
A task runner’s first number is how many tasks to run at once. Too few
and cores sit idle. Too many and tasks fight for the same cores, each
one slower, and the run ends later than it would have with fewer.
The obvious answer is “one per CPU”. Inside a container, that answer is
wrong.
A CI job often runs in a container with a CPU limit: Docker’s
--cpus=2, a Kubernetes limits.cpu, a hosted runner’s plan. The
kernel enforces that limit through the job’s cgroup. But the CPU count
a program reads (navigator.hardwareConcurrency in Bun, os.cpus() in
Node) is the HOST’s.
So on a 32-core host, a job limited to two cores still reads 32. A
runner that starts 32 tasks there gets two cores of work done, slower,
with 30 tasks waiting on the scheduler of the kernel instead of its own.
Memory has the same trap: os.totalmem() reports the host’s RAM, not
the container’s limit.
On Linux, vx reads the limit from the cgroup filesystem:
cgroup v2: cpu.max holds <quota> <period>, or max for no
limit. 200000 100000 is two cores.
cgroup v1: cpu.cfs_quota_us over cpu.cfs_period_us, where a
quota of -1 means no limit.
A limit set on a parent cgroup binds too, so vx walks from its own
cgroup up to the root and keeps the tightest one. The worker count is
the CPU count capped by that quota, rounded up and never below one: a
1.5-core quota gets two workers, since half a core still does work.
Memory follows the same walk (memory.max, or memory.limit_in_bytes
on v1).
On macOS there are no cgroups, so the machine’s numbers stand.
vxruntest--all--concurrency50%# half the cores, never below one
vxruntest--all--concurrency1# one at a time, still in graph order
concurrency in vx.workspace.ts sets a workspace default, and the
flag still wins. A percentage above 100% is allowed for tasks that mostly
wait on the network or disk.
Cache restores. A hit only copies files back from the cache. It is
disk work, so restores run on their own lane, up to twice the worker
count, and never hold up a task that has to execute.
Remote tasks. An executor that runs tasks on another machine
declares its own capacity. Its tasks count against that pool, so a
64-worker remote pool stays full even from a 10-core laptop, or a run
with --concurrency 1.
When more tasks are ready than workers, a runner has to pick. Five rules for picking, the graphs where each one wins and loses, measured with vx, and why recorded timings beat every rule that has to guess.
With @vzn/vx-schedule-history, vx starts the longest chain first and packs tasks by the memory they really used. vx history shows what it learned, task by task.
A broken build skips what depends on it and nothing else. Every skipped task names the failure that blocked it. Pick fail-fast or run-everything with one flag, and set how many tasks run at once as a count or a share of the CPUs.
Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.