knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  out.width = "100%",
  fig.width = 8,
  fig.height = 6
)

# unzip missing data folders
zips <- dir("./test_sessions", pattern = ".zip", full.names = TRUE)
folders_exist <- dir.exists(sub("\\.zip$", "", zips))
if (any(!folders_exist)) {
  lapply(zips[!folders_exist], unzip, exdir = "./test_sessions")
}

ggplot2::theme_set(ggplot2::theme_grey(12))

This vignette shows you how to analyse the results of your load tests, explaining the plots produced in the report, and giving a few tips on interpretation.

library(shinyloadtest)
library(dplyr)

Loading data

Start by loading the shinycannon results into R with load_runs(). If you have a single run, just give it the directory of the results:

df <- load_runs("test_sessions/demo1")

If you have multiple runs, give it multiple directories, optionally providing informative labels. Here I'm loading three runs that replay the same script simulating 1, 4, and 16 simultaneous uses:

df <- load_runs(
  `1 user` = "test_sessions/demo1",
  `4 users` = "test_sessions/demo4",
  `16 users` = "test_sessions/demo16"
)

load_runs() returns a tidy tibble:

df

The first three variables identify each simulated session:

?load_runs provides a full description of all the variables.

Report output

You won't generally work with this data directly --- instead, you'll usually start by creating a standalone HTML report:

shinyloadtest_report(df, "report.html")

This report contains six pages of diagnostic information: sessions, session duration, event waterfall, latency, event duration, and event concurrency. I describe each page below.

Sessions

The Sessions page displays the summarises the simulated sessions performed by each shinycannon worker. Each row represent one worker and each rectangle represents one event. The width of the rectangle gives the duration of the event, and the colour gives the event type. For example, the following plot shows the actions performed by 4 workers over 5 minutes (600s):

df %>%
  filter(run == "4 users") %>%
  slt_user()

If you use more workers, you'll see more rows. The following plot shows the results from 16 simultaneous workers:

df %>%
  filter(run == "16 users") %>%
  slt_user()

The first thing to look for in this plot is the balance between the user waiting for the app (coloured rectangles) and the app waiting for the user (gray background shows through). The more brightly coloured the plot, the more time that the user spends waiting. The precise colour of the block is also informative, as there are five types of action that proceed in roughly the same order in every session:

Also note the dotted vertical lines. These show the three major phases of the load test:

The rest of the report will focus on sessions in the maintenance phase since these represent an app under the full load of simultaneous users. If the maintenance period is a only a small proportion of the full app, you'll need to re-run the simulation with a greater --loaded-duration-minutes value.

The sessions tab gives you an overview of the experimental design (how sessions were assigned to workers), so you'll generally only spend a small amount of time on this page before moving on to the session duration.

Session duration

The Session duration page uses similar graphical conventions to the Session page but instead of one worker per row, we now have one session per row, and the time on the x-axis is relative to session start. A red line shows the running time of the original script.

df %>%
  filter(run == "4 users") %>%
  slt_session_duration(cutoff = 16.72) +
  ggplot2::labs(title = "4 simulatenous users")

This plot shows us that most sessions are completed in about the same amount of time as the original script, which suggests that the app can comfortably handle four simultaneous users. We get a very different picture if we look at the performance of the app with 16 simultaneous users:

df %>%
  filter(run == "16 users") %>%
  slt_session_duration(cutoff = 16.72) +
  ggplot2::labs(title = "16 simultaneous users")

The fastest session now takes \~4 times longer than the original recording, and there's one session that takes twice again as long again. This suggests that we've overloaded the server and we either need to increase server resources or optimise the app.

Event waterfall

The Event Waterfall gives a more precise view into the individual events within a recording. Each line represents a session, with events running from top to bottom, and elapsed time on the x-axis.

df %>%
  filter(run == "4 users") %>%
  slt_waterfall()

In this plot you're primarily looking for uniform shape. In particular, you want the lines to be parallel, because a crossing line indicates that user A started before user B, but finished afterwards (this is like seeing the table that ordered after you get their food first). We can see this problem in different run, where a file was uploaded and many POST requests were made.

upload <- load_runs("test_sessions/upload-dt/", verbose = FALSE)
upload %>% slt_waterfall()

Latency

The Latency page summarises the amount of time spent waiting, broken down in to total HTTP (homepage + js/css) and websockets (representing calculation). The Total HTTP latency plot shows how long a user has to wait before they see something in their browser. Ideally, this will be very short since the server is just providing static files.

df %>%
  filter(run == "4 users") %>%
  slt_http_latency()

The variability is because R is single threaded; if Shiny is performing a computation for another worker, it can't launch the transmission of static files.

The Maximum WebSocket latency shows how much time time a user spends waiting for outputs to be updated. This basically represents how much time R spends computing. To make this faster, you'll need to make your R code faster.

df %>%
  filter(run == "4 users") %>%
  slt_websocket_latency()

Event duration

The Event Duration page helps understand the variability of individual events. Each event is summarised with a boxplot, with run on the x-axis and time on the y-axis:

df %>% filter(input_line_number == 1) %>% slt_time_boxplot()

Since there are typically many events, and most are uninteresting, the report gives you three ways to find interesting events:

These plots can help you find events that are either slow, or have high variability. You'll then need to backtrack to the underlying Shiny code and think about how to improve performance.

Event concurrency

The Event Concurrency page helps you understand how events responded to varying degrees of concurrent load. Each simulated session is represented by a point with number of concurrent processes on the x-axis and time taken on the y-axis. The points are coloured by run. Each run also gets a line of best fit, which gives a sense for how the event respond to increasing concurrency.

df %>% filter(input_line_number == 16) %>% slt_time_concurrency()

Like Event Duration, most events are uninteresting, so the report gives you three ways of sorting to find interesting events:

If you want your app to handle as many user as possible, look for events where the time increases steeply with concurrency - these are places where optimisation will have the greatest impact.



rstudio/shinyloadtest documentation built on Jan. 21, 2023, 11:14 p.m.