Skip to content

Commit

Permalink
Continuing to brain dump on packaging/testing/performance
Browse files Browse the repository at this point in the history
  • Loading branch information
hadley committed Jun 29, 2020
1 parent 3b94cc2 commit 8ac140e
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 38 deletions.
12 changes: 12 additions & 0 deletions apps/shiny-test/app.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
library(shiny)
ui <- fluidPage(
numericInput("x", "x", 0),
numericInput("y", "y", 0)
)
server <- function(input, output, session) {
observeEvent(input$x, {
updateNumericInput(session, "y", value = input$x * 2)
})
}

shinyApp(ui, server)
2 changes: 1 addition & 1 deletion scaling-functions.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Whenever you have a long reactive (say \>10 lines) you should consider pulling i
The key benefits of a function in the UI tend to be around reducing duplication. The key benefits of functions in a server tend to be around isolation and testing.
### Reading uploaded data
### Reading uploaded data {#function-upload}
Take this server from Section \@ref(uploading-data). It contains a moderately complex `reactive()`:
Expand Down
79 changes: 67 additions & 12 deletions scaling-packages.Rmd
Original file line number Diff line number Diff line change
@@ -1,39 +1,94 @@
# Packages {#scaling-packaging}

If you are creating a large/long-term Shiny app, I strongly recommend that you organise your app in the same way you'd organise an R package:
```{r, include = FALSE}
source("common.R")
options(tibble.print_min = 6, tibble.print_max = 6)
```

- You have a `DESCRIPTION` file that declares the dependencies of your code.
If you are creating a large/long-term Shiny app, I strongly recommend that you organise your app in the same way you'd organise an R package. This primarily means two things:

- You put all of your R code in `R/`

- You have a function that runs your app.
- You have a `DESCRIPTION` file.

Using the same basic structure as a package allows you to make use of existing tools that ease the development process. Tools like devtools and usethis.
And then you create a function that runs your app. (This implies that a package can contain multiple apps, which may be useful if you have a series of related apps).

Here I'm going to show you the bare minimum you need to know to turn your app into a package. If you want to learn more, I highly recommend <http://engineering-shiny.org/>.
In brief, a package is just a set of standard for how you organise your R code. Doing the two things above gives you the absolute minimal compliance with the standard. That's still useful because it activates some convenient RStudio keyboard shortcuts, and

