diff --git a/R/test_dependencies.R b/R/test_dependencies.R index 7dce5ff..e9e6b05 100644 --- a/R/test_dependencies.R +++ b/R/test_dependencies.R @@ -25,40 +25,46 @@ library(sys) # define paths and constants -app_url <- "https://rsc.training.rstudio.com/bricktest/" +app_url <- "https://rsc.training.posit.co/brickapp_shell/" recording_file <- "R/recording.log" shinyloadtest::record_session( - app_link, - output_file = recording_file, - connect_api_key = Sys.getenv("RSCONNECT_KEY") + app_url, + output_file = recording_file + #connect_api_key = Sys.getenv("RSCONNECT_KEY") ) # use the exec_wait function from sys shinycannon_path <- "utils/shinycannon-1.1.3-dd43f6b.jar" +source(file.path(here::here(), "R", "shinycannon.R")) + shinycannon( shinycannon_path, recording_file, app_url, - output_dir = "R/run1" + output_dir = "R/run1", + loaded_duration_minutes = 1, + log_level = "debug" ) -# baseline run -exec_wait( - cmd = "java", - args = c( - "-jar", - shinycannon_path, - recording_file, - app_link, - "--workers", - 1, - "--loaded-duration-minutes", - 3, - "--output-dir", - "R/run1", - "--overwrite-output" +shinyloadtest::record_session( + # baseline run + exec_wait( + cmd = "java", + args = c( + "-jar", + shinycannon_path, + recording_file, + app_link, + "--workers", + 1, + "--loaded-duration-minutes", + 3, + "--output-dir", + "R/run1", + "--overwrite-output" + ) ) ) diff --git a/_freeze/materials/d1-01-welcome/index/execute-results/html.json b/_freeze/materials/d1-01-welcome/index/execute-results/html.json index 8cebb06..f34670e 100644 --- a/_freeze/materials/d1-01-welcome/index/execute-results/html.json +++ b/_freeze/materials/d1-01-welcome/index/execute-results/html.json @@ -1,7 +1,7 @@ { - "hash": "9b034e1897d2c80566bd7e8adb09f406", + "hash": "e52373b51786da5f08b11a9ccc61e0f2", "result": { - "markdown": "---\ntitle: \"Welcome!\"\nsubtitle: \"Shiny in Production: Tools & Techniques
posit::conf(2023)\"\nfooter: \"[{{< var workshop_short_url >}}]({{< var workshop_full_url >}})\"\nformat: \n revealjs:\n theme: [default, ../slides.scss] # moon= teal bg | dark\n css: assets/web/postit.css\n scrollable: true\n incremental: false\n slide-number: c/t # c/t | c | h/v | h.v\n show-slide-number: print\n slide-tone: false #true\n code-line-numbers: false\n history: false\n---\n\n\n## Welcome to posit::conf(2023)! {background-color=\"black\" background-image=\"assets/img/bg/rstudioconf_crowd.jpg\" background-size=\"cover\"}\n\n## Conference Logistics {visibility=\"hidden\"}\n\n::: {.nonincremental}\n1. Identify the exits closes to you in case of emergency\n1. Gender neutral bathrooms located {{< var bathrooms_location >}}\n1. Lactation room located at {{< var lactation_location >}}\n1. A meditation room is available at {{< var quiet_location >}} (Open 8 AM - 5 PM).\n1. Please do not photograph anyone wearing red lanyards\n:::\n\n## Workshop Policies \n\n::: {.nonincremental}\n1. Please review the `posit::conf` [Code of Conduct](https://posit.co/code-of-conduct/).\n1. Issues can be addressed in following ways:\n 1. **In person:** Contact any `posit::conf` staff member, identifiable by their staff t-shirt shirt, or visit the conference registration desk.\n 1. **By email:** Send a message to `conf@posit.com`; event organizers will respond promptly.\n 1. **By phone:** call `844—448—1212`\n\n:::\n\n## Meet the Team!\n\n:::: {.columns}\n\n::: {.column width=\"50%\"}\n![](assets/img/people/enantz.jpg){fig-alt=\"Eric Nantz\" fig-align=\"center\" width=\"70%\"}\n:::\n\n::: {.column width=\"50%\"}\n![](assets/img/people/mthomas.jpeg){fig-alt=\"Michael Thomas\" fig-align=\"center\"}\n:::\n\n::::\n\n::: footer\nTODO: Link to Meet the Team Section on Home Page\n:::\n\n## Your Turn\n\nIntroduce yourself to your neighbor(s)\n\nWhat is your most memorable Shiny application in production experience?\n\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n
\n02:00\n
\n```\n\n:::\n:::\n\n\n---\n\n:::: {.columns .v-center-container}\n\n::: {.column width=\"23%\"}\n\n### WiFi\n\n
\n\n``{=html}\n\n
\n\n### `{{< var wifi_username >}}`\n\n:::\n\n::: {.column width=\"47%\"}\n\n![](assets/img/bg/surf_internet.jpg)\n\n:::\n\n::: {.column width=\"30%\"}\n\n### Password\n\n
\n\n``{=html}\n\n
\n\n### `{{< var wifi_password >}}`\n\n:::\n\n::::\n\n## About those post-its\n\n:::: {.columns .v-center-container}\n\n::: {.column width=\"50%\"}\n\n\n```{=html}\n\n```\n\n\n:::\n\n::: {.column width=\"50%\"}\n\n\n```{=html}\n\n```\n\n\n:::\n\n:::\n\n# Power-On (Setup)\n\nFollow [Setup Procedure](../setup.qmd) to connect with the workshop resources:\n\n* Posit Connect\n* Posit Cloud\n* Account Integrations\n\n# The Beginning ... {background-color=\"black\" background-image=\"assets/img/bg/beginning.png\" background-size=\"cover\"}\n\n::: {.notes}\n* You've seen Shiny could be a game-changer for a project\n* Build an application quickly, purely with R code\n* You send it to a key stakeholder, and they love it\n:::\n\n# A New World {background-image=\"assets/img/bg/metroid_ship.jpg\" background-size=\"cover\" background-color=\"black\"}\n\n::: {.notes}\n* Everyone who sees it thinks the app is amazing\n* Suddenly you are asked (or told) this needs to get into PRODUCTION\n* ..... and now what?\n:::\n\n\n## Many Users {background-image=\"assets/img/bg/many_users.jpg\" background-size=\"contain\" background-color=\"black\"}\n\n## High-Profile Situations {background-image=\"assets/img/bg/nasa_mission_control.jpg\" background-size=\"contain\" background-color=\"black\"}\n\n\n## Part of a Critical Pipeline {background-image=\"assets/img/bg/pinky_brain_take_over_world.jpg\" background-size=\"contain\" background-color=\"black\"}\n\n## The Journey Ahead\n\n
\n\n**Production** has more than one meaning for Shiny apps\n\n. . .\n\n
\n\nThe tooling & principles discussed in this workshop will guide you to the destination\n\n# The Challenge\n\n## Rebrickable Inventory Dashboard\n\n* Rebrickable: Portal for LEGO builders to discover what sets they can build with their own inventory\n* A comprehensive database of LEGO items available for free!\n* We will build a Shiny dashboard to explore the inventory and enable users to generate predictions\n\n## {background-image=\"assets/img/bg/rebrickable_schema.png\" background-size=\"cover\"}\n\n::: footer\n\n:::\n\n## Applied Example {background-image=\"https://cdn.rebrickable.com/media/sets/42138-1.jpg\" background-size=\"contain\"}\n\n## Raw Data\n\n\n::: {.cell}\n\n:::\n\n\n::: {.panel-tabset}\n\n### sets\n\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n\n```\n\n:::\n:::\n\n\n### inventories\n\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n\n```\n\n:::\n:::\n\n\n### inventory parts\n\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n\n```\n\n:::\n:::\n\n\n### parts\n\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n\n```\n\n:::\n:::\n\n\n:::\n\n## Our Journey to Production\n\n* Create a robust infrastructure for application source code\n* Identify and solve performance bottlenecks\n* Ensure app is only doing the \"bare minimum\" for a snappy user experience\n* Tune deployment to meet demand of potentially many simultaneous users", + "markdown": "---\ntitle: \"Welcome!\"\ntitle-slide-attributes:\n data-background-image: assets/img/bg/rstudioconf_crowd.jpg\n data-background-size: cover\n data-background-opacity: \"0.3\"\nsubtitle: \"Shiny in Production: Tools & Techniques
posit::conf(2023)\"\nfooter: \"[{{< var workshop_short_url >}}]({{< var workshop_full_url >}})\"\nformat: \n revealjs:\n theme: [default, ../slides.scss] # moon= teal bg | dark\n css: assets/web/postit.css\n scrollable: true\n incremental: false\n slide-number: c/t # c/t | c | h/v | h.v\n show-slide-number: print\n slide-tone: false #true\n code-line-numbers: false\n history: false\n---\n\n\n## Conference Logistics {visibility=\"hidden\"}\n\n::: {.nonincremental}\n1. Identify the exits closes to you in case of emergency\n1. Gender neutral bathrooms located {{< var bathrooms_location >}}\n1. Lactation room located at {{< var lactation_location >}}\n1. A meditation room is available at {{< var quiet_location >}} (Open 8 AM - 5 PM).\n1. Please do not photograph anyone wearing red lanyards\n:::\n\n## Workshop Policies \n\n::: {.nonincremental}\n1. Please review the `posit::conf` [Code of Conduct](https://posit.co/code-of-conduct/).\n1. Issues can be addressed in following ways:\n 1. **In person:** Contact any `posit::conf` staff member, identifiable by their staff t-shirt shirt, or visit the conference registration desk.\n 1. **By email:** Send a message to `conf@posit.com`; event organizers will respond promptly.\n 1. **By phone:** call `844—448—1212`\n\n:::\n\n## Meet the Team! {.smaller}\n\n:::: {.columns}\n\n::: {.column width=\"50%\"}\n![](assets/img/people/enantz.jpg){fig-alt=\"Eric Nantz\" fig-align=\"center\" width=\"70%\"}\n\n* ``{=html} [r-podcast.org](https://r-podcast.org)\n* ``{=html} [shinydevseries.com](https://shinydevseries.com)\n* ``{=html} [rweekly.org/podcast](https://rweekly.org/podcast)\n\n:::\n\n::: {.column width=\"50%\"}\n![](assets/img/people/mthomas.jpeg){fig-alt=\"Michael Thomas\" fig-align=\"center\"}\n\n* ``{=html} [www.ketchbrookanalytics.com/](https://www.ketchbrookanalytics.com)\n* ``{=html} [rweekly.org/podcast](https://rweekly.org/podcast)\n:::\n\n::::\n\n::: footer\n[posit-conf-2023.github.io/shiny-r-prod/#workshop-instructors](https://posit-conf-2023.github.io/shiny-r-prod/#workshop-instructors)\n:::\n\n---\n\n:::: {.columns .v-center-container}\n\n::: {.column width=\"23%\"}\n\n### WiFi\n\n
\n\n``{=html}\n\n
\n\n### `{{< var wifi_username >}}`\n\n:::\n\n::: {.column width=\"47%\"}\n\n![](assets/img/bg/surf_internet.jpg)\n\n:::\n\n::: {.column width=\"30%\"}\n\n### Password\n\n
\n\n``{=html}\n\n
\n\n### `{{< var wifi_password >}}`\n\n:::\n\n::::\n\n## About those post-its\n\n:::: {.columns .v-center-container}\n\n::: {.column width=\"50%\"}\n\n```{=html}\n\n```\n\n:::\n\n::: {.column width=\"50%\"}\n\n```{=html}\n\n```\n\n:::\n\n:::\n\n## Your Turn\n\nComplete any remaining [Setup Procedures](../setup.qmd)\n\n* Posit Cloud\n* Posit Connect\n* Account Integrations\n\nIntroduce yourself to your neighbor(s)\n\nWhat is your most memorable Shiny application in production experience?\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n
\n05:00\n
\n```\n\n:::\n:::\n\n::: footer\n[posit-conf-2023.github.io/shiny-r-prod/setup.html](https://posit-conf-2023.github.io/shiny-r-prod/setup.html)\n:::\n\n# The Beginning ... {background-color=\"black\" background-image=\"assets/img/bg/beginning.png\" background-size=\"cover\"}\n\n::: footer\n\n:::\n\n::: {.notes}\n* You've seen Shiny could be a game-changer for a project\n* Build an application quickly, purely with R code\n* You send it to a key stakeholder, and they love it\n:::\n\n# A New World {background-image=\"assets/img/bg/metroid_ship.jpg\" background-size=\"cover\" background-color=\"black\"}\n\n::: footer\n\n:::\n\n::: {.notes}\n* Everyone who sees it thinks the app is amazing\n* Suddenly you are asked (or told) this needs to get into PRODUCTION\n* ..... and now what?\n:::\n\n\n## Many Users {background-image=\"assets/img/bg/many_users.jpg\" background-size=\"contain\" background-color=\"black\"}\n\n::: footer\n\n:::\n\n## High-Profile Situations {background-image=\"assets/img/bg/nasa_mission_control.jpg\" background-size=\"contain\" background-color=\"black\"}\n\n::: footer\n\n:::\n\n## Part of a Critical Pipeline {background-image=\"assets/img/bg/pinky_brain_take_over_world.jpg\" background-size=\"contain\" background-color=\"black\"}\n\n::: footer\n\n:::\n\n## The Journey Ahead\n\n
\n\n**Production** has more than one meaning for Shiny apps\n\n. . .\n\n
\n\nThe tooling & principles discussed in this workshop will guide you to the destination\n\n# The Challenge\n\n## Rebrickable Inventory Dashboard\n\n* Rebrickable: Portal for LEGO builders to discover what sets they can build with their own inventory\n* A comprehensive database of LEGO items available for free!\n* We will build a Shiny dashboard to explore the inventory and enable users to generate predictions\n\n::: footer\n[rebrickable.com](https://rebrickable.com)\n:::\n\n## {background-image=\"assets/img/bg/rebrickable_schema.png\" background-size=\"cover\"}\n\n::: footer\n\n:::\n\n## Applied Example {background-image=\"https://cdn.rebrickable.com/media/sets/42138-1.jpg\" background-size=\"contain\"}\n\n::: footer\n[Ford Mustang Shelby GT500](https://rebrickable.com/sets/42138-1/ford-mustang-shelby-gt500/?inventory=1#parts)\n:::\n\n## Raw Data\n\n::: {.cell}\n\n:::\n\n::: {.panel-tabset}\n\n### sets\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n\n```\n\n:::\n:::\n\n### inventories\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n\n```\n\n:::\n:::\n\n### inventory parts\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n\n```\n\n:::\n:::\n\n### parts\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n\n```\n\n:::\n:::\n\n:::\n\n## Our Journey to Production\n\n* Create a robust infrastructure for application source code\n* Identify and solve performance bottlenecks\n* Ensure app is only doing the \"bare minimum\" for a snappy user experience\n* Tune deployment to meet demand of potentially many simultaneous users", "supporting": [ "index_files" ], diff --git a/_freeze/materials/d1-02-structure/index/execute-results/html.json b/_freeze/materials/d1-02-structure/index/execute-results/html.json new file mode 100644 index 0000000..2b7a5d6 --- /dev/null +++ b/_freeze/materials/d1-02-structure/index/execute-results/html.json @@ -0,0 +1,21 @@ +{ + "hash": "df5024907ac0a3253be0f2e64a57973a", + "result": { + "markdown": "---\ntitle: \"Application Structure\"\ntitle-slide-attributes:\n data-background-image: assets/img/lego_city.jpg\n data-background-size: contain\n data-background-opacity: \"0.3\"\nsubtitle: \"posit::conf(2023)
Shiny in Production: Tools & Techniques\"\nfooter: \"[{{< var workshop_short_url >}}]({{< var workshop_full_url >}})\"\nformat:\n revealjs:\n theme: [default, ../slides.scss] # moon= teal bg | dark\n scrollable: true\n incremental: false\n slide-number: c/t # c/t | c | h/v | h.v\n slide-tone: false #true\n code-line-numbers: false\n history: false\nfrom: markdown+emoji\nrevealjs-plugins:\n - codewindow\n---\n\n\n## It's Never Just Shiny\n\n... at least for production-quality apps!\n\n* External data sources\n* Connections to other execution backends\n* Additional R packages!\n\n\n# Application Structure Options\n\n## A Single Point: `app.R`\n\nPrototype apps can coast by with a single `app.R`\n\n* More inputs, visualizations, modules, tabs ...\n* Eventually the `app.R` almost explodes\n* Difficult to collaborate without conflicts\n\n## `R` Directory\n\n* Shiny supports auto-loading scripts in an `R` directory\n* Nested directories not supported\n* More information on the [App Formats](https://shiny.rstudio.com/articles/app-formats.html ) article\n\n## Enter the [`{golem}`](https://thinkr-open.github.io/golem/)\n\n> Opinionated framework for building production-grade Shiny applications as **R packages**\n\n* Scripts guide you with first steps akin to `{usethis}` & `{devtools}`\n* Encourages Shiny best practices (especially **modules**)\n* Streamlines deployment on multiple platforms\n\n::: {.notes}\n* Developed by Colin Fay & the ThinkR team in France\n* The moral of the stoary is that Shiny apps can get really big, really fast\n + We need to provide a lot of structure and guardrails; similar to what we do when developing *R packages*\n + This is why {golem} apps *are* R packages\n* Once you go {golem}, you never go back\n:::\n\n## {golem} project structure\n\n```\n├── DESCRIPTION\n├── NAMESPACE\n├── R\n│ ├── app_config.R\n│ ├── app_server.R\n│ ├── app_ui.R\n│ └── run_app.R\n├── dev\n│ ├── 01_start.R\n│ ├── 02_dev.R\n│ ├── 03_deploy.R\n│ └── run_dev.R\n├── inst\n│ ├── app\n│ │ └── www\n│ │ └── favicon.ico\n│ └── golem-config.yml\n└── man\n └── run_app.Rd\n```\n\n::: {.notes}\n* Looks a LOT like an R package's structure (because it is one!)\n* I want to just highlight the two directories `dev` and `R`\n + The scripts in `dev` have functions that safely do other things within the package (create scripts, add dependencies, etc.)\n + Some scripts in this directory will create other scripts in the `R` directory\n + The `R` directory is where all of the logic lives (your modules, custom functions, and UI/Server scripts)\n:::\n\n## Getting Started with {golem}\n\nUse helper functions in `dev/01_start.R` to...\n\n* Create `DESCRIPTION`, `README`, `LICENSE` (etc.) files\n* Use git, tests\n* Add your own favicon 🚀\n\n::: {.notes}\n- This file and the functions it includes help you manage things at the *project*-level\n:::\n\n## Developing with {golem}\n\nUse the helper functions in `dev/02_dev.R` to...\n\n* Add R package dependencies\n* Create custom functions\n* Add Shiny modules\n\n. . .\n\nAnd use `dev/run_dev.R` to run your app\n\n::: {.notes}\n- As you run these functions and watch in awe as your `DESCRIPTION` file changes automatically, as new .R files magically appear out of thin air that have already handled all of the tricky parts for you, you too will realize that {golem} is the ChatGPT of Shiny development\n:::\n\n## Deploying with {golem}\n\nUse the helper functions in `dev/03_deploy.R` to...\n\n* Check your R package ( `devtools::check()` )\n* Build your R package ( `R CMD build mypackage` )\n* Generate deployment files for\n + RStudio Connect\n + Shinyapps.io\n + Your own Shiny Server\n + Dockerized deployments (e.g., ShinyProxy)\n\n# All About Modules\n\n## What are Modules?\n\n. . .\n\n### Building blocks to compose any Shiny app out of smaller, more understandable pieces\n\n* Avoids namespace collisions when using same widget across different areas of your app\n* Allow you to encapsulate distinct app interfaces\n* Organize code into logical and easy-to-understand components\n* Facilitate collaboration\n\n## Sound familiar?\n\n* R functions also help avoid collisions in variable names with general R code\n* Essential for creating non-trivial and extensive workflows\n\n# Module Code Example\n\n## Anatomy of a Function (UI) {auto-animate=true}\n\n:::: {.columns}\n\n::: {.column width=\"60%\"}\n::: {.codewindow .r}\npicker.R\n```r\nset_picker_ui <- function() {\n tagList(\n selectInput(\n inputId = \"set_num\",\n label = \"Select a set\"\n choices = c(\"set1\", \"set2\"),\n selected = \"set1\",\n multiple = FALSE\n )\n )\n}\n```\n:::\n:::\n\n::: {.column width=\"40%\"}\n\n:::\n\n::::\n\n## Anatomy of a Module (UI) {auto-animate=true}\n\n:::: {.columns}\n\n::: {.column width=\"60%\"}\n\n::: {.codewindow .r}\nmod_picker.R\n```r\nset_picker_ui <- function(id) {\n ns <- NS(id)\n tagList(\n selectInput(\n inputId = ns(\"set_num\"),\n label = \"Select a set\"\n choices = c(),\n multiple = FALSE\n )\n )\n}\n```\n:::\n:::\n\n::: {.column width=\"40%\"}\n\n:::\n\n::::\n\n## Anatomy of a Module (UI)\n\n:::: {.columns}\n\n::: {.column width=\"60%\"}\n\n::: {.codewindow .r}\nmod_picker.R\n```{.r code-line-numbers=\"1,2,5\"}\nset_picker_ui <- function(id) {\n ns <- NS(id)\n tagList(\n selectInput(\n inputId = ns(\"set_num\"),\n label = \"Select a set\"\n choices = c(),\n multiple = FALSE\n )\n )\n}\n```\n:::\n:::\n\n::: {.column width=\"40%\"}\n\n* `id`: String to use for namespace\n* `ns <- NS(id)`: Create proper namespace function\n\n:::\n\n::::\n\n## Anatomy of a Module (Server) {auto-animate=true}\n\n::: {.codewindow .r}\nmod_picker.R\n```r\nset_picker_server <- function(input, output, session, sets_rv) {\n set_choices <- reactive({\n # do something with sets_rv\n })\n\n observeEvent(set_choices(), {\n req(set_choices())\n updateSelectInput(\n \"set_num\",\n choices = set_choices()\n )\n })\n}\n```\n:::\n\n## Anatomy of a Module (Server) {auto-animate=true}\n\n::: {.codewindow .r}\nmod_picker.R\n```r\nset_picker_server <- function(id, sets_rv) {\n moduleServer(\n id,\n function(input, output, session) {\n set_choices <- reactive({\n # do something with sets_rv\n })\n\n observeEvent(set_choices(), {\n req(set_choices())\n updateSelectInput(\n \"set_num\",\n choices = set_choices()\n )\n })\n }\n )\n}\n```\n:::\n\n\nMinimal changes necessary\n\n## Anatomy of a Module (Server) {auto-animate=true}\n\n:::: {.columns}\n\n::: {.column width=\"70%\"}\n\n```{.r code-line-numbers=\"1,2\"}\nset_picker_server <- function(id, sets_rv) {\n moduleServer(\n id,\n function(input, output, session) {\n set_choices <- reactive({\n # do something with sets_rv\n })\n\n observeEvent(set_choices(), {\n req(set_choices())\n updateSelectInput(\n \"set_num\",\n choices = set_choices()\n )\n })\n }\n )\n}\n```\n\n:::\n\n::: {.column width=\"30%\"}\n\n:thinking: `id`\n\n:::\n\n::::\n\n::: {style=\"font-size: 70%\"}\n`moduleServer()`: Encapsulate server-side logic with namespace applied.\n:::\n\n## Invoking Modules\n\n::: {.codewindow .r}\napp.R\n```{.r}\nlibrary(shiny)\nlibrary(bslib)\nui <- page_fluid(\n set_picker_ui(\"mod1\")\n)\n\nserver <- function(input, output, session) {\n sets_rv <- reactive({\n # processing\n })\n\n set_picker_server(\"mod1\", sets_rv)\n}\n\nshinyApp(ui, server)\n```\n:::\n\n## Giving and Receiving\n\n::: {.codewindow .r}\nmod_picker.R\n```r\nset_picker_ui <- function(id, label = \"Select a set\") {\n ns <- NS(id)\n tagList(\n selectInput(\n inputId = ns(\"set_num\"),\n label = label,\n choices = c(),\n multiple = FALSE\n )\n )\n}\n```\n:::\n\n::: {style=\"font-size: 80%\"}\n* Reasonable inputs: static values, vectors, flags\n* Avoid **reactive** parameters\n* Return value: `tagList()` of inputs, output placeholders, and other UI elements\n:::\n\n## Giving and Receiving\n\n::: {.codewindow .r}\nmod_picker.R\n```{.r}\nset_picker_server <- function(id, sets_rv) {\n moduleServer(\n id,\n function(input, output, session) {\n set_choices <- reactive({\n # do something with sets_rv\n })\n\n observeEvent(set_choices(), {\n req(set_choices())\n updateSelectInput(\n \"set_num\",\n choices = set_choices()\n )\n })\n }\n )\n}\n```\n:::\n\n::: {style=\"font-size: 80%\"}\nInput & return values can be a mix of static and **reactive** objects\n:::\n\n\n## To () or not to ()\n\n:::: {.columns}\n\n::: {.column width=\"45%\"}\n\n::: {.codewindow .r}\napp_server.R\n```{.r}\n# app server\nsets_rv <- reactive({\n # processing\n})\n\nset_picker_server(\"mod1\", sets_rv)\n```\n:::\n\n:::\n\n::: {.column width=\"55%\"}\n\n::: {.codewindow .r}\nmod_picker.R\n```{.r}\nset_picker_server <- function(id, sets_rv) {\n moduleServer(\n id,\n function(input, output, session) {\n # ...\n\n set_selection <- reactive({\n input$set_num\n })\n\n set_selection\n }\n )\n}\n```\n:::\n:::\n\n::::\n\n::: {style=\"font-size: 70%;\"}\n* Reactive parameters reference by **name**: `sets_rv`\n* Inside module, **invoke** reactive parameter as you would any other reactive in Shiny: `sets_rv()`\n* Any reactive(s) returned by module should also be reference by **name**: `set_selection`, ~~`set_selection()`~~\n:::\n\n::: footer\n:::\n\n# Code-Along {background-color=\"#17395c\"}\n\nAdd a new Shiny module to pick LEGO set themes\n\n* [Details](codealong-1.html){target=\"_blank\"}\n* Posit Cloud project: **Application Structure Code-along 1**\n\n## Your Turn: [Exercise 1](ex-1.html){target=\"_blank\"}\n\nCreate a new Shiny module with LEGO data metrics!\n\n* [Details](ex-1.html){target=\"_blank\"}\n* Posit Cloud project: **Application Structure Exercise 1**\n\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n
\n10:00\n
\n```\n\n:::\n:::\n\n\n\n# Dependency Management\n\n## Turned Upside-Down\n\nImagine your application is working great!\n\n
\n\n. . .\n\n:::: {.columns .v-center-container}\n\n::: {.column width=\"50%\"}\n\n```r\nupdate.packages(ask = FALSE)\nremotes::install_github(\"pkg\")\n```\n\n:::\n\n::: {.column width=\"50%}\n\n![](https://rfortherestofus.com/wp-content/uploads/2020/09/update-packages-prompt.png)\n\n:::\n\n::::\n\n## Turned Upside-Down\n\n:::: {.columns .v-center-container}\n\n::: {.column width=\"50%\"}\n\n`ggplot2` version `0.9.3`\n\n![](assets/img/computer_user_happy.png){width=\"60%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n\n`ggplot2` version `1.0.0`\n\n![](http://i.giphy.com/RhEvCHIeZAZ6E.gif)\n\n:::\n\n::::\n\n## Take Control with [`{renv}`](https://rstudio.github.io/renv/)\n\n> Create **r**eproducible **env**ironments for your R projects.\n\n* Next generation of `{packrat}`\n* Isolated package library from rest of your system\n* Transfer projects to different collaborators / platforms\n* **Reproducible** package installation\n* Easily create new projects or convert existing projects with RStudio or built-in functions.\n\n## Under the Hood\n\nUpon initializing a project:\n\n1. Project-level `.Rprofile` to activate custom package library on startup\n1. Lockfile `renv.lock` to describe state of project library\n1. `renv/library` to hold private project library\n1. `renv/activate.R` performs activation\n\n## Develop a Routine\n\nSticking with `{renv}` will pay off (trust me)\n\n* Fair play to mix packages from CRAN, GitHub, and proprietary sources\n* Roll back when a package upgrade doesn't play nicely\n* **You** make the call when to update your library!\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": { + "include-in-header": [ + "\n\n" + ], + "include-after-body": [ + "\n\n\n" + ] + }, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/materials/d1-03-performance/codealong-1/execute-results/html.json b/_freeze/materials/d1-03-performance/codealong-1/execute-results/html.json index a99eb2e..f170504 100644 --- a/_freeze/materials/d1-03-performance/codealong-1/execute-results/html.json +++ b/_freeze/materials/d1-03-performance/codealong-1/execute-results/html.json @@ -1,8 +1,10 @@ { - "hash": "c12dcd124a82cea6178e9a18ef6d6d65", + "hash": "83fe9dd48489747ef580dbf6bd90aaa4", "result": { - "markdown": "---\ntitle: Asynchronous Processing of LEGO Model Prediction\nformat:\n html:\n code-line-numbers: false\n execute:\n echo: true\n eval: false\n---\n\n\n## Requirements\n\nThe 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}`](https://httr2.r-lib.org/) package. Here is the function wrapping the API execution:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n#' @importFrom httr2 request req_body_json req_perform resp_body_json\nrun_prediction <- function(df, endpoint_url, back_transform = TRUE, round_result = TRUE) {\n # create request object\n req <- request(endpoint_url)\n\n # perform request\n resp <- req |>\n req_body_json(df) |>\n req_perform()\n\n # extract predictions from response\n pred_values <- resp_body_json(resp)$.pred |> unlist()\n\n # back-transform log10 value of predicted number of parts if requested\n if (back_transform) {\n pred_values <- 10 ^ pred_values\n }\n\n # round result up to nearest integer if requested\n if (round_result) pred_values <- ceiling(pred_values)\n\n # append predictions to supplied data frame\n dplyr::mutate(df, predicted_num_parts = pred_values)\n}\n```\n:::\n\n\nUnfortunately, 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}`\n\n## Plan\n\n1. Establish reactive values for tracking the status of the prediction calls\n1. Create a new controller to launch new R processes when new prediction tasks are launched\n1. 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.\n1. 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.\n\n## Solution \n\nFirst we create the following `reactiveVal` objects to keep track of the prediction state:\n\n\n::: {.cell}\n\n```{.r .cell-code}\npred_status <- reactiveVal(\"No prediction submitted yet.\")\npred_poll <- reactiveVal(FALSE)\n```\n:::\n\n\nNext we set up a new controller:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# establish async processing with crew\ncontroller <- crew_controller_local(workers = 4, seconds_idle = 10)\ncontroller$start()\n\n# make sure to terminate the controller on stop #NEW\nonStop(function() controller$terminate())\n```\n:::\n\n\nInside the `observeEvent` for the user clicking the prediction button, we update the logic to push the prediction task to the controller:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncontroller$push(\n command = run_prediction(df),\n data = list(\n run_prediction = run_prediction,\n df = pred_data_rv$data\n ),\n packages = c(\"httr2\", \"dplyr\")\n)\n\npred_poll(TRUE)\n```\n:::\n\n\nLastly, 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:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nobserve({\n req(pred_poll())\n\n invalidateLater(millis = 100)\n result <- controller$pop()$result\n\n if (!is.null(result)) {\n pred_data_rv$data <- result[[1]]\n print(controller$summary()) \n }\n\n if (isFALSE(controller$nonempty())) {\n pred_status(\"Prediction Complete\")\n pred_poll(controller$nonempty())\n removeNotification(id = \"pred_message\")\n }\n})\n```\n:::\n", - "supporting": [], + "markdown": "---\ntitle: Using `.parquet` files in Shiny\nformat:\n html:\n code-line-numbers: false\n execute:\n echo: true\n eval: false\n---\n\n\n## Requirements\n\nThe 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.\n\n## Plan\n\nUpon 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.\n\nKnowing 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}`](https://arrow.apache.org/docs/r/) package.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(dplyr)\nlibrary(arrow)\npart_meta_df <- gen_part_metaset(min_set_parts = 1)\n\nwrite_parquet(\n part_meta_df,\n \"inst/extdata/part_meta_df.parquet\")\n)\n```\n:::\n\n\nWith the processed data set available in the app infrastructure, we can utilize it inside the application with the following:\n\n\n::: {.cell}\n\n```{.r .cell-code}\npart_meta_df <- arrow::read_parquet(app_sys(\"extdata\", \"part_meta_df.parquet\"), as_data_frame = FALSE)\n```\n:::\n\n\nWhy 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.\n\nWe 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}`!", + "supporting": [ + "codealong-1_files" + ], "filters": [ "rmarkdown/pagebreak.lua" ], diff --git a/_freeze/materials/d1-03-performance/codealong-2/execute-results/html.json b/_freeze/materials/d1-03-performance/codealong-2/execute-results/html.json new file mode 100644 index 0000000..a99eb2e --- /dev/null +++ b/_freeze/materials/d1-03-performance/codealong-2/execute-results/html.json @@ -0,0 +1,14 @@ +{ + "hash": "c12dcd124a82cea6178e9a18ef6d6d65", + "result": { + "markdown": "---\ntitle: Asynchronous Processing of LEGO Model Prediction\nformat:\n html:\n code-line-numbers: false\n execute:\n echo: true\n eval: false\n---\n\n\n## Requirements\n\nThe 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}`](https://httr2.r-lib.org/) package. Here is the function wrapping the API execution:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n#' @importFrom httr2 request req_body_json req_perform resp_body_json\nrun_prediction <- function(df, endpoint_url, back_transform = TRUE, round_result = TRUE) {\n # create request object\n req <- request(endpoint_url)\n\n # perform request\n resp <- req |>\n req_body_json(df) |>\n req_perform()\n\n # extract predictions from response\n pred_values <- resp_body_json(resp)$.pred |> unlist()\n\n # back-transform log10 value of predicted number of parts if requested\n if (back_transform) {\n pred_values <- 10 ^ pred_values\n }\n\n # round result up to nearest integer if requested\n if (round_result) pred_values <- ceiling(pred_values)\n\n # append predictions to supplied data frame\n dplyr::mutate(df, predicted_num_parts = pred_values)\n}\n```\n:::\n\n\nUnfortunately, 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}`\n\n## Plan\n\n1. Establish reactive values for tracking the status of the prediction calls\n1. Create a new controller to launch new R processes when new prediction tasks are launched\n1. 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.\n1. 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.\n\n## Solution \n\nFirst we create the following `reactiveVal` objects to keep track of the prediction state:\n\n\n::: {.cell}\n\n```{.r .cell-code}\npred_status <- reactiveVal(\"No prediction submitted yet.\")\npred_poll <- reactiveVal(FALSE)\n```\n:::\n\n\nNext we set up a new controller:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# establish async processing with crew\ncontroller <- crew_controller_local(workers = 4, seconds_idle = 10)\ncontroller$start()\n\n# make sure to terminate the controller on stop #NEW\nonStop(function() controller$terminate())\n```\n:::\n\n\nInside the `observeEvent` for the user clicking the prediction button, we update the logic to push the prediction task to the controller:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncontroller$push(\n command = run_prediction(df),\n data = list(\n run_prediction = run_prediction,\n df = pred_data_rv$data\n ),\n packages = c(\"httr2\", \"dplyr\")\n)\n\npred_poll(TRUE)\n```\n:::\n\n\nLastly, 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:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nobserve({\n req(pred_poll())\n\n invalidateLater(millis = 100)\n result <- controller$pop()$result\n\n if (!is.null(result)) {\n pred_data_rv$data <- result[[1]]\n print(controller$summary()) \n }\n\n if (isFALSE(controller$nonempty())) {\n pred_status(\"Prediction Complete\")\n pred_poll(controller$nonempty())\n removeNotification(id = \"pred_message\")\n }\n})\n```\n:::\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/materials/d1-03-performance/ex-1/execute-results/html.json b/_freeze/materials/d1-03-performance/ex-1/execute-results/html.json new file mode 100644 index 0000000..8516a04 --- /dev/null +++ b/_freeze/materials/d1-03-performance/ex-1/execute-results/html.json @@ -0,0 +1,14 @@ +{ + "hash": "c5d56b9141dff51b58a10ea0827719ca", + "result": { + "markdown": "---\ntitle: \"Profile the LEGO Bricks App\"\n---\n\n\n## Access Instructions\n\nThe project used for this particular exercise is hosted on [Posit Cloud](https://posit.cloud) in this [space](https://posit.cloud/spaces/400774/join?access_code=DDgV_peF5WCCCpB5JHjQtMN2aHByWoNF0k5p8Wp7). The project for this exercise is called **performance-exercise1**.\n\n## Setup\n\nUsing what you just learned about the [`{profvis}`](http://rstudio.github.io/profvis/examples.html#example-3---profiling-a-shiny-application), 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:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nprofvis::profvis({\n print(run_app())\n})\n```\n:::\n\n\nOnce 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.\n\n## Questions\n\n* Are you able to identify any performance bottlenecks in the application?\n* If so, can you think of ways to fix the issues?", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/materials/d1-03-performance/index/execute-results/html.json b/_freeze/materials/d1-03-performance/index/execute-results/html.json new file mode 100644 index 0000000..3de927e --- /dev/null +++ b/_freeze/materials/d1-03-performance/index/execute-results/html.json @@ -0,0 +1,23 @@ +{ + "hash": "90b89c0a71110d66dcad800485d33fe1", + "result": { + "markdown": "---\ntitle: \"Performance\"\nsubtitle: \"posit::conf(2023)
Shiny in Production: Tools & Techniques\"\nfooter: \"[{{< var workshop_short_url >}}]({{< var workshop_full_url >}})\"\nformat:\n revealjs:\n theme: [default, ../slides.scss] # moon= teal bg | dark\n scrollable: true\n incremental: false\n slide-number: c/t # c/t | c | h/v | h.v\n slide-tone: false #true\n code-line-numbers: false\n history: false\nrevealjs-plugins:\n - codewindow\n---\n\n\n## Performance Agenda\n\n* Profiling your Shiny app with {profvis}\n* Lightning-quick data loading/querying with {arrow} & *.parquet* files\n* Asynchronous processes with {crew}\n\n# {profvis}: an R package for profiling R code
(including Shiny apps)\n\n## What is {profvis}?\n\nR package for visualizing how (and how fast/slow) your R code runs\n\n[{profvis} website](https://rstudio.github.io/profvis/index.html)\n\n![](assets/img/example_profvis_report.png)\n\n::: {.notes}\n* The package is from Posit\n* Help you understand the bottlenecks in your R code\n* and, you guessed it, it works with Shiny too\n:::\n\n## Working with {profvis} & {golem} {auto-animate=true}\n\nOur `dev/run_dev.R` script is where we can profile our app interactively.\n\n```{.r}\noptions(golem.app.prod = FALSE)\n\ngolem::detach_all_attached()\n\ngolem::document_and_reload()\n\nrun_app()\n```\n## Working with {profvis} & {golem} {auto-animate=true}\n\nOur `dev/run_dev.R` script is where we can profile our app interactively.\n\n```{.r}\noptions(golem.app.prod = FALSE)\n\ngolem::detach_all_attached()\n\ngolem::document_and_reload()\n\nprofvis::profvis({\n print(run_app())\n})\n```\n\n::: {.notes}\nYou need to wrap the `run_app()` function in `print()`, before passing it to `profvis::profvis()`\n:::\n\n## Demo!\n\n![](assets/img/myspace_meme.png)\n\n::: {.notes}\n- Show how to change the `dev/run_dev.R` script to enable {profvis} profiling\n- Launch the app, navigate to the \"Overview\" page, and change a filter\n- Stop the app, and wait for the report to load\n- Describe the flamegraph, change the filters to only show events that took time\n- Navigate to the 'data' tab and discuss what took the most time\n:::\n\n## Your Turn: [Exercise 1](ex-1.html){target=\"_blank\"}\n\nProfile the LEGO Bricks app!\n\n* [Details](ex-1.html){target=\"_blank\"}\n* Posit Cloud project: **Performance Exercise 1**\n\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n
\n05:00\n
\n```\n\n:::\n:::\n\n\n\n# {arrow} & the *.parquet* file format\n\n## What are *.parquet* files?\n\n* *.parquet* is a *columnar* storage format\n* *.parquet* files not only store data, but they also store metadata about your data (i.e., data types for each column, number of rows in the file, etc.)\n* Smaller files\n* Faster read speed\n\n::: {.notes}\n- HOT TAKE INCOMING: parquet is the new csv\n- parquet files typically are the data structure that lives behind projects like the open source Delta Lake\n- faster across pretty much all benchmarks\n:::\n\n## What is the {arrow} R package?\n\n* Part of the larger Apache Arrow project\n* Connect to your data with {arrow}...\n* ... and query it with {dplyr}\n\n. . .\n\n[Apache Arrow Homepage](https://arrow.apache.org/)\n\n[Shiny + Arrow Article](https://posit.co/blog/shiny-and-arrow/)\n\n::: {.notes}\n- \"multi-language toolbox for accelerated data interchange and in-memory processing\"\n- I.e., a set of data manipulation standards (particularly against parquet files) that has been implemented in a bunch of languages including R, Python, Rust, Go, and more\n- {arrow} let's you use {dplyr} verbs against a single parquet file (or, perhaps more importantly, a *set* of parquet files) to query the data in those files\n- When it comes to building Shiny apps, we should look for easy places where we can gain efficiency & speed to improve our user experience (you don't want users waiting 20 seconds for your data prep logic to run against a single massive csv); it's very likely that the combination of .parquet + {arrow} + {dplyr} can meet your app performance needs (it does for at least 95% of my use cases -- there are very few cases where I have to go beyond that and start looking into other engines for faster data manipulation)\n:::\n\n# Code-Along {background-color=\"#17395c\"}\n\nUsing `.parquet` in the LEGO Bricks Shiny app\n\n* [Details](codealong-1.html){target=\"_blank\"}\n* Posit Cloud project: **Performance Exercise 1**\n\n# Async Processing\n\n## Single (threaded) Line {background-image=\"assets/img/lego_line_pay.jpg\" background-size=\"cover\"}\n\n* A single R process managing the different tasks in a Shiny application\n* Executed one-by-one\n\n# Should I care? It Depends ...\n\n# If you are the __only__ user for a quick and efficient app: Likely not!\n\n## Crowd Pleaser\n\nMultiple users accessing the app __concurrently__: \n\n* Single-threaded R process serving multiple users in typical deployments\n\n## Asynchronous Processing (circa 2018)\n\n:::: {.columns}\n\n::: {.column width=\"50%\"}\n\n### 📦 [`{promises}`](https://rstudio.github.io/promises/index.html)\n\nHandle objects representing the (eventual) result of an async operation\n\n:::\n\n::: {.column width=\"50%\"}\n\n### 📦 [`{future}`](https://future.futureverse.org/)\n\nLaunch tasks without blocking current R session\n\n:::\n\n::::\n\n::: footer\n[Using promises with Shiny](https://rstudio.github.io/promises/articles/promises_06_shiny.html)\n:::\n\n## Introducing [`{crew}`](https://wlandau.github.io/crew/)\n\n> A distributed worker launcher for asynchronous tasks\n\n* Extends use of the [mirai](https://github.com/shikokuchuo/mirai) task scheduler to multiple computing backends\n* Central controller object manages tasks (scales on fly)\n* Supports multiple [controller groups](https://wlandau.github.io/crew/articles/controller_groups.html) for specialized worker types\n* Fits nicely with [`{targets}`](https://docs.ropensci.org/targets/) and ...\n\n. . .\n\n![](assets/img/shiny.png){.absolute top=0 left=200}\n\n## Watch-Along {background-color=\"#17395c\"}\n\nUsing `{crew}` inside a Shiny application:\n\n* Vignette: \n* Application: \n\n## Setting up for Success\n\n1. Create functions for long-running tasks\n1. Create multiple [`reactiveVal`](https://shiny.posit.co/r/reference/shiny/latest/reactiveval) objects for bookkeeping\n1. Set up a `{crew}` controller \n1. Establish an event-driven push of task to the controller with monitoring of worker status\n\n# Code-Along {background-color=\"#17395c\"}\n\nAsynchronous calls of a model prediction API.\n\n* [Details](codealong-2.html){target=\"_blank\"}\n* Posit Cloud project: **Performance Code-along 2**\n\n", + "supporting": [ + "index_files" + ], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": { + "include-in-header": [ + "\n\n" + ], + "include-after-body": [ + "\n\n\n" + ] + }, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/materials/d1-04-loadtesting/index/execute-results/html.json b/_freeze/materials/d1-04-loadtesting/index/execute-results/html.json new file mode 100644 index 0000000..1d56567 --- /dev/null +++ b/_freeze/materials/d1-04-loadtesting/index/execute-results/html.json @@ -0,0 +1,20 @@ +{ + "hash": "62fdb7f53e97ff82dedd15c9daf6432c", + "result": { + "markdown": "---\ntitle: \"Load Testing\"\nsubtitle: \"posit::conf(2023)
Shiny in Production: Tools & Techniques\"\nfooter: \"[{{< var workshop_short_url >}}]({{< var workshop_full_url >}})\"\nformat: \n revealjs:\n theme: [default, ../slides.scss] # moon= teal bg | dark\n scrollable: true\n incremental: false\n slide-number: c/t # c/t | c | h/v | h.v\n slide-tone: false #true\n code-line-numbers: true\n history: false\nrevealjs-plugins:\n - codewindow\n---\n\n\n## One to Many\n\n:::: {.columns}\n\n::: {.column width=\"50%\"}\n![](assets/img/lego_single_user_computer.png)\n:::\n\n::: {.column width=\"50%\"}\n![](assets/img/lego_crowd.jpg)\n:::\n\n::::\n\n## Optimization Loop Method\n\n![](assets/img/loop.svg)\n\n::: footer\n[rstudio.github.io/shinyloadtest/articles/case-study-scaling.html](https://rstudio.github.io/shinyloadtest/articles/case-study-scaling.html)\n:::\n\n## Double the (Load-Testing) Fun\n\n#### `{shinyloadtest}`\n\n* Record the events of a Shiny application session\n* Process and analyze metrics associated with application runs\n\n#### `shinycannon`\n\n* Command-line utility to launch multiple application sessions and collect event-based metrics\n* Cross-platform (built with Kotlin)\n\n## Hosting Requirements\n\n* Application deployed to a server supporting [SockJS](https://github.com/sockjs/sockjs-client) (Posit Connect, Shiny Server)\n* If authentication required for app on Posit Connect, you'll need an API key\n\n## Recording a Session\n\n::: {.codewindow .r}\nshinyloadtest_recording.R\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shinyloadtest)\nrecord_session(\"https://my-lego-app.me\", output_file = \"recording.log\")\n```\n:::\n\n:::\n\n* Emulate a real-world usage of your application\n* Avoid rapid clicks / selections of your inputs\n\n## The log\n\n::: {.codewindow}\nrecording.log\n```.bash\n# version: 1\n# target_url: https://my-lego-app.me/\n# target_type: RStudio Server Connect\n# rscApiKeyRequired: false\n{\"type\":\"REQ_HOME\",\"begin\":\"2023-09-07T13:03:39.042Z\",\"end\":\"2023-09-07T13:03:40.138Z\",\"status\":200,\"url\":\"/\"}\n{\"type\":\"REQ_GET\",\"begin\":\"2023-09-07T13:03:40.332Z\",\"end\":\"2023-09-07T13:03:40.502Z\",\"status\":200,\"url\":\"/_w_${WORKER}/shiny-sass-1.7.5/shiny-sass.css\"}\n{\"type\":\"REQ_GET\",\"begin\":\"2023-09-07T13:03:40.517Z\",\"end\":\"2023-09-07T13:03:40.678Z\",\"status\":200,\"url\":\"/_w_${WORKER}/bslib-grid-styles-0.5.1/grid.css\"}\n{\"type\":\"REQ_GET\",\"begin\":\"2023-09-07T13:03:40.692Z\",\"end\":\"2023-09-07T13:03:40.886Z\",\"status\":200,\"url\":\"/_w_${WORKER}/bootstrap-5.2.2/bootstrap.min.css\"}\n...\n...\n{\"type\":\"WS_SEND\",\"begin\":\"2023-09-07T13:04:17.805Z\",\"message\":\"[\\\"D#0|m|{\\\\\\\"method\\\\\\\":\\\\\\\"update\\\\\\\",\\\\\\\"data\\\\\\\":{\\\\\\\"n_parts_display\\\\\\\":\\\\\\\"20\\\\\\\"}}\\\"]\"}\n{\"type\":\"WS_CLOSE\",\"begin\":\"2023-09-07T13:04:35.561Z\"}\n```\n:::\n\n. . .\n\n![](assets/img/confused_minifig.jpg){.absolute top=50 right=250 width=\"500px\" height=\"500px\"}\n\n## Loading the (shiny) cannon\n\n::: {.codewindow}\nbash\n\n::: {.cell}\n\n```{.bash .cell-code}\njava -jar shinycannon-1.1.3-dd43f6b.jar \\\n recording.log \\\n https://my-lego-app.me \\\n --loaded-duration-minutes 2 \\\n --workers 1 \\\n --output-dir run1\n```\n:::\n\n:::\n\n. . .\n\n::: {.codewindow .r}\nrun_shinycannon.R\n\n::: {.cell}\n\n```{.r .cell-code}\nsource(file.path(here::here(), \"R\", \"shinycannon.R\"))\n\nshinycannon(\n \"shinycannon-1.1.3-dd43f6b.jar\",\n \"recording.log\",\n \"https://rsc.training.posit.co/brickapp-sync/\",\n loaded_duration_minutes = 2,\n workers = 1,\n output_dir = \"run1\"\n)\n```\n:::\n\n:::\n\n## Metrics\n\n\n::: {.cell}\n\n:::\n\n\n::: {.codewindow .r}\nanalyze_recording.R\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shinyloadtest)\ndf <- load_runs(\"Run 1\" = \"run1\")\n```\n:::\n\n:::\n\n\n::: {.smaller-output}\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\n# A tibble: 80 × 13\n run user_id session_id iteration input_line_number event start end time\n \n 1 Run 1 0 0 0 4 REQ_… 0 24.2 24.2 \n 2 Run 1 0 0 0 5 REQ_… 24.2 24.5 0.259\n 3 Run 1 0 0 0 6 REQ_… 24.5 24.6 0.155\n 4 Run 1 0 0 0 7 REQ_… 24.6 24.9 0.284\n 5 Run 1 0 0 0 8 REQ_… 24.9 25.3 0.382\n 6 Run 1 0 0 0 9 REQ_… 25.3 25.5 0.170\n 7 Run 1 0 0 0 10 REQ_… 25.5 25.7 0.242\n 8 Run 1 0 0 0 11 REQ_… 25.7 25.9 0.25 \n 9 Run 1 0 0 0 12 REQ_… 25.9 26.3 0.395\n10 Run 1 0 0 0 13 REQ_… 26.3 26.5 0.198\n# ℹ 70 more rows\n# ℹ 4 more variables: concurrency , maintenance , label ,\n# json \n```\n\n\n:::\n:::\n\n:::\n\n## But Wait .. There's More\n\n::: {.codewindow .r}\ngen_report.R\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(shinyloadtest)\ndf <- load_runs(\"Run 1\" = \"run1\")\nshinyloadtest_report(df, output = \"report_test.html\")\n```\n:::\n\n:::\n\n![](assets/img/report_screenshot.png)\n\n# Code-Along {background-color=\"#17395c\"}\n\nRecord and analyze load-testing sessions with `shinyCannon` and `{shinyloadtest}`\n\n* Application URL: ", + "supporting": [ + "index_files" + ], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": { + "include-after-body": [ + "\n\n\n" + ] + }, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/materials/d1-05-deploy-admin/index/execute-results/html.json b/_freeze/materials/d1-05-deploy-admin/index/execute-results/html.json new file mode 100644 index 0000000..93e7a33 --- /dev/null +++ b/_freeze/materials/d1-05-deploy-admin/index/execute-results/html.json @@ -0,0 +1,23 @@ +{ + "hash": "31358eec55390f8b122b9d659980662f", + "result": { + "markdown": "---\ntitle: \"Deployment & Administration\"\nsubtitle: \"posit::conf(2023)
Shiny in Production: Tools & Techniques\"\nfooter: \"[{{< var workshop_short_url >}}]({{< var workshop_full_url >}})\"\nformat: \n revealjs:\n theme: [default, ../slides.scss] # moon= teal bg | dark\n scrollable: true\n incremental: false\n slide-number: c/t # c/t | c | h/v | h.v\n slide-tone: false #true\n code-line-numbers: true\n history: false\nrevealjs-plugins:\n - codewindow\n---\n\n\n## Choose your Adventure\n\n\n::: {.cell}\n\n:::\n\n\n:::: columns\n::: {.column width=\"50%\"}\n### Posit Connect\n\n![](assets/img/shiny.png){width=\"80%\"}\n\n:::\n\n::: {.column width=\"50%\"}\n### Containers\n\n![](assets/img/Rlogo.png){width=\"50%\"}\n\n![](assets/img/linux.png){width=\"50%\"}\n\n:::\n::::\n\n## Deployment Checklist\n\n✅ Create `app.R` in directory\n\n✅ Take note of any custom environment variables\n\n✅ Ensure `DESCRIPTION` is up-to-date with required packages\n\n✅ Remove outdated functions / scripts\n\n\n\n## {.center-xy}\n\n::: mono\n::: {.codewindow .r}\n03_deploy.R\n\n\n::: {.cell}\n\n```{.r .cell-code}\n## RStudio ----\n## If you want to deploy on RStudio related platforms\ngolem::add_rstudioconnect_file()\ngolem::add_shinyappsio_file()\ngolem::add_shinyserver_file()\n```\n:::\n\n:::\n:::\n\n---\n\n::: mono\n::: {.codewindow .r width=\"1000px\"}\n03_deploy.R\n\n::: {.cell}\n\n```{.r .cell-code}\nrsconnect::deployApp(\n appName = \"legobricksapp\",\n appTitle = desc::desc_get_field(\"Package\"),\n appFiles = c(\n \"R/\",\n \"inst/\",\n \"data/\",\n \"NAMESPACE\",\n \"DESCRIPTION\",\n \"app.R\"\n ),\n appId = rsconnect::deployments(\".\")$appId,\n lint = FALSE,\n forceUpdate = TRUE\n)\n```\n:::\n\n:::\n:::\n\n# Code-Along {background-color=\"#17395c\"}\n\nDeploy your application(s) to Posit Connect:\n\n* Use any of the applications from a previous exercise\n* Attempt both deployment types (push-button & `{rsconnect}`)\n* Explore application settings in Posit Connect\n\n## Balancing Act {.centerheading}\n\n:::: {.columns}\n\n::: {.column width=\"33%\"}\n\n🔼 Max Processes\n\n* More resources dedicated to particular application\n* Potential for server overload\n\n:::\n\n::: {.column width=\"33%\"}\n\n![](assets/img/teeter_totter.png)\n\n:::\n\n::: {.column width=\"33%\"}\n\n🔼 Min Processes\n\n* Resources dedicated even when app is idle\n* Potentially wasteful if app is not used concurrently often\n\n:::\n\n::::\n\n## Containers for Shiny Deployment\n\n* Anyone with a container runtime can execute your container **without R on their system**\n + Popular container runtimes: Docker, Podman, LXC\n\n* A multitude of web services offer *serverless* app deployments with containers\n + Bring your container image, they take care of the rest\n + Terrific reviews available at [hosting.analythium.io](https://hosting.analythium.io)\n\n. . . \n\n**Coming soon**: Container support for Posit Connect\n\n## Business Talk {background-image=\"assets/img/lego_boss_office.jpg\" background-size=\"cover\" background-color=\"black\"}\n\n## You Might be Asked ...\n\n::: {.fragment}\n\n\"How many people used your app last year?\"\n\n:::\n\n::: {.fragment}\n\n\"How long are users on the app at a given time?\"\n\n:::\n\n::: {.fragment}\n::: {.fragment .strike}\n\"What is the return on investment (ROI) this app is bringing the company?\"\n:::\n:::\n\n::: {.fragment}\n\n:::: {.columns}\n\n::: {.column width=\"55%\"}\n![](assets/img/daniel-bryan-no.gif){width=90%}\n:::\n\n::: {.column width=\"30%\"}\n### NOT TODAY!\n:::\n\n::::\n\n:::\n\n## 📦 Introducing [`{connectapi}`](https://pkgs.rstudio.com/connectapi/)\n\n> R client for the [Posit Connect Server API](https://docs.posit.co/connect/api/) as well as helpful functions that utilize the client\n\nChecklist:\n\n✅ Create API key for your Posit Connect account\n\n✅ Create `.Renviron` with following variables: `CONNECT_SERVER`, `CONNECT_API_KEY`\n\n## Introduce Yourself\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(connectapi)\n\nclient <- connect(\n server = Sys.getenv(\"CONNECT_SERVER\"),\n api_key = Sys.getenv(\"CONNECT_API_KEY\")\n)\n\nclient\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nPosit Connect API Client: \n Posit Connect Server: https://rsc.training.rstudio.com\n Posit Connect API Key: ***********FWxW\n```\n\n\n:::\n:::\n\n\n* `client` is an R6 object representing the API client\n* Required parameter for many convenience functions\n\n## Know Yourself\n\n\n::: {.cell}\n\n```{.r .cell-code}\nmy_guid <- user_guid_from_username(client, \"rpodcast\")\n\nmy_guid\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] \"47e1338f-54a8-4b0e-9b3c-e4380611b30f\"\n```\n\n\n:::\n:::\n\n\n* All content/users have a **unique indentifier (guid)**\n\n## Obtain (Your) Shiny Apps\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(dplyr)\n\nmeta_df <- get_content(client) |>\n filter(owner_guid == my_guid) |>\n select(guid, name, title)\n\nmeta_df\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n# A tibble: 27 × 3\n guid name title \n \n 1 40dd2969-5d67-4940-89c4-08fe31fd1c74 brickapp brickapp \n 2 fba6896e-49e4-45c6-856f-03a3bc4f498f legobricksapp legobricks.app \n 3 626d6bb1-f36d-4c9b-bc9e-5cb44f46e698 hellogolem22 hellogolem2 \n 4 6f8116dc-eabf-4a64-aacb-4aeffbb4eaa4 golembrochure2 golembrochure \n 5 a4e6809f-4858-4888-a848-94925d3df4a0 golembrochure golembrochure \n 6 f268d424-a286-41b9-91f1-ae458797e64a golemhellolocal golemhellolocal\n 7 4d9124a0-158b-4bd9-9b65-07b877db0906 test test \n 8 c60de16e-0b29-4267-a2c5-0eb6815d7999 hellogolem3 hellogolem \n 9 2aec4c5b-144f-4a6c-8abf-7d07a17d78b2 hellogolem hellogolem \n10 344b9f1b-ddbe-4145-a1f0-eb6608662f08 hellogolem2 hellogolem \n# ℹ 17 more rows\n```\n\n\n:::\n:::\n\n\n## Usage Metrics\n\nSession durations for the `legobricksapp`:\n\n\n::: {.cell}\n\n```{.r .cell-code}\napp_guid <- \"fba6896e-49e4-45c6-856f-03a3bc4f498f\"\n\nget_usage_shiny(\n client,\n content_guid = app_guid,\n limit = 5\n ) |>\n filter(!is.na(ended)) |>\n mutate(session_duration = ended - started) |>\n select(user_guid, session_duration)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n# A tibble: 5 × 2\n user_guid session_duration\n \n1 47e1338f-54a8-4b0e-9b3c-e4380611b30f 10.933333 mins \n2 47e1338f-54a8-4b0e-9b3c-e4380611b30f 1.216667 mins \n3 47e1338f-54a8-4b0e-9b3c-e4380611b30f 4.733333 mins \n4 47e1338f-54a8-4b0e-9b3c-e4380611b30f 6.766667 mins \n5 47e1338f-54a8-4b0e-9b3c-e4380611b30f 1.183333 mins \n```\n\n\n:::\n:::\n\n\n## Usage Metrics\n\nHow many users visited `legobricksapp` in last 28 days?\n\n\n::: {.cell}\n\n```{.r .cell-code}\napp_guid <- \"fba6896e-49e4-45c6-856f-03a3bc4f498f\"\ntime_start <- Sys.Date() - lubridate::days(28)\nget_usage_shiny(\n client,\n content_guid = app_guid,\n limit = 100,\n from = time_start\n ) |>\n pull(user_guid) |>\n unique() |>\n length()\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] 4\n```\n\n\n:::\n:::\n\n\n## Your Turn\n\nObtain metadata for your deployed application(s) on Posit Connect\n\n* Explore the data as you like!\n\n\n::: {.cell}\n::: {.cell-output-display}\n\n```{=html}\n
\n
\n05:00\n
\n```\n\n:::\n:::\n\n\n\n## Going Further with Metrics\n\nCollection of interesting use cases available at [github.com/sol-eng/connect-usage](https://github.com/sol-eng/connect-usage)\n\n* Most-viewed content in last 30 days\n* [Interactive dashboard](https://colorado.posit.co/rsc/usage-interactive/) of usage metrics\n* [`{connectViz}`](https://github.com/RinteRface/connectViz): Collection of customizable functions for visualizing content usage for an organization\n\n## Our Journey is Complete!\n\n:::: {.columns}\n\n::: {.column width=\"70%\"}\n\n![](assets/img/lego_victory.jpg)\n\n:::\n\n::: {.column width=\"30%\"}\n\n::: {style=\"font-size: 70%\"}\n* Robust app infrastructure with `{golem}`\n* Profiling your app with `{profvis}`\n* Solving performance bottlenecks\n* Assessing your app under high user load\n* Efficient app deployments and tuning\n* Obtaining usage metrics\n:::\n\n:::\n\n::::\n\n::: footer\n[pos.it/conf-workshop-survey](https://pos.it/conf-workshop-survey)\n:::", + "supporting": [ + "index_files" + ], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": { + "include-in-header": [ + "\n\n" + ], + "include-after-body": [ + "\n\n\n" + ] + }, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/materials/d1-01-welcome/index.qmd b/materials/d1-01-welcome/index.qmd index 7e4355d..f8fef0a 100644 --- a/materials/d1-01-welcome/index.qmd +++ b/materials/d1-01-welcome/index.qmd @@ -1,5 +1,9 @@ --- title: "Welcome!" +title-slide-attributes: + data-background-image: assets/img/bg/rstudioconf_crowd.jpg + data-background-size: cover + data-background-opacity: "0.3" subtitle: "Shiny in Production: Tools & Techniques
posit::conf(2023)" footer: "[{{< var workshop_short_url >}}]({{< var workshop_full_url >}})" format: @@ -15,8 +19,6 @@ format: history: false --- -## Welcome to posit::conf(2023)! {background-color="black" background-image="assets/img/bg/rstudioconf_crowd.jpg" background-size="cover"} - ## Conference Logistics {visibility="hidden"} ::: {.nonincremental} @@ -38,36 +40,31 @@ format: ::: -## Meet the Team! +## Meet the Team! {.smaller} :::: {.columns} ::: {.column width="50%"} ![](assets/img/people/enantz.jpg){fig-alt="Eric Nantz" fig-align="center" width="70%"} + +* `r fontawesome::fa("microphone")` [r-podcast.org](https://r-podcast.org) +* `r fontawesome::fa("camera")` [shinydevseries.com](https://shinydevseries.com) +* `r fontawesome::fa("microphone")` [rweekly.org/podcast](https://rweekly.org/podcast) + ::: ::: {.column width="50%"} ![](assets/img/people/mthomas.jpeg){fig-alt="Michael Thomas" fig-align="center"} + +* `r fontawesome::fa("link")` [www.ketchbrookanalytics.com/](https://www.ketchbrookanalytics.com) +* `r fontawesome::fa("microphone")` [rweekly.org/podcast](https://rweekly.org/podcast) ::: :::: ::: footer -TODO: Link to Meet the Team Section on Home Page +[posit-conf-2023.github.io/shiny-r-prod/#workshop-instructors](https://posit-conf-2023.github.io/shiny-r-prod/#workshop-instructors) ::: - -## Your Turn - -Introduce yourself to your neighbor(s) - -What is your most memorable Shiny application in production experience? - -```{r} -#| label: timer-prodquality -library(countdown) -countdown(minutes = 2, seconds = 0) -``` - --- :::: {.columns .v-center-container} @@ -138,16 +135,34 @@ countdown(minutes = 2, seconds = 0) ::: -# Power-On (Setup) +## Your Turn -Follow [Setup Procedure](../setup.qmd) to connect with the workshop resources: +Complete any remaining [Setup Procedures](../setup.qmd) -* Posit Connect * Posit Cloud +* Posit Connect * Account Integrations +Introduce yourself to your neighbor(s) + +What is your most memorable Shiny application in production experience? + +```{r} +#| label: timer-prodquality +library(countdown) +countdown(minutes = 5, seconds = 0) +``` + +::: footer +[posit-conf-2023.github.io/shiny-r-prod/setup.html](https://posit-conf-2023.github.io/shiny-r-prod/setup.html) +::: + # The Beginning ... {background-color="black" background-image="assets/img/bg/beginning.png" background-size="cover"} +::: footer + +::: + ::: {.notes} * You've seen Shiny could be a game-changer for a project * Build an application quickly, purely with R code @@ -156,6 +171,10 @@ Follow [Setup Procedure](../setup.qmd) to connect with the workshop resources: # A New World {background-image="assets/img/bg/metroid_ship.jpg" background-size="cover" background-color="black"} +::: footer + +::: + ::: {.notes} * Everyone who sees it thinks the app is amazing * Suddenly you are asked (or told) this needs to get into PRODUCTION @@ -165,11 +184,22 @@ Follow [Setup Procedure](../setup.qmd) to connect with the workshop resources: ## Many Users {background-image="assets/img/bg/many_users.jpg" background-size="contain" background-color="black"} +::: footer + +::: + ## High-Profile Situations {background-image="assets/img/bg/nasa_mission_control.jpg" background-size="contain" background-color="black"} +::: footer + +::: ## Part of a Critical Pipeline {background-image="assets/img/bg/pinky_brain_take_over_world.jpg" background-size="contain" background-color="black"} +::: footer + +::: + ## The Journey Ahead
@@ -190,6 +220,10 @@ The tooling & principles discussed in this workshop will guide you to the destin * A comprehensive database of LEGO items available for free! * We will build a Shiny dashboard to explore the inventory and enable users to generate predictions +::: footer +[rebrickable.com](https://rebrickable.com) +::: + ## {background-image="assets/img/bg/rebrickable_schema.png" background-size="cover"} ::: footer @@ -198,6 +232,10 @@ The tooling & principles discussed in this workshop will guide you to the destin ## Applied Example {background-image="https://cdn.rebrickable.com/media/sets/42138-1.jpg" background-size="contain"} +::: footer +[Ford Mustang Shelby GT500](https://rebrickable.com/sets/42138-1/ford-mustang-shelby-gt500/?inventory=1#parts) +::: + ## Raw Data ```{r} @@ -207,9 +245,9 @@ library(reactable) library(reactablefmtr) options(reactable.theme = reactableTheme( - tableStyle = list(fontSize = 14), - headerStyle = list(fontSize = 16), - paginationStyle = list(fontSize = 12) + tableStyle = list(fontSize = 18), + headerStyle = list(fontSize = 20), + paginationStyle = list(fontSize = 16) )) df_sets <- readRDS("../../data_examples/df_sets.rds") diff --git a/materials/d1-02-structure/index.qmd b/materials/d1-02-structure/index.qmd index ec54620..983052d 100644 --- a/materials/d1-02-structure/index.qmd +++ b/materials/d1-02-structure/index.qmd @@ -15,6 +15,9 @@ format: slide-tone: false #true code-line-numbers: false history: false +from: markdown+emoji +revealjs-plugins: + - codewindow --- ## It's Never Just Shiny @@ -25,81 +28,6 @@ format: * Connections to other execution backends * Additional R packages! -## Turned Upside-Down - -Imagine your application is working great! - -
- -. . . - -:::: {.columns .v-center-container} - -::: {.column width="50%"} - -```r -update.packages(ask = FALSE) -remotes::install_github("pkg") -``` - -::: - -::: {.column width="50%} - -![](https://rfortherestofus.com/wp-content/uploads/2020/09/update-packages-prompt.png) - -::: - -:::: - -## Turned Upside-Down - -:::: {.columns .v-center-container} - -::: {.column width="50%"} - -`ggplot2` version `0.9.3` - -![](assets/img/computer_user_happy.png){width="60%"} - -::: - -::: {.column width="50%"} - -`ggplot2` version `1.0.0` - -![](http://i.giphy.com/RhEvCHIeZAZ6E.gif) - -::: - -:::: - -## Take Control with [`{renv}`](https://rstudio.github.io/renv/) - -> Create **r**eproducible **env**ironments 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 -1. Lockfile `renv.lock` to describe state of project library -1. `renv/library` to hold private project library -1. `renv/activate.R` performs activation - -## 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! # Application Structure Options @@ -225,15 +153,13 @@ Use the helper functions in `dev/03_deploy.R` to... # Module Code Example -::: {.notes} -Change the example code below to match LEGO data. Delete this note when finished. -::: - ## Anatomy of a Function (UI) {auto-animate=true} :::: {.columns} ::: {.column width="60%"} +::: {.codewindow .r} +picker.R ```r set_picker_ui <- function() { tagList( @@ -248,6 +174,7 @@ set_picker_ui <- function() { } ``` ::: +::: ::: {.column width="40%"} @@ -261,6 +188,8 @@ set_picker_ui <- function() { ::: {.column width="60%"} +::: {.codewindow .r} +mod_picker.R ```r set_picker_ui <- function(id) { ns <- NS(id) @@ -275,6 +204,7 @@ set_picker_ui <- function(id) { } ``` ::: +::: ::: {.column width="40%"} @@ -288,7 +218,8 @@ set_picker_ui <- function(id) { ::: {.column width="60%"} - +::: {.codewindow .r} +mod_picker.R ```{.r code-line-numbers="1,2,5"} set_picker_ui <- function(id) { ns <- NS(id) @@ -302,7 +233,7 @@ set_picker_ui <- function(id) { ) } ``` - +::: ::: ::: {.column width="40%"} @@ -316,10 +247,8 @@ set_picker_ui <- function(id) { ## Anatomy of a Module (Server) {auto-animate=true} -:::: {.columns} - -::: {.column width="75%"} - +::: {.codewindow .r} +mod_picker.R ```r set_picker_server <- function(input, output, session, sets_rv) { set_choices <- reactive({ @@ -335,21 +264,12 @@ set_picker_server <- function(input, output, session, sets_rv) { }) } ``` - -::: - -::: {.column width="25%"} - ::: -:::: - ## Anatomy of a Module (Server) {auto-animate=true} -:::: {.columns} - -::: {.column width="75%"} - +::: {.codewindow .r} +mod_picker.R ```r set_picker_server <- function(id, sets_rv) { moduleServer( @@ -370,17 +290,11 @@ set_picker_server <- function(id, sets_rv) { ) } ``` - ::: -::: {.column width="25%"} Minimal changes necessary -::: - -:::: - ## Anatomy of a Module (Server) {auto-animate=true} :::: {.columns} @@ -418,10 +332,14 @@ set_picker_server <- function(id, sets_rv) { :::: -* ``moduleServer()`: Encapsulate server-side logic with namespace applied. +::: {style="font-size: 70%"} +`moduleServer()`: Encapsulate server-side logic with namespace applied. +::: ## Invoking Modules +::: {.codewindow .r} +app.R ```{.r} library(shiny) library(bslib) @@ -439,13 +357,12 @@ server <- function(input, output, session) { shinyApp(ui, server) ``` +::: ## Giving and Receiving -:::: {.columns} - -::: {.column width="60%"} - +::: {.codewindow .r} +mod_picker.R ```r set_picker_ui <- function(id, label = "Select a set") { ns <- NS(id) @@ -461,18 +378,16 @@ set_picker_ui <- function(id, label = "Select a set") { ``` ::: -::: {.column width="40%"} - +::: {style="font-size: 80%"} * Reasonable inputs: static values, vectors, flags * Avoid **reactive** parameters * Return value: `tagList()` of inputs, output placeholders, and other UI elements - ::: -:::: - ## Giving and Receiving +::: {.codewindow .r} +mod_picker.R ```{.r} set_picker_server <- function(id, sets_rv) { moduleServer( @@ -493,15 +408,21 @@ set_picker_server <- function(id, sets_rv) { ) } ``` +::: + +::: {style="font-size: 80%"} +Input & return values can be a mix of static and **reactive** objects +::: -* Input parameters (and return values) can be a mix of static and **reactive** objects -## To () or not to () {.smaller} +## To () or not to () :::: {.columns} -::: {.column width="40%"} +::: {.column width="45%"} +::: {.codewindow .r} +app_server.R ```{.r} # app server sets_rv <- reactive({ @@ -510,11 +431,14 @@ sets_rv <- reactive({ set_picker_server("mod1", sets_rv) ``` +::: ::: -::: {.column width="60%"} +::: {.column width="55%"} +::: {.codewindow .r} +mod_picker.R ```{.r} set_picker_server <- function(id, sets_rv) { moduleServer( @@ -531,19 +455,115 @@ set_picker_server <- function(id, sets_rv) { ) } ``` - +::: ::: :::: +::: {style="font-size: 70%;"} * 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()`~~ +::: + +::: footer +::: # Code-Along {background-color="#17395c"} -Code-Along 1: Add a new Shiny module to pick themes +Add a new Shiny module to pick LEGO set themes + +* [Details](codealong-1.html){target="_blank"} +* Posit Cloud project: **Application Structure Code-along 1** ## Your Turn: [Exercise 1](ex-1.html){target="_blank"} Create a new Shiny module with LEGO data metrics! + +* [Details](ex-1.html){target="_blank"} +* Posit Cloud project: **Application Structure Exercise 1** + +```{r} +#| label: timer-modex1 +library(countdown) +countdown(minutes = 10, seconds = 0) +``` + + +# Dependency Management + +## Turned Upside-Down + +Imagine your application is working great! + +
+ +. . . + +:::: {.columns .v-center-container} + +::: {.column width="50%"} + +```r +update.packages(ask = FALSE) +remotes::install_github("pkg") +``` + +::: + +::: {.column width="50%} + +![](https://rfortherestofus.com/wp-content/uploads/2020/09/update-packages-prompt.png) + +::: + +:::: + +## Turned Upside-Down + +:::: {.columns .v-center-container} + +::: {.column width="50%"} + +`ggplot2` version `0.9.3` + +![](assets/img/computer_user_happy.png){width="60%"} + +::: + +::: {.column width="50%"} + +`ggplot2` version `1.0.0` + +![](http://i.giphy.com/RhEvCHIeZAZ6E.gif) + +::: + +:::: + +## Take Control with [`{renv}`](https://rstudio.github.io/renv/) + +> Create **r**eproducible **env**ironments 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 +1. Lockfile `renv.lock` to describe state of project library +1. `renv/library` to hold private project library +1. `renv/activate.R` performs activation + +## 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! diff --git a/materials/d1-03-performance/codealong-1.qmd b/materials/d1-03-performance/codealong-1.qmd index f91c260..74d2d84 100644 --- a/materials/d1-03-performance/codealong-1.qmd +++ b/materials/d1-03-performance/codealong-1.qmd @@ -1,5 +1,5 @@ --- -title: Asynchronous Processing of LEGO Model Prediction +title: Using `.parquet` files in Shiny format: html: code-line-numbers: false @@ -10,98 +10,33 @@ format: ## 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}`](https://httr2.r-lib.org/) package. Here is the function wrapping the API execution: - -```{r} -#' @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 -1. Create a new controller to launch new R processes when new prediction tasks are launched -1. 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. -1. 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. - -## Solution - -First we create the following `reactiveVal` objects to keep track of the prediction state: - -```{r} -pred_status <- reactiveVal("No prediction submitted yet.") -pred_poll <- reactiveVal(FALSE) -``` - -Next we set up a new controller: - -```{r} -# 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()) -``` +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. -Inside the `observeEvent` for the user clicking the prediction button, we update the logic to push the prediction task to the controller: +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}`](https://arrow.apache.org/docs/r/) package. ```{r} -controller$push( - command = run_prediction(df), - data = list( - run_prediction = run_prediction, - df = pred_data_rv$data - ), - packages = c("httr2", "dplyr") +#| echo: true +#| eval: false +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") ) - -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: +With the processed data set available in the app infrastructure, we can utilize it inside the application with the following: ```{r} -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}`! \ No newline at end of file diff --git a/materials/d1-03-performance/codealong-2.qmd b/materials/d1-03-performance/codealong-2.qmd new file mode 100644 index 0000000..f91c260 --- /dev/null +++ b/materials/d1-03-performance/codealong-2.qmd @@ -0,0 +1,107 @@ +--- +title: Asynchronous Processing of LEGO Model Prediction +format: + html: + code-line-numbers: false + execute: + echo: true + eval: false +--- + +## 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}`](https://httr2.r-lib.org/) package. Here is the function wrapping the API execution: + +```{r} +#' @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 +1. Create a new controller to launch new R processes when new prediction tasks are launched +1. 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. +1. 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. + +## Solution + +First we create the following `reactiveVal` objects to keep track of the prediction state: + +```{r} +pred_status <- reactiveVal("No prediction submitted yet.") +pred_poll <- reactiveVal(FALSE) +``` + +Next we set up a new controller: + +```{r} +# 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: + +```{r} +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: + +```{r} +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") + } +}) +``` + diff --git a/materials/d1-03-performance/ex-1.qmd b/materials/d1-03-performance/ex-1.qmd new file mode 100644 index 0000000..9189d92 --- /dev/null +++ b/materials/d1-03-performance/ex-1.qmd @@ -0,0 +1,26 @@ +--- +title: "Profile the LEGO Bricks App" +--- + +## Access Instructions + +The project used for this particular exercise is hosted on [Posit Cloud](https://posit.cloud) in this [space](https://posit.cloud/spaces/400774/join?access_code=DDgV_peF5WCCCpB5JHjQtMN2aHByWoNF0k5p8Wp7). The project for this exercise is called **performance-exercise1**. + +## Setup + +Using what you just learned about the [`{profvis}`](http://rstudio.github.io/profvis/examples.html#example-3---profiling-a-shiny-application), 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: + +```{r} +#| echo: true +#| eval: false +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.qmd b/materials/d1-03-performance/index.qmd index 586ab5b..19f06fb 100644 --- a/materials/d1-03-performance/index.qmd +++ b/materials/d1-03-performance/index.qmd @@ -82,6 +82,20 @@ You need to wrap the `run_app()` function in `print()`, before passing it to `pr - Navigate to the 'data' tab and discuss what took the most time ::: +## Your Turn: [Exercise 1](ex-1.html){target="_blank"} + +Profile the LEGO Bricks app! + +* [Details](ex-1.html){target="_blank"} +* Posit Cloud project: **Performance Exercise 1** + +```{r} +#| label: timer-profile1 +library(countdown) +countdown(minutes = 5, seconds = 0) +``` + + # {arrow} & the *.parquet* file format ## What are *.parquet* files? @@ -116,6 +130,13 @@ You need to wrap the `run_app()` function in `print()`, before passing it to `pr - When it comes to building Shiny apps, we should look for easy places where we can gain efficiency & speed to improve our user experience (you don't want users waiting 20 seconds for your data prep logic to run against a single massive csv); it's very likely that the combination of .parquet + {arrow} + {dplyr} can meet your app performance needs (it does for at least 95% of my use cases -- there are very few cases where I have to go beyond that and start looking into other engines for faster data manipulation) ::: +# Code-Along {background-color="#17395c"} + +Using `.parquet` in the LEGO Bricks Shiny app + +* [Details](codealong-1.html){target="_blank"} +* Posit Cloud project: **Performance Exercise 1** + # Async Processing ## Single (threaded) Line {background-image="assets/img/lego_line_pay.jpg" background-size="cover"} @@ -123,15 +144,9 @@ You need to wrap the `run_app()` function in `print()`, before passing it to `pr * A single R process managing the different tasks in a Shiny application * Executed one-by-one -# Should I care? - -## It Depends ... - -If you are the __only__ user for a quick and efficient app: Likely not +# Should I care? It Depends ... -::: {.notes} -TODO: Find a way to center the sentence vertically in the slide -::: +# If you are the __only__ user for a quick and efficient app: Likely not! ## Crowd Pleaser @@ -194,4 +209,8 @@ Using `{crew}` inside a Shiny application: # Code-Along {background-color="#17395c"} -[Code-Along 1](codealong-1.html){target="_blank"}: Asynchronous calls of a web API \ No newline at end of file +Asynchronous calls of a model prediction API. + +* [Details](codealong-2.html){target="_blank"} +* Posit Cloud project: **Performance Code-along 2** + diff --git a/materials/d1-9001-loadtesting/assets/img/confused_minifig.jpg b/materials/d1-04-loadtesting/assets/img/confused_minifig.jpg similarity index 100% rename from materials/d1-9001-loadtesting/assets/img/confused_minifig.jpg rename to materials/d1-04-loadtesting/assets/img/confused_minifig.jpg diff --git a/materials/d1-9001-loadtesting/assets/img/lego_crowd.jpg b/materials/d1-04-loadtesting/assets/img/lego_crowd.jpg similarity index 100% rename from materials/d1-9001-loadtesting/assets/img/lego_crowd.jpg rename to materials/d1-04-loadtesting/assets/img/lego_crowd.jpg diff --git a/materials/d1-9001-loadtesting/assets/img/lego_single_user_computer.png b/materials/d1-04-loadtesting/assets/img/lego_single_user_computer.png similarity index 100% rename from materials/d1-9001-loadtesting/assets/img/lego_single_user_computer.png rename to materials/d1-04-loadtesting/assets/img/lego_single_user_computer.png diff --git a/materials/d1-9001-loadtesting/assets/img/loop.svg b/materials/d1-04-loadtesting/assets/img/loop.svg similarity index 100% rename from materials/d1-9001-loadtesting/assets/img/loop.svg rename to materials/d1-04-loadtesting/assets/img/loop.svg diff --git a/materials/d1-04-loadtesting/assets/img/report_screenshot.png b/materials/d1-04-loadtesting/assets/img/report_screenshot.png new file mode 100644 index 0000000..669a810 Binary files /dev/null and b/materials/d1-04-loadtesting/assets/img/report_screenshot.png differ diff --git a/materials/d1-9001-loadtesting/index.qmd b/materials/d1-04-loadtesting/index.qmd similarity index 89% rename from materials/d1-9001-loadtesting/index.qmd rename to materials/d1-04-loadtesting/index.qmd index d52e969..4663464 100644 --- a/materials/d1-9001-loadtesting/index.qmd +++ b/materials/d1-04-loadtesting/index.qmd @@ -1,7 +1,6 @@ --- title: "Load Testing" subtitle: "posit::conf(2023)
Shiny in Production: Tools & Techniques" -author: "TBD" footer: "[{{< var workshop_short_url >}}]({{< var workshop_full_url >}})" format: revealjs: @@ -21,11 +20,11 @@ revealjs-plugins: :::: {.columns} ::: {.column width="50%"} -![single-user](assets/img/lego_single_user_computer.png) +![](assets/img/lego_single_user_computer.png) ::: ::: {.column width="50%"} -![crowd](assets/img/lego_crowd.jpg) +![](assets/img/lego_crowd.jpg) ::: :::: @@ -57,6 +56,8 @@ revealjs-plugins: ## Recording a Session +::: {.codewindow .r} +shinyloadtest_recording.R ```{r} #| eval: false #| echo: true @@ -64,15 +65,16 @@ revealjs-plugins: library(shinyloadtest) record_session("https://my-lego-app.me", output_file = "recording.log") ``` - -## Recorder Tips +::: * Emulate a real-world usage of your application * Avoid rapid clicks / selections of your inputs ## The log -``` +::: {.codewindow} +recording.log +```.bash # version: 1 # target_url: https://my-lego-app.me/ # target_type: RStudio Server Connect @@ -86,6 +88,7 @@ record_session("https://my-lego-app.me", output_file = "recording.log") {"type":"WS_SEND","begin":"2023-09-07T13:04:17.805Z","message":"[\"D#0|m|{\\\"method\\\":\\\"update\\\",\\\"data\\\":{\\\"n_parts_display\\\":\\\"20\\\"}}\"]"} {"type":"WS_CLOSE","begin":"2023-09-07T13:04:35.561Z"} ``` +::: . . . @@ -120,7 +123,7 @@ source(file.path(here::here(), "R", "shinycannon.R")) shinycannon( "shinycannon-1.1.3-dd43f6b.jar", "recording.log", - "https://my-lego-app.me", + "https://rsc.training.posit.co/brickapp-sync/", loaded_duration_minutes = 2, workers = 1, output_dir = "run1" @@ -170,10 +173,10 @@ shinyloadtest_report(df, output = "report_test.html") ``` ::: -::: {.callout-warning title="TODO"} -Add screenshot of HTML report with hyperlink -::: +![](assets/img/report_screenshot.png) + +# Code-Along {background-color="#17395c"} -# `r fontawesome::fa("people-carry", "white")` Code-Along {background-color="#17395c"} +Record and analyze load-testing sessions with `shinyCannon` and `{shinyloadtest}` -Code-Along 1: Record and analyze a baseline application session \ No newline at end of file +* Application URL: \ No newline at end of file diff --git a/materials/d1-04-deploy-admin/assets/img/.gitkeep b/materials/d1-05-deploy-admin/assets/img/.gitkeep similarity index 100% rename from materials/d1-04-deploy-admin/assets/img/.gitkeep rename to materials/d1-05-deploy-admin/assets/img/.gitkeep diff --git a/materials/d1-05-deploy-admin/assets/img/Rlogo.png b/materials/d1-05-deploy-admin/assets/img/Rlogo.png new file mode 100644 index 0000000..be48e30 Binary files /dev/null and b/materials/d1-05-deploy-admin/assets/img/Rlogo.png differ diff --git a/materials/d1-04-deploy-admin/assets/img/daniel-bryan-no.gif b/materials/d1-05-deploy-admin/assets/img/daniel-bryan-no.gif similarity index 100% rename from materials/d1-04-deploy-admin/assets/img/daniel-bryan-no.gif rename to materials/d1-05-deploy-admin/assets/img/daniel-bryan-no.gif diff --git a/materials/d1-04-deploy-admin/assets/img/lego_boss_office.jpg b/materials/d1-05-deploy-admin/assets/img/lego_boss_office.jpg similarity index 100% rename from materials/d1-04-deploy-admin/assets/img/lego_boss_office.jpg rename to materials/d1-05-deploy-admin/assets/img/lego_boss_office.jpg diff --git a/materials/d1-05-deploy-admin/assets/img/lego_victory.jpg b/materials/d1-05-deploy-admin/assets/img/lego_victory.jpg new file mode 100644 index 0000000..1f49d99 Binary files /dev/null and b/materials/d1-05-deploy-admin/assets/img/lego_victory.jpg differ diff --git a/materials/d1-05-deploy-admin/assets/img/lego_winning.webp b/materials/d1-05-deploy-admin/assets/img/lego_winning.webp new file mode 100644 index 0000000..50134e1 Binary files /dev/null and b/materials/d1-05-deploy-admin/assets/img/lego_winning.webp differ diff --git a/materials/d1-05-deploy-admin/assets/img/linux.png b/materials/d1-05-deploy-admin/assets/img/linux.png new file mode 100644 index 0000000..6ab6ec4 Binary files /dev/null and b/materials/d1-05-deploy-admin/assets/img/linux.png differ diff --git a/materials/d1-04-deploy-admin/assets/img/optimized.gif b/materials/d1-05-deploy-admin/assets/img/optimized.gif similarity index 100% rename from materials/d1-04-deploy-admin/assets/img/optimized.gif rename to materials/d1-05-deploy-admin/assets/img/optimized.gif diff --git a/materials/d1-05-deploy-admin/assets/img/shiny.png b/materials/d1-05-deploy-admin/assets/img/shiny.png new file mode 100644 index 0000000..281845f Binary files /dev/null and b/materials/d1-05-deploy-admin/assets/img/shiny.png differ diff --git a/materials/d1-04-deploy-admin/assets/img/teeter_totter.png b/materials/d1-05-deploy-admin/assets/img/teeter_totter.png similarity index 100% rename from materials/d1-04-deploy-admin/assets/img/teeter_totter.png rename to materials/d1-05-deploy-admin/assets/img/teeter_totter.png diff --git a/materials/d1-04-deploy-admin/index.qmd b/materials/d1-05-deploy-admin/index.qmd similarity index 68% rename from materials/d1-04-deploy-admin/index.qmd rename to materials/d1-05-deploy-admin/index.qmd index f9c20d9..73713ca 100644 --- a/materials/d1-04-deploy-admin/index.qmd +++ b/materials/d1-05-deploy-admin/index.qmd @@ -15,6 +15,8 @@ revealjs-plugins: - codewindow --- +## Choose your Adventure + ```{css} .centerheading h2 { text-align: center; @@ -35,21 +37,23 @@ revealjs-plugins: } ``` -## Choose your Adventure - -::: columns +:::: columns ::: {.column width="50%"} ### Posit Connect + +![](assets/img/shiny.png){width="80%"} + ::: ::: {.column width="50%"} ### Containers -::: -::: -::: notes -TODO: Find pictures demonstrating the "App & Packages Only" (Posit Connect, Shinyapps.io) versus "The whole environment" (Containers) +![](assets/img/Rlogo.png){width="50%"} + +![](assets/img/linux.png){width="50%"} + ::: +:::: ## Deployment Checklist @@ -81,10 +85,6 @@ golem::add_shinyserver_file() ::: ::: -# `r fontawesome::fa("people-carry", "white")` Code-Along {background-color="#17395c"} - -Push-Button Deployment to Posit Connect - --- ::: mono @@ -112,6 +112,14 @@ rsconnect::deployApp( ::: ::: +# Code-Along {background-color="#17395c"} + +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 {.centerheading} :::: {.columns} @@ -142,6 +150,19 @@ rsconnect::deployApp( :::: +## 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](https://hosting.analythium.io) + +. . . + +**Coming soon**: Container support for Posit Connect + ## Business Talk {background-image="assets/img/lego_boss_office.jpg" background-size="cover" background-color="black"} ## You Might be Asked ... @@ -266,4 +287,54 @@ get_usage_shiny( pull(user_guid) |> unique() |> length() -``` \ No newline at end of file +``` + +## Your Turn + +Obtain metadata for your deployed application(s) on Posit Connect + +* Explore the data as you like! + +```{r} +#| label: timer-connect-metrics +library(countdown) +countdown(minutes = 5, seconds = 0) +``` + + +## Going Further with Metrics + +Collection of interesting use cases available at [github.com/sol-eng/connect-usage](https://github.com/sol-eng/connect-usage) + +* Most-viewed content in last 30 days +* [Interactive dashboard](https://colorado.posit.co/rsc/usage-interactive/) of usage metrics +* [`{connectViz}`](https://github.com/RinteRface/connectViz): Collection of customizable functions for visualizing content usage for an organization + +## Our Journey is Complete! + +:::: {.columns} + +::: {.column width="70%"} + +![](assets/img/lego_victory.jpg) + +::: + +::: {.column width="30%"} + +::: {style="font-size: 70%"} +* 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 +::: + +::: + +:::: + +::: footer +[pos.it/conf-workshop-survey](https://pos.it/conf-workshop-survey) +::: \ No newline at end of file diff --git a/schedule.qmd b/schedule.qmd index ca793e5..f0a43ee 100644 --- a/schedule.qmd +++ b/schedule.qmd @@ -6,11 +6,10 @@ listing: contents: - units/d1-*.qmd type: table - fields: [subtitle, title, author] + fields: [subtitle, title] field-display-names: subtitle: "Time" - title: "Module" - author: "Presenter" + title: "Workshop Module" date-format: "MMMM DD YYYY" sort: [filename] sort-ui: false @@ -19,7 +18,3 @@ tbl-colwidths: [20, 50, 25] format: html --- -::: callout-warning -This page is under construction and will be updated as the materials are posted online. Ordering of topics is subject to change. -::: - diff --git a/units/d1-01-welcome.qmd b/units/d1-01-welcome.qmd index 618455b..295c32e 100644 --- a/units/d1-01-welcome.qmd +++ b/units/d1-01-welcome.qmd @@ -3,27 +3,10 @@ title: "Welcome & Setup" subtitle: "9:00 - 9:30" author: "Eric Nantz & Michael Thomas" date: "2023-09-18" -listing: - - id: exercises - contents: - - ../materials/d1-01-welcome/ex-*.qmd - type: table - fields: [subtitle, title] - field-display-names: - subtitle: "Exercise" - sort: [filename] - sort-ui: false - filter-ui: false - image-placeholder: assets/img/placeholder.png -tbl-colwidths: [5,20,75] --- ## Slides -::: callout-warning -These slides are under construction and will be finalized prior to the workshop date. -::: - [View slides in full screen](../materials/d1-01-welcome/index.html) ```{=html} diff --git a/units/d1-02-structure.qmd b/units/d1-02-structure.qmd index 9c7fb7b..7ec505d 100644 --- a/units/d1-02-structure.qmd +++ b/units/d1-02-structure.qmd @@ -8,9 +8,9 @@ listing: contents: - ../materials/d1-02-structure/ex-*.qmd type: table - fields: [subtitle, title] + fields: [title] field-display-names: - subtitle: "Exercise" + title: "Code-along Title" sort: [filename] sort-ui: false filter-ui: false @@ -19,9 +19,9 @@ listing: contents: - ../materials/d1-02-structure/codealong-*.qmd type: table - fields: [subtitle, title] + fields: [title] field-display-names: - subtitle: "Code-Along" + title: "Exercise Title" sort: [filename] sort-ui: false filter-ui: false @@ -31,10 +31,6 @@ tbl-colwidths: [5,20,75] ## Slides -::: callout-warning -These slides are under construction and will be finalized prior to the workshop date. -::: - [View slides in full screen](../materials/d1-02-structure/index.html) ```{=html} diff --git a/units/d1-03-performance.qmd b/units/d1-03-performance.qmd index 879899b..d5f6f9a 100644 --- a/units/d1-03-performance.qmd +++ b/units/d1-03-performance.qmd @@ -1,6 +1,6 @@ --- title: "Performance" -subtitle: "TBD" +subtitle: "11:00 - 12:30" author: "Eric Nantz & Michael Thomas" date: "2023-09-18" listing: @@ -8,9 +8,9 @@ listing: contents: - ../materials/d1-03-performance/ex-*.qmd type: table - fields: [subtitle, title] + fields: [title] field-display-names: - subtitle: "Exercise" + title: "Exercise Title" sort: [filename] sort-ui: false filter-ui: false @@ -19,9 +19,9 @@ listing: contents: - ../materials/d1-03-performance/codealong-*.qmd type: table - fields: [subtitle, title] + fields: [title] field-display-names: - subtitle: "Code-Along" + title: "Code-along Title" sort: [filename] sort-ui: false filter-ui: false diff --git a/units/d1-04-deploy-admin.qmd b/units/d1-04-deploy-admin.qmd deleted file mode 100644 index ec54e10..0000000 --- a/units/d1-04-deploy-admin.qmd +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: "Deployment & Administration" -subtitle: "TBD" -author: "Eric Nantz & Michael Thomas" -date: "2023-09-18" -listing: - - id: exercises - contents: - - ../materials/d1-04-deploy-admin/ex-*.qmd - type: table - fields: [subtitle, title] - field-display-names: - subtitle: "Exercise" - sort: [filename] - sort-ui: false - filter-ui: false - image-placeholder: assets/img/placeholder.png -tbl-colwidths: [5,20,75] ---- - -## Slides - -::: callout-warning -These slides are under construction and will be finalized prior to the workshop date. -::: - - -[View slides in full screen](../materials/d1-04-deploy-admin/index.html) - -```{=html} - -``` - diff --git a/units/d1-04-loadtesting.qmd b/units/d1-04-loadtesting.qmd new file mode 100644 index 0000000..26649eb --- /dev/null +++ b/units/d1-04-loadtesting.qmd @@ -0,0 +1,16 @@ +--- +title: "Load Testing" +subtitle: "13:30 - 15:00" +author: "Eric Nantz & Michael Thomas" +date: "2023-09-18" +tbl-colwidths: [5,20,75] +--- + +## Slides + +[View slides in full screen](../materials/d1-04-loadtesting/index.html) + +```{=html} + +``` + diff --git a/units/d1-05-deploy-admin.qmd b/units/d1-05-deploy-admin.qmd new file mode 100644 index 0000000..8fe9660 --- /dev/null +++ b/units/d1-05-deploy-admin.qmd @@ -0,0 +1,16 @@ +--- +title: "Deployment & Administration" +subtitle: "15:30 - 17:00" +author: "Eric Nantz & Michael Thomas" +date: "2023-09-18" +tbl-colwidths: [5,20,75] +--- + +## Slides + +[View slides in full screen](../materials/d1-05-deploy-admin/index.html) + +```{=html} + +``` + diff --git a/units/d1-9001-loadtesting.qmd b/units/d1-9001-loadtesting.qmd deleted file mode 100644 index 17a0c2d..0000000 --- a/units/d1-9001-loadtesting.qmd +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "Load Testing" -subtitle: "TBD" -author: "Eric Nantz & Michael Thomas" -date: "2023-09-18" -listing: - - id: exercises - contents: - - ../materials/d1-9001-loadtesting/ex-*.qmd - type: table - fields: [subtitle, title] - field-display-names: - subtitle: "Exercise" - sort: [filename] - sort-ui: false - filter-ui: false - image-placeholder: assets/img/placeholder.png -tbl-colwidths: [5,20,75] ---- - -## Slides - -::: callout-warning -These slides are under construction and will be finalized prior to the workshop date. -::: - -[View slides in full screen](../materials/d1-9001-loadtesting/index.html) - -```{=html} - -``` -