diff --git a/.nojekyll b/.nojekyll index e70cbde..f0c44e6 100644 --- a/.nojekyll +++ b/.nojekyll @@ -1 +1 @@ -65a7a637 \ No newline at end of file +c8176aa1 \ No newline at end of file diff --git a/LICENSE.html b/LICENSE.html index 3d3cb76..e8ad999 100644 --- a/LICENSE.html +++ b/LICENSE.html @@ -2,7 +2,7 @@ - + diff --git a/index.html b/index.html index 30f37e8..3ea0872 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + diff --git a/listings.json b/listings.json index 136f0cd..4a8050a 100644 --- a/listings.json +++ b/listings.json @@ -1,17 +1,10 @@ [ { - "listing": "/units/d1-01-welcome.html", - "items": [] - }, - { - "listing": "/units/d1-04-deploy-admin.html", - "items": [] - }, - { - "listing": "/units/d1-02-structure.html", + "listing": "/units/d1-03-performance.html", "items": [ - "/materials/d1-02-structure/ex-1.html", - "/materials/d1-02-structure/codealong-1.html" + "/materials/d1-03-performance/ex-1.html", + "/materials/d1-03-performance/codealong-1.html", + "/materials/d1-03-performance/codealong-2.html" ] }, { @@ -22,19 +15,16 @@ "/units/d1-02b-break.html", "/units/d1-03-performance.html", "/units/d1-03b-lunch.html", - "/units/d1-04-deploy-admin.html", + "/units/d1-04-loadtesting.html", "/units/d1-04b-break.html", - "/units/d1-9001-loadtesting.html" + "/units/d1-05-deploy-admin.html" ] }, { - "listing": "/units/d1-03-performance.html", + "listing": "/units/d1-02-structure.html", "items": [ - "/materials/d1-03-performance/codealong-1.html" + "/materials/d1-02-structure/ex-1.html", + "/materials/d1-02-structure/codealong-1.html" ] - }, - { - "listing": "/units/d1-9001-loadtesting.html", - "items": [] } ] \ No newline at end of file diff --git a/materials/d1-01-welcome/index.html b/materials/d1-01-welcome/index.html index 5548550..19c23ec 100644 --- a/materials/d1-01-welcome/index.html +++ b/materials/d1-01-welcome/index.html @@ -8,7 +8,7 @@ - + Shiny in Production - Welcome! @@ -338,16 +338,13 @@
-
+

Welcome!

Shiny in Production: Tools & Techniques
posit::conf(2023)

-
-
-

Welcome to posit::conf(2023)!

@@ -364,7 +361,7 @@

Workshop Policies

-
+

Meet the Team!

@@ -375,6 +372,11 @@

Meet the Team!

+
@@ -383,23 +385,14 @@

Meet the Team!

+ - -
-

Your Turn

-

Introduce yourself to your neighbor(s)

-

What is your most memorable Shiny application in production experience?

-
-
-
-
-02:00 -
-
+

posit-conf-2023.github.io/shiny-r-prod/#workshop-instructors

@@ -446,18 +439,33 @@

About those post-its

-
-

Power-On (Setup)

-

Follow Setup Procedure to connect with the workshop resources:

+
+

Your Turn

+

Complete any remaining Setup Procedures

+

Introduce yourself to your neighbor(s)

+

What is your most memorable Shiny application in production experience?

+
+
+
+
+05:00 +
+
+
+
-

The Beginning …

+

@@ -537,6 +560,9 @@

Applied Example

+

Raw Data

@@ -546,32 +572,32 @@

Raw Data

-
- +
+
-
- +
+
-
- +
+
-
- +
+
diff --git a/materials/d1-02-structure/codealong-1.html b/materials/d1-02-structure/codealong-1.html index 2762b44..5301143 100644 --- a/materials/d1-02-structure/codealong-1.html +++ b/materials/d1-02-structure/codealong-1.html @@ -2,7 +2,7 @@ - + diff --git a/materials/d1-02-structure/ex-1.html b/materials/d1-02-structure/ex-1.html index 613ac74..04294e5 100644 --- a/materials/d1-02-structure/ex-1.html +++ b/materials/d1-02-structure/ex-1.html @@ -2,7 +2,7 @@ - + diff --git a/materials/d1-02-structure/index.html b/materials/d1-02-structure/index.html index 3270d24..5385f5e 100644 --- a/materials/d1-02-structure/index.html +++ b/materials/d1-02-structure/index.html @@ -8,7 +8,7 @@ - + Shiny in Production - Application Structure @@ -98,6 +98,7 @@ + + + @@ -410,83 +413,6 @@

It’s Never Just Shiny

  • Additional R packages!
  • -
    -

    Turned Upside-Down

    -

    Imagine your application is working great!

    -


    -
    -
    -
    -
    update.packages(ask = FALSE)
    -remotes::install_github("pkg")
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -

    Turned Upside-Down

    -
    -
    -

    ggplot2 version 0.9.3

    -
    -
    -
    - -
    -
    -
    -
    -

    ggplot2 version 1.0.0

    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -

    Take Control with {renv}

    -
    -

    Create reproducible environments for your R projects.

    -
    - -
    -
    -

    Under the Hood

    -

    Upon initializing a project:

    -
      -
    1. Project-level .Rprofile to activate custom package library on startup
    2. -
    3. Lockfile renv.lock to describe state of project library
    4. -
    5. renv/library to hold private project library
    6. -
    7. renv/activate.R performs activation
    8. -
    -
    -
    -

    Develop a Routine

    -

    Sticking with {renv} will pay off (trust me)

    - -

    Application Structure Options

    @@ -677,35 +603,26 @@

    Sound familiar?

    Module Code Example

    - +

    Anatomy of a Function (UI)

    -
    set_picker_ui <- function() {
    -  tagList(
    -    selectInput(
    -      inputId = "set_num",
    -      label = "Select a set"
    -      choices = c("set1", "set2"),
    -      selected = "set1",
    -      multiple = FALSE
    -    )
    -  )
    -}
    +
    +

    picker.R

    +
    set_picker_ui <- function() {
    +  tagList(
    +    selectInput(
    +      inputId = "set_num",
    +      label = "Select a set"
    +      choices = c("set1", "set2"),
    +      selected = "set1",
    +      multiple = FALSE
    +    )
    +  )
    +}
    +
    @@ -715,17 +632,20 @@

    Anatomy of a Function (UI)

    Anatomy of a Module (UI)

    -
    set_picker_ui <- function(id) {
    -  ns <- NS(id)
    -  tagList(
    -    selectInput(
    -      inputId = ns("set_num"),
    -      label = "Select a set"
    -      choices = c(),
    -      multiple = FALSE
    -    )
    -  )
    -}
    +
    +

    mod_picker.R

    +
    set_picker_ui <- function(id) {
    +  ns <- NS(id)
    +  tagList(
    +    selectInput(
    +      inputId = ns("set_num"),
    +      label = "Select a set"
    +      choices = c(),
    +      multiple = FALSE
    +    )
    +  )
    +}
    +
    @@ -735,17 +655,20 @@

    Anatomy of a Module (UI)

    Anatomy of a Module (UI)

    -
    set_picker_ui <- function(id) {
    -  ns <- NS(id)
    -  tagList(
    -    selectInput(
    -      inputId = ns("set_num"),
    -      label = "Select a set"
    -      choices = c(),
    -      multiple = FALSE
    -    )
    -  )
    -}
    +
    +

    mod_picker.R

    +
    set_picker_ui <- function(id) {
    +  ns <- NS(id)
    +  tagList(
    +    selectInput(
    +      inputId = ns("set_num"),
    +      label = "Select a set"
    +      choices = c(),
    +      multiple = FALSE
    +    )
    +  )
    +}
    +
    • id: String to use for namespace
    • @@ -756,190 +679,300 @@

      Anatomy of a Module (UI)

    Anatomy of a Module (Server)

    -
    -
    -
    set_picker_server <- function(input, output, session, sets_rv) {
    -  set_choices <- reactive({
    -    # do something with sets_rv
    -  })
    -
    -  observeEvent(set_choices(), {
    -    req(set_choices())
    -    updateSelectInput(
    -      "set_num",
    -      choices = set_choices()
    -    )
    -  })
    -}
    -
    - -
    +
    +

    mod_picker.R

    +
    set_picker_server <- function(input, output, session, sets_rv) {
    +  set_choices <- reactive({
    +    # do something with sets_rv
    +  })
    +
    +  observeEvent(set_choices(), {
    +    req(set_choices())
    +    updateSelectInput(
    +      "set_num",
    +      choices = set_choices()
    +    )
    +  })
    +}

    Anatomy of a Module (Server)

    -
    -
    -
    set_picker_server <- function(id, sets_rv) {
    -  moduleServer(
    -    id,
    -    function(input, output, session) {
    -      set_choices <- reactive({
    -        # do something with sets_rv
    -      })
    -
    -      observeEvent(set_choices(), {
    -        req(set_choices())
    -        updateSelectInput(
    -          "set_num",
    -          choices = set_choices()
    -        )
    -      })
    -    }
    -  )
    -}
    -
    -

    Minimal changes necessary

    -
    +
    +

    mod_picker.R

    +
    set_picker_server <- function(id, sets_rv) {
    +  moduleServer(
    +    id,
    +    function(input, output, session) {
    +      set_choices <- reactive({
    +        # do something with sets_rv
    +      })
    +
    +      observeEvent(set_choices(), {
    +        req(set_choices())
    +        updateSelectInput(
    +          "set_num",
    +          choices = set_choices()
    +        )
    +      })
    +    }
    +  )
    +}
    +

    Minimal changes necessary

    Anatomy of a Module (Server)

    -
    set_picker_server <- function(id, sets_rv) {
    -  moduleServer(
    -    id,
    -    function(input, output, session) {
    -      set_choices <- reactive({
    -        # do something with sets_rv
    -      })
    -
    -      observeEvent(set_choices(), {
    -        req(set_choices())
    -        updateSelectInput(
    -          "set_num",
    -          choices = set_choices()
    -        )
    -      })
    -    }
    -  )
    -}
    +
    set_picker_server <- function(id, sets_rv) {
    +  moduleServer(
    +    id,
    +    function(input, output, session) {
    +      set_choices <- reactive({
    +        # do something with sets_rv
    +      })
    +
    +      observeEvent(set_choices(), {
    +        req(set_choices())
    +        updateSelectInput(
    +          "set_num",
    +          choices = set_choices()
    +        )
    +      })
    +    }
    +  )
    +}
    -

    :thinking: id

    +

    🤔 id

    -
      -
    • `moduleServer(): Encapsulate server-side logic with namespace applied.
    • -
    +
    +

    moduleServer(): Encapsulate server-side logic with namespace applied.

    +

    Invoking Modules

    -
    library(shiny)
    -library(bslib)
    -ui <- page_fluid(
    -  set_picker_ui("mod1")
    -)
    -
    -server <- function(input, output, session) {
    -  sets_rv <- reactive({
    -    # processing
    -  })
    -
    -  set_picker_server("mod1", sets_rv)
    -}
    -
    -shinyApp(ui, server)
    +
    +

    app.R

    +
    library(shiny)
    +library(bslib)
    +ui <- page_fluid(
    +  set_picker_ui("mod1")
    +)
    +
    +server <- function(input, output, session) {
    +  sets_rv <- reactive({
    +    # processing
    +  })
    +
    +  set_picker_server("mod1", sets_rv)
    +}
    +
    +shinyApp(ui, server)
    +

    Giving and Receiving

    -
    -
    -
    set_picker_ui <- function(id, label = "Select a set") {
    -  ns <- NS(id)
    -  tagList(
    -    selectInput(
    -      inputId = ns("set_num"),
    -      label = label,
    -      choices = c(),
    -      multiple = FALSE
    -    )
    -  )
    -}
    -
    +
    +

    mod_picker.R

    +
    set_picker_ui <- function(id, label = "Select a set") {
    +  ns <- NS(id)
    +  tagList(
    +    selectInput(
    +      inputId = ns("set_num"),
    +      label = label,
    +      choices = c(),
    +      multiple = FALSE
    +    )
    +  )
    +}
    +
    +
    • Reasonable inputs: static values, vectors, flags
    • Avoid reactive parameters
    • Return value: tagList() of inputs, output placeholders, and other UI elements
    -

    Giving and Receiving

    -
    set_picker_server <- function(id, sets_rv) {
    -  moduleServer(
    -    id,
    -    function(input, output, session) {
    -      set_choices <- reactive({
    -        # do something with sets_rv
    -      })
    -
    -      observeEvent(set_choices(), {
    -        req(set_choices())
    -        updateSelectInput(
    -          "set_num",
    -          choices = set_choices()
    -        )
    -      })
    -    }
    -  )
    -}
    -
      -
    • Input parameters (and return values) can be a mix of static and reactive objects
    • -
    +
    +

    mod_picker.R

    +
    set_picker_server <- function(id, sets_rv) {
    +  moduleServer(
    +    id,
    +    function(input, output, session) {
    +      set_choices <- reactive({
    +        # do something with sets_rv
    +      })
    +
    +      observeEvent(set_choices(), {
    +        req(set_choices())
    +        updateSelectInput(
    +          "set_num",
    +          choices = set_choices()
    +        )
    +      })
    +    }
    +  )
    +}
    +
    +
    +

    Input & return values can be a mix of static and reactive objects

    +
    -
    +

    To () or not to ()

    -
    -
    # app server
    -sets_rv <- reactive({
    -  # processing
    -})
    -
    -set_picker_server("mod1", sets_rv)
    -
    -
    set_picker_server <- function(id, sets_rv) {
    -  moduleServer(
    -    id,
    -    function(input, output, session) {
    -      # ...
    -
    -      set_selection  <- reactive({
    -        input$set_num
    -      })
    -
    -      set_selection
    -    }
    -  )
    -}
    +
    +
    +

    app_server.R

    +
    # app server
    +sets_rv <- reactive({
    +  # processing
    +})
    +
    +set_picker_server("mod1", sets_rv)
    +
    +
    +

    mod_picker.R

    +
    set_picker_server <- function(id, sets_rv) {
    +  moduleServer(
    +    id,
    +    function(input, output, session) {
    +      # ...
    +
    +      set_selection  <- reactive({
    +        input$set_num
    +      })
    +
    +      set_selection
    +    }
    +  )
    +}
    +
    +
    +
    • Reactive parameters reference by name: sets_rv
    • Inside module, invoke reactive parameter as you would any other reactive in Shiny: sets_rv()
    • Any reactive(s) returned by module should also be reference by name: set_selection, set_selection()
    +
    +

    Code-Along

    -

    Code-Along 1: Add a new Shiny module to pick themes

    +

    Add a new Shiny module to pick LEGO set themes

    +
      +
    • Details
    • +
    • Posit Cloud project: Application Structure Code-along 1
    • +

    Your Turn: Exercise 1

    Create a new Shiny module with LEGO data metrics!

    +
      +
    • Details
    • +
    • Posit Cloud project: Application Structure Exercise 1
    • +
    +
    +
    +
    +
    +10:00 +
    +
    +
    +
    +
    +
    +

    Dependency Management

    + +
    +
    +

    Turned Upside-Down

    +

    Imagine your application is working great!

    +


    +
    +
    +
    +
    update.packages(ask = FALSE)
    +remotes::install_github("pkg")
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +

    Turned Upside-Down

    +
    +
    +

    ggplot2 version 0.9.3

    +
    +
    +
    + +
    +
    +
    +
    +

    ggplot2 version 1.0.0

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    Take Control with {renv}

    +
    +

    Create reproducible environments for your R projects.

    +
    +
      +
    • Next generation of {packrat}
    • +
    • Isolated package library from rest of your system
    • +
    • Transfer projects to different collaborators / platforms
    • +
    • Reproducible package installation
    • +
    • Easily create new projects or convert existing projects with RStudio or built-in functions.
    • +
    +
    +
    +

    Under the Hood

    +

    Upon initializing a project:

    +
      +
    1. Project-level .Rprofile to activate custom package library on startup
    2. +
    3. Lockfile renv.lock to describe state of project library
    4. +
    5. renv/library to hold private project library
    6. +
    7. renv/activate.R performs activation
    8. +
    +
    +
    +

    Develop a Routine

    +

    Sticking with {renv} will pay off (trust me)

    +
      +
    • Fair play to mix packages from CRAN, GitHub, and proprietary sources
    • +
    • Roll back when a package upgrade doesn’t play nicely
    • +
    • You make the call when to update your library!
    • +
    @@ -165,7 +164,7 @@

    On this page

    -

    Asynchronous Processing of LEGO Model Prediction

    +

    Using .parquet files in Shiny

    @@ -183,93 +182,28 @@

    Asynchronous Processing of LEGO Model Prediction

    Requirements

    -

    The current version of our Shiny application contains a module for generating predictions of the number of LEGO parts in a set using the number of unique colors and number of unique part categories. The API is executed and processed using the {httr2} package. Here is the function wrapping the API execution:

    -
    -
    #' @importFrom httr2 request req_body_json req_perform resp_body_json
    -run_prediction <- function(df, endpoint_url, back_transform = TRUE, round_result = TRUE) {
    -  # create request object
    -  req <- request(endpoint_url)
    -
    -  # perform request
    -  resp <- req |>
    -    req_body_json(df) |>
    -    req_perform()
    -
    -  # extract predictions from response
    -  pred_values <- resp_body_json(resp)$.pred |> unlist()
    -
    -  # back-transform log10 value of predicted number of parts if requested
    -  if (back_transform) {
    -    pred_values <- 10 ^ pred_values
    -  }
    -
    -  # round result up to nearest integer if requested
    -  if (round_result) pred_values <- ceiling(pred_values)
    -
    -  # append predictions to supplied data frame
    -  dplyr::mutate(df, predicted_num_parts = pred_values)
    -}
    -
    -

    Unfortunately, the prediction API call takes a bit of time to execute due to some extremely sophisticated processing 😅. As a result, any interactions within the application will not be processed until the prediction call completes. Our goal is to convert the prediction processing from synchronous to asynchronous using {crew}

    +

    The current version of our Shiny application performs additional data processing to generate part summaries that are utilized by reactive data frames. The custom function is called gen_part_metaset() which is located in the R/fct_data_processing.R script. For the purposes of this exercise, we are not going to try and optimize this specific function (certainly you are welcome to try after the workshop), instead we are going to see if we can more efficiently utilize the results of the function inside our Shiny application.

    Plan

    -
      -
    1. Establish reactive values for tracking the status of the prediction calls
    2. -
    3. Create a new controller to launch new R processes when new prediction tasks are launched
    4. -
    5. Modify the existing observeEvent to push the prediction task to the controller, ensuring the key objects and required packages are passed on to the controller.
    6. -
    7. Create a poll that’s invalidated every 100 milliseconds to query the status of the submitted tasks in the controller and update the prediction result reactive value when complete.
    8. -
    -
    -
    -

    Solution

    -

    First we create the following reactiveVal objects to keep track of the prediction state:

    -
    -
    pred_status <- reactiveVal("No prediction submitted yet.")
    -pred_poll <- reactiveVal(FALSE)
    -
    -

    Next we set up a new controller:

    -
    -
    # establish async processing with crew
    -controller <- crew_controller_local(workers = 4, seconds_idle = 10)
    -controller$start()
    -
    -# make sure to terminate the controller on stop #NEW
    -onStop(function() controller$terminate())
    -
    -

    Inside the observeEvent for the user clicking the prediction button, we update the logic to push the prediction task to the controller:

    +

    Upon closer inspection, we see that the calls to gen_part_metaset() do not take any dynamic parameters when used in the application. In addition, the function is called multiple times inside a set of reactive expressions. A first attempt at removing the bottleneck would be to move this function call to the beginning of the app_server.R logic and feeding the resulting object directly into the reactives that consume it.

    +

    Knowing that the processing function is not leveraging any dynamic parameters, we can do even better. In our mission to ensure the Shiny application performs only the data processing that it absolutely needs to do, we can instead run this function outside of the application, and save the result of the processing as a .parquet file inside the inst/extdata directory using the {arrow} package.

    -
    controller$push(
    -  command = run_prediction(df),
    -  data = list(
    -    run_prediction = run_prediction,
    -    df = pred_data_rv$data
    -  ),
    -  packages = c("httr2", "dplyr")
    -)
    -
    -pred_poll(TRUE)
    +
    library(dplyr)
    +library(arrow)
    +part_meta_df <- gen_part_metaset(min_set_parts = 1)
    +
    +write_parquet(
    +  part_meta_df,
    +  "inst/extdata/part_meta_df.parquet")
    +)
    -

    Lastly, we create a new observe block that periodically checks whether the running {crew} tasks have completed, ensuring that this is only executed when a prediction has been launched:

    +

    With the processed data set available in the app infrastructure, we can utilize it inside the application with the following:

    -
    observe({
    -  req(pred_poll())
    -
    -  invalidateLater(millis = 100)
    -  result <- controller$pop()$result
    -
    -  if (!is.null(result)) {
    -    pred_data_rv$data <- result[[1]]
    -    print(controller$summary()) 
    -  }
    -
    -  if (isFALSE(controller$nonempty())) {
    -    pred_status("Prediction Complete")
    -    pred_poll(controller$nonempty())
    -    removeNotification(id = "pred_message")
    -  }
    -})
    +
    part_meta_df <- arrow::read_parquet(app_sys("extdata", "part_meta_df.parquet"), as_data_frame = FALSE)
    +

    Why do set the parameter as_data_frame to FALSE in the call above? This ensures the contents of the data file are not read into R’s memory right away, and we can perform data processing on this file in a tidyverse-like pipeline and collect() the results at the end of the pipeline to minimize overhead.

    +

    We could add this call to the top of our app_server.R logic, which would already lead to decreased processing time. For an application that is being used very infrequently, that might be good enough. But if we have an application that is going to be used concurrently by multiple users, we may be able to increase performance by ensuring this data file is read in R once for each process launched that servers the application, instead of once for each R session corresponding to different user’s Shiny sessions. More to come later in the workshop on how we can accomplish this with {golem}!

    diff --git a/materials/d1-03-performance/codealong-2.html b/materials/d1-03-performance/codealong-2.html new file mode 100644 index 0000000..3c24d14 --- /dev/null +++ b/materials/d1-03-performance/codealong-2.html @@ -0,0 +1,785 @@ + + + + + + + + + +Shiny in Production - Asynchronous Processing of LEGO Model Prediction + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + +
    + +
    + + + + +
    + +
    +
    +

    Asynchronous Processing of LEGO Model Prediction

    +
    + + + +
    + + + + +
    + + + +
    + +
    +

    Requirements

    +

    The current version of our Shiny application contains a module for generating predictions of the number of LEGO parts in a set using the number of unique colors and number of unique part categories. The API is executed and processed using the {httr2} package. Here is the function wrapping the API execution:

    +
    +
    #' @importFrom httr2 request req_body_json req_perform resp_body_json
    +run_prediction <- function(df, endpoint_url, back_transform = TRUE, round_result = TRUE) {
    +  # create request object
    +  req <- request(endpoint_url)
    +
    +  # perform request
    +  resp <- req |>
    +    req_body_json(df) |>
    +    req_perform()
    +
    +  # extract predictions from response
    +  pred_values <- resp_body_json(resp)$.pred |> unlist()
    +
    +  # back-transform log10 value of predicted number of parts if requested
    +  if (back_transform) {
    +    pred_values <- 10 ^ pred_values
    +  }
    +
    +  # round result up to nearest integer if requested
    +  if (round_result) pred_values <- ceiling(pred_values)
    +
    +  # append predictions to supplied data frame
    +  dplyr::mutate(df, predicted_num_parts = pred_values)
    +}
    +
    +

    Unfortunately, the prediction API call takes a bit of time to execute due to some extremely sophisticated processing 😅. As a result, any interactions within the application will not be processed until the prediction call completes. Our goal is to convert the prediction processing from synchronous to asynchronous using {crew}

    +
    +
    +

    Plan

    +
      +
    1. Establish reactive values for tracking the status of the prediction calls
    2. +
    3. Create a new controller to launch new R processes when new prediction tasks are launched
    4. +
    5. Modify the existing observeEvent to push the prediction task to the controller, ensuring the key objects and required packages are passed on to the controller.
    6. +
    7. Create a poll that’s invalidated every 100 milliseconds to query the status of the submitted tasks in the controller and update the prediction result reactive value when complete.
    8. +
    +
    +
    +

    Solution

    +

    First we create the following reactiveVal objects to keep track of the prediction state:

    +
    +
    pred_status <- reactiveVal("No prediction submitted yet.")
    +pred_poll <- reactiveVal(FALSE)
    +
    +

    Next we set up a new controller:

    +
    +
    # establish async processing with crew
    +controller <- crew_controller_local(workers = 4, seconds_idle = 10)
    +controller$start()
    +
    +# make sure to terminate the controller on stop #NEW
    +onStop(function() controller$terminate())
    +
    +

    Inside the observeEvent for the user clicking the prediction button, we update the logic to push the prediction task to the controller:

    +
    +
    controller$push(
    +  command = run_prediction(df),
    +  data = list(
    +    run_prediction = run_prediction,
    +    df = pred_data_rv$data
    +  ),
    +  packages = c("httr2", "dplyr")
    +)
    +
    +pred_poll(TRUE)
    +
    +

    Lastly, we create a new observe block that periodically checks whether the running {crew} tasks have completed, ensuring that this is only executed when a prediction has been launched:

    +
    +
    observe({
    +  req(pred_poll())
    +
    +  invalidateLater(millis = 100)
    +  result <- controller$pop()$result
    +
    +  if (!is.null(result)) {
    +    pred_data_rv$data <- result[[1]]
    +    print(controller$summary()) 
    +  }
    +
    +  if (isFALSE(controller$nonempty())) {
    +    pred_status("Prediction Complete")
    +    pred_poll(controller$nonempty())
    +    removeNotification(id = "pred_message")
    +  }
    +})
    +
    + + +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/materials/d1-03-performance/ex-1.html b/materials/d1-03-performance/ex-1.html new file mode 100644 index 0000000..b290bff --- /dev/null +++ b/materials/d1-03-performance/ex-1.html @@ -0,0 +1,716 @@ + + + + + + + + + +Shiny in Production - Profile the LEGO Bricks App + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + +
    + +
    + + + + +
    + +
    +
    +

    Profile the LEGO Bricks App

    +
    + + + +
    + + + + +
    + + + +
    + +
    +

    Access Instructions

    +

    The project used for this particular exercise is hosted on Posit Cloud in this space. The project for this exercise is called performance-exercise1.

    +
    +
    +

    Setup

    +

    Using what you just learned about the {profvis}, your task is to run the profiler for the LEGO Bricks application contained in this project. Recall that to run the profiler in a Shiny app created with {golem}, change the run_app() call at the end of the dev/run_dev.R script to the following:

    +
    +
    profvis::profvis({
    +  print(run_app())
    +})
    +
    +

    Once the application is open, try changing a couple of the inputs contained in the sidebar. Once the outputs in the app refresh, close the profiler by stopping the R process in the R console. You should now see the {profvis} report appear as a new tab in your web browser.

    +
    +
    +

    Questions

    +
      +
    • Are you able to identify any performance bottlenecks in the application?
    • +
    • If so, can you think of ways to fix the issues?
    • +
    + + +
    + +
    + +
    + + + + + \ No newline at end of file diff --git a/materials/d1-03-performance/index.html b/materials/d1-03-performance/index.html index dd84f22..f38b9db 100644 --- a/materials/d1-03-performance/index.html +++ b/materials/d1-03-performance/index.html @@ -8,7 +8,7 @@ - + Shiny in Production - Performance @@ -386,6 +386,8 @@ margin-right: 0; } + + @@ -497,6 +499,22 @@

    Demo!

    overflow: hidden!important; display:block!important; } +
    +
    +

    Your Turn: Exercise 1

    +

    Profile the LEGO Bricks app!

    +
      +
    • Details
    • +
    • Posit Cloud project: Performance Exercise 1
    • +
    +
    +
    +
    +
    +05:00 +
    +
    +
    @@ -559,6 +577,15 @@

    What is the {arrow} R package?

    }
    +
    +

    Code-Along

    +

    Using .parquet in the LEGO Bricks Shiny app

    +
      +
    • Details
    • +
    • Posit Cloud project: Performance Exercise 1
    • +
    +
    +

    Async Processing

    @@ -571,27 +598,15 @@

    Single (threaded) Line

  • Executed one-by-one
  • -
    -
    -

    Should I care?

    +
    +

    Should I care? It Depends …

    -
    -

    It Depends …

    -

    If you are the only user for a quick and efficient app: Likely not

    - + +
    +
    +

    If you are the only user for a quick and efficient app: Likely not!

    +

    Crowd Pleaser

    @@ -653,9 +668,13 @@

    Setting up for Success

  • Establish an event-driven push of task to the controller with monitoring of worker status
  • -
    +

    Code-Along

    -

    Code-Along 1: Asynchronous calls of a web API

    +

    Asynchronous calls of a model prediction API.

    +
      +
    • Details
    • +
    • Posit Cloud project: Performance Code-along 2
    • +
    -
    - +
    +

    Choose your Adventure

    -
    -
    -

    Choose your Adventure

    Posit Connect

    +
    +
    +
    + +
    +
    +

    Containers

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    -

    Deployment Checklist

    @@ -471,11 +478,6 @@

    -
    -
    -

    Code-Along

    -

    Push-Button Deployment to Posit Connect

    -
    @@ -501,6 +503,16 @@

    Code-Along

    +

    Deploy your application(s) to Posit Connect:

    +
      +
    • Use any of the applications from a previous exercise
    • +
    • Attempt both deployment types (push-button & {rsconnect})
    • +
    • Explore application settings in Posit Connect
    • +
    +

    Balancing Act

    @@ -511,10 +523,10 @@

    Balancing Act

  • Potential for server overload
  • -
    +
    - +
    @@ -527,6 +539,23 @@

    Balancing Act

    +
    +

    Containers for Shiny Deployment

    +
      +
    • Anyone with a container runtime can execute your container without R on their system +
        +
      • Popular container runtimes: Docker, Podman, LXC
      • +
    • +
    • A multitude of web services offer serverless app deployments with containers +
        +
      • Bring your container image, they take care of the rest
      • +
      • Terrific reviews available at hosting.analythium.io
      • +
    • +
    +
    +

    Coming soon: Container support for Posit Connect

    +
    +

    Business Talk

    @@ -546,10 +575,10 @@

    You Might be Asked …

    -
    +
    - +
    @@ -677,6 +706,58 @@

    Usage Metrics

    [1] 4
    +
    +
    +

    Your Turn

    +

    Obtain metadata for your deployed application(s) on Posit Connect

    +
      +
    • Explore the data as you like!
    • +
    +
    +
    +
    +
    +05:00 +
    +
    +
    +
    +
    +

    Going Further with Metrics

    +

    Collection of interesting use cases available at github.com/sol-eng/connect-usage

    +
      +
    • Most-viewed content in last 30 days
    • +
    • Interactive dashboard of usage metrics
    • +
    • {connectViz}: Collection of customizable functions for visualizing content usage for an organization
    • +
    +
    +
    +

    Our Journey is Complete!

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
      +
    • Robust app infrastructure with {golem}
    • +
    • Profiling your app with {profvis}
    • +
    • Solving performance bottlenecks
    • +
    • Assessing your app under high user load
    • +
    • Efficient app deployments and tuning
    • +
    • Obtaining usage metrics
    • +
    +
    +
    +
    +
    @@ -165,7 +132,7 @@

    On this page

    Load Testing

    -

    TBD

    +

    13:30 - 15:00

    @@ -195,45 +162,13 @@

    Load Testing

    Slides

    -
    -
    -
    - -
    -
    -Warning -
    -
    -
    -

    These slides are under construction and will be finalized prior to the workshop date.

    -
    -
    -

    View slides in full screen

    - - +

    View slides in full screen

    +
    -
    - - - - - - - - - -
    -Exercise - -Title -
    -
    -No matching items -
    -
    + - - @@ -71,41 +69,10 @@ "search-label": "Search" } } - - - - - + @@ -157,7 +124,7 @@

    On this page

    - +
    @@ -165,7 +132,7 @@

    On this page

    Deployment & Administration

    -

    TBD

    +

    15:30 - 17:00

    @@ -195,45 +162,13 @@

    Deployment & Administration

    Slides

    -
    -
    -
    - -
    -
    -Warning -
    -
    -
    -

    These slides are under construction and will be finalized prior to the workshop date.

    -
    -
    -

    View slides in full screen

    - - +

    View slides in full screen

    +
    -
    - - - - - - - - - -
    -Exercise - -Title -
    -
    -No matching items -
    -
    +