|
| 1 | +# Relink don't Rebuild |
| 2 | + |
| 3 | + |
| 4 | +| Metadata | | |
| 5 | +| -------- | --- | |
| 6 | +| Point of contact | @yaahc | |
| 7 | +| Teams | <!-- TEAMS WITH ASKS --> | |
| 8 | +| Task owners | <!-- TASK OWNERS --> | |
| 9 | +| Status | Proposed | |
| 10 | +| Tracking issue | | |
| 11 | +| Zulip channel | | |
| 12 | + |
| 13 | +## Summary |
| 14 | + |
| 15 | +Work towards avoiding rebuilds of a crate's dependents for changes that don't affect the crate's |
| 16 | +public interface. |
| 17 | + |
| 18 | +<!--Our end goal is to have some flags available on nightly that enable avoiding unnecessary rebuilds in certain cases. --> |
| 19 | + |
| 20 | +<!-- |
| 21 | +Links: |
| 22 | + - https://github.com/rust-lang/compiler-team/issues/790 |
| 23 | + - Piotr's cargo issue: https://github.com/rust-lang/cargo/issues/14604 |
| 24 | + + branch: https://github.com/rust-lang/cargo/compare/master...osiewicz:cargo:api-fingerprinting |
| 25 | + - Piotr's rustc branch: https://github.com/rust-lang/rust/compare/master...osiewicz:rust:api-fingerprinting |
| 26 | +--> |
| 27 | + |
| 28 | +## Motivation |
| 29 | + |
| 30 | +<!--*Begin with a few sentences summarizing the problem you are attacking and why it is important.*--> |
| 31 | + |
| 32 | +Changing a comment, reordering use statements, adding a `dbg!` statement to a non-inlinable |
| 33 | +function, formatting code, or moving item definitions from one impl block to another |
| 34 | +identical one all cause rebuilds of reverse dependencies of that crate. |
| 35 | + |
| 36 | +This clashes with users' intuition for what needs to be rebuilt when certain changes are made |
| 37 | +and makes iterating more painful. |
| 38 | + |
| 39 | +As a point of reference, in C and C++ – where there is a strict separation between interface |
| 40 | +and implementation in the form of header files – equivalent changes would only cause a |
| 41 | +rebuild of the translation unit whose source has been modified. For other units, existing |
| 42 | +compiler outputs would be reused (and re-linked into the final binary). |
| 43 | + |
| 44 | +Our goal is to work towards making `cargo` and `rustc` smarter about when they do or don't need to |
| 45 | +rebuild dependent crates (reverse dependencies). |
| 46 | + |
| 47 | +### The status quo |
| 48 | + |
| 49 | +<!--* |
| 50 | +Elaborate in more detail about the problem you are trying to solve. This section is making |
| 51 | +the case for why this particular problem is worth prioritizing with project bandwidth. A |
| 52 | +strong status quo section will (a) identify the target audience and (b) give specifics about |
| 53 | +the problems they are facing today. |
| 54 | +
|
| 55 | +Sometimes it may be useful to start sketching out how you think those problems will be |
| 56 | +addressed by your change, as well, though it's not necessary. |
| 57 | +*--> |
| 58 | + |
| 59 | +As an example, consider the [`rg` binary in the `ripgrep` package][rg]. |
| 60 | + |
| 61 | +[rg]: https://github.com/BurntSushi/ripgrep/blob/3b7fd442a6f3aa73f650e763d7cbb902c03d700e/Cargo.toml |
| 62 | + |
| 63 | +Its crate dependency graph (narrowed to only include dependents of `globset`, a particular |
| 64 | +crate in `ripgrep`'s Cargo workspace) looks like this: |
| 65 | +``` |
| 66 | +❯ cargo tree --invert globset |
| 67 | +globset v0.4.16 |
| 68 | +├── grep-cli v0.1.11 |
| 69 | +│ └── grep v0.3.2 |
| 70 | +│ └── ripgrep v14.1.1 |
| 71 | +└── ignore v0.4.23 |
| 72 | + └── ripgrep v14.1.1 |
| 73 | +``` |
| 74 | + |
| 75 | +```mermaid |
| 76 | +flowchart TB |
| 77 | + globset |
| 78 | + grep-cli |
| 79 | + grep |
| 80 | + ignore |
| 81 | + ripgrep |
| 82 | +
|
| 83 | + globset --> ignore --> ripgrep |
| 84 | +
|
| 85 | + globset --> grep-cli --> grep --> ripgrep |
| 86 | +``` |
| 87 | + |
| 88 | +Consider a change that does not alter the interface of the `globset` crate (for example, |
| 89 | +modifying a private item or changing a comment within `globset`'s source code). |
| 90 | + |
| 91 | +Here is the output of `cargo build --timings` for an incremental build of `ripgrep` where only |
| 92 | +such a change was made to `globset`: |
| 93 | + |
| 94 | +<!--  --> |
| 95 | + |
| 96 | +<p align="center"> |
| 97 | + <img src="https://raw.githubusercontent.com/wiki/rrbutani/rust/rdr-cargo-timings-output.png" /> |
| 98 | +</p> |
| 99 | + |
| 100 | + |
| 101 | +In the above we see `globset` and all transitive "upstream" dependent crates (up to `ripgrep`) |
| 102 | +being rebuilt. |
| 103 | + |
| 104 | +_Ideally_, in this scenario, the transitive dependents of `globset` (that only depend on |
| 105 | +`globset`'s "interface") would _not_ need to be rebuilt. This would allow us to skip the |
| 106 | +`grep-cli`, `ignore`, `grep`, and `ripgrep` re-compiles and only redo linking of the final |
| 107 | +binary ("relink, don't rebuild")[^caveat_linking]. |
| 108 | + |
| 109 | +[^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 |
| 110 | + |
| 111 | +For smaller/shallow dep graphs (like the above) the extra rebuilds are tolerable, but for deeper |
| 112 | +graphs, these rebuilds significantly impact edit-debug cycle times. |
| 113 | + |
| 114 | +<!-- |
| 115 | +From original MCP: https://github.com/rust-lang/compiler-team/issues/790 |
| 116 | +
|
| 117 | +> Over at Zed we've noticed that even seemingly innocent changes to the crate that has many |
| 118 | +> dependents causes us to rebuild (almost) the whole world. Adding a `dbg!` statement to the |
| 119 | +> body of non-inlineable function, formatting code, updating a dependency to a new minor |
| 120 | +> version and such still forces us to rebuild all dependants of an affected crate. |
| 121 | +
|
| 122 | +see numbers from piotr's measurements as an example of the upper bound for potential speedup |
| 123 | +--> |
| 124 | + |
| 125 | +--- |
| 126 | + |
| 127 | +#### Transitive Deps and the Build System View |
| 128 | + |
| 129 | +Ideally the crate-level dependency graph above would (morally) correspond to a build graph like |
| 130 | +this[^caveat_pipelining]: |
| 131 | + |
| 132 | +```mermaid |
| 133 | +flowchart TB |
| 134 | + subgraph globset[globset compile] |
| 135 | + globset.rmeta:::rmeta |
| 136 | + globset.rlib:::rlib |
| 137 | + end |
| 138 | + subgraph grep-cli[grep-cli compile] |
| 139 | + grep-cli.rmeta:::rmeta |
| 140 | + grep-cli.rlib:::rlib |
| 141 | + end |
| 142 | + subgraph grep[grep compile] |
| 143 | + grep.rmeta:::rmeta |
| 144 | + grep.rlib:::rlib |
| 145 | + end |
| 146 | + subgraph ignore[ignore compile] |
| 147 | + ignore.rmeta:::rmeta |
| 148 | + ignore.rlib:::rlib |
| 149 | + end |
| 150 | + subgraph ripgrep[ripgrep compile] |
| 151 | + %% ripgrep.rmeta:::rmeta |
| 152 | + ripgrep.rlib:::rlib |
| 153 | + end |
| 154 | +
|
| 155 | + ripgrep_bin["`rg (bin)`"] |
| 156 | +
|
| 157 | + classDef rmeta fill:#ea76cb |
| 158 | + classDef rlib fill:#2e96f5 |
| 159 | +
|
| 160 | + %% linker inputs (`rlib`s): |
| 161 | + globset.rlib & grep-cli.rlib & grep.rlib & ignore.rlib & ripgrep.rlib -.-> ripgrep_bin |
| 162 | +
|
| 163 | + %% direct deps (`rmeta`s): |
| 164 | + globset.rmeta --> ignore |
| 165 | + ignore.rmeta --> ripgrep |
| 166 | +
|
| 167 | + globset.rmeta --> grep-cli |
| 168 | + grep-cli.rmeta --> grep |
| 169 | + grep.rmeta --> ripgrep |
| 170 | +``` |
| 171 | + |
| 172 | +[^caveat_pipelining]: |
| 173 | + We have taken some liberties in the above graph w.r.t. pipelining. |
| 174 | + <br><br> |
| 175 | + Today, `cargo` preforms a single `rustc` invocation to produce the `rlib` and `rmeta` |
| 176 | + for each crate – `rmeta` is modeled as an "early out". |
| 177 | + <br><br> |
| 178 | + Additionally, producing `ripgrep.rlib` and linking (the `rg (bin)` node) happens as part |
| 179 | + of a single `rustc` invocation. |
| 180 | + |
| 181 | +In particular, note that crate compiles use the `rmeta`s of their direct dependencies. |
| 182 | + |
| 183 | +However, in reality crate compiles need access to all _transitive_ `rmeta`s: |
| 184 | +```mermaid |
| 185 | +flowchart TB |
| 186 | + subgraph globset[globset compile] |
| 187 | + globset.rmeta:::rmeta |
| 188 | + globset.rlib:::rlib |
| 189 | + end |
| 190 | + subgraph grep-cli[grep-cli compile] |
| 191 | + grep-cli.rmeta:::rmeta |
| 192 | + grep-cli.rlib:::rlib |
| 193 | + end |
| 194 | + subgraph grep[grep compile] |
| 195 | + grep.rmeta:::rmeta |
| 196 | + grep.rlib:::rlib |
| 197 | + end |
| 198 | + subgraph ignore[ignore compile] |
| 199 | + ignore.rmeta:::rmeta |
| 200 | + ignore.rlib:::rlib |
| 201 | + end |
| 202 | + subgraph ripgrep[ripgrep compile] |
| 203 | + %% ripgrep.rmeta:::rmeta |
| 204 | + ripgrep.rlib:::rlib |
| 205 | + end |
| 206 | +
|
| 207 | + ripgrep_bin["`rg (bin)`"] |
| 208 | +
|
| 209 | + classDef rmeta fill:#ea76cb |
| 210 | + classDef rlib fill:#2e96f5 |
| 211 | +
|
| 212 | + %% linker inputs (`rlib`s): |
| 213 | + globset.rlib & grep-cli.rlib & grep.rlib & ignore.rlib & ripgrep.rlib -.-> ripgrep_bin |
| 214 | +
|
| 215 | + %% direct deps (`rmeta`s): |
| 216 | + globset.rmeta --> ignore |
| 217 | + ignore.rmeta --> ripgrep |
| 218 | +
|
| 219 | + globset.rmeta --> grep-cli |
| 220 | + grep-cli.rmeta --> grep |
| 221 | + grep.rmeta --> ripgrep |
| 222 | +
|
| 223 | + %% transitive deps (`rmeta`s): |
| 224 | + globset.rmeta ==> ripgrep & grep |
| 225 | + grep-cli.rmeta ==> ripgrep |
| 226 | +``` |
| 227 | + |
| 228 | +This means that when a crate's `rmeta` changes, the `rustc` invocations corresponding to all |
| 229 | +transitive dependents of that crate are rerun (even if intermediate `rmeta`s are the |
| 230 | +same). |
| 231 | + |
| 232 | +More concretely: when `globset.rmeta` changes, `grep` is rebuilt – even if `grep-cli.rmeta` |
| 233 | +(after `grep-cli` is re-compiled) hasn't changed. |
| 234 | + |
| 235 | +The fact that crate compiles depend on the `rmeta`s for all transitive dependencies is |
| 236 | +significant because it inhibits our ability to get "early cutoff" (ECO). In reality, crates |
| 237 | +compiles are only _actually_ sensitive to the subset of their transitive deps exposed via |
| 238 | +their direct deps but under this view (file-level, in the eyes of the build system) crates are |
| 239 | +sensitive to transitive dependencies in their entirety. |
| 240 | + |
| 241 | +More concretely: the `grep` crate is only sensitive to the parts of `globset` accessible via |
| 242 | +`grep_cli` – if a change is made to `globset` that doesn't affect this subset, we'd expect to |
| 243 | +see `grep_cli` being rebuilt but the existing `grep` outputs being reused (no `grep` rebuild). |
| 244 | + |
| 245 | +> [!NOTE] |
| 246 | +> ["Early cutoff" (ECO)][bsac] refers to a build system optimization where we are able to detect |
| 247 | +> that a freshly-built artifact is identical to a prior one and to then reuse existing |
| 248 | +> artifacts of dependent crates from then on (instead of continuing to rebuild them). |
| 249 | +
|
| 250 | +[bsac]: https://www.microsoft.com/en-us/research/wp-content/uploads/2020/04/build-systems-jfp.pdf |
| 251 | + |
| 252 | +### The next 6 months |
| 253 | + |
| 254 | +<!--*Sketch out the specific things you are trying to achieve in this goal period. This should be short and high-level -- we don't want to see the design!*--> |
| 255 | + |
| 256 | +* Identify and remove "oversensitivity" in `.rmeta` |
| 257 | + - i.e. changes to spans, comments, etc. will not affect the `.rmeta` |
| 258 | + - coupled with cargo's unstable [`checksum-freshness` feature](https://github.com/rust-lang/cargo/issues/14136), |
| 259 | + this would avoid triggering rebuilds for dependent crates |
| 260 | +* Make `DefId`s more stable when items are added or reordered |
| 261 | + - today this is a major source of differences in compiler output |
| 262 | + - there are other things like `SymbolIndex`es which we may also want to stabilize |
| 263 | +* Work on designs for enabling "transitive" ECO |
| 264 | + - i.e. the decision to rebuild should factor in what parts of a transitive crate dep are |
| 265 | + accessible via direct deps |
| 266 | + |
| 267 | +### The "shiny future" we are working towards |
| 268 | + |
| 269 | +<!-- *If this goal is part of a larger plan that will extend beyond this goal period, sketch out the goal you are working towards. It may be worth adding some text about why these particular goals were chosen as the next logical step to focus on.* > |
| 270 | +
|
| 271 | +*This text is NORMATIVE, in the sense that teams should review this and make sure they are aligned. If not, then the shiny future should be moved to frequently asked questions with a title like "what might we do next".*--> |
| 272 | + |
| 273 | +Only changes to a crate that affect the public interface of the crate should cause downstream |
| 274 | +crates to rebuild. |
| 275 | + |
| 276 | +<!-- |
| 277 | +## Design axioms |
| 278 | +
|
| 279 | +(NOTE: not including) |
| 280 | +
|
| 281 | + - go after the common case (change private function body/change a comment/add private item) |
| 282 | + - incremental improvements? (i.e. cover more cases over time) |
| 283 | + - correctness/testability ((try to be as-or-more correct than rustc-incremental)) |
| 284 | +--> |
| 285 | + |
| 286 | +## Ownership and team asks |
| 287 | + |
| 288 | +<!-- |
| 289 | +**Owner:** *Identify a specific person or small group of people if possible, else the group that will provide the owner. Github user names are commonly used to remove ambiguity.* |
| 290 | +
|
| 291 | +*This section defines the specific work items that are planned and who is expected to do them. It should also include what will be needed from Rust teams. The table below shows some common sets of asks and work, but feel free to adjust it as needed. Every row in the table should either correspond to something done by a contributor or something asked of a team. For items done by a contributor, list the contributor, or ![Help wanted][] if you don't yet know who will do it. For things asked of teams, list ![Team][] and the name of the team. The things typically asked of teams are defined in the [Definitions](#definitions) section below.* --> |
| 292 | + |
| 293 | +| Task | Owner(s) or team(s) | Notes | |
| 294 | +| ----------------------------- | ----------------------- | ----- | |
| 295 | +| Design meeting | ![Team][] [compiler] | | |
| 296 | +| Discussion and moral support | ![Team][] [compiler] ![Team][] [cargo] | | |
| 297 | +| Nightly experiment for RDR | | | |
| 298 | +| ↳ Author MCP | @osiewicz | [already accepted](https://github.com/rust-lang/compiler-team/issues/790) | |
| 299 | +| ↳ Rustc Implementation | | [WIP](https://github.com/osiewicz/rust/tree/api-fingerprinting) | |
| 300 | +| ↳ Cargo Implementation | | [WIP](https://github.com/osiewicz/cargo/tree/api-fingerprinting) | |
| 301 | +| Improve DefId stability | @dropbear32 | | |
| 302 | +| Standard reviews | ![Team][] [compiler] [cargo] | | |
| 303 | + |
| 304 | +### Definitions |
| 305 | + |
| 306 | +Definitions for terms used above: |
| 307 | + |
| 308 | +* *Discussion and moral support* is the lowest level offering, basically committing the team to nothing but good vibes and general support for this endeavor. |
| 309 | +* *Design meeting* means holding a synchronous meeting to review a proposal and provide feedback (no decision expected). |
| 310 | +* *Standard reviews* refers to reviews for PRs against the repository; these PRs are not expected to be unduly large or complicated. |
| 311 | +* Other kinds of decisions: |
| 312 | + * 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. |
| 313 | + |
| 314 | +## Frequently asked questions |
| 315 | + |
| 316 | +### Isn't `rustc` incremental enough? |
| 317 | + |
| 318 | +Theoretically, yes: under a system like `rust-analyzer` where there isn't chunking of work along |
| 319 | +crate/file/process invocation boundaries, incremental compilation would obviate this effort. |
| 320 | + |
| 321 | +However under `rustc`'s current architecture (1 process invocation per crate, new process |
| 322 | +invocation for each compile rather than a daemon): RDR (i.e. being able to skip `rustc` |
| 323 | +invocations) still matters. |
| 324 | + |
| 325 | +Right now even when 100% of a compile's incremental queries hit the cache (such as when you |
| 326 | +`touch` a source file; i.e. [`incr-unchanged`](https://perf.rust-lang.org/)) it still takes |
| 327 | +non-negligible amounts of time to replay those queries and re-emit compiler outputs (see |
| 328 | +[zulip thread](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/rmeta.20stability/near/501691783)). |
0 commit comments