Using the same basic structure as a package allows you to make use of existing tools that ease the development process. Tools like devtools and usethis. Will become particularly important when you start testing your app, because the devtools comes with a bunch of tools to make it easy to run tests, and see exactly what code you're testing. It also helps you document the pieces of complex apps, although we won't discuss that in this book. (See <https://r-pkgs.org/man.html> for information about how to document function with roxygen2).

Here I'm going to show you the bare minimum you need to know to turn your app into a package. If you want to learn more, I highly recommend [http://engineering-shiny.org](http://engineering-shiny.org/){.uri}. If you want to learn more about R packages, I recommend <https://r-pkgs.org>.

```{r setup}
library(shiny)
```

## Converting an existing app

Converting an app to a package requires a little upfront work. And it's better to do earlier in the lifecycle of your app, so you don't have so many bad habits to undo.

Follow these steps to create an package. Assuming that you app is called `myApp` and lives in directory `myApp/`.

- If it doesn't exist already, create an `R` directory

- Move `app.R` into `R/` and where you currently have `shinyApp(ui, server)` change it to:
- Move `app.R` into `R/` and where you currently have `shinyApp(ui, server)` change it to: (Inspired by <https://engineering-shiny.org/structure.html#deploy>).

```{r}
myApp <- function(...) {
shinyApp(ui, server, ...)
}
```
- Call `usethis::use_description()` to create a description file.
- If you're deploying your app using a standard tool, add a new `app.R`
Next, there are a few things you'll need to do to your R code:
```{r, eval = FALSE}
pkgload::load_all()
myApp::myApp()
```
- Remove any calls to `library()` or `require()` and instead call `usethis::use_package("name")` to add the required package to the `DESCRIPTION`.
- Call `usethis::use_description()` to create a description file.
- Restart RStudio.
- Move data to ...
This gives your app the basic structure of a package, which enables some useful keyboard shortcuts that we'll talk about next. It's possible to turn your app in to a "real" package, which means that it passes the automated `R CMD check`. That's not essential, but it can be useful if you're sharing with others.
## Workflow
- Re-load all code in the app with `cmd + shift + L` (`devtools::load_all()`)
- Re-load all code in the app with `cmd + shift + L` (`devtools::load_all()`).
- Re-run the app with `myApp()`.
## `R CMD check`
You may also want to use `devtools::check()` which calls `R CMD check` and ensures that your app is fully compliant with the package standard. In the short term, there are relatively few benefits to this. But in the long-term this will protect you against a number of potential problems, and make it easier to share your app with others.
Don't recommend that you do this now.
To make an app pass `R CMD check`, you'll also need to:
- Remove any calls to `library()` or `require()` and instead replace them with a declaration in your `DESCRIPTION`. `usethis::use_package("name")` to add the required package to the `DESCRIPTION`[^1].
At a minimum, you'll need `usethis::use_package("shiny")`!.
Need to decide whether to use depends or be explicit with namespace. The first is easy, the second is a little more work but makes it easier to understand where functions are coming from
- `use_license_…()` (<https://github.com/r-lib/usethis/issues/1163>)
- Add `app.R` to `.Rbuildignore` with `usethis::use_build_ignore("app.R")` or similar.
- Document and export your app function.
- If you app contains small reference datasets put it in `data` or `inst/extdata`. Advantage of `data/` is that it's stored in `.rda` format, which is faster to load, and is lazy loaded so if it's not used by the app, it's not loaded in memory. (Although if that's a bottleneck you might want qs instead). Load using `system.file("exdata", "path", package = "myApp")`.
- Remove any calls to `source()` or `shiny::loadSupport()`. The package code loading process now takes care of these.
- Files in `www`? `inst/www`?
- You can also change your `app.R` to use an the package. This requires that your available somewhere that your deployment machine can install from. For public work this means a CRAN or GitHub package; for private work this means something like RSPM.
```{r, eval = FALSE}
library(myApp)
myApp::myApp()
```
- Re-run the app with `myApp()`
[^1]: The distinction between Imports and Suggests is not generally important for app packages. If you do want to make a distinction, the most useful is to use Imports for packages that need to be present on the deployment machine (in order for the app to work) and Suggests for packages that need to prsent on the development machine in order to develop the app.
73 changes: 70 additions & 3 deletions scaling-performance.Rmd
Original file line number Diff line number Diff line change
@@ -1,11 +1,78 @@
# Performance {#performance}

Joe's keynote. https://resources.rstudio.com/rstudio-conf-2018/scaling-shiny-sean-lopp
```{r, include = FALSE}
source("common.R")
options(tibble.print_min = 6, tibble.print_max = 6)
```

Joe's keynote. <https://rstudio.com/resources/rstudioconf-2019/shiny-in-production-principles-practices-and-tools/>. Use cran whales example from talk?

<https://resources.rstudio.com/rstudio-conf-2018/scaling-shiny-sean-lopp>

Shiny can support thousands or tens of thousands of users, if developed correctly. Even if your app doesn't need to support so many people, the advice in this chapter will help you make your app faster.

## profvis
One new challenge with Shiny apps that's a bit different than improving the performance of regular R code is that you need to be more conscious of memory usage. Typically, to get cost effective performance each R process will need to serve multiple people, which means that multiple copies of the app will be loaded at the same time. If you're working with large datasets, that means you need to think more carefully about how to ensure that the same data is shared across multiple users.

If your app loads a large dataset and then filters it, you may instead want to consider putting the data in a database, and then only retrieving the data that the user specifically asks for. This will make individual apps slower (often only by a small amount), but will make each app take up much less memory so that you can more easily server multiple users.

Focus is on increasing the performance for multiple users. If you're concerned about single user performance (i.e. it's just you using the app), it's very unlikely that Shiny is the bottleneck. Instead, improving the performance of your app is really about improving the performance of your R code. Many of the same techniques apply; in particular you'll still need to profile the performance of your app.

```{r setup}
library(shiny)
```

## Planning

Start with a plan:

- How many people? How many people at the same time?

- What's the computing budget? i.e. how many cores, how many machines?

Is it fast enough? shinyloadtest.

What's making it slow? profvis

Make it fast:

- Do less work

- Make code faster

- Use caching (speed-time tradeoff)

- Use async

## Load testing
## Load testing

- Record sample app session: `shinyloadtest::record_session()`

- Replay with multiple users: shinycannon.

- Analyse: `shinyloadtest::report()`.

<https://rstudio.github.io/shinyloadtest/>

## profvis

What do you measure the performance of? Your skills from testing will come in handy here, because it's useful to prepare an

## Make it faster

### Horizontal scaling

Make it faster by throwing more computers at it.

### Precomputation

Number one most important advice is if you want to make your Shiny app fast: make it do less! If your app is doing a lot of computation, the chances are that it's doing the same computation for multiple people. There's no need to duplicate all that work!

Instead, create a centralised process that's run on a regular interval that does all the computing and then saves the results. Then all the instances can load the precomuted data. Scheduled task: RStudio connect with scheduled rmarkdown reports. Or cron. Or whatever other technology you use.

If there's a lot of data, make sure you're using a fast function to load it (e.g. `vroom::vroom` or `data.table::fread`), save it as an binary RDS file, or try the [qs](https://github.com/traversc/qs#qs) package.

Make sure any data is loaded outside of the server function. That way the data is loaded once, rather than once per user, and there's a single copy in memory, instead of a single copy per user.

### Caching

If you have a computation that's going to return the same thing every time, and you call it a bunch of times --- maybe save the results and look it up!
Loading

0 comments on commit 8ac140e

Please sign in to comment.