Limiting number of build jobs based on memory

Our team is working on a relatively large Rust, with a lot of chunky artifacts being produced as the result of the build and we've started to run into issues, where builds are running out of memory on certain systems. Too many binaries being linked at the same time, each requiring e.g. 7GB of memory (lots of code, LTO, release build).

This makes it difficult to script/package Rust code for general purpose consumption. We can add -j 1, but it makes the build unnecessarily slow and underutilized memory. We can lower the build settings, but that just delays the problem etc.

Ideally, there would be some way to cap memory usage. E.g. we could specify via env vars / arguments / cargo config that we expect a single crate to need certain amount of memory, and Rust would adjust the effective -j based on the free memory available on the system. On systems will capacity, all cores would be utililized, on systems with relatively little memory compared to the number of cores, fewer.

Currently, we are considering just adding some shell scripting to calculate safe -j based on outputs of nproc and free,

LLVM's build scripts have options to limit the number of link jobs run in parallel based on either limiting to a specified number or by calculating from available ram, maybe cargo should gain options for that?

1 Like

Automatically limiting memory consumption is much harder than limiting CPU consumption because CPU use relatively closely corresponds to threads (as long as the fraction of time spent in IO or acquiring locks is low, which is the case for rustc). It's even possible to reactively limit CPU use by interleaving multiple threads on a physical core or limiting their scheduler budget, so the compilation process can still make progress.

Memory has none of that ease. You can't really predict how much memory each crate will take. And it's also not possible to budget memory because once the budget runs out then there's no practical way to claw it back from process A to let process B make progress... not without either using swap or killing the process.

There are approaches that might work some of the time, but they wouldn't be as reliable as limiting CPU utilization.

Too many binaries being linked at the same time

For that one wouldn't have to limit build-parallelism in general, just the linking phase.

That's why I'm siding on ability for the project developers to customize the estimation of memory needed for a single job/crate/linker-invocation via cargo config.

That's OK, it's still worthwhile to try to do it.

However, hopefully the memory technology will keep giving us more and more memory even on low end devices, so maybe it's not worth doing because projects hitting these limits are not very common, and the amount of memory used will relatively keep being smaller.

I already rolled out a bandaid wrapper script that will tweak -j based on the system's memory, and that should be good enough for us.