Try   HackMD

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

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.

See also https://github.com/rust-lang/cargo/issues/14604

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.

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

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

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
  • 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)

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.rss 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

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

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