Load Testing

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

One to Many

Optimization Loop Method

Double the (Load-Testing) Fun

{shinyloadtest}

  • Record the events of a Shiny application session
  • Process and analyze metrics associated with application runs

shinycannon

  • Command-line utility to launch multiple application sessions and collect event-based metrics
  • Cross-platform (built with Kotlin)

Hosting Requirements

  • Application deployed to a server supporting SockJS (Posit Connect, Shiny Server)
  • If authentication required for app on Posit Connect, you’ll need an API key

Recording a Session

shinyloadtest_recording.R

library(shinyloadtest)
record_session("https://my-lego-app.me", output_file = "recording.log")
  • Emulate a real-world usage of your application
  • Avoid rapid clicks / selections of your inputs

The log

recording.log

# version: 1
# target_url: https://my-lego-app.me/
# target_type: RStudio Server Connect
# rscApiKeyRequired: false
{"type":"REQ_HOME","begin":"2023-09-07T13:03:39.042Z","end":"2023-09-07T13:03:40.138Z","status":200,"url":"/"}
{"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"}
{"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"}
{"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"}
...
...
{"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"}

Loading the (shiny) cannon

bash

java -jar shinycannon-1.1.3-dd43f6b.jar \
  recording.log \
  https://my-lego-app.me \
  --loaded-duration-minutes 2 \
  --workers 1 \
  --output-dir run1

run_shinycannon.R

source(file.path(here::here(), "R", "shinycannon.R"))

shinycannon(
  "shinycannon-1.1.3-dd43f6b.jar",
  "recording.log",
  "https://rsc.training.posit.co/brickapp-sync/",
  loaded_duration_minutes = 2,
  workers = 1,
  output_dir = "run1"
)

Metrics

analyze_recording.R

library(shinyloadtest)
df <- load_runs("Run 1" = "run1")
# A tibble: 80 × 13
   run   user_id session_id iteration input_line_number event start   end   time
   <ord>   <int>      <int>     <int>             <int> <chr> <dbl> <dbl>  <dbl>
 1 Run 1       0          0         0                 4 REQ_…   0    24.2 24.2  
 2 Run 1       0          0         0                 5 REQ_…  24.2  24.5  0.259
 3 Run 1       0          0         0                 6 REQ_…  24.5  24.6  0.155
 4 Run 1       0          0         0                 7 REQ_…  24.6  24.9  0.284
 5 Run 1       0          0         0                 8 REQ_…  24.9  25.3  0.382
 6 Run 1       0          0         0                 9 REQ_…  25.3  25.5  0.170
 7 Run 1       0          0         0                10 REQ_…  25.5  25.7  0.242
 8 Run 1       0          0         0                11 REQ_…  25.7  25.9  0.25 
 9 Run 1       0          0         0                12 REQ_…  25.9  26.3  0.395
10 Run 1       0          0         0                13 REQ_…  26.3  26.5  0.198
# ℹ 70 more rows
# ℹ 4 more variables: concurrency <dbl>, maintenance <lgl>, label <ord>,
#   json <list>

But Wait .. There’s More

gen_report.R

library(shinyloadtest)
df <- load_runs("Run 1" = "run1")
shinyloadtest_report(df, output = "report_test.html")

Code-Along

Record and analyze load-testing sessions with shinyCannon and {shinyloadtest}