diff --git a/src/2025h2/relink-dont-rebuild.md b/src/2025h2/relink-dont-rebuild.md new file mode 100644 index 00000000..1feca133 --- /dev/null +++ b/src/2025h2/relink-dont-rebuild.md @@ -0,0 +1,328 @@ +# Relink don't Rebuild + + +| Metadata | | +| -------- | --- | +| Point of contact | @yaahc | +| Teams | | +| Task owners | | +| Status | Proposed | +| Tracking issue | | +| Zulip channel | | + +## Summary + +Work towards avoiding rebuilds of a crate's dependents for changes that don't affect the crate's +public interface. + + + + + +## Motivation + + + +Changing a comment, reordering use statements, adding a `dbg!` statement to a non-inlinable +function, formatting code, or moving item definitions from one impl block to another +identical one all cause rebuilds of reverse dependencies of that crate. + +This clashes with users' intuition for what needs to be rebuilt when certain changes are made +and makes iterating more painful. + +As a point of reference, in C and C++ – where there is a strict separation between interface +and implementation in the form of header files – equivalent changes would only cause a +rebuild of the translation unit whose source has been modified. For other units, existing +compiler outputs would be reused (and re-linked into the final binary). + +Our goal is to work towards making `cargo` and `rustc` smarter about when they do or don't need to +rebuild dependent crates (reverse dependencies). + +### The status quo + + + +As an example, consider the [`rg` binary in the `ripgrep` package][rg]. + +[rg]: https://github.com/BurntSushi/ripgrep/blob/3b7fd442a6f3aa73f650e763d7cbb902c03d700e/Cargo.toml + +Its crate dependency graph (narrowed to only include dependents of `globset`, a particular +crate in `ripgrep`'s Cargo workspace) looks like this: +``` +❯ cargo tree --invert globset +globset v0.4.16 +├── grep-cli v0.1.11 +│ └── grep v0.3.2 +│ └── ripgrep v14.1.1 +└── ignore v0.4.23 + └── ripgrep v14.1.1 +``` + +```mermaid +flowchart TB + globset + grep-cli + grep + ignore + ripgrep + + globset --> ignore --> ripgrep + + globset --> grep-cli --> grep --> ripgrep +``` + +Consider a change that does not alter the interface of the `globset` crate (for example, +modifying a private item or changing a comment within `globset`'s source code). + +Here is the output of `cargo build --timings` for an incremental build of `ripgrep` where only +such a change was made to `globset`: + + + +

+ +

+ + +In the above we see `globset` and all transitive "upstream" dependent crates (up to `ripgrep`) +being rebuilt. + +_Ideally_, in this scenario, the transitive dependents of `globset` (that only depend on +`globset`'s "interface") would _not_ need to be rebuilt. This would allow us to skip the +`grep-cli`, `ignore`, `grep`, and `ripgrep` re-compiles and only redo linking of the final +binary ("relink, don't rebuild")[^caveat_linking]. + +[^caveat_linking]: `cargo --timings` output does not currently differentiate between time spent compiling (i.e. producing the `rlib` for) and linking the final binary (`rg`); the `rg` bar covers time spent for both + +For smaller/shallow dep graphs (like the above) the extra rebuilds are tolerable, but for deeper +graphs, these rebuilds significantly impact edit-debug cycle times. + + + +--- + +#### Transitive Deps and the Build System View + +Ideally the crate-level dependency graph above would (morally) correspond to a build graph like +this[^caveat_pipelining]: + +```mermaid +flowchart TB + subgraph globset[globset compile] + globset.rmeta:::rmeta + globset.rlib:::rlib + end + subgraph grep-cli[grep-cli compile] + grep-cli.rmeta:::rmeta + grep-cli.rlib:::rlib + end + subgraph grep[grep compile] + grep.rmeta:::rmeta + grep.rlib:::rlib + end + subgraph ignore[ignore compile] + ignore.rmeta:::rmeta + ignore.rlib:::rlib + end + subgraph ripgrep[ripgrep compile] + %% ripgrep.rmeta:::rmeta + ripgrep.rlib:::rlib + end + + ripgrep_bin["`rg (bin)`"] + + classDef rmeta fill:#ea76cb + classDef rlib fill:#2e96f5 + + %% linker inputs (`rlib`s): + globset.rlib & grep-cli.rlib & grep.rlib & ignore.rlib & ripgrep.rlib -.-> ripgrep_bin + + %% direct deps (`rmeta`s): + globset.rmeta --> ignore + ignore.rmeta --> ripgrep + + globset.rmeta --> grep-cli + grep-cli.rmeta --> grep + grep.rmeta --> ripgrep +``` + +[^caveat_pipelining]: + We have taken some liberties in the above graph w.r.t. pipelining. +

+ Today, `cargo` preforms a single `rustc` invocation to produce the `rlib` and `rmeta` + for each crate – `rmeta` is modeled as an "early out". +

+ Additionally, producing `ripgrep.rlib` and linking (the `rg (bin)` node) happens as part + of a single `rustc` invocation. + +In particular, note that crate compiles use the `rmeta`s of their direct dependencies. + +However, in reality crate compiles need access to all _transitive_ `rmeta`s: +```mermaid +flowchart TB + subgraph globset[globset compile] + globset.rmeta:::rmeta + globset.rlib:::rlib + end + subgraph grep-cli[grep-cli compile] + grep-cli.rmeta:::rmeta + grep-cli.rlib:::rlib + end + subgraph grep[grep compile] + grep.rmeta:::rmeta + grep.rlib:::rlib + end + subgraph ignore[ignore compile] + ignore.rmeta:::rmeta + ignore.rlib:::rlib + end + subgraph ripgrep[ripgrep compile] + %% ripgrep.rmeta:::rmeta + ripgrep.rlib:::rlib + end + + ripgrep_bin["`rg (bin)`"] + + classDef rmeta fill:#ea76cb + classDef rlib fill:#2e96f5 + + %% linker inputs (`rlib`s): + globset.rlib & grep-cli.rlib & grep.rlib & ignore.rlib & ripgrep.rlib -.-> ripgrep_bin + + %% direct deps (`rmeta`s): + globset.rmeta --> ignore + ignore.rmeta --> ripgrep + + globset.rmeta --> grep-cli + grep-cli.rmeta --> grep + grep.rmeta --> ripgrep + + %% transitive deps (`rmeta`s): + globset.rmeta ==> ripgrep & grep + grep-cli.rmeta ==> ripgrep +``` + +This means that when a crate's `rmeta` changes, the `rustc` invocations corresponding to all +transitive dependents of that crate are rerun (even if intermediate `rmeta`s are the +same). + +More concretely: when `globset.rmeta` changes, `grep` is rebuilt – even if `grep-cli.rmeta` +(after `grep-cli` is re-compiled) hasn't changed. + +The fact that crate compiles depend on the `rmeta`s for all transitive dependencies is +significant because it inhibits our ability to get "early cutoff" (ECO). In reality, crates +compiles are only _actually_ sensitive to the subset of their transitive deps exposed via +their direct deps but under this view (file-level, in the eyes of the build system) crates are +sensitive to transitive dependencies in their entirety. + +More concretely: the `grep` crate is only sensitive to the parts of `globset` accessible via +`grep_cli` – if a change is made to `globset` that doesn't affect this subset, we'd expect to +see `grep_cli` being rebuilt but the existing `grep` outputs being reused (no `grep` rebuild). + +> [!NOTE] +> ["Early cutoff" (ECO)][bsac] refers to a build system optimization where we are able to detect +> that a freshly-built artifact is identical to a prior one and to then reuse existing +> artifacts of dependent crates from then on (instead of continuing to rebuild them). + +[bsac]: https://www.microsoft.com/en-us/research/wp-content/uploads/2020/04/build-systems-jfp.pdf + +### The next 6 months + + + +* Identify and remove "oversensitivity" in `.rmeta` + - i.e. changes to spans, comments, etc. will not affect the `.rmeta` + - coupled with cargo's unstable [`checksum-freshness` feature](https://github.com/rust-lang/cargo/issues/14136), + this would avoid triggering rebuilds for dependent crates +* Make `DefId`s more stable when items are added or reordered + - today this is a major source of differences in compiler output + - there are other things like `SymbolIndex`es which we may also want to stabilize +* Work on designs for enabling "transitive" ECO + - i.e. the decision to rebuild should factor in what parts of a transitive crate dep are + accessible via direct deps + +### The "shiny future" we are working towards + + + +Only changes to a crate that affect the public interface of the crate should cause downstream +crates to rebuild. + + + +## Ownership and team asks + + + +| Task | Owner(s) or team(s) | Notes | +| ----------------------------- | ----------------------- | ----- | +| Design meeting | ![Team][] [compiler] | | +| Discussion and moral support | ![Team][] [compiler] ![Team][] [cargo] | | +| Nightly experiment for RDR | | | +| ↳ Author MCP | @osiewicz | [already accepted](https://github.com/rust-lang/compiler-team/issues/790) | +| ↳ Rustc Implementation | | [WIP](https://github.com/osiewicz/rust/tree/api-fingerprinting) | +| ↳ Cargo Implementation | | [WIP](https://github.com/osiewicz/cargo/tree/api-fingerprinting) | +| Improve DefId stability | @dropbear32 | | +| Standard reviews | ![Team][] [compiler] [cargo] | | + +### Definitions + +Definitions for terms used above: + +* *Discussion and moral support* is the lowest level offering, basically committing the team to nothing but good vibes and general support for this endeavor. +* *Design meeting* means holding a synchronous meeting to review a proposal and provide feedback (no decision expected). +* *Standard reviews* refers to reviews for PRs against the repository; these PRs are not expected to be unduly large or complicated. +* Other kinds of decisions: + * Compiler [Major Change Proposal (MCP)](https://forge.rust-lang.org/compiler/mcp.html) is used to propose a 'larger than average' change and get feedback from the compiler team. + +## Frequently asked questions + +### Isn't `rustc` incremental enough? + +Theoretically, yes: under a system like `rust-analyzer` where there isn't chunking of work along +crate/file/process invocation boundaries, incremental compilation would obviate this effort. + +However under `rustc`'s current architecture (1 process invocation per crate, new process +invocation for each compile rather than a daemon): RDR (i.e. being able to skip `rustc` +invocations) still matters. + +Right now even when 100% of a compile's incremental queries hit the cache (such as when you +`touch` a source file; i.e. [`incr-unchanged`](https://perf.rust-lang.org/)) it still takes +non-negligible amounts of time to replay those queries and re-emit compiler outputs (see +[zulip thread](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/rmeta.20stability/near/501691783)).