-
Notifications
You must be signed in to change notification settings - Fork 0
/
reactivity-graph.Rmd
364 lines (260 loc) · 18.6 KB
/
reactivity-graph.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# The reactive graph {#reactive-graph}
```{r include=FALSE}
source("common.R")
```
## Introduction
To understand reactive computation you must first understand the reactive graph.
In this chapter, we'll dive in to the details of the graph, paying more attention to precise order in which things happen.
In particular, you'll learn about the importance of invalidation, the process which is key to ensuring that Shiny does the minimum amount of work.
You'll also learn about the reactlog package which can automatically draw the reactive graph for real apps.
If it's been a while since you looked at Chapter \@ref(basic-reactivity), I highly recommend that you re-familiarise yourself with it before continuing.
It lays the groundwork for the concepts that we'll explore in more detail here.
## A step-by-step tour of reactive execution {#step-through}
To explain the process of reactive execution, we'll use the graphic shown in Figure \@ref(fig:graph-complete).
It contains three reactive inputs, three reactive expressions, and three outputs[^reactivity-graph-1].
Recall that reactive inputs and expressions are collectively called reactive producers; reactive expressions and outputs are reactive consumers.
[^reactivity-graph-1]: Anywhere you see output, you can also think observer.
The primary difference is that certain outputs that aren't visible will never be computed.
We'll discuss the details in Section \@ref(observers-details).
```{r graph-complete, echo = FALSE, out.width = NULL, fig.cap = "Complete reactive graph of an imaginary app containing three inputs, three reactive expressions, and three outputs."}
knitr::include_graphics("diagrams/reactivity-tracking/08.png", dpi = 300)
```
The connections between the components are directional, with the arrows indicating the direction of reactivity.
The direction might surprise you, as it's easy to think of a consumer taking dependencies on one or more producers.
Shortly, however, you'll see that the flow of reactivity is more accurately modelled in the opposite direction.
The underlying app is not important, but if it helps you to have something concrete, you could pretend that it was derived from this not-very-useful app.
```{r}
ui <- fluidPage(
numericInput("a", "a", value = 10),
numericInput("b", "b", value = 1),
numericInput("c", "c", value = 1),
plotOutput("x"),
tableOutput("y"),
textOutput("z")
)
server <- function(input, output, session) {
rng <- reactive(input$a * 2)
smp <- reactive(sample(rng(), input$b, replace = TRUE))
bc <- reactive(input$b * input$c)
output$x <- renderPlot(hist(smp()))
output$y <- renderTable(max(smp()))
output$z <- renderText(bc())
}
```
Let's get started!
## A session begins
Figure \@ref(fig:graph-init) shows the reactive graph right after the app has started and the server function has been executed for the first time.
```{r graph-init, echo = FALSE, out.width = NULL, fig.cap = "Initial state after app load. There are no connections between objects and all reactive expressions are invalidated (grey). There are six reactive consumers and six reactive producers."}
knitr::include_graphics("diagrams/reactivity-tracking/01.png", dpi = 300)
```
There are three important messages in this figure:
- There are no connections between the elements because Shiny has no *a priori* knowledge of the relationships between reactives.
- All reactive expressions and outputs are in their starting state, **invalidated** (grey), which means that they have yet to be run.
- The reactive inputs are ready (green) indicating that their values are available for computation.
### Execution begins
Now we start the execution phase, as shown in Figure \@ref(fig:graph-execute).
```{r graph-execute, echo = FALSE, out.width = NULL, fig.cap = "Shiny starts executing an arbitrary observer/output, coloured orange."}
knitr::include_graphics("diagrams/reactivity-tracking/02.png", dpi = 300)
```
In this phase, Shiny picks an invalidated output and starts executing it (orange).
You might wonder how Shiny decides which of the invalidated outputs to execute.
In short, you should act as if it's random: your observers and outputs shouldn't care what order they execute in, because they've been designed to function independently[^reactivity-graph-2].
[^reactivity-graph-2]: If you have observers whose side effects must happen in a certain order, you're generally better off re-designing your system.
Failing that, you can control the relative order of observers with the `priority` argument to `observe()`.
### Reading a reactive expression
Executing an output may require a value from a reactive, as in Figure \@ref(fig:graph-read-reactive).
```{r graph-read-reactive, echo = FALSE, out.width = NULL, fig.cap = "The output needs the value of a reactive expression, so it starts executing the expression."}
knitr::include_graphics("diagrams/reactivity-tracking/03.png", dpi = 300)
```
Reading a reactive changes the graph in two ways:
- The reactive expression also needs to start computing its value (turn orange).
Note that the output is still computing: it's waiting on the reactive expression to return its value so its own execution can continue, just like a regular function call in R.
- Shiny records a relationship between the output and reactive expression (i.e. we draw an arrow).
The direction of the arrow is important: the expression records that it is used by the output; the output doesn't record that it uses the expression.
This is a subtle distinction, but its importance will become more clear when you learn about invalidation.
### Reading an input
This particular reactive expression happens to read a reactive input.
Again, a dependency/dependent relationship is established, so in Figure \@ref(fig:graph-read-input) we add another arrow.
```{r graph-read-input, echo = FALSE, out.width = NULL, fig.cap = "The reactive expression also reads from a reactive value, so we add another arrow."}
knitr::include_graphics("diagrams/reactivity-tracking/04.png", dpi = 300)
```
Unlike reactive expressions and outputs, reactive inputs have nothing to execute so they can return immediately.
### Reactive expression completes
In our example, the reactive expression reads another reactive expression, which in turn reads another input.
We'll skip over the blow-by-blow description of those steps, since they're a repeat of what we've already described, and jump directly to Figure \@ref(fig:graph-reactive-complete).
```{r graph-reactive-complete, echo = FALSE, out.width = NULL, fig.cap = "The reactive expression has finished computing so turns green."}
knitr::include_graphics("diagrams/reactivity-tracking/05.png", dpi = 300)
```
Now that the reactive expression has finished executing it turns green to indicate that it's ready.
It caches the result so it doesn't need to recompute unless its inputs change.
### Output completes
Now that the reactive expression has returned its value, the output can finish executing, and change colour to green, as in Figure \@ref(fig:graph-output-complete).
```{r graph-output-complete, echo = FALSE, out.width = NULL, fig.cap = "The output has finished computation and turns green."}
knitr::include_graphics("diagrams/reactivity-tracking/06.png", dpi = 300)
```
### The next output executes
Now that the first output is complete, Shiny chooses another to execute.
This output turns orange, Figure \@ref(fig:graph-output-next), and starts reading values from reactive producers.
```{r graph-output-next, echo = FALSE, out.width = NULL, fig.cap ="The next output starts computing, turning orange."}
knitr::include_graphics("diagrams/reactivity-tracking/07.png", dpi = 300)
```
Complete reactives can return their values immediately; invalidated reactives will kick off their own execution graph.
This cycle will repeat until every invalidated output enters the complete (green) state.
### Execution completes, outputs flushed
Now all of the outputs have finished execution and are idle, Figure \@ref(fig:graph-complete-2).
```{r graph-complete-2, echo = FALSE, out.width = NULL, fig.cap = "All output and reactive expressions have finished and turned green."}
knitr::include_graphics("diagrams/reactivity-tracking/08.png", dpi = 300)
```
This round of reactive execution is complete, and no more work will occur until some external force acts on the system (e.g. the user of the Shiny app moving a slider in the user interface).
In reactive terms, this session is now at rest.
Let's stop here for a moment and think about what we've done.
We've read some inputs, calculated some values, and generated some outputs.
But more importantly we also discovered the *relationships* between the reactive objects.
When a reactive input changes we know exactly which reactives we need to update.
## An input changes {#input-changes}
The previous step left off with our Shiny session in a fully idle state.
Now imagine that the user of the application changes the value of a slider.
This causes the browser to send a message to the server function, instructing Shiny to update the corresponding reactive input.
This kicks off an **invalidation phase**, which has three parts: invalidating the input, notifying the dependencies, then removing the existing connections.
### Invalidating the inputs
The invalidation phase starts at the changed input/value, which we'll fill with grey, our usual colour for invalidation, as in Figure \@ref(fig:graph-input-changes).
```{r graph-input-changes, echo = FALSE, out.width = NULL, fig.cap = "The user interacts with the app, invalidating an input."}
knitr::include_graphics("diagrams/reactivity-tracking/09.png", dpi = 300)
```
### Notifying dependencies
Now, we follow the arrows that we drew earlier, colouring each node in grey, and colouring the arrows we followed in light-grey.
This yields Figure \@ref(fig:graph-invalidation).
```{r graph-invalidation, echo = FALSE, out.width = NULL, fig.cap = "Invalidation flows out from the input, following every arrow from left to right. Arrows that Shiny has followed during invalidation are coloured in a lighter grey."}
knitr::include_graphics("diagrams/reactivity-tracking/10.png", dpi = 300)
```
### Removing relationships
Next, each invalidated reactive expression and output "erases" all of the arrows coming in to and out of it, yielding Figure \@ref(fig:graph-forgetting), and completing the invalidation phase.
```{r graph-forgetting, echo = FALSE, out.width = NULL, fig.cap = "Invalidated nodes forget all their previous relationships so they can be discovered afresh"}
knitr::include_graphics("diagrams/reactivity-tracking/11.png", dpi = 300)
```
The arrows coming out of a node are one-shot notifications that will fire the *next* time a value changes.
Now that they've fired, they've fulfilled their purpose and we can erase them.
It's less obvious why we erase the arrows coming *in* to an invalidated node, even if the node they're coming from isn't invalidated.
While those arrows represent notifications that haven't fired, the invalidated node no longer cares about them: reactive consumers only care about notifications in order to invalidate themselves and that has already happened.
It may seem perverse that we put so much value on those relationships, and now we've thrown them away!
But this is a key part of Shiny's reactive programming model: though these particular arrows *were* important, they are now out of date.
The only way to ensure that our graph stays accurate is to erase arrows when they become stale, and let Shiny rediscover the relationships around these nodes as they re-execute.
We'll come back to this important topic in Section \@ref(dynamism).
### Re-execution
Now we're in a pretty similar situation to when we executed the second output, with a mix of valid and invalid reactives.
It's time to do exactly what we did then: execute the invalidated outputs, one at a time, starting off in Figure \@ref(fig:graph-reexec).
```{r graph-reexec, echo = FALSE, out.width = NULL, fig.cap = "Now re-execution proceeds in the same way as execution, but there's less work to do since we're not starting from scratch."}
knitr::include_graphics("diagrams/reactivity-tracking/12.png", dpi = 300)
```
Again, I won't show you the details, but the end result will be a reactive graph at rest, with all nodes marked in green.
The neat thing about this process is that Shiny has done the minimum amount of work --- we've only done the work needed to update the outputs that are actually affected by the changed inputs.
### Exercises
1. Draw the reactive graph for the following server function and then explain why the reactives are not run.
```{r}
server <- function(input, output, session) {
sum <- reactive(input$x + input$y + input$z)
prod <- reactive(input$x * input$y * input$z)
division <- reactive(prod() / sum())
}
```
2. The following reactive graph simulates long running computation by using `Sys.sleep()`:
```{r}
x1 <- reactiveVal(1)
x2 <- reactiveVal(2)
x3 <- reactiveVal(3)
y1 <- reactive({
Sys.sleep(1)
x1()
})
y2 <- reactive({
Sys.sleep(1)
x2()
})
y3 <- reactive({
Sys.sleep(1)
x2() + x3() + y2() + y2()
})
observe({
print(y1())
print(y2())
print(y3())
})
```
How long will the graph take to recompute if `x1` changes?
What about `x2` or `x3`?
3. What happens if you attempt to create a reactive graph with cycles?
```{r, eval = FALSE}
x <- reactiveVal(1)
y <- reactive(x + y())
y()
```
## Dynamism
In Section \@ref(removing-relationships), you learned that Shiny "forgets" the connections between reactive components that it spend so much effort recording.
This makes Shiny's reactive dynamic, because it can change while your app runs.
This dynamism is so important that I want to reinforce it with a simple example:
```{r}
ui <- fluidPage(
selectInput("choice", "A or B?", c("a", "b")),
numericInput("a", "a", 0),
numericInput("b", "b", 10),
textOutput("out")
)
server <- function(input, output, session) {
output$out <- renderText({
if (input$choice == "a") {
input$a
} else {
input$b
}
})
}
```
You might expect the reactive graph to look like Figure \@ref(fig:dynamic-wrong).
```{r dynamic-wrong, echo = FALSE, out.width = NULL, fig.cap = "If Shiny analysed reactivity statically, the reactive graph would always connect `choice`, `a`, and `b` to `out`."}
knitr::include_graphics("diagrams/reactivity-tracking/dynamic.png", dpi = 300)
```
But because that Shiny dynamically reconstructs the graph after the output has been invalidated it actually looks like either of the graphs in Figure \@ref(fig:dynamic-right), depending on the value of `input$choice`.
This ensures that Shiny does the minimum amount of work when an input is invalidated.
In this, if `input$choice` is set to "b", then the value of `input$a` doesn't affect the `output$out` and there's no need to recompute it.
```{r dynamic-right, echo = FALSE, out.width = NULL, fig.cap = "But Shiny's reactive graph is dynamic, so the graph either connects `out` to `choice` and `a` (left) or `choice` and `b` (right)."}
knitr::include_graphics("diagrams/reactivity-tracking/dynamic2.png", dpi = 300)
```
It's worth noting (as Yindeng Jiang does in [their blog](https://shinydata.wordpress.com/2015/02/02/a-few-things-i-learned-about-shiny-and-reactive-programming/)) that a minor change will cause the output to always depend on both `a` and `b`:
```{r, eval = FALSE}
output$out <- renderText({
a <- input$a
b <- input$b
if (input$choice == "a") {
a
} else {
b
}
})
```
This would have no impact on the output of normal R code, but it makes a difference here because the reactive dependency is established when you read a value from `input`, not when you use that value.
## The reactlog package
Drawing the reactive graph by hand is a powerful technique to help you understand simple apps and build up an accurate mental model of reactive programming.
But it's painful to do for real apps that have many moving parts.
Wouldn't it be great if we could automatically draw the graph using what Shiny knows about it?
This is the job of the [reactlog](https://rstudio.github.io/reactlog/) package, which generates the so called **reactlog** which shows how the reactive graph evolves over time.
To see the reactlog, you'll need to first install the reactlog package, turn it on with `reactlog::reactlog_enable()`, then start your app.
You then have two options:
- While the app is running, press Cmd + F3 (Ctrl + F3 on Windows), to show the reactlog generated up to that point.
- After the app has closed, run `shiny::reactlogShow()` to see the log for the complete session.
reactlog uses the same graphical conventions as this chapter, so it should feel instantly familiar.
The biggest difference is that reactlog draws every dependency, even if it's not currently used, in order to keep the automated layout stable.
Connections that are not currently active (but were in the past or will be in the future) are drawn as thin dotted lines.
Figure \@ref(fig:reactlog) shows the reactive graph that reactlog draws for the app we used above.
There's a surprise in this screenshot: there are three additional reactive inputs (`clientData$output_x_height`, `clientData$output_x_width`, and `clientData$pixelratio`) that don't appear in the source code.
These exist because plots have an implicit dependency on the size of the output; whenever the output changes size the plot needs be redrawn.
```{r reactlog, echo = FALSE, out.width = NULL, fig.cap = "The reactive graph of our hypothetic app as drawn by reactlog"}
knitr::include_graphics("images/reactivity-graph/reactlog.png", dpi = 300)
```
Note that while reactive inputs and outputs have names, reactive expressions and observers do not, so they're labelled with their contents.
To make things easier to understand you may want use the `label` argument to `reactive()` and `observe()`, which will then appear in the reactlog.
You can use emojis to make particularly important reactives stand out visually.
## Summary
In this chapter, you've learned precisely how the reactive graph operates.
In particular, you've learned for the first time about the invalidation phase, which doesn't immediately cause recomputation, but instead marks reactive consumers as invalid, so that they will be recomputed when need.
The invalidation cycle is also important because it clears out previously discovered dependencies so that they can be automatically rediscovered, making the reactive graphic dynamic.
Now that you've got the big picture under your belt, the next chapter will give some additional details about the underlying data structures that power reactive values, expressions, and output, and we'll discuss the related concept of timed invalidation.