## 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