diff --git a/.devcontainer/Dockerfile_rstudio b/.devcontainer/Dockerfile_rstudio
index 5e223a2..421d5b2 100644
--- a/.devcontainer/Dockerfile_rstudio
+++ b/.devcontainer/Dockerfile_rstudio
@@ -1,5 +1,10 @@
FROM ghcr.io/rocker-org/devcontainer/tidyverse:4.3
+ENV QUARTO_VERSION=prerelease
+
+# install preview version of quarto
+RUN /rocker_scripts/install_quarto.sh
+
# key dependencies for utilities
RUN apt-get update -qq \
&& export DEBIAN_FRONTEND=noninteractive \
@@ -23,3 +28,4 @@ RUN apt-get update -qq \
# install R package dependencies
RUN install2.r renv httpgd languageserver shiny rmarkdown markdown countdown fontawesome profvis golem rsconnect devtools
+
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 4c0c0f3..2620efc 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -10,7 +10,7 @@
"features": {
"ghcr.io/rocker-org/devcontainer-features/pandoc:1": {},
"ghcr.io/rocker-org/devcontainer-features/quarto-cli:1": {
- "version": "1.3.433"
+ "version": "1.4.352"
},
"ghcr.io/meaningful-ooo/devcontainer-features/fish:1": {},
"ghcr.io/guiyomh/features/vim:0": {}
diff --git a/R/shinycannon.R b/R/shinycannon.R
new file mode 100644
index 0000000..86f4ee9
--- /dev/null
+++ b/R/shinycannon.R
@@ -0,0 +1,54 @@
+#' Run the shinycannon utility
+#'
+#' @param shinycannon_path Path to the Shinycannon JAR file.
+#' @param recording_file Path to recording file
+#' @param app_url URL of the Shiny application to interact with
+#' @param output_dir Path to directory to store session logs in for this
+#' test run
+#' @param workers Number of workers to simulate. Default is 1.
+#' @param loaded_duration_minutes Number of minutes to continue simulating
+#' sessions in each worker after all workers have completed one session.
+#' Can be fractional. Default is 5.
+#' @param overwrite_output Delete the output directory before starting,
+#' if it exists already. Default is TRUE.
+#' @param debug_log Produce a debug.log in the output directory. File can get
+#' very large. Default is FALSE.
+#' @param log_level Log level. Possible values include warn, info, error,
+#' debug. Default is warn.
+#'
+#' @return exit code from `sys::exec_wait()`
+shinycannon <- function(
+ shinycannon_path,
+ recording_file,
+ app_url,
+ output_dir,
+ workers = 1,
+ loaded_duration_minutes = 5,
+ overwrite_output = TRUE,
+ debug_log = FALSE,
+ log_level = "warn") {
+
+ # assemble command-line arguments
+ cli_args <- c(
+ "-jar",
+ shinycannon_path,
+ recording_file,
+ app_url,
+ "--workers",
+ workers,
+ "--loaded-duration-minutes",
+ loaded_duration_minutes,
+ "--output-dir",
+ output_dir,
+ "--log-level",
+ log_level
+ )
+
+ if (debug_log) cli_args <- c(cli_args, "--debug-log")
+ if (overwrite_output) cli_args <- c(cli_args, "--overwrite-output")
+
+ sys::exec_wait(
+ cmd = "java",
+ args = cli_args
+ )
+}
\ No newline at end of file
diff --git a/R/test_dependencies.R b/R/test_dependencies.R
index 202ca3c..de2ea85 100644
--- a/R/test_dependencies.R
+++ b/R/test_dependencies.R
@@ -25,18 +25,25 @@ library(sys)
# define paths and constants
-app_link <- "https://rsc.training.rstudio.com/bricktest/"
+app_url <- "https://rsc.training.rstudio.com/bricktest/"
recording_file <- "R/recording.log"
shinyloadtest::record_session(
app_link,
- output_file = "R/recording.log",
+ 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"
+shinycannon(
+ shinycannon_path,
+ recording_file,
+ app_url,
+ output_dir = "R/run1"
+)
+
# baseline run
exec_wait(
cmd = "java",
@@ -55,6 +62,18 @@ exec_wait(
)
)
+df <- load_runs("Run 1" = "R/run1")
+
+shinyloadtest_report(
+ df,
+ output = "R/report_test.html"
+)
+
+# experiment with creating single plots
+dur_plot <- slt_session_duration(df)
+plotly::ggplotly(dur_plot)
+
+
# comparison run (3 workers)
exec_wait(
cmd = "java",
@@ -104,8 +123,8 @@ shinyloadtest_report(df, "R/report1.html")
library(connectapi)
client <- connect(
- server = paste0("https://", Sys.getenv("RSCONNECT_SERVER")),
- api_key = Sys.getenv("RSCONNECT_KEY")
+ server = paste0("https://", Sys.getenv("CONNECT_SERVER")),
+ api_key = Sys.getenv("CONNECT_API_KEY")
)
content_df <- get_content(client)
diff --git a/_extensions/emilhvitfeldt/codewindow/_extension.yml b/_extensions/emilhvitfeldt/codewindow/_extension.yml
new file mode 100644
index 0000000..322dd04
--- /dev/null
+++ b/_extensions/emilhvitfeldt/codewindow/_extension.yml
@@ -0,0 +1,12 @@
+title: codewindow
+author: Emil Hvitfeldt
+version: 1.1.0
+quarto-required: ">=1.4.0"
+contributes:
+ revealjs-plugins:
+ - name: RevealCodewindow
+ script:
+ - codewindow.js
+ stylesheet:
+ - codewindow.css
+
diff --git a/_extensions/emilhvitfeldt/codewindow/codewindow.css b/_extensions/emilhvitfeldt/codewindow/codewindow.css
new file mode 100644
index 0000000..0ebd16b
--- /dev/null
+++ b/_extensions/emilhvitfeldt/codewindow/codewindow.css
@@ -0,0 +1,48 @@
+/* TODO: CSS for plugin */
+
+.codewindow {
+ overflow-wrap: anywhere;
+ margin-bottom: 36px;
+ font-size: 0.45em;
+ font-family: monospace;
+ background: white;
+ border-radius: 10px;
+ box-shadow: #26394d 0px 20px 30px -10px;
+}
+
+.codewindow .header {
+ display: flex;
+ background: #e2e8f0;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ padding-left: 16px;
+ gap: 16px;
+ height: 30px;
+}
+
+.codewindow .header .file {
+ margin-top: 5px;
+ padding-left: 20px;
+ padding-right: 20px;
+ display: flex;
+ background: white;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ height: 90%;
+ align-items: center;
+ gap: 4px;
+}
+
+.codewindow .textarea {
+ padding: 21px;
+}
+
+.codewindow .textarea .sourceCode {
+ background: unset;
+ border: unset;
+ font-size: unset;
+}
+
+.codewindow .textarea pre {
+ font-size: unset;
+}
diff --git a/_extensions/emilhvitfeldt/codewindow/codewindow.js b/_extensions/emilhvitfeldt/codewindow/codewindow.js
new file mode 100644
index 0000000..f565150
--- /dev/null
+++ b/_extensions/emilhvitfeldt/codewindow/codewindow.js
@@ -0,0 +1,112 @@
+window.RevealCodewindow = function () {
+ return {
+ id: "RevealCodewindow",
+ init: function(deck) {
+ initCodewindow(deck);
+ }
+ };
+};
+
+
+const initCodewindow = function(Reveal) {
+
+ const svg_buttons = ``
+
+ const svg_sass = ``
+
+ const svg_r = ``
+
+ const svg_python = ``
+
+ const svg_html = ``
+
+ const svg_css = ``
+
+ const svg_js = ``
+
+ const svg_quarto = ``
+
+ const svg_julia = ``
+
+ window.addEventListener( 'ready', function(event) {
+
+ var content;
+ var new_content;
+ var empty_file;
+
+ // Remove configured margin of the presentation
+ var codewindows = document.getElementsByClassName("codewindow");
+
+ for (var i = 0; i < codewindows.length; i++) {
+ content = codewindows[i];
+
+ empty_file = true;
+
+ new_content = document.createElement("div");
+ new_content.classList.add("codewindow");
+
+ header = document.createElement("div");
+ header.classList.add("header");
+ header.innerHTML = svg_buttons;
+
+ file = document.createElement("div");
+
+ file.classList.add("file");
+ if (content.classList.contains("sass")) {
+ file.innerHTML += svg_sass;
+ empty_file = false;
+ }
+ if (content.classList.contains("r")) {
+ file.innerHTML += svg_r;
+ empty_file = false;
+ }
+ if (content.classList.contains("python")) {
+ file.innerHTML += svg_python;
+ empty_file = false;
+ }
+ if (content.classList.contains("html")) {
+ file.innerHTML += svg_html;
+ empty_file = false;
+ }
+ if (content.classList.contains("css")) {
+ file.innerHTML += svg_css;
+ empty_file = false;
+ }
+ if (content.classList.contains("js")) {
+ file.innerHTML += svg_js;
+ empty_file = false;
+ }
+ if (content.classList.contains("quarto")) {
+ file.innerHTML += svg_quarto;
+ empty_file = false;
+ }
+ if (content.classList.contains("julia")) {
+ file.innerHTML += svg_julia;
+ empty_file = false;
+ }
+
+ file_name = content.querySelector("p");
+ if (file_name !== null) {
+ file.innerHTML += file_name.innerText;
+ empty_file = false;
+ }
+
+ textarea = document.createElement("div");
+ textarea.classList.add("textarea");
+ textarea.appendChild(content.querySelector("div"));
+
+ if (!empty_file) {
+ header.appendChild(file);
+ }
+
+ new_content.appendChild(header);
+ new_content.appendChild(textarea);
+
+ codewindows[i].innerHTML = new_content.innerHTML;
+
+ if (content.attributes.width !== undefined) {
+ codewindows[i].style.width = content.attributes.width.value;
+ }
+ }
+ });
+};
\ No newline at end of file
diff --git a/_extensions/emilhvitfeldt/highlightword/_extension.yml b/_extensions/emilhvitfeldt/highlightword/_extension.yml
new file mode 100644
index 0000000..73f92a4
--- /dev/null
+++ b/_extensions/emilhvitfeldt/highlightword/_extension.yml
@@ -0,0 +1,9 @@
+title: highlightword
+author: Emil Hvitfeldt
+version: 1.0.0
+quarto-required: ">=1.4.0"
+contributes:
+ revealjs-plugins:
+ - name: RevealHighlightword
+ script:
+ - highlightword.js
diff --git a/_extensions/emilhvitfeldt/highlightword/highlightword.js b/_extensions/emilhvitfeldt/highlightword/highlightword.js
new file mode 100644
index 0000000..73f6e72
--- /dev/null
+++ b/_extensions/emilhvitfeldt/highlightword/highlightword.js
@@ -0,0 +1,81 @@
+window.RevealHighlightword= function () {
+ return {
+ id: "RevealCodewindow",
+ init: function(deck) {
+ initCodewindow(deck);
+ }
+ };
+};
+
+function replaceOccurrence(string, regex, n, replace) {
+ var i = 0;
+ return string.replace(regex, function(match) {
+ i+=1;
+ if(i===n) return replace;
+ return match;
+ });
+}
+
+const highlight_apply = function(fragment) {
+ if (fragment.classList.contains("highlightword")) {
+ var chunk_id = 0
+ if (fragment.dataset.chunk !== undefined) {
+ chunk_id = fragment.dataset.chunk - 1;
+ }
+ var chunk = Reveal.getCurrentSlide().querySelectorAll("code.sourceCode")[chunk_id]
+
+ word = fragment.dataset.word;
+ if (word === undefined) {
+ return
+ }
+
+ replacement = document.createElement("span");
+ replacement.innerText = word
+ replacement.style.cssText = fragment.style.cssText;
+
+ var number = 1
+ if (fragment.dataset.number !== undefined) {
+ number = Number(fragment.dataset.number);
+ }
+
+ let t = 0;
+ chunk.innerHTML = chunk.innerHTML.replaceAll(
+ word,
+ match => ++t === number ? replacement.outerHTML : match
+ );
+ }
+}
+
+const highlight_reverse = function(fragment) {
+ if (fragment.classList.contains("highlightword")) {
+ var chunk_id = 0
+ if (fragment.dataset.chunk !== undefined) {
+ chunk_id = fragment.dataset.chunk - 1;
+ }
+ var chunk = Reveal.getCurrentSlide().querySelectorAll("code.sourceCode")[chunk_id]
+
+ word = fragment.dataset.word;
+ if (word === undefined) {
+ return
+ }
+
+ replacement = document.createElement("span");
+ replacement.innerText = word
+ replacement.style.cssText = fragment.style.cssText;
+
+ let t = 0;
+ chunk.innerHTML = chunk.innerHTML.replace(
+ replacement.outerHTML, word
+ );
+ }
+}
+
+const initCodewindow = function(window) {
+ window.on( 'fragmentshown', event => {
+ event.fragments.forEach(highlight_apply);
+ });
+
+ window.on( 'fragmenthidden', event => {
+ event.fragments.forEach(highlight_reverse);
+ });
+};
\ No newline at end of file
diff --git a/_freeze/materials/d1-9001-loadtesting/index/execute-results/html.json b/_freeze/materials/d1-9001-loadtesting/index/execute-results/html.json
new file mode 100644
index 0000000..1c4315c
--- /dev/null
+++ b/_freeze/materials/d1-9001-loadtesting/index/execute-results/html.json
@@ -0,0 +1,20 @@
+{
+ "hash": "86308c1c83aa589f5231cb9463fc2773",
+ "result": {
+ "markdown": "---\ntitle: \"Load Testing\"\nsubtitle: \"posit::conf(2023)
Shiny in Production: Tools & Techniques\"\nauthor: \"TBD\"\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![single-user](assets/img/lego_single_user_computer.png)\n:::\n\n::: {.column width=\"50%\"}\n![crowd](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\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## Recorder Tips\n\n* Emulate a real-world usage of your application\n* Avoid rapid clicks / selections of your inputs\n\n## The log\n\n```\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![](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://my-lego-app.me\",\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: 138 × 13\n run user_id session_id iteration input_line_number event start end time\n \n 1 Run 1 0 0 0 5 REQ_H… 0 0.951 0.951\n 2 Run 1 0 0 0 6 REQ_G… 0.952 1.27 0.320\n 3 Run 1 0 0 0 7 REQ_G… 1.27 1.54 0.271\n 4 Run 1 0 0 0 8 REQ_G… 1.54 1.85 0.308\n 5 Run 1 0 0 0 9 REQ_G… 1.85 2.19 0.334\n 6 Run 1 0 0 0 10 REQ_G… 2.19 2.44 0.253\n 7 Run 1 0 0 0 11 REQ_G… 2.44 2.86 0.422\n 8 Run 1 0 0 0 12 REQ_G… 2.86 3.12 0.257\n 9 Run 1 0 0 0 13 REQ_G… 3.12 3.37 0.252\n10 Run 1 0 0 0 14 REQ_G… 3.37 3.62 0.253\n# ℹ 128 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::: {.callout-warning title=\"TODO\"}\nAdd screenshot of HTML report with hyperlink\n:::\n\n# ``{=html} Code-Along {background-color=\"#17395c\"}\n\nCode-Along 1: Record and analyze a baseline application session",
+ "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/site_libs/revealjs/dist/theme/quarto.css b/_freeze/site_libs/revealjs/dist/theme/quarto.css
index dd9eded..d9282d5 100644
--- a/_freeze/site_libs/revealjs/dist/theme/quarto.css
+++ b/_freeze/site_libs/revealjs/dist/theme/quarto.css
@@ -1,4 +1,4 @@
-@import"./fonts/source-sans-pro/source-sans-pro.css";.v-center-container{display:flex;justify-content:center;align-items:center;height:90%}:root{--r-background-color: #fff;--r-main-font: Source Sans Pro, Helvetica, sans-serif;--r-main-font-size: 40px;--r-main-color: #222;--r-block-margin: 12px;--r-heading-margin: 0 0 12px 0;--r-heading-font: Source Sans Pro, Helvetica, sans-serif;--r-heading-color: #222;--r-heading-line-height: 1.2;--r-heading-letter-spacing: normal;--r-heading-text-transform: none;--r-heading-text-shadow: none;--r-heading-font-weight: 600;--r-heading1-text-shadow: none;--r-heading1-size: 2.5em;--r-heading2-size: 1.6em;--r-heading3-size: 1.3em;--r-heading4-size: 1em;--r-code-font: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;--r-link-color: #2a76dd;--r-link-color-dark: #1a53a1;--r-link-color-hover: #5692e4;--r-selection-background-color: #98bdef;--r-selection-color: #fff}.reveal-viewport{background:#fff;background-color:var(--r-background-color)}.reveal{font-family:var(--r-main-font);font-size:var(--r-main-font-size);font-weight:normal;color:var(--r-main-color)}.reveal ::selection{color:var(--r-selection-color);background:var(--r-selection-background-color);text-shadow:none}.reveal ::-moz-selection{color:var(--r-selection-color);background:var(--r-selection-background-color);text-shadow:none}.reveal .slides section,.reveal .slides section>section{line-height:1.3;font-weight:inherit}.reveal h1,.reveal h2,.reveal h3,.reveal h4,.reveal h5,.reveal h6{margin:var(--r-heading-margin);color:var(--r-heading-color);font-family:var(--r-heading-font);font-weight:var(--r-heading-font-weight);line-height:var(--r-heading-line-height);letter-spacing:var(--r-heading-letter-spacing);text-transform:var(--r-heading-text-transform);text-shadow:var(--r-heading-text-shadow);word-wrap:break-word}.reveal h1{font-size:var(--r-heading1-size)}.reveal h2{font-size:var(--r-heading2-size)}.reveal h3{font-size:var(--r-heading3-size)}.reveal h4{font-size:var(--r-heading4-size)}.reveal h1{text-shadow:var(--r-heading1-text-shadow)}.reveal p{margin:var(--r-block-margin) 0;line-height:1.3}.reveal h1:last-child,.reveal h2:last-child,.reveal h3:last-child,.reveal h4:last-child,.reveal h5:last-child,.reveal h6:last-child{margin-bottom:0}.reveal img,.reveal video,.reveal iframe{max-width:95%;max-height:95%}.reveal strong,.reveal b{font-weight:bold}.reveal em{font-style:italic}.reveal ol,.reveal dl,.reveal ul{display:inline-block;text-align:left;margin:0 0 0 1em}.reveal ol{list-style-type:decimal}.reveal ul{list-style-type:disc}.reveal ul ul{list-style-type:square}.reveal ul ul ul{list-style-type:circle}.reveal ul ul,.reveal ul ol,.reveal ol ol,.reveal ol ul{display:block;margin-left:40px}.reveal dt{font-weight:bold}.reveal dd{margin-left:40px}.reveal blockquote{display:block;position:relative;width:70%;margin:var(--r-block-margin) auto;padding:5px;font-style:italic;background:rgba(255,255,255,.05);box-shadow:0px 0px 2px rgba(0,0,0,.2)}.reveal blockquote p:first-child,.reveal blockquote p:last-child{display:inline-block}.reveal q{font-style:italic}.reveal pre{display:block;position:relative;width:90%;margin:var(--r-block-margin) auto;text-align:left;font-size:.55em;font-family:var(--r-code-font);line-height:1.2em;word-wrap:break-word;box-shadow:0px 5px 15px rgba(0,0,0,.15)}.reveal code{font-family:var(--r-code-font);text-transform:none;tab-size:2}.reveal pre code{display:block;padding:5px;overflow:auto;max-height:400px;word-wrap:normal}.reveal .code-wrapper{white-space:normal}.reveal .code-wrapper code{white-space:pre}.reveal table{margin:auto;border-collapse:collapse;border-spacing:0}.reveal table th{font-weight:bold}.reveal table th,.reveal table td{text-align:left;padding:.2em .5em .2em .5em;border-bottom:1px solid}.reveal table th[align=center],.reveal table td[align=center]{text-align:center}.reveal table th[align=right],.reveal table td[align=right]{text-align:right}.reveal table tbody tr:last-child th,.reveal table tbody tr:last-child td{border-bottom:none}.reveal sup{vertical-align:super;font-size:smaller}.reveal sub{vertical-align:sub;font-size:smaller}.reveal small{display:inline-block;font-size:.6em;line-height:1.2em;vertical-align:top}.reveal small *{vertical-align:top}.reveal img{margin:var(--r-block-margin) 0}.reveal a{color:var(--r-link-color);text-decoration:none;transition:color .15s ease}.reveal a:hover{color:var(--r-link-color-hover);text-shadow:none;border:none}.reveal .roll span:after{color:#fff;background:var(--r-link-color-dark)}.reveal .r-frame{border:4px solid var(--r-main-color);box-shadow:0 0 10px rgba(0,0,0,.15)}.reveal a .r-frame{transition:all .15s linear}.reveal a:hover .r-frame{border-color:var(--r-link-color);box-shadow:0 0 20px rgba(0,0,0,.55)}.reveal .controls{color:var(--r-link-color)}.reveal .progress{background:rgba(0,0,0,.2);color:var(--r-link-color)}@media print{.backgrounds{background-color:var(--r-background-color)}}.top-right{position:absolute;top:1em;right:1em}.hidden{display:none !important}.zindex-bottom{z-index:-1 !important}.quarto-layout-panel{margin-bottom:1em}.quarto-layout-panel>figure{width:100%}.quarto-layout-panel>figure>figcaption,.quarto-layout-panel>.panel-caption{margin-top:10pt}.quarto-layout-panel>.table-caption{margin-top:0px}.table-caption p{margin-bottom:.5em}.quarto-layout-row{display:flex;flex-direction:row;align-items:flex-start}.quarto-layout-valign-top{align-items:flex-start}.quarto-layout-valign-bottom{align-items:flex-end}.quarto-layout-valign-center{align-items:center}.quarto-layout-cell{position:relative;margin-right:20px}.quarto-layout-cell:last-child{margin-right:0}.quarto-layout-cell figure,.quarto-layout-cell>p{margin:.2em}.quarto-layout-cell img{max-width:100%}.quarto-layout-cell .html-widget{width:100% !important}.quarto-layout-cell div figure p{margin:0}.quarto-layout-cell figure{display:inline-block;margin-inline-start:0;margin-inline-end:0}.quarto-layout-cell table{display:inline-table}.quarto-layout-cell-subref figcaption,figure .quarto-layout-row figure figcaption{text-align:center;font-style:italic}.quarto-figure{position:relative;margin-bottom:1em}.quarto-figure>figure{width:100%;margin-bottom:0}.quarto-figure-left>figure>p,.quarto-figure-left>figure>div{text-align:left}.quarto-figure-center>figure>p,.quarto-figure-center>figure>div{text-align:center}.quarto-figure-right>figure>p,.quarto-figure-right>figure>div{text-align:right}figure>p:empty{display:none}figure>p:first-child{margin-top:0;margin-bottom:0}figure>figcaption{margin-top:.5em}div[id^=tbl-]{position:relative}.quarto-figure>.anchorjs-link{position:absolute;top:.6em;right:.5em}div[id^=tbl-]>.anchorjs-link{position:absolute;top:.7em;right:.3em}.quarto-figure:hover>.anchorjs-link,div[id^=tbl-]:hover>.anchorjs-link,h2:hover>.anchorjs-link,h3:hover>.anchorjs-link,h4:hover>.anchorjs-link,h5:hover>.anchorjs-link,h6:hover>.anchorjs-link,.reveal-anchorjs-link>.anchorjs-link{opacity:1}#title-block-header{margin-block-end:1rem;position:relative;margin-top:-1px}#title-block-header .abstract{margin-block-start:1rem}#title-block-header .abstract .abstract-title{font-weight:600}#title-block-header a{text-decoration:none}#title-block-header .author,#title-block-header .date,#title-block-header .doi{margin-block-end:.2rem}#title-block-header .quarto-title-block>div{display:flex}#title-block-header .quarto-title-block>div>h1{flex-grow:1}#title-block-header .quarto-title-block>div>button{flex-shrink:0;height:2.25rem;margin-top:0}tr.header>th>p:last-of-type{margin-bottom:0px}table,.table{caption-side:top;margin-bottom:1.5rem}caption,.table-caption{padding-top:.5rem;padding-bottom:.5rem;text-align:center}.utterances{max-width:none;margin-left:-8px}iframe{margin-bottom:1em}details{margin-bottom:1em}details[show]{margin-bottom:0}details>summary{color:#6f6f6f}details>summary>p:only-child{display:inline}pre.sourceCode,code.sourceCode{position:relative}p code:not(.sourceCode){white-space:pre-wrap}code{white-space:pre}@media print{code{white-space:pre-wrap}}pre>code{display:block}pre>code.sourceCode{white-space:pre}pre>code.sourceCode>span>a:first-child::before{text-decoration:none}pre.code-overflow-wrap>code.sourceCode{white-space:pre-wrap}pre.code-overflow-scroll>code.sourceCode{white-space:pre}code a:any-link{color:inherit;text-decoration:none}code a:hover{color:inherit;text-decoration:underline}ul.task-list{padding-left:1em}[data-tippy-root]{display:inline-block}.tippy-content .footnote-back{display:none}.quarto-embedded-source-code{display:none}.quarto-unresolved-ref{font-weight:600}.quarto-cover-image{max-width:35%;float:right;margin-left:30px}.cell-output-display .widget-subarea{margin-bottom:1em}.cell-output-display:not(.no-overflow-x),.knitsql-table:not(.no-overflow-x){overflow-x:auto}.panel-input{margin-bottom:1em}.panel-input>div,.panel-input>div>div{display:inline-block;vertical-align:top;padding-right:12px}.panel-input>p:last-child{margin-bottom:0}.layout-sidebar{margin-bottom:1em}.layout-sidebar .tab-content{border:none}.tab-content>.page-columns.active{display:grid}div.sourceCode>iframe{width:100%;height:300px;margin-bottom:-0.5em}div.ansi-escaped-output{font-family:monospace;display:block}/*!
+@import"./fonts/source-sans-pro/source-sans-pro.css";.v-center-container{display:flex;justify-content:center;align-items:center;height:90%}:root{--r-background-color: #fff;--r-main-font: Source Sans Pro, Helvetica, sans-serif;--r-main-font-size: 40px;--r-main-color: #222;--r-block-margin: 12px;--r-heading-margin: 0 0 12px 0;--r-heading-font: Source Sans Pro, Helvetica, sans-serif;--r-heading-color: #222;--r-heading-line-height: 1.2;--r-heading-letter-spacing: normal;--r-heading-text-transform: none;--r-heading-text-shadow: none;--r-heading-font-weight: 600;--r-heading1-text-shadow: none;--r-heading1-size: 2.5em;--r-heading2-size: 1.6em;--r-heading3-size: 1.3em;--r-heading4-size: 1em;--r-code-font: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;--r-link-color: #2a76dd;--r-link-color-dark: #1a53a1;--r-link-color-hover: #5692e4;--r-selection-background-color: #98bdef;--r-selection-color: #fff}.reveal-viewport{background:#fff;background-color:var(--r-background-color)}.reveal{font-family:var(--r-main-font);font-size:var(--r-main-font-size);font-weight:normal;color:var(--r-main-color)}.reveal ::selection{color:var(--r-selection-color);background:var(--r-selection-background-color);text-shadow:none}.reveal ::-moz-selection{color:var(--r-selection-color);background:var(--r-selection-background-color);text-shadow:none}.reveal .slides section,.reveal .slides section>section{line-height:1.3;font-weight:inherit}.reveal h1,.reveal h2,.reveal h3,.reveal h4,.reveal h5,.reveal h6{margin:var(--r-heading-margin);color:var(--r-heading-color);font-family:var(--r-heading-font);font-weight:var(--r-heading-font-weight);line-height:var(--r-heading-line-height);letter-spacing:var(--r-heading-letter-spacing);text-transform:var(--r-heading-text-transform);text-shadow:var(--r-heading-text-shadow);word-wrap:break-word}.reveal h1{font-size:var(--r-heading1-size)}.reveal h2{font-size:var(--r-heading2-size)}.reveal h3{font-size:var(--r-heading3-size)}.reveal h4{font-size:var(--r-heading4-size)}.reveal h1{text-shadow:var(--r-heading1-text-shadow)}.reveal p{margin:var(--r-block-margin) 0;line-height:1.3}.reveal h1:last-child,.reveal h2:last-child,.reveal h3:last-child,.reveal h4:last-child,.reveal h5:last-child,.reveal h6:last-child{margin-bottom:0}.reveal img,.reveal video,.reveal iframe{max-width:95%;max-height:95%}.reveal strong,.reveal b{font-weight:bold}.reveal em{font-style:italic}.reveal ol,.reveal dl,.reveal ul{display:inline-block;text-align:left;margin:0 0 0 1em}.reveal ol{list-style-type:decimal}.reveal ul{list-style-type:disc}.reveal ul ul{list-style-type:square}.reveal ul ul ul{list-style-type:circle}.reveal ul ul,.reveal ul ol,.reveal ol ol,.reveal ol ul{display:block;margin-left:40px}.reveal dt{font-weight:bold}.reveal dd{margin-left:40px}.reveal blockquote{display:block;position:relative;width:70%;margin:var(--r-block-margin) auto;padding:5px;font-style:italic;background:rgba(255,255,255,.05);box-shadow:0px 0px 2px rgba(0,0,0,.2)}.reveal blockquote p:first-child,.reveal blockquote p:last-child{display:inline-block}.reveal q{font-style:italic}.reveal pre{display:block;position:relative;width:90%;margin:var(--r-block-margin) auto;text-align:left;font-size:.55em;font-family:var(--r-code-font);line-height:1.2em;word-wrap:break-word;box-shadow:0px 5px 15px rgba(0,0,0,.15)}.reveal code{font-family:var(--r-code-font);text-transform:none;tab-size:2}.reveal pre code{display:block;padding:5px;overflow:auto;max-height:400px;word-wrap:normal}.reveal .code-wrapper{white-space:normal}.reveal .code-wrapper code{white-space:pre}.reveal table{margin:auto;border-collapse:collapse;border-spacing:0}.reveal table th{font-weight:bold}.reveal table th,.reveal table td{text-align:left;padding:.2em .5em .2em .5em;border-bottom:1px solid}.reveal table th[align=center],.reveal table td[align=center]{text-align:center}.reveal table th[align=right],.reveal table td[align=right]{text-align:right}.reveal table tbody tr:last-child th,.reveal table tbody tr:last-child td{border-bottom:none}.reveal sup{vertical-align:super;font-size:smaller}.reveal sub{vertical-align:sub;font-size:smaller}.reveal small{display:inline-block;font-size:.6em;line-height:1.2em;vertical-align:top}.reveal small *{vertical-align:top}.reveal img{margin:var(--r-block-margin) 0}.reveal a{color:var(--r-link-color);text-decoration:none;transition:color .15s ease}.reveal a:hover{color:var(--r-link-color-hover);text-shadow:none;border:none}.reveal .roll span:after{color:#fff;background:var(--r-link-color-dark)}.reveal .r-frame{border:4px solid var(--r-main-color);box-shadow:0 0 10px rgba(0,0,0,.15)}.reveal a .r-frame{transition:all .15s linear}.reveal a:hover .r-frame{border-color:var(--r-link-color);box-shadow:0 0 20px rgba(0,0,0,.55)}.reveal .controls{color:var(--r-link-color)}.reveal .progress{background:rgba(0,0,0,.2);color:var(--r-link-color)}@media print{.backgrounds{background-color:var(--r-background-color)}}.top-right{position:absolute;top:1em;right:1em}.hidden{display:none !important}.zindex-bottom{z-index:-1 !important}.quarto-layout-panel{margin-bottom:1em}.quarto-layout-panel>figure{width:100%}.quarto-layout-panel>figure>figcaption,.quarto-layout-panel>.panel-caption{margin-top:10pt}.quarto-layout-panel>.table-caption{margin-top:0px}.table-caption p{margin-bottom:.5em}.quarto-layout-row{display:flex;flex-direction:row;align-items:flex-start}.quarto-layout-valign-top{align-items:flex-start}.quarto-layout-valign-bottom{align-items:flex-end}.quarto-layout-valign-center{align-items:center}.quarto-layout-cell{position:relative;margin-right:20px}.quarto-layout-cell:last-child{margin-right:0}.quarto-layout-cell figure,.quarto-layout-cell>p{margin:.2em}.quarto-layout-cell img{max-width:100%}.quarto-layout-cell .html-widget{width:100% !important}.quarto-layout-cell div figure p{margin:0}.quarto-layout-cell figure{display:inline-block;margin-inline-start:0;margin-inline-end:0}.quarto-layout-cell table{display:inline-table}.quarto-layout-cell-subref figcaption,figure .quarto-layout-row figure figcaption{text-align:center;font-style:italic}.quarto-figure{position:relative;margin-bottom:1em}.quarto-figure>figure{width:100%;margin-bottom:0}.quarto-figure-left>figure>p,.quarto-figure-left>figure>div{text-align:left}.quarto-figure-center>figure>p,.quarto-figure-center>figure>div{text-align:center}.quarto-figure-right>figure>p,.quarto-figure-right>figure>div{text-align:right}.quarto-figure>figure>div.cell-annotation,.quarto-figure>figure>div code{text-align:left}figure>p:empty{display:none}figure>p:first-child{margin-top:0;margin-bottom:0}figure>figcaption{margin-top:.5em}div[id^=tbl-]{position:relative}.quarto-figure>.anchorjs-link{position:absolute;top:.6em;right:.5em}div[id^=tbl-]>.anchorjs-link{position:absolute;top:.7em;right:.3em}.quarto-figure:hover>.anchorjs-link,div[id^=tbl-]:hover>.anchorjs-link,h2:hover>.anchorjs-link,h3:hover>.anchorjs-link,h4:hover>.anchorjs-link,h5:hover>.anchorjs-link,h6:hover>.anchorjs-link,.reveal-anchorjs-link>.anchorjs-link{opacity:1}#title-block-header{margin-block-end:1rem;position:relative;margin-top:-1px}#title-block-header .abstract{margin-block-start:1rem}#title-block-header .abstract .abstract-title{font-weight:600}#title-block-header a{text-decoration:none}#title-block-header .author,#title-block-header .date,#title-block-header .doi{margin-block-end:.2rem}#title-block-header .quarto-title-block>div{display:flex}#title-block-header .quarto-title-block>div>h1{flex-grow:1}#title-block-header .quarto-title-block>div>button{flex-shrink:0;height:2.25rem;margin-top:0}tr.header>th>p:last-of-type{margin-bottom:0px}table,.table{caption-side:top;margin-bottom:1.5rem}caption,.table-caption{padding-top:.5rem;padding-bottom:.5rem;text-align:center}.utterances{max-width:none;margin-left:-8px}iframe{margin-bottom:1em}details{margin-bottom:1em}details[show]{margin-bottom:0}details>summary{color:#6f6f6f}details>summary>p:only-child{display:inline}pre.sourceCode,code.sourceCode{position:relative}p code:not(.sourceCode){white-space:pre-wrap}code{white-space:pre}@media print{code{white-space:pre-wrap}}pre>code{display:block}pre>code.sourceCode{white-space:pre}pre>code.sourceCode>span>a:first-child::before{text-decoration:none}pre.code-overflow-wrap>code.sourceCode{white-space:pre-wrap}pre.code-overflow-scroll>code.sourceCode{white-space:pre}code a:any-link{color:inherit;text-decoration:none}code a:hover{color:inherit;text-decoration:underline}ul.task-list{padding-left:1em}[data-tippy-root]{display:inline-block}.tippy-content .footnote-back{display:none}.quarto-embedded-source-code{display:none}.quarto-unresolved-ref{font-weight:600}.quarto-cover-image{max-width:35%;float:right;margin-left:30px}.cell-output-display .widget-subarea{margin-bottom:1em}.cell-output-display:not(.no-overflow-x),.knitsql-table:not(.no-overflow-x){overflow-x:auto}.panel-input{margin-bottom:1em}.panel-input>div,.panel-input>div>div{display:inline-block;vertical-align:top;padding-right:12px}.panel-input>p:last-child{margin-bottom:0}.layout-sidebar{margin-bottom:1em}.layout-sidebar .tab-content{border:none}.tab-content>.page-columns.active{display:grid}div.sourceCode>iframe{width:100%;height:300px;margin-bottom:-0.5em}a{text-underline-offset:3px}div.ansi-escaped-output{font-family:monospace;display:block}/*!
*
* ansi colors from IPython notebook's
*
diff --git a/_freeze/site_libs/revealjs/plugin/reveal-codewindow/codewindow.css b/_freeze/site_libs/revealjs/plugin/reveal-codewindow/codewindow.css
new file mode 100644
index 0000000..0ebd16b
--- /dev/null
+++ b/_freeze/site_libs/revealjs/plugin/reveal-codewindow/codewindow.css
@@ -0,0 +1,48 @@
+/* TODO: CSS for plugin */
+
+.codewindow {
+ overflow-wrap: anywhere;
+ margin-bottom: 36px;
+ font-size: 0.45em;
+ font-family: monospace;
+ background: white;
+ border-radius: 10px;
+ box-shadow: #26394d 0px 20px 30px -10px;
+}
+
+.codewindow .header {
+ display: flex;
+ background: #e2e8f0;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ padding-left: 16px;
+ gap: 16px;
+ height: 30px;
+}
+
+.codewindow .header .file {
+ margin-top: 5px;
+ padding-left: 20px;
+ padding-right: 20px;
+ display: flex;
+ background: white;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ height: 90%;
+ align-items: center;
+ gap: 4px;
+}
+
+.codewindow .textarea {
+ padding: 21px;
+}
+
+.codewindow .textarea .sourceCode {
+ background: unset;
+ border: unset;
+ font-size: unset;
+}
+
+.codewindow .textarea pre {
+ font-size: unset;
+}
diff --git a/_freeze/site_libs/revealjs/plugin/reveal-codewindow/codewindow.js b/_freeze/site_libs/revealjs/plugin/reveal-codewindow/codewindow.js
new file mode 100644
index 0000000..f565150
--- /dev/null
+++ b/_freeze/site_libs/revealjs/plugin/reveal-codewindow/codewindow.js
@@ -0,0 +1,112 @@
+window.RevealCodewindow = function () {
+ return {
+ id: "RevealCodewindow",
+ init: function(deck) {
+ initCodewindow(deck);
+ }
+ };
+};
+
+
+const initCodewindow = function(Reveal) {
+
+ const svg_buttons = ``
+
+ const svg_sass = ``
+
+ const svg_r = ``
+
+ const svg_python = ``
+
+ const svg_html = ``
+
+ const svg_css = ``
+
+ const svg_js = ``
+
+ const svg_quarto = ``
+
+ const svg_julia = ``
+
+ window.addEventListener( 'ready', function(event) {
+
+ var content;
+ var new_content;
+ var empty_file;
+
+ // Remove configured margin of the presentation
+ var codewindows = document.getElementsByClassName("codewindow");
+
+ for (var i = 0; i < codewindows.length; i++) {
+ content = codewindows[i];
+
+ empty_file = true;
+
+ new_content = document.createElement("div");
+ new_content.classList.add("codewindow");
+
+ header = document.createElement("div");
+ header.classList.add("header");
+ header.innerHTML = svg_buttons;
+
+ file = document.createElement("div");
+
+ file.classList.add("file");
+ if (content.classList.contains("sass")) {
+ file.innerHTML += svg_sass;
+ empty_file = false;
+ }
+ if (content.classList.contains("r")) {
+ file.innerHTML += svg_r;
+ empty_file = false;
+ }
+ if (content.classList.contains("python")) {
+ file.innerHTML += svg_python;
+ empty_file = false;
+ }
+ if (content.classList.contains("html")) {
+ file.innerHTML += svg_html;
+ empty_file = false;
+ }
+ if (content.classList.contains("css")) {
+ file.innerHTML += svg_css;
+ empty_file = false;
+ }
+ if (content.classList.contains("js")) {
+ file.innerHTML += svg_js;
+ empty_file = false;
+ }
+ if (content.classList.contains("quarto")) {
+ file.innerHTML += svg_quarto;
+ empty_file = false;
+ }
+ if (content.classList.contains("julia")) {
+ file.innerHTML += svg_julia;
+ empty_file = false;
+ }
+
+ file_name = content.querySelector("p");
+ if (file_name !== null) {
+ file.innerHTML += file_name.innerText;
+ empty_file = false;
+ }
+
+ textarea = document.createElement("div");
+ textarea.classList.add("textarea");
+ textarea.appendChild(content.querySelector("div"));
+
+ if (!empty_file) {
+ header.appendChild(file);
+ }
+
+ new_content.appendChild(header);
+ new_content.appendChild(textarea);
+
+ codewindows[i].innerHTML = new_content.innerHTML;
+
+ if (content.attributes.width !== undefined) {
+ codewindows[i].style.width = content.attributes.width.value;
+ }
+ }
+ });
+};
\ No newline at end of file
diff --git a/_quarto.yml b/_quarto.yml
index b9e79ce..84feb38 100644
--- a/_quarto.yml
+++ b/_quarto.yml
@@ -25,6 +25,7 @@ format:
#css: styles.css
toc: true
link-external-newwindow: true
+ code-line-numbers: true
execute:
freeze: auto
diff --git a/materials/d1-9001-loadtesting/assets/img/confused_minifig.jpg b/materials/d1-9001-loadtesting/assets/img/confused_minifig.jpg
new file mode 100644
index 0000000..117f982
Binary files /dev/null and b/materials/d1-9001-loadtesting/assets/img/confused_minifig.jpg differ
diff --git a/materials/d1-9001-loadtesting/assets/img/lego_crowd.jpg b/materials/d1-9001-loadtesting/assets/img/lego_crowd.jpg
new file mode 100644
index 0000000..2a4c1fe
Binary files /dev/null and b/materials/d1-9001-loadtesting/assets/img/lego_crowd.jpg differ
diff --git a/materials/d1-9001-loadtesting/assets/img/lego_single_user_computer.png b/materials/d1-9001-loadtesting/assets/img/lego_single_user_computer.png
new file mode 100644
index 0000000..33fd32c
Binary files /dev/null and b/materials/d1-9001-loadtesting/assets/img/lego_single_user_computer.png differ
diff --git a/materials/d1-9001-loadtesting/assets/img/loop.svg b/materials/d1-9001-loadtesting/assets/img/loop.svg
new file mode 100644
index 0000000..8b9e3fe
--- /dev/null
+++ b/materials/d1-9001-loadtesting/assets/img/loop.svg
@@ -0,0 +1,43 @@
+
+
\ No newline at end of file
diff --git a/materials/d1-9001-loadtesting/index.qmd b/materials/d1-9001-loadtesting/index.qmd
new file mode 100644
index 0000000..d52e969
--- /dev/null
+++ b/materials/d1-9001-loadtesting/index.qmd
@@ -0,0 +1,179 @@
+---
+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:
+ theme: [default, ../slides.scss] # moon= teal bg | dark
+ scrollable: true
+ incremental: false
+ slide-number: c/t # c/t | c | h/v | h.v
+ slide-tone: false #true
+ code-line-numbers: true
+ history: false
+revealjs-plugins:
+ - codewindow
+---
+
+## One to Many
+
+:::: {.columns}
+
+::: {.column width="50%"}
+![single-user](assets/img/lego_single_user_computer.png)
+:::
+
+::: {.column width="50%"}
+![crowd](assets/img/lego_crowd.jpg)
+:::
+
+::::
+
+## Optimization Loop Method
+
+![](assets/img/loop.svg)
+
+::: footer
+[rstudio.github.io/shinyloadtest/articles/case-study-scaling.html](https://rstudio.github.io/shinyloadtest/articles/case-study-scaling.html)
+:::
+
+## 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](https://github.com/sockjs/sockjs-client) (Posit Connect, Shiny Server)
+* If authentication required for app on Posit Connect, you'll need an API key
+
+## Recording a Session
+
+```{r}
+#| eval: false
+#| echo: true
+
+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
+
+```
+# 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"}
+```
+
+. . .
+
+![](assets/img/confused_minifig.jpg){.absolute top=50 right=250 width="500px" height="500px"}
+
+## Loading the (shiny) cannon
+
+::: {.codewindow}
+bash
+```{bash}
+#| eval: false
+#| echo: true
+java -jar shinycannon-1.1.3-dd43f6b.jar \
+ recording.log \
+ https://my-lego-app.me \
+ --loaded-duration-minutes 2 \
+ --workers 1 \
+ --output-dir run1
+```
+:::
+
+. . .
+
+::: {.codewindow .r}
+run_shinycannon.R
+```{r}
+#| eval: false
+#| echo: true
+
+source(file.path(here::here(), "R", "shinycannon.R"))
+
+shinycannon(
+ "shinycannon-1.1.3-dd43f6b.jar",
+ "recording.log",
+ "https://my-lego-app.me",
+ loaded_duration_minutes = 2,
+ workers = 1,
+ output_dir = "run1"
+)
+```
+:::
+
+## Metrics
+
+```{css echo=FALSE}
+.smaller-output{
+ font-size: 80%;
+}
+```
+
+::: {.codewindow .r}
+analyze_recording.R
+```{r}
+#| eval: false
+#| echo: true
+library(shinyloadtest)
+df <- load_runs("Run 1" = "run1")
+```
+:::
+
+
+::: {.smaller-output}
+```{r}
+#| eval: true
+#| echo: false
+
+shinyloadtest::load_runs("Run 1" = file.path(here::here(), "R", "run1"))
+
+```
+:::
+
+## But Wait .. There's More
+
+::: {.codewindow .r}
+gen_report.R
+```{r}
+#| eval: false
+#| echo: true
+library(shinyloadtest)
+df <- load_runs("Run 1" = "run1")
+shinyloadtest_report(df, output = "report_test.html")
+```
+:::
+
+::: {.callout-warning title="TODO"}
+Add screenshot of HTML report with hyperlink
+:::
+
+# `r fontawesome::fa("people-carry", "white")` Code-Along {background-color="#17395c"}
+
+Code-Along 1: Record and analyze a baseline application session
\ No newline at end of file
diff --git a/units/d1-9001-loadtesting.qmd b/units/d1-9001-loadtesting.qmd
new file mode 100644
index 0000000..17a0c2d
--- /dev/null
+++ b/units/d1-9001-loadtesting.qmd
@@ -0,0 +1,32 @@
+---
+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}
+
+```
+