]>
Commit | Line | Data |
---|---|---|
f035d41b XL |
1 | # Summary |
2 | ||
3 | - Introduce a user-visibile concept of `Durability` | |
4 | - Adjusting the "durability" of an input can allow salsa to skip a lot of validation work | |
5 | - Garbage collection -- particularly of interned values -- however becomes more complex | |
6 | - Possible future expansion: automatic detection of more "durable" input values | |
7 | ||
8 | # Motivation | |
9 | ||
10 | ## Making validation faster by optimizing for "durability" | |
11 | ||
12 | Presently, salsa's validation logic requires traversing all | |
13 | dependencies to check that they have not changed. This can sometimes | |
14 | be quite costly in practice: rust-analyzer for example sometimes | |
15 | spends as much as 90ms revalidating the results from a no-op | |
16 | change. One option to improve this is simply optimization -- | |
17 | [salsa#176] for example reduces validation times significantly, and | |
18 | there remains opportunity to do better still. However, even if we are | |
19 | able to traverse the dependency graph more efficiently, it will still | |
20 | be an O(n) process. It would be nice if we could do better. | |
21 | ||
22 | [salsa#176]: https://github.com/salsa-rs/salsa/pull/176 | |
23 | ||
24 | One observation is that, in practice, there are often input values | |
25 | that are known to change quite infrequently. For example, in | |
26 | rust-analyzer, the standard library and crates downloaded from | |
27 | crates.io are unlikely to change (though changes are possible; see | |
28 | below). Similarly, the `Cargo.toml` file for a project changes | |
29 | relatively infrequently compared to the sources. We say then that | |
30 | these inputs are more **durable** -- that is, they change less frequently. | |
31 | ||
32 | This RFC proposes a mechanism to take advantage of durability for | |
33 | optimization purposes. Imagine that we have some query Q that depends | |
34 | solely on the standard library. The idea is that we can track the last | |
35 | revision R when the standard library was changed. Then, when | |
36 | traversing dependencies, we can skip traversing the dependencies of Q | |
37 | if it was last validated after the revision R. Put another way, we | |
38 | only need to traverse the dependencies of Q when the standard library | |
39 | changes -- which is unusual. If the standard library *does* change, | |
40 | for example by user's tinkering with the internal sources, then yes we | |
41 | walk the dependencies of Q to see if it is affected. | |
42 | ||
43 | # User's guide | |
44 | ||
45 | ## The durability type | |
46 | ||
47 | We add a new type `salsa::Durability` which has there associated constants: | |
48 | ||
3dfed10e | 49 | ```rust,ignore |
f035d41b XL |
50 | #[derive(Copy, Clone, Debug, Ord)] |
51 | pub struct Durability(..); | |
52 | ||
53 | impl Durability { | |
54 | // Values that change regularly, like the source to the current crate. | |
55 | pub const LOW: Durability; | |
56 | ||
57 | // Values that change infrequently, like Cargo.toml. | |
58 | pub const MEDIUM: Durability; | |
59 | ||
60 | // Values that are not expected to change, like sources from crates.io or the stdlib. | |
61 | pub const HIGH: Durability; | |
62 | } | |
63 | ``` | |
64 | ||
65 | h## Specifying the durability of an input | |
66 | ||
67 | When setting an input `foo`, one can now invoke a method | |
68 | `set_foo_with_durability`, which takes a `Durability` as the final | |
69 | argument: | |
70 | ||
3dfed10e | 71 | ```rust,ignore |
f035d41b XL |
72 | // db.set_foo(key, value) is equivalent to: |
73 | db.set_foo_with_durability(key, value, Durability::LOW); | |
74 | ||
75 | // This would indicate that `foo` is not expected to change: | |
76 | db.set_foo_with_durability(key, value, Durability::HIGH); | |
77 | ``` | |
78 | ||
79 | ## Durability of interned values | |
80 | ||
81 | Interned values are always considered `Durability::HIGH`. This makes | |
82 | sense as many queries that only use high durability inputs will also | |
83 | make use of interning internally. A consequence of this is that they | |
84 | will not be garbage collected unless you use the specific patterns | |
85 | recommended below. | |
86 | ||
87 | ## Synthetic writes | |
88 | ||
89 | Finally, we add one new method, `synthetic_write(durability)`, | |
90 | available on the salsa runtime: | |
91 | ||
3dfed10e | 92 | ```rust,ignore |
f035d41b XL |
93 | db.salsa_runtime().synthetic_write(Durability::HIGH) |
94 | ``` | |
95 | ||
96 | As the name suggests, `synthetic_write` causes salsa to act *as | |
97 | though* a write to an input of the given durability had taken | |
98 | place. This can be used for benchmarking, but it's also important to | |
99 | controlling what values get garbaged collected, as described below. | |
100 | ||
101 | ## Tracing and garbage collection | |
102 | ||
103 | Durability affects garbage collection. The `SweepStrategy` struct is | |
104 | modified as follows: | |
105 | ||
3dfed10e | 106 | ```rust,ignore |
f035d41b XL |
107 | /// Sweeps values which may be outdated, but which have not |
108 | /// been verified since the start of the current collection. | |
109 | /// These are typically memoized values from previous computations | |
110 | /// that are no longer relevant. | |
111 | pub fn sweep_outdated(self) -> SweepStrategy; | |
112 | ||
113 | /// Sweeps values which have not been verified since the start | |
114 | /// of the current collection, even if they are known to be | |
115 | /// up to date. This can be used to collect "high durability" values | |
116 | /// that are not *directly* used by the main query. | |
117 | /// | |
118 | /// So, for example, imagine a main query `result` which relies | |
119 | /// on another query `threshold` and (indirectly) on a `threshold_inner`: | |
120 | /// | |
121 | /// ``` | |
122 | /// result(10) [durability: Low] | |
123 | /// | | |
124 | /// v | |
125 | /// threshold(10) [durability: High] | |
126 | /// | | |
127 | /// v | |
128 | /// threshold_inner(10) [durability: High] | |
129 | /// ``` | |
130 | /// | |
131 | /// If you modify a low durability input and then access `result`, | |
132 | /// then `result(10)` and its *immediate* dependencies will | |
133 | /// be considered "verified". However, because `threshold(10)` | |
134 | /// has high durability and no high durability input was modified, | |
135 | /// we will not verify *its* dependencies, so `threshold_inner` is not | |
136 | /// verified (but it is also not outdated). | |
137 | /// | |
138 | /// Collecting unverified things would therefore collect `threshold_inner(10)`. | |
139 | /// Collecting only *outdated* things (i.e., with `sweep_outdated`) | |
140 | /// would collect nothing -- but this does mean that some high durability | |
141 | /// queries that are no longer relevant to your main query may stick around. | |
142 | /// | |
143 | /// To get the most precise garbage collection, do a synthetic write with | |
144 | /// high durability -- this will force us to verify *all* values. You can then | |
145 | /// sweep unverified values. | |
146 | pub fn sweep_unverified(self) -> SweepStrategy; | |
147 | ``` | |
148 | ||
149 | # Reference guide | |
150 | ||
151 | ## Review: The need for GC to collect outdated values | |
152 | ||
153 | In general, salsa's lazy validation scheme can lead to the accumulation | |
154 | of garbage that is no longer needed. Consider a query like this one: | |
155 | ||
3dfed10e | 156 | ```rust,ignore |
f035d41b XL |
157 | fn derived1(db: &impl Database, start: usize) { |
158 | let middle = self.input(start); | |
159 | self.derived2(middle) | |
160 | } | |
161 | ``` | |
162 | ||
163 | Now imagine that, on some particular run, we compute `derived1(22)`: | |
164 | ||
165 | - `derived1(22)` | |
166 | - executes `input(22)`, which returns `44` | |
167 | - then executes `derived2(44)` | |
168 | ||
169 | The end result of this execution will be a dependency graph | |
170 | like: | |
171 | ||
3dfed10e | 172 | ```notrust |
f035d41b XL |
173 | derived1(22) -> derived2(44) |
174 | | | |
175 | v | |
176 | input(22) | |
177 | ``` | |
178 | ||
179 | Now. imagine that the user modifies `input(22)` to have the value `45`. | |
180 | The next time `derived1(22)` executes, it will load `input(22)` as before, | |
181 | but then execute `derived2(45)`. This leaves us with a dependency | |
182 | graph as follows: | |
183 | ||
3dfed10e | 184 | ```notrust |
f035d41b XL |
185 | derived1(22) -> derived2(45) |
186 | | | |
187 | v | |
188 | input(22) derived2(44) | |
189 | ``` | |
190 | ||
191 | Notice that we still see `derived2(44)` in the graph. This is because | |
192 | we memoized the result in last round and then simply had no use for it | |
193 | in this round. The role of GC is to collect "outdated" values like | |
194 | this one. | |
195 | ||
196 | ###Review: Tracing and GC before durability | |
197 | ||
198 | In the absence of durability, when you execute a query Q in some new | |
199 | revision where Q has not previously executed, salsa must trace back | |
200 | through all the queries that Q depends on to ensure that they are | |
201 | still up to date. As each of Q's dependencies is validated, we mark it | |
202 | to indicate that it has been checked in the current revision (and | |
203 | thus, within a particular revision, we would never validate or trace a | |
204 | particular query twice). | |
205 | ||
206 | So, to continue our example, when we first executed `derived1(22)` | |
207 | in revision R1, we might have had a graph like: | |
208 | ||
209 | ||
3dfed10e | 210 | ```notrust |
f035d41b XL |
211 | derived1(22) -> derived2(44) |
212 | [verified: R1] [verified: R1] | |
213 | | | |
214 | v | |
215 | input(22) | |
216 | ``` | |
217 | ||
218 | Now, after we modify `input(22)` and execute `derived1(22)` again, we | |
219 | would have a graph like: | |
220 | ||
3dfed10e | 221 | ```notrust |
f035d41b XL |
222 | derived1(22) -> derived2(45) |
223 | [verified: R2] [verified: R2] | |
224 | | | |
225 | v | |
226 | input(22) derived2(44) | |
227 | [verified: R1] | |
228 | ``` | |
229 | ||
230 | Note that `derived2(44)`, the outdated value, never had its "verified" | |
231 | revision updated, because we never accessed it. | |
232 | ||
233 | Salsa leverages this validation stamp to serve as the "marking" phase | |
234 | of a simple mark-sweep garbage collector. The idea is that the sweep | |
235 | method can collect any values that are "outdated" (whose "verified" | |
236 | revision is less than the current revision). | |
237 | ||
238 | The intended model is that one can do a "mark-sweep" style garbage | |
239 | collection like so: | |
240 | ||
3dfed10e | 241 | ```rust,ignore |
f035d41b XL |
242 | // Modify some input, triggering a new revision. |
243 | db.set_input(22, 45); | |
244 | ||
245 | // The **mark** phase: execute the "main query", with the intention | |
246 | // that we wish to retain all the memoized values needed to compute | |
247 | // this main query, but discard anything else. For example, in an IDE | |
248 | // context, this might be a "compute all errors" query. | |
249 | db.derived1(22); | |
250 | ||
251 | // The **sweep** phase: discard anything that was not traced during | |
252 | // the mark phase. | |
253 | db.sweep_all(...); | |
254 | ``` | |
255 | ||
256 | In the case of our example, when we execute `sweep_all`, it would | |
257 | collect `derived2(44)`. | |
258 | ||
259 | ## Challenge: Durability lets us avoid tracing | |
260 | ||
261 | This tracing model is affected by the move to durability. Now, if some | |
262 | derived value has a high durability, we may skip tracing its | |
263 | descendants altogether. This means that they would never be "verified" | |
264 | -- that is, their "verified date" would never be updated. | |
265 | ||
266 | This is why we modify the definition of "outdated" as follows: | |
267 | ||
268 | - For a query value `Q` with durability `D`, let `R_lc` be the revision when | |
269 | values of durability `D` last changed. Let `R_v` be the revision when | |
270 | `Q` was last verified. | |
271 | - `Q` is outdated if `R_v < R_lc`. | |
272 | - In other words, if `Q` may have changed since it was last verified. | |
273 | ||
274 | ## Collecting interned and untracked values | |
275 | ||
276 | Most values can be collected whenever we like without influencing | |
277 | correctness. However, interned values and those with untracked | |
278 | dependencies are an exception -- **they can only be collected when | |
279 | outdated**. This is because their values may not be reproducible -- | |
280 | in other words, re-executing an interning query (or one with untracked | |
281 | dependencies, which can read arbitrary program state) twice in a row | |
282 | may produce a different value. In the case of an interning query, for | |
283 | example, we may wind up using a different integer than we did before. | |
284 | If the query is outdated, this is not a problem: anything that | |
285 | dependend on its result must also be outdated, and hence would be | |
286 | re-executed and would observe the new value. But if the query is *not* | |
287 | outdated, then we could get inconsistent result.s | |
288 | ||
289 | # Alternatives and future work | |
290 | ||
291 | ## Rejected: Arbitrary durabilities | |
292 | ||
293 | We considered permitting arbitrary "levels" of durability -- for | |
294 | example, allowing the user to specify a number -- rather than offering | |
295 | just three. Ultimately it seemed like that level of control wasn't | |
296 | really necessary and that having just three levels would be sufficient | |
297 | and simpler. | |
298 | ||
299 | ## Rejected: Durability lattices | |
300 | ||
301 | We also considered permitting a "lattice" of durabilities -- e.g., to | |
302 | mirror the crate DAG in rust-analyzer -- but this is tricky because | |
303 | the lattice itself would be dependent on other inputs. | |
304 |