Track evaluation dependencies and cache results #1517
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This adds a mechanism for tracking how cells depend on each other in terms of variables, imports, aliases, modules, process dictionary, etc. Based on this information cells are marked as "stale" and reevaluated only if necessary.
Note: this requires Elixir v1.14.2 to work as expected for variables.
Motivation
Currently, whenever a cell is evaluated, all subsequent cells are marked as stale and require reevaluation. This happens regardless of whether those cells depend on the evaluated cell. This simple approach ensures reproducability by always evaluating cells sequentially.
The main issue with this "greedy" approach is that a cell may do a long computation and changing anything above it require running the long computation again.
Idea
We now track which identifiers each cell references and defines (or redefines), then when a cell is reevaluated we know which cells it affects and we mark only those as "stale".
At evaluation level, instead of storing full evaluation context (all variables/aliases after an evaluation), we store diffs (new variables/aliases defined during an evaluation). Then, when evaluating a cell, we combine all the diffs from previous cells into full evaluation context. For example:
The diffs for cells 1 and 2 are
[x: 1]
and[y: 1]
respectively. Now, when we change the first cell tox = 2
, the diff becomes[x: 2]
. Then to evaluate cell 3 we merge[x: 2]
with[y: 1]
and have[x: 2, y: 1]
as the context, without reevaluating cell 2.Implementation details
Session data
On the Livebook side, each cell has an additional information:
An identifier can be anything, a variable name, a module name, a fixed term such as
:pdict
. Each defined identifier has a version, which again can be anything, an hash digest, a random id, a fixed value.This information is used when computing which cells are stale. To determine cell validity we already compute snapshots, but now a cell snapshot looks only at the parent cells that define identifiers used by that cell, and the identifier versions.
Evaluator
On the Runtime side (specifically in the evaluator), after an evaluation we determine the identifiers it depends on, mostly by using a compilation tracer. The identifiers are reported/tracked with varying granularity, for example we have
{:variable, name}
,{:module, name}
to track individual variables/modules, but we also have a single identifier:pdict
to atomically track the process dictionary.Depending on the identifier type, we approach the "version" differently:
x = 1
changes the snapshots anyway):ok
version