|
| 1 | +# Dependency graph for incremental compilation |
| 2 | + |
| 3 | +This module contains the infrastructure for managing the incremental |
| 4 | +compilation dependency graph. This README aims to explain how it ought |
| 5 | +to be used. In this document, we'll first explain the overall |
| 6 | +strategy, and then share some tips for handling specific scenarios. |
| 7 | + |
| 8 | +The high-level idea is that we want to instrument the compiler to |
| 9 | +track which parts of the AST and other IR are read/written by what. |
| 10 | +This way, when we come back later, we can look at this graph and |
| 11 | +determine what work needs to be redone. |
| 12 | + |
| 13 | +### The dependency graph |
| 14 | + |
| 15 | +The nodes of the graph are defined by the enum `DepNode`. They represent |
| 16 | +one of three things: |
| 17 | + |
| 18 | +1. HIR nodes (like `Hir(DefId)`) represent the HIR input itself. |
| 19 | +2. Data nodes (like `ItemSignature(DefId)`) represent some computed |
| 20 | + information about a particular item. |
| 21 | +3. Procedure notes (like `CoherenceCheckImpl(DefId)`) represent some |
| 22 | + procedure that is executing. Usually this procedure is |
| 23 | + performing some kind of check for errors. You can think of them as |
| 24 | + computed values where the value being computed is `()` (and the |
| 25 | + value may fail to be computed, if an error results). |
| 26 | + |
| 27 | +An edge `N1 -> N2` is added between two nodes if either: |
| 28 | + |
| 29 | +- the value of `N1` is used to compute `N2`; |
| 30 | +- `N1` is read by the procedure `N2`; |
| 31 | +- the procedure `N1` writes the value `N2`. |
| 32 | + |
| 33 | +The latter two conditions are equivalent to the first one if you think |
| 34 | +of procedures as values. |
| 35 | + |
| 36 | +### Basic tracking |
| 37 | + |
| 38 | +There is a very general strategy to ensure that you have a correct, if |
| 39 | +sometimes overconservative, dependency graph. The two main things you have |
| 40 | +to do are (a) identify shared state and (b) identify the current tasks. |
| 41 | + |
| 42 | +### Identifying shared state |
| 43 | + |
| 44 | +Identify "shared state" that will be written by one pass and read by |
| 45 | +another. In particular, we need to identify shared state that will be |
| 46 | +read "across items" -- that is, anything where changes in one item |
| 47 | +could invalidate work done for other items. So, for example: |
| 48 | + |
| 49 | +1. The signature for a function is "shared state". |
| 50 | +2. The computed type of some expression in the body of a function is |
| 51 | + not shared state, because if it changes it does not itself |
| 52 | + invalidate other functions (though it may be that it causes new |
| 53 | + monomorphizations to occur, but that's handled independently). |
| 54 | + |
| 55 | +Put another way: if the HIR for an item changes, we are going to |
| 56 | +recompile that item for sure. But we need the dep tracking map to tell |
| 57 | +us what *else* we have to recompile. Shared state is anything that is |
| 58 | +used to communicate results from one item to another. |
| 59 | + |
| 60 | +### Identifying the current task |
| 61 | + |
| 62 | +The dep graph always tracks a current task: this is basically the |
| 63 | +`DepNode` that the compiler is computing right now. Typically it would |
| 64 | +be a procedure node, but it can also be a data node (as noted above, |
| 65 | +the two are kind of equivalent). |
| 66 | + |
| 67 | +You set the current task by calling `dep_graph.in_task(node)`. For example: |
| 68 | + |
| 69 | +```rust |
| 70 | +let _task = tcx.dep_graph.in_task(DepNode::Privacy); |
| 71 | +``` |
| 72 | + |
| 73 | +Now all the code until `_task` goes out of scope will be considered |
| 74 | +part of the "privacy task". |
| 75 | + |
| 76 | +The tasks are maintained in a stack, so it is perfectly fine to nest |
| 77 | +one task within another. Because pushing a task is considered to be |
| 78 | +computing a value, when you nest a task `N2` inside of a task `N1`, we |
| 79 | +automatically add an edge `N2 -> N1` (since `N1` presumably needed the |
| 80 | +result of `N2` to complete): |
| 81 | + |
| 82 | +```rust |
| 83 | +let _n1 = tcx.dep_graph.in_task(DepNode::N1); |
| 84 | +let _n2 = tcx.dep_graph.in_task(DepNode::N2); |
| 85 | +// this will result in an edge N1 -> n2 |
| 86 | +``` |
| 87 | + |
| 88 | +### Ignore tasks |
| 89 | + |
| 90 | +Although it is rarely needed, you can also push a special "ignore" |
| 91 | +task: |
| 92 | + |
| 93 | +```rust |
| 94 | +let _ignore = tc.dep_graph.in_ignore(); |
| 95 | +``` |
| 96 | + |
| 97 | +This will cause all read/write edges to be ignored until it goes out |
| 98 | +of scope or until something else is pushed. For example, we could |
| 99 | +suppress the edge between nested tasks like so: |
| 100 | + |
| 101 | +```rust |
| 102 | +let _n1 = tcx.dep_graph.in_task(DepNode::N1); |
| 103 | +let _ignore = tcx.dep_graph.in_ignore(); |
| 104 | +let _n2 = tcx.dep_graph.in_task(DepNode::N2); |
| 105 | +// now no edge is added |
| 106 | +``` |
| 107 | + |
| 108 | +### Tracking reads and writes |
| 109 | + |
| 110 | +We need to identify what shared state is read/written by the current |
| 111 | +task as it executes. The most fundamental way of doing that is to invoke |
| 112 | +the `read` and `write` methods on `DepGraph`: |
| 113 | + |
| 114 | +```rust |
| 115 | +// Adds an edge from DepNode::Hir(some_def_id) to the current task |
| 116 | +tcx.dep_graph.read(DepNode::Hir(some_def_id)) |
| 117 | + |
| 118 | +// Adds an edge from the current task to DepNode::ItemSignature(some_def_id) |
| 119 | +tcx.dep_graph.write(DepNode::ItemSignature(some_def_id)) |
| 120 | +``` |
| 121 | + |
| 122 | +However, you should rarely need to invoke those methods directly. |
| 123 | +Instead, the idea is to *encapsulate* shared state into some API that |
| 124 | +will invoke `read` and `write` automatically. The most common way to |
| 125 | +do this is to use a `DepTrackingMap`, described in the next section, |
| 126 | +but any sort of abstraction barrier will do. In general, the strategy |
| 127 | +is that getting access to information implicitly adds an appropriate |
| 128 | +`read`. So, for example, when you use the |
| 129 | +`dep_graph::visit_all_items_in_krate` helper method, it will visit |
| 130 | +each item `X`, start a task `Foo(X)` for that item, and automatically |
| 131 | +add an edge `Hir(X) -> Foo(X)`. This edge is added because the code is |
| 132 | +being given access to the HIR node for `X`, and hence it is expected |
| 133 | +to read from it. Similarly, reading from the `tcache` map for item `X` |
| 134 | +(which is a `DepTrackingMap`, described below) automatically invokes |
| 135 | +`dep_graph.read(ItemSignature(X))`. |
| 136 | + |
| 137 | +To make this strategy work, a certain amount of indirection is |
| 138 | +required. For example, modules in the HIR do not have direct pointers |
| 139 | +to the items that they contain. Rather, they contain node-ids -- one |
| 140 | +can then ask the HIR map for the item with a given node-id. This gives |
| 141 | +us an opportunity to add an appropriate read edge. |
| 142 | + |
| 143 | +#### Explicit calls to read and write when starting a new subtask |
| 144 | + |
| 145 | +One time when you *may* need to call `read` and `write` directly is |
| 146 | +when you push a new task onto the stack, either by calling `in_task` |
| 147 | +as shown above or indirectly, such as with the `memoize` pattern |
| 148 | +described below. In that case, any data that the task has access to |
| 149 | +from the surrounding environment must be explicitly "read". For |
| 150 | +example, in `librustc_typeck`, the collection code visits all items |
| 151 | +and, among other things, starts a subtask producing its signature |
| 152 | +(what follows is simplified pseudocode, of course): |
| 153 | + |
| 154 | +```rust |
| 155 | +fn visit_item(item: &hir::Item) { |
| 156 | + // Here, current subtask is "Collect(X)", and an edge Hir(X) -> Collect(X) |
| 157 | + // has automatically been added by `visit_all_items_in_krate`. |
| 158 | + let sig = signature_of_item(item); |
| 159 | +} |
| 160 | + |
| 161 | +fn signature_of_item(item: &hir::Item) { |
| 162 | + let def_id = tcx.map.local_def_id(item.id); |
| 163 | + let task = tcx.dep_graph.in_task(DepNode::ItemSignature(def_id)); |
| 164 | + tcx.dep_graph.read(DepNode::Hir(def_id)); // <-- the interesting line |
| 165 | + ... |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +Here you can see that, in `signature_of_item`, we started a subtask |
| 170 | +corresponding to producing the `ItemSignature`. This subtask will read from |
| 171 | +`item` -- but it gained access to `item` implicitly. This means that if it just |
| 172 | +reads from `item`, there would be missing edges in the graph: |
| 173 | + |
| 174 | + Hir(X) --+ // added by the explicit call to `read` |
| 175 | + | | |
| 176 | + | +---> ItemSignature(X) -> Collect(X) |
| 177 | + | ^ |
| 178 | + | | |
| 179 | + +---------------------------------+ // added by `visit_all_items_in_krate` |
| 180 | + |
| 181 | +In particular, the edge from `Hir(X)` to `ItemSignature(X)` is only |
| 182 | +present because we called `read` ourselves when entering the `ItemSignature(X)` |
| 183 | +task. |
| 184 | + |
| 185 | +So, the rule of thumb: when entering a new task yourself, register |
| 186 | +reads on any shared state that you inherit. (This actually comes up |
| 187 | +fairly infrequently though: the main place you need caution is around |
| 188 | +memoization.) |
| 189 | + |
| 190 | +#### Dependency tracking map |
| 191 | + |
| 192 | +`DepTrackingMap` is a particularly convenient way to correctly store |
| 193 | +shared state. A `DepTrackingMap` is a special hashmap that will add |
| 194 | +edges automatically when `get` and `insert` are called. The idea is |
| 195 | +that, when you get/insert a value for the key `K`, we will add an edge |
| 196 | +from/to the node `DepNode::Variant(K)` (for some variant specific to |
| 197 | +the map). |
| 198 | + |
| 199 | +Each `DepTrackingMap` is parameterized by a special type `M` that |
| 200 | +implements `DepTrackingMapConfig`; this trait defines the key and value |
| 201 | +types of the map, and also defines a fn for converting from the key to |
| 202 | +a `DepNode` label. You don't usually have to muck about with this by |
| 203 | +hand, there is a macro for creating it. You can see the complete set |
| 204 | +of `DepTrackingMap` definitions in `librustc/middle/ty/maps.rs`. |
| 205 | + |
| 206 | +As an example, let's look at the `adt_defs` map. The `adt_defs` map |
| 207 | +maps from the def-id of a struct/enum to its `AdtDef`. It is defined |
| 208 | +using this macro: |
| 209 | + |
| 210 | +```rust |
| 211 | +dep_map_ty! { AdtDefs: ItemSignature(DefId) -> ty::AdtDefMaster<'tcx> } |
| 212 | +// ~~~~~~~ ~~~~~~~~~~~~~ ~~~~~ ~~~~~~~~~~~~~~~~~~~~~~ |
| 213 | +// | | Key type Value type |
| 214 | +// | DepNode variant |
| 215 | +// Name of map id type |
| 216 | +``` |
| 217 | + |
| 218 | +this indicates that a map id type `AdtDefs` will be created. The key |
| 219 | +of the map will be a `DefId` and value will be |
| 220 | +`ty::AdtDefMaster<'tcx>`. The `DepNode` will be created by |
| 221 | +`DepNode::ItemSignature(K)` for a given key. |
| 222 | + |
| 223 | +Once that is done, you can just use the `DepTrackingMap` like any |
| 224 | +other map: |
| 225 | + |
| 226 | +```rust |
| 227 | +let mut map: DepTrackingMap<M> = DepTrackingMap::new(dep_graph); |
| 228 | +map.insert(key, value); // registers dep_graph.write |
| 229 | +map.get(key; // registers dep_graph.read |
| 230 | +``` |
| 231 | + |
| 232 | +#### Memoization |
| 233 | + |
| 234 | +One particularly interesting case is memoization. If you have some |
| 235 | +shared state that you compute in a memoized fashion, the correct thing |
| 236 | +to do is to define a `RefCell<DepTrackingMap>` for it and use the |
| 237 | +`memoize` helper: |
| 238 | + |
| 239 | +```rust |
| 240 | +map.memoize(key, || /* compute value */) |
| 241 | +``` |
| 242 | + |
| 243 | +This will create a graph that looks like |
| 244 | + |
| 245 | + ... -> MapVariant(key) -> CurrentTask |
| 246 | + |
| 247 | +where `MapVariant` is the `DepNode` variant that the map is associated with, |
| 248 | +and `...` are whatever edges the `/* compute value */` closure creates. |
| 249 | + |
| 250 | +In particular, using the memoize helper is much better than writing |
| 251 | +the obvious code yourself: |
| 252 | + |
| 253 | +``` |
| 254 | +if let Some(result) = map.get(key) { |
| 255 | + return result; |
| 256 | +} |
| 257 | +let value = /* compute value */; |
| 258 | +map.insert(key, value); |
| 259 | +``` |
| 260 | + |
| 261 | +If you write that code manually, the dependency graph you get will |
| 262 | +include artificial edges that are not necessary. For example, imagine that |
| 263 | +two tasks, A and B, both invoke the manual memoization code, but A happens |
| 264 | +to go first. The resulting graph will be: |
| 265 | + |
| 266 | + ... -> A -> MapVariant(key) -> B |
| 267 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ // caused by A writing to MapVariant(key) |
| 268 | + ~~~~~~~~~~~~~~~~~~~~ // caused by B reading from MapVariant(key) |
| 269 | + |
| 270 | +This graph is not *wrong*, but it encodes a path from A to B that |
| 271 | +should not exist. In contrast, using the memoized helper, you get: |
| 272 | + |
| 273 | + ... -> MapVariant(key) -> A |
| 274 | + | |
| 275 | + +----------> B |
| 276 | + |
| 277 | +which is much cleaner. |
| 278 | + |
| 279 | +**Be aware though that the closure is executed with `MapVariant(key)` |
| 280 | +pushed onto the stack as the current task!** That means that you must |
| 281 | +add explicit `read` calls for any shared state that it accesses |
| 282 | +implicitly from its environment. See the section on "explicit calls to |
| 283 | +read and write when starting a new subtask" above for more details. |
| 284 | + |
| 285 | +### How to decide where to introduce a new task |
| 286 | + |
| 287 | +Certainly, you need at least one task on the stack: any attempt to |
| 288 | +`read` or `write` shared state will panic if there is no current |
| 289 | +task. But where does it make sense to introduce subtasks? The basic |
| 290 | +rule is that a subtask makes sense for any discrete unit of work you |
| 291 | +may want to skip in the future. Adding a subtask separates out the |
| 292 | +reads/writes from *that particular subtask* versus the larger |
| 293 | +context. An example: you might have a 'meta' task for all of borrow |
| 294 | +checking, and then subtasks for borrow checking individual fns. (Seen |
| 295 | +in this light, memoized computations are just a special case where we |
| 296 | +may want to avoid redoing the work even within the context of one |
| 297 | +compilation.) |
| 298 | + |
| 299 | +The other case where you might want a subtask is to help with refining |
| 300 | +the reads/writes for some later bit of work that needs to be memoized. |
| 301 | +For example, we create a subtask for type-checking the body of each |
| 302 | +fn. However, in the initial version of incr. comp. at least, we do |
| 303 | +not expect to actually *SKIP* type-checking -- we only expect to skip |
| 304 | +trans. However, it's still useful to create subtasks for type-checking |
| 305 | +individual items, because, otherwise, if a fn sig changes, we won't |
| 306 | +know which callers are affected -- in fact, because the graph would be |
| 307 | +so coarse, we'd just have to retrans everything, since we can't |
| 308 | +distinguish which fns used which fn sigs. |
| 309 | + |
| 310 | +### Testing the dependency graph |
| 311 | + |
| 312 | +There are various ways to write tests against the dependency graph. |
| 313 | +The simplest mechanism are the |
| 314 | +`#[rustc_if_this_changed]` and `#[rustc_then_this_would_need]` |
| 315 | +annotations. These are used in compile-fail tests to test whether the |
| 316 | +expected set of paths exist in the dependency graph. As an example, |
| 317 | +see `src/test/compile-fail/dep-graph-caller-callee.rs`. |
| 318 | + |
| 319 | +The idea is that you can annotate a test like: |
| 320 | + |
| 321 | +```rust |
| 322 | +#[rustc_if_this_changed] |
| 323 | +fn foo() { } |
| 324 | + |
| 325 | +#[rustc_then_this_would_need(TypeckItemBody)] //~ ERROR OK |
| 326 | +fn bar() { foo(); } |
| 327 | + |
| 328 | +#[rustc_then_this_would_need(TypeckItemBody)] //~ ERROR no path |
| 329 | +fn baz() { } |
| 330 | +``` |
| 331 | + |
| 332 | +This will check whether there is a path in the dependency graph from |
| 333 | +`Hir(foo)` to `TypeckItemBody(bar)`. An error is reported for each |
| 334 | +`#[rustc_then_this_would_need]` annotation that indicates whether a |
| 335 | +path exists. `//~ ERROR` annotations can then be used to test if a |
| 336 | +path is found (as demonstrated above). |
| 337 | + |
| 338 | +### Debugging the dependency graph |
| 339 | + |
| 340 | +The compiler is also capable of dumping the dependency graph for your |
| 341 | +debugging pleasure. To do so, pass the `-Z dump-dep-graph` flag. The |
| 342 | +graph will be dumped to `dep_graph.{txt,dot}` in the current |
| 343 | +directory. You can override the filename with the `RUST_DEP_GRAPH` |
| 344 | +environment variable. |
| 345 | + |
| 346 | +Frequently, though, the full dep graph is quite overwhelming and not |
| 347 | +particularly helpful. Therefore, the compiler also allows you to filter |
| 348 | +the graph. You can filter in three ways: |
| 349 | + |
| 350 | +1. All edges originating in a particular set of nodes (usually a single node). |
| 351 | +2. All edges reaching a particular set of nodes. |
| 352 | +3. All edges that lie between given start and end nodes. |
| 353 | + |
| 354 | +To filter, use the `RUST_DEP_GRAPH_FILTER` environment variable, which should |
| 355 | +look like one of the following: |
| 356 | + |
| 357 | +``` |
| 358 | +source_filter // nodes originating from source_filter |
| 359 | +-> target_filter // nodes that can reach target_filter |
| 360 | +source_filter -> target_filter // nodes in between source_filter and target_filter |
| 361 | +``` |
| 362 | + |
| 363 | +`source_filter` and `target_filter` are a `&`-separated list of strings. |
| 364 | +A node is considered to match a filter if all of those strings appear in its |
| 365 | +label. So, for example: |
| 366 | + |
| 367 | +``` |
| 368 | +RUST_DEP_GRAPH_FILTER='-> TypeckItemBody' |
| 369 | +``` |
| 370 | + |
| 371 | +would select the predecessors of all `TypeckItemBody` nodes. Usually though you |
| 372 | +want the `TypeckItemBody` node for some particular fn, so you might write: |
| 373 | + |
| 374 | +``` |
| 375 | +RUST_DEP_GRAPH_FILTER='-> TypeckItemBody & bar' |
| 376 | +``` |
| 377 | + |
| 378 | +This will select only the `TypeckItemBody` nodes for fns with `bar` in their name. |
| 379 | + |
| 380 | +Perhaps you are finding that when you change `foo` you need to re-type-check `bar`, |
| 381 | +but you don't think you should have to. In that case, you might do: |
| 382 | + |
| 383 | +``` |
| 384 | +RUST_DEP_GRAPH_FILTER='Hir&foo -> TypeckItemBody & bar' |
| 385 | +``` |
| 386 | + |
| 387 | +This will dump out all the nodes that lead from `Hir(foo)` to |
| 388 | +`TypeckItemBody(bar)`, from which you can (hopefully) see the source |
| 389 | +of the erroneous edge. |
| 390 | + |
0 commit comments