## Workflows
Important considerations when asking "what is slow?"
- What workflows are you having problems with?
- What workflows are you avoiding because they are slow?
X% faster isn't always important but the end-user impact.
For example, dropping 1 minute off of a 20 minute CI won't change user behavior.
Instead CI time falls into buckets like "instantenous", "get a coffee", "work on something else", "come back the next day".
### Incremental compilation and testing
Rebuilds when switching workspace members from feature changes:
Within a large workspace, as the user moves around between crates, the enabled features might be different causing rebuilds of dependencies.
Check everything above current crate still builds:
Our rebuild detection should mean that people only see a slowdown from things that actually depend on what was changed.
However, when changing implementation details, only relinking should be needed and not rebuilding.
Even the re-linking of every test binary, even if you consolidate integration test binaries, can be significant.
Check everything above current crate passes tests:
Depending on your tests, this can dwarf the build times from above.
Being able to more precisely target what to test would help keep this down.
### CI builds
Precise caching:
Without careful attention to cached contents,
network transfers and packing/unpacking can overwhelm the benefits of caching.
### rust-analyzer and `cargo`
Locking of TARGET_DIR:
The two can fight over access to the TARGET_DIR.
This can be worked around by setting a custom one or switching r-a to a profile
RUSTFLAGS:
r-a users reported they set `RUSTFLAGS=--cfg=test` to get tests analyzed.
This causes cache thrashing between the two.
### "Sporadic" full rebuilds
Besides rebuilds mentioned above (features, `RUSTFLAGS`), it can be difficult to identify why a rebuild happened to avoid it.
### `cargo fix`
Run `cargo fix --workspace` to address some kind lint.
This is can be slow because `cargo fix` serializes the build of each build target in each workspace member.
### `CARGO_COMPLETE`
For the new [completions](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#native-completions),
we'll need Cargo to be fast for querying manifests and lockfiles for different state.
## Solution ideas
### Re-organize the target dir
Teams:
- T-cargo
Purpose: unblock other work
Re-organize intermediate artfacts in target dir from being organized along role to being organized along package+hash.
This will require passing every directory needed to rustc rather than it doing the lookup by hash.
Risk:
- Costs / complexity from passing everything to rustc explicitly
To clarify what we are re-organizing,
it is the build-dir from https://github.com/rust-lang/cargo/issues/14125.
### ~~Changing RUSTFLAGS~~
Teams:
- T-cargo
Hash the RUSTFLAGS so that a distinct intermediate artifact is created.
See also https://github.com/rust-lang/cargo/issues/8716, https://github.com/rust-lang/cargo/pull/14830
### Fine grained target dir locking
Teams:
- T-cargo
Depends on:
- "Re-organize the target dir"
Only exclusively lock immutable artifact dirs when being created, otherwise grab a shared lock.
See also https://github.com/rust-lang/cargo/issues/4282
### Unifying features
Teams:
- T-cargo
Give users the ability to opt-in to feature unification across their workspace so they can avoid rebuilding dependencies because of which package they select.
See also https://github.com/rust-lang/rfcs/pull/3692
### `cargo report` for previous builds
Teams:
- T-cargo
Cargo logs rebuild information and allows playing it back (maybe with timings as well?) so users can look into why a previous build rebuilt
See also https://github.com/rust-lang/cargo/issues/2904
### Opt-in mir-only rlibs
Teams:
- T-compiler
- T-cargo
As a profile setting (which includes per-package values), skip code-gen until linking all of the rlibs together.
This will be especially important for cases like aws sdk, windows-sys, etc.
This could even remove the need for a lot of the use of cargo features.
We may want to consider packages being able to set a default profile setting for when they are built into another package, like aws sdk or windows-sys being able to enable mir-only rlibs. See https://github.com/rust-lang/cargo/issues/7940
Literal mir-only rlibs is unlikely, see https://github.com/rust-lang/rust/pull/119017
But we could do something like "inline=yes".
### mir-only rlibs
Teams:
- T-cargo
Depedns on:
- "Opt-in mir-only rlibs"
Risks
- Having many test binaries may cause a lot of duplicate codegen, slowing things down
- Can no longer set opt-level per dependency which is important for developers with dependencies running a hotloop, like graphics rendering
### Intermediate build artifact caching
Teams:
- T-cargo
Depends on:
- "Re-organize the target dir"
Benefits from:
- "RUSTFLAGS and Target dir"
May conflict with:
- "mir-only rlibs": compilation of mir-only rlibs may be fast enough that the cost of caching might outweigh the cost of building
We could share build artifacts between projects.
See also https://rust-lang.github.io/rust-project-goals/2024h2/user-wide-cache.html
### Remote intermediate build artifact caching
Teams:
- T-cargo
Depends on:
- "Intermediate build artifact caching"
We could have cache process plugins to support reading and/or writing to remote cache stores.
Where CI's support it, this could also allow for precise caching between CI jobs.
e.g. a contributor who trusts their CI can enable support for reading the cache from CI. This would most help when dependencies get updated.
### Intermediate build artifact GC
Teams:
- T-cargo
Depends on:
- "Intermediate build artifact caching"
Besides keeping disk space low, this can help CI have slightly more precise caching between CI jobs.
See also https://github.com/rust-lang/cargo/issues/12633
### Hash API of dependencies, not implementation
Teams:
- T-compiler
- T-cargo
May conflict with:
- "mir-only rlibs": compilation of mir-only rlibs may be fast enough to not need this
If a developer changes a test function or the body of a function,
only that rlib needs to be rebuilt and then the final result relinked, rather than rebuilding everything, similar to C++ development where `.cpp` file changes only cause that compilation unit to rebuild while `.h` changes cause all dependents to rebuild.
At minimum, this runs into problems with DefIds which was also solved for Incremental Compilation. See also "`cargo build` reuses rmeta from `cargo check`".
See also https://github.com/rust-lang/cargo/issues/14604
### `cargo build` reuses rmeta from `cargo check`
Teams:
- T-compiler
- T-cargo
May conflict with:
- "Inform build scripts when `check`ing and `build`ing"
Frequently, editors run `cargo check`. If we could reuse that, then a follow up `cargo build` could fully parallelize the backend work of all of the packages, speeding up builds for test feedback.
This would require making the `cargo check` rmeta the same as `cargo build` as well as making DefId's stable.
See also "Hash API of dependencies, not implementation"
See also https://github.com/rust-lang/cargo/issues/3501
### Inform build scripts when `check`ing and `build`ing
Teams:
- T-cargo
May conflict with:
- "`cargo build` reuses rmeta from `cargo check`"
- Work in general to [reduce the reliance on build scripts](https://github.com/rust-lang/cargo/issues/14948) (though if this is more for `-sys` crates, we'll likely always need "something")
This would allow build scripts to skip work that isn't needed for a `cargo check`, making it faster.
However, this means the build script runs between `cargo check` and `cargo build` can't be reused
See also https://github.com/rust-lang/cargo/issues/4001
### Only rebuild tests on unit test changes
Teams:
- T-cargo
May conflict with:
- "Hash API of dependencies, not implementation": this might mitigate it sufficiently to not need to encourage higher friction workflows
Commonly people put tests in a `mod tests {}`.
If they change a test,
the production `lib` gets rebuilt and everything that depends on it.
If the user instead puts it in a `test.rs` with an external `#[cfg(test)]`,
this goes away.
We could shift the workflow to encourage this, e.g.
- https://github.com/rust-lang/rust-clippy/issues/13589
### Consolidate test binaries
Teams:
- T-lang
- T-compiler
- T-cargo
Re-linking test binaries can be slow.
The default path in Cargo is to have a test binary per test source file.
See also https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html
Option 1: consolidate test binaries automatically, see also https://github.com/rust-lang/cargo/issues/13450
Option 2: Add an "auto" mod feature and document a single test binary with it
Risks:
- "auto" mod: in a directory with multiple build targets, like `src/main.rs` and `src/lib.rs`, how does each know what to pull in?
- There will still be the problem of several test binaries per package in a large workspace
### Consolidated doctests
Teams:
- T-rustdoc
Improved in 2024 by having fewer things to compile
### Parallel running of test binaries
Teams:
- T-cargo
- T-testing-devex
As we shore up libtest's API, including json support, Cargo can invoke multiple libtest binaries at once, handling all of the rendering.
Maybe we'd want jobserver support in libtest to manage the number of threads.
For prior art, see
- `cargo-nextest`
See also https://github.com/rust-lang/cargo/issues/5609
In the future, we could even have tests run in parallel to the build, see https://github.com/rust-lang/cargo/issues/12327
### Don't rebuild doctests if nothing changed
Teams:
- T-rustdoc
- T-cargo
Today, Cargo tells rustdoc "here is a library, extract, compile, and run the doctests".
This bypasses Cargo's normal caching in builds.
Similarly, I assume that doctests do not participate in incremental compilation when you only change a small part of your doctests.
Putting Cargo in charge of compiling somehow, whether it has rustdoc extract doctests or rustc turns doctests into `#[test]` functions, would allow Cargo to avoid rebuilding.
### Parallel running of doctests with regular tests
Teams:
- T-rustdoc
- T-cargo
Dpeneds on:
- "Parallel running of test binaries"
Today, Cargo tells rustdoc "here is a library, extract, compile, and run the doctests".
This bypasses Cargo's future work on "Parallel running of test binaries".
Putting Cargo in charge of running the test binaries would allow Cargo to run these in parallel as well.
### Alternative linkers
Teams:
- T-compiler
May conflict with:
- "Consolidate test binaries" as there will be less linked
- "Consolidated doctests" as there will be less linked
By changing the default linker, we can reduce edit-test cycle times.
The first step is switching the default linker to something else.
Once we've done that once, the cost for exploring whats the "best" default linker is significantly lower.
See also https://blog.rust-lang.org/2024/05/17/enabling-rust-lld-on-linux.html
### Alternative compiler backends
Teams:
- T-compiler
May conflict with:
- "mir-only rlibs" as codegen settings may become global, rather than allowing per-package codegen settings
By changing to a fast-to-compile backend for workspace members or tests, we can reduce the edit-test cycle times.
Risks
- Tests need optimizations to complete in reasonable amount of time
### Code coverage support
Teams:
- T-cargo
Purpose: unblock other work (within the scope of performance)
See also https://github.com/rust-lang/cargo/issues/13040
### Coverage-driven test filtering
Teams:
- T-cargo
Depends on:
- "Code coverage support"
Automatically filter tests to only those that call into code that was changed since the last run.
Risks
- Determining what has changed
### `cfg(version)`, `cfg(accessible)`
Teams:
- T-lang
- T-compiler
By stabilizing these,
we take away the need to do version detection via `build.rs`.
While this would require an MSRV bump, the MSRV-aware resolver can allow that without affecting previous users.
Combined with bumping the minor version field on MSRV-bump, a maintainer could even still offer the possibility of support to older MSRVs.
Note: the Cargo team previously rejected supporting users doing nightly version detection in https://github.com/rust-lang/cargo/issues/11244
Related:
- "Intermediate build artifact caching" could cache these packages if they no longer have a `build.rs`
See also https://github.com/rust-lang/rfcs/pull/2523
### libs required for no_std support
With `HashMap` in `std` because of its default hasher being `std`-only and users then needing a no_std hasher, requires adding dependencies that could have been avoided otherwise.
### `cfg_value!("name")`
Teams:
- T-libs-api
- T-compiler?
A purpose for build scripts is to read `cfg` values (as `CARGO_CFG_*`) and make them available at compile time.
By being able to read these directly, we can cut build scripts out of the loop, reducing the need for them
Example use cases
- Reading [user configuration](https://internals.rust-lang.org/t/pre-rfc-mutually-excusive-global-features/19618)
- Reporting build configuration in `--version`, `--bugreport`, panics, etc
Potential cases to support
- Single value vs many value
- Always present vs conditionally present
- Parsing to const types (e.g. an integer for a const-generic parameter)
### `cfg_alias`
Teams:
- T-lang
- T-compiler
- T-cargo
Some uses of `build.rs` are for creating aliases
See also https://github.com/rust-lang/rfcs/pull/3804
### `CARGO_HOST_TARGET`
Teams:
- T-cargo
A purpose for build scripts is to read `TARGET` and make that available
- Cargo reports the host target in verbose version string
- `escargot` uses it to get *a* target tuple for to help users programmatically perform builds
### Polish system-deps
Teams:
- T-cargo
If we can systemize the `build.rs`s of `-sys` crates, we can make them easier to get correct, be consistent across the ecosystem, and reduce how much `build.rs` logic needs to be audited.
This may also make it easier to control for env variables accidentally causing rebuilds.
### Delegating `build.rs`
Teams:
- T-cargo
Depends on:
- "Polish system-deps"
If we can delegate `build.rs` to system-deps,
then that reduces the number of build targets that need to be built.
See also
- https://github.com/rust-lang/cargo/issues/14903
### Declarative derives / attribute macros
Teams:
- T-lang
- T-compiler
By moving as many macros as possible away from proc-macros towards declarative macros,
these macros are compile-time enforced to be pure,
making them easier to audit and reduce the number of dependencies needed.
Related:
- "Intermediate build artifact caching" could cache packages that use these macros because we know they are pure
See also
- https://github.com/rust-lang/rfcs/pull/3698
- https://github.com/rust-lang/rfcs/pull/3697
### Macro expansion caching
Teams:
- T-compiler
In some cases this can help, in some cases it can hurt.
For proc-macros to leverage this, we'd need more insight into when to force an expansion (maybe not an issue if we migrate to declarative macros)
### Speculative `cargo fix`
Teams:
- T-cargo
Instead of running build-targets serially,
`cargo fix` would shell out to `cargo check` to build everything,
fixing what is reported through json output.
To avoid the class of problems that `cargo fix` works around by running serially, we could cancel and rerun `cargo check` once we've fixed suggestions for a package.
This would likely make it easier to implement other features, like an interactive mode as it would live in the top-level command rather than a rustc driver.
See also https://github.com/rust-lang/cargo/issues/13214
Risks
- When there are changes in a lot of the crates, this could be slower
- There still might be problems that running serially addresses, particularly around a source file existing in multiple build targets.
### No-op dependency resolve times
Teams:
- T-cargo
When Cargo verifies the lock file and then builds, it does
- Resole of lockfile (full resolve)
- Resolve to filter lockfile to current build (full resolve)
- Resolve features
We could instead encode a "verify lockfile" operation and a "filter resolve" operation, removing the resolver overhead completely.
### Manifest parsing
Teams:
- T-cargo
Cargo parses every dependencies manifest.
TOML is optimized for humans and not for machines.
We could use an alternative format for immutable dependencies,
whether caching it or publishing it in the `.crate` (which wouldn't be available for git dependencies).
See also https://rust-lang.zulipchat.com/#narrow/channel/246057-t-cargo/topic/.60cargo.20metadata.60.20performance/near/476523460
### Manifest load time
Teams:
- T-cargo
Cargo performs a lot of analysis on manifest when loading them.
We could defer some of the lint analysis out to our regular lint analysis phase so we can know whether its worth running or not, reducing manifest load time.
See also https://rust-lang.zulipchat.com/#narrow/channel/246057-t-cargo/topic/.60cargo.20metadata.60.20performance/near/476523460
### ~~Avoid git submodule check~~
Teams:
- T-cargo
See also https://github.com/rust-lang/cargo/issues/14603
Merged in https://github.com/rust-lang/cargo/pull/14605
### Cache name -> path lookup for git dependencies
Teams:
- T-cargo
See also https://github.com/rust-lang/cargo/issues/14395
Risks
- The user will referene all of the manifests anyways, adding risk with no benefit
### Faster `dev` profile
Teams:
- T-cargo
- T-compiler?
Cargo would need to determine how to split up the debug and dev-but-not-debug profiles for faster non-debugger iteration time.
If that is possible, it might be of interest to create special debug levels for
- `check` runs if we never leverage `cargo check` files in `cargo build` and if there is anything to do here
- `test` runs for (1) fast to compile while being (2) fast to run tests (e.g. cheap steps to reduce the amount of code gen, inlining)
See also https://blog.rust-lang.org/inside-rust/2024/12/13/this-development-cycle-in-cargo-1.84/#improving-the-built-in-profiles