CircosHeatmap-aardio/dist/lib/r-library/promises/doc/promises_08_casestudy.html

1234 lines
1.6 MiB
HTML
Raw Permalink Normal View History

2025-01-12 00:52:51 +08:00
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Joe Cheng (joe@rstudio.com)" />
<title>Case study: converting a Shiny app to async</title>
<script>// Pandoc 2.9 adds attributes on both header and div. We remove the former (to
// be compatible with the behavior of Pandoc < 2.8).
document.addEventListener('DOMContentLoaded', function(e) {
var hs = document.querySelectorAll("div.section[class*='level'] > :first-child");
var i, h, a;
for (i = 0; i < hs.length; i++) {
h = hs[i];
if (!/^h[1-6]$/i.test(h.tagName)) continue; // it should be a header h1-h6
a = h.attributes;
while (a.length > 0) h.removeAttribute(a[0].name);
}
});
</script>
<style type="text/css">
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
span.underline{text-decoration: underline;}
div.column{display: inline-block; vertical-align: top; width: 50%;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
ul.task-list{list-style: none;}
</style>
<style type="text/css">
code {
white-space: pre;
}
.sourceCode {
overflow: visible;
}
</style>
<style type="text/css" data-origin="pandoc">
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
.sourceCode { overflow: visible; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; }
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; }
code span.at { color: #7d9029; }
code span.bn { color: #40a070; }
code span.bu { color: #008000; }
code span.cf { color: #007020; font-weight: bold; }
code span.ch { color: #4070a0; }
code span.cn { color: #880000; }
code span.co { color: #60a0b0; font-style: italic; }
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; }
code span.do { color: #ba2121; font-style: italic; }
code span.dt { color: #902000; }
code span.dv { color: #40a070; }
code span.er { color: #ff0000; font-weight: bold; }
code span.ex { }
code span.fl { color: #40a070; }
code span.fu { color: #06287e; }
code span.im { color: #008000; font-weight: bold; }
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; }
code span.kw { color: #007020; font-weight: bold; }
code span.op { color: #666666; }
code span.ot { color: #007020; }
code span.pp { color: #bc7a00; }
code span.sc { color: #4070a0; }
code span.ss { color: #bb6688; }
code span.st { color: #4070a0; }
code span.va { color: #19177c; }
code span.vs { color: #4070a0; }
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; }
</style>
<script>
// apply pandoc div.sourceCode style to pre.sourceCode instead
(function() {
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
if (sheets[i].ownerNode.dataset["origin"] !== "pandoc") continue;
try { var rules = sheets[i].cssRules; } catch (e) { continue; }
var j = 0;
while (j < rules.length) {
var rule = rules[j];
// check if there is a div.sourceCode rule
if (rule.type !== rule.STYLE_RULE || rule.selectorText !== "div.sourceCode") {
j++;
continue;
}
var style = rule.style.cssText;
// check if color or background-color is set
if (rule.style.color === '' && rule.style.backgroundColor === '') {
j++;
continue;
}
// replace div.sourceCode by a pre.sourceCode rule
sheets[i].deleteRule(j);
sheets[i].insertRule('pre.sourceCode{' + style + '}', j);
}
}
})();
</script>
<style type="text/css">body {
background-color: #fff;
margin: 1em auto;
max-width: 700px;
overflow: visible;
padding-left: 2em;
padding-right: 2em;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.35;
}
#TOC {
clear: both;
margin: 0 0 10px 10px;
padding: 4px;
width: 400px;
border: 1px solid #CCCCCC;
border-radius: 5px;
background-color: #f6f6f6;
font-size: 13px;
line-height: 1.3;
}
#TOC .toctitle {
font-weight: bold;
font-size: 15px;
margin-left: 5px;
}
#TOC ul {
padding-left: 40px;
margin-left: -1.5em;
margin-top: 5px;
margin-bottom: 5px;
}
#TOC ul ul {
margin-left: -2em;
}
#TOC li {
line-height: 16px;
}
table {
margin: 1em auto;
border-width: 1px;
border-color: #DDDDDD;
border-style: outset;
border-collapse: collapse;
}
table th {
border-width: 2px;
padding: 5px;
border-style: inset;
}
table td {
border-width: 1px;
border-style: inset;
line-height: 18px;
padding: 5px 5px;
}
table, table th, table td {
border-left-style: none;
border-right-style: none;
}
table thead, table tr.even {
background-color: #f7f7f7;
}
p {
margin: 0.5em 0;
}
blockquote {
background-color: #f6f6f6;
padding: 0.25em 0.75em;
}
hr {
border-style: solid;
border: none;
border-top: 1px solid #777;
margin: 28px 0;
}
dl {
margin-left: 0;
}
dl dd {
margin-bottom: 13px;
margin-left: 13px;
}
dl dt {
font-weight: bold;
}
ul {
margin-top: 0;
}
ul li {
list-style: circle outside;
}
ul ul {
margin-bottom: 0;
}
pre, code {
background-color: #f7f7f7;
border-radius: 3px;
color: #333;
white-space: pre-wrap;
}
pre {
border-radius: 3px;
margin: 5px 0px 10px 0px;
padding: 10px;
}
pre:not([class]) {
background-color: #f7f7f7;
}
code {
font-family: Consolas, Monaco, 'Courier New', monospace;
font-size: 85%;
}
p > code, li > code {
padding: 2px 0px;
}
div.figure {
text-align: center;
}
img {
background-color: #FFFFFF;
padding: 2px;
border: 1px solid #DDDDDD;
border-radius: 3px;
border: 1px solid #CCCCCC;
margin: 0 5px;
}
h1 {
margin-top: 0;
font-size: 35px;
line-height: 40px;
}
h2 {
border-bottom: 4px solid #f7f7f7;
padding-top: 10px;
padding-bottom: 2px;
font-size: 145%;
}
h3 {
border-bottom: 2px solid #f7f7f7;
padding-top: 10px;
font-size: 120%;
}
h4 {
border-bottom: 1px solid #f7f7f7;
margin-left: 8px;
font-size: 105%;
}
h5, h6 {
border-bottom: 1px solid #ccc;
font-size: 105%;
}
a {
color: #0033dd;
text-decoration: none;
}
a:hover {
color: #6666ff; }
a:visited {
color: #800080; }
a:visited:hover {
color: #BB00BB; }
a[href^="http:"] {
text-decoration: underline; }
a[href^="https:"] {
text-decoration: underline; }
code > span.kw { color: #555; font-weight: bold; }
code > span.dt { color: #902000; }
code > span.dv { color: #40a070; }
code > span.bn { color: #d14; }
code > span.fl { color: #d14; }
code > span.ch { color: #d14; }
code > span.st { color: #d14; }
code > span.co { color: #888888; font-style: italic; }
code > span.ot { color: #007020; }
code > span.al { color: #ff0000; font-weight: bold; }
code > span.fu { color: #900; font-weight: bold; }
code > span.er { color: #a61717; background-color: #e3d2d2; }
</style>
</head>
<body>
<h1 class="title toc-ignore">Case study: converting a Shiny app to
async</h1>
<h4 class="author">Joe Cheng (<a href="mailto:joe@rstudio.com" class="email">joe@rstudio.com</a>)</h4>
<p>In this case study, well work through an application of reasonable
complexity, turning its slowest operations into futures/promises and
modifying all the downstream reactive expressions and outputs to deal
with promises.</p>
<div id="motivation" class="section level2">
<h2>Motivation</h2>
<blockquote>
<p>As a web service increases in popularity, so does the number of rogue
scripts that abuse it for no apparent reason.</p>
<p><em>—Chengs Law of Why We Cant Have Nice Things</em></p>
</blockquote>
<p>I first noticed this in 2011, when the then-new RStudio IDE was
starting to gather steam. We had a dashboard that tracked how often
RStudio was being downloaded, and the numbers were generally tracking
smoothly upward. But once every few months, wed have huge spikes in the
download counts, ten times greater than normal—and invariably, wed find
that all of the unexpected increase could be tracked to one or two IP
addresses.</p>
<p>For hours or days wed be inundated with thousands of downloads per
hour, then just as suddenly, theyd cease. I didnt know what was
happening then, and I still dont know today. Was it the worlds least
competent denial-of-service attempt? Did someone write a download script
with an accidental <code>while (TRUE)</code> around it?</p>
<p>Our application will let us examine downloads from CRAN for this kind
of behavior. For any given day on CRAN, well see what the top
downloaders are and how theyre behaving.</p>
</div>
<div id="our-source-data" class="section level2">
<h2>Our source data</h2>
<p>RStudio maintains the popular <code>0-Cloud</code> CRAN mirror, and
the log files it generates are freely available at <a href="http://cran-logs.rstudio.com/" class="uri">http://cran-logs.rstudio.com/</a>. Each day is a separate
gzipped CSV file, and each row is a single package download. For
privacy, IP addresses are anonymized by substituting each days IP
addresses with unique integer IDs.</p>
<p>Here are the first few lines of <a href="http://cran-logs.rstudio.com/2018/2018-05-26.csv.gz" class="uri">http://cran-logs.rstudio.com/2018/2018-05-26.csv.gz</a>
:</p>
<pre><code>&quot;date&quot;,&quot;time&quot;,&quot;size&quot;,&quot;r_version&quot;,&quot;r_arch&quot;,&quot;r_os&quot;,&quot;package&quot;,&quot;version&quot;,&quot;country&quot;,&quot;ip_id&quot;
&quot;2018-05-26&quot;,&quot;20:42:23&quot;,450377,&quot;3.4.4&quot;,&quot;x86_64&quot;,&quot;linux-gnu&quot;,&quot;lubridate&quot;,&quot;1.7.4&quot;,&quot;NL&quot;,1
&quot;2018-05-26&quot;,&quot;20:42:30&quot;,484348,NA,NA,NA,&quot;homals&quot;,&quot;0.9-7&quot;,&quot;GB&quot;,2
&quot;2018-05-26&quot;,&quot;20:42:21&quot;,98484,&quot;3.3.1&quot;,&quot;x86_64&quot;,&quot;darwin13.4.0&quot;,&quot;miniUI&quot;,&quot;0.1.1.1&quot;,&quot;NL&quot;,1
&quot;2018-05-26&quot;,&quot;20:42:27&quot;,518,&quot;3.4.4&quot;,&quot;x86_64&quot;,&quot;linux-gnu&quot;,&quot;RCurl&quot;,&quot;1.95-4.10&quot;,&quot;US&quot;,3</code></pre>
<p>Fortunately for our purposes, theres no need to analyze these logs
at a high level to figure out which days are affected by badly behaved
download scripts. These CRAN mirrors are popular enough that, according
to Chengs Law, there should be plenty of rogue scripts hitting it every
day of the year.</p>
</div>
<div id="a-tour-of-the-app" class="section level2">
<h2>A tour of the app</h2>
<p>The app I built to explore this data, <strong>cranwhales</strong>,
let us examine the behavior of the top downloaders (“whales”) for any
given day, at varying levels of detail. You can view this app live at <a href="https://gallery.shinyapps.io/cranwhales/" class="uri">https://gallery.shinyapps.io/cranwhales/</a>, or download
and run the code yourself at <a href="https://github.com/rstudio/cranwhales" class="uri">https://github.com/rstudio/cranwhales</a>.</p>
<p>When the app starts, the “All traffic” tab shows you the number of
package downloads per hour for all users vs. whales. In this screenshot,
you can see the proportion of files downloaded by the top six
downloaders on May 28, 2018. It may not look like a huge fraction at
first, but keep in mind, we are only talking about six downloaders out
of 52,815 total!</p>
<p><img src="data:image/png;base64,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
<p>The “Biggest whales” tab simply shows the most prolific downloaders,
with their number of downloads performed. Each anonymized IP address has
been assigned an easier-to-remember name, and you can also see the
country code of the original IP address.</p>
<p><img src="data:image/png;base64,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
<p>The “Whales by hour” tab shows the hourly download counts for each
whale individually. In this screenshot, you can see that the
Netherlands <code>relieved_snake</code> downloaded at an extremely
consistent rate during the whole day, while the American
<code>curly_capabara</code> was active only during business hours in
Eastern Standard Time. Still others, like <code>colossal_chicken</code>
out of Hong Kong, was busy all day but at varying rates.</p>
<p><img src="data:image/png;base64,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
<p>The “Detail View” has perhaps the most illuminating information. It
lets you view every download made by a given whale on the day in
question. The x dimension is time and the y dimension is what package
they downloaded, so you can see at a glance exactly how many packages
were downloaded, and how their various package downloads relate to each
other. In this case, <code>relieved_snake</code> downloaded 104
different packages, in the same order, continuously, for the entire
day.</p>
<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABQoAAAOVCAYAAAAhvd30AAAKvmlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU1kax+976SGhBUKH0Jt0gQDSa+jSwUZIIIQSQgoqNlQGR3AsiIiAMqIjIAqOSh0LIoqFQUAB+4AMCuo4WLChMg9Yws7u2d2z/3Pueb98777vfd99957zDwCkMSafnwbLApDOEwnCfD1oMbFxNNwTAAEYSAN5YMJkCfnuoaGBANH89e96P4DMRnTbbCbXv9//r5JjJwpZAEChCCewhax0hM8io5vFF4gAQOUicd3VIv4MVyOsIEAKRPjcDHPmuGeGE+b499k5EWGeCH8EAE9iMgUcAEhoJE7LYnGQPCQ9hC15bC4P4QiEXVjJTDbCRQgvSk/PmOE2hI0S/ikP5285EyQ5mUyOhOd6mRXeiyvkpzHX/p/L8b+Vniaef4cOMkjJAr8w5GqArFl1akaAhHkJwSHzzGXPzp/lZLFf5DyzhJ5x88xmegXMszg10n2emYKFZ7kiRsQ8CzLCJPl5acGBkvyJDAknCr3D5zmJ68OY5+zkiOh5zuJGBc+zMDU8YGGOpyQuEIdJak4S+Eh6TBcu1MZiLrxLlBzht1BDjKQedqKXtyTOi5TM54s8JDn5aaEL9af5SuLCrHDJsyJkg81zCtM/dCFPqGR9QARIBmLAA2yQCAQgAWSANCACNOAFuEAI+MgvJkC2hyhxjWimCc8M/loBl5MsorkjpyiRxuCxzBfRrC2t6ADMnMm5T/6WOnvWIOqNhVhmGwAO+UiQsxBj6gLQ8hQAyvuFmO4bZLvsBuB8D0ssyJqLzWxbgAFEIAMUgArQBLrACJgBa2AHnIAb8Ab+IATpJBasBCykn3Skk9VgPdgM8kAB2A32gVJQAY6AanASnAZN4By4BK6Cm6AH9IMHYAiMghdgArwHUxAE4SAyRIFUIC1IHzKFrCE65AJ5Q4FQGBQLxUMciAeJofXQVqgAKoRKocNQDfQz1AJdgq5DvdA9aBgah95An2EUTIIVYA3YALaA6bA7HABHwCtgDpwJZ8O58E64BK6ET8CN8CX4JtwPD8Ev4EkUQEmhqChtlBmKjvJEhaDiUEkoAWojKh9VjKpE1aFaUZ2o26gh1EvUJzQWTUHT0GZoJ7QfOhLNQmeiN6J3oEvR1ehGdAf6NnoYPYH+hiFj1DGmGEcMAxOD4WBWY/IwxZhjmAbMFUw/ZhTzHovFUrGGWHusHzYWm4Jdh92BPYitx7Zhe7Ej2EkcDqeCM8U540JwTJwIl4c7gDuBu4jrw43iPuKl8Fp4a7wPPg7Pw2/BF+OP4y/g+/DP8FMEWYI+wZEQQmAT1hJ2EY4SWgm3CKOEKaIc0ZDoTIwgphA3E0uIdcQrxIfEt1JSUjpSDlJLpbhSOVIlUqekrkkNS30iyZNMSJ6k5SQxaSepitRGukd6SyaTDchu5DiyiLyTXEO+TH5M/ihNkTaXZkizpTdJl0k3SvdJv5IhyOjLuMuslMmWKZY5I3NL5qUsQdZA1lOWKbtRtky2RXZQdlKOImclFyKXLrdD7rjcdbkxeZy8gby3PFs+V/6I/GX5EQqKokvxpLAoWylHKVcoowpYBUMFhkKKQoHCSYVuhQlFecXFilGKaxTLFM8rDlFRVAMqg5pG3UU9TR2gflbSUHJXSlTarlSn1Kf0QVlN2U05UTlfuV65X/mzCk3FWyVVZY9Kk8ojVbSqiepS1dWqh1SvqL5UU1BzUmOp5audVruvDqubqIepr1M/ot6lPqmhqeGrwdc4oHFZ46UmVdNNM0WzSPOC5rgWRctFi6tVpHVR6zlNkeZOS6OV0DpoE9rq2n7aYu3D2t3aUzqGOpE6W3TqdR7pEnXpukm6RbrtuhN6WnpBeuv1avXu6xP06frJ+vv1O/U/GBgaRBtsM2gyGDNUNmQYZhvWGj40Ihu5GmUaVRrdMcYa041TjQ8a95jAJrYmySZlJrdMYVM7U67pQdPeRZhFDot4iyoXDZqRzNzNssxqzYbNqeaB5lvMm8xfWehZxFnssei0+GZpa5lmedTygZW8lb/VFqtWqzfWJtYs6zLrOzZkGx+bTTbNNq8Xmy5OXHxo8V1bim2Q7TbbdtuvdvZ2Ars6u3F7Pft4+3L7QboCPZS+g37NAePg4bDJ4ZzDJ0c7R5Hjacc/ncycUp2OO40tMVySuOTokhFnHWem82HnIReaS7zLjy5DrtquTNdK1yduum5st2Nuz9yN3VPcT7i/8rD0EHg0eHzwdPTc4NnmhfLy9cr36vaW9470LvV+7KPjw/Gp9ZnwtfVd59vmh/EL8NvjN8jQYLAYNYwJf3v/Df4dAaSA8IDSgCeBJoGCwNYgOMg/aG/Qw2D9YF5wUwgIYYTsDXkUahiaGfrLUuzS0KVlS5+GWYWtD+sMp4SvCj8e/j7CI2JXxINIo0hxZHuUTNTyqJqoD9Fe0YXRQzEWMRtibsaqxnJjm+NwcVFxx+Iml3kv27dsdLnt8rzlAysMV6xZcX2l6sq0ledXyaxirjoTj4mPjj8e/4UZwqxkTiYwEsoTJlierP2sF2w3dhF7PNE5sTDxWZJzUmHSGMeZs5cznuyaXJz8kuvJLeW+TvFLqUj5kBqSWpU6nRadVp+OT49Pb+HJ81J5HRmaGWsyevmm/Dz+UKZj5r7MCUGA4JgQEq4QNosUEPPTJTYSfyceznLJKsv6uDpq9Zk1cmt4a7rWmqzdvvZZtk/2T+vQ61jr2tdrr9+8fniD+4bDG6GNCRvbN+luyt00muObU72ZuDl1869bLLcUbnm3NXpra65Gbk7uyHe+39XmSecJ8ga3OW2r+B79Pff77u022w9s/5bPzr9RYFlQXPBlB2vHjR+sfij5YXpn0s7uXXa7Du3G7ubtHtjjuqe6UK4wu3Bkb9DexiJaUX7Ru32r9l0vXlxcsZ+4X7x/qCSwpPmA3oHdB76UJpf2l3mU1Zerl28v/3CQfbDvkNuhugqNioKKzz9yf7x72PdwY6VBZfER7JGsI0+PRh3t/In+U80x1WMFx75W8aqGqsOqO2rsa2qOqx/fVQvXimvHTyw/0XPS62RznVnd4XpqfcEpcEp86vnP8T8PnA443X6GfqburP7Z8gZKQ34j1Li2caIpuWmoOba5t8W/pb3VqbXhF/Nfqs5pnys7r3h+1wXihdwL0xezL0628dteXuJcGmlf1f7gcszlOx1LO7qvBFy5dtXn6uVO986L15yvnbvueL3lBv1G0027m41dtl0Nv9r+2tBt1914y/5Wc49DT2vvkt4Lfa59l2573b56h3HnZn9wf+9A5MDdweWDQ3fZd8fupd17fT/r/tSDnIeYh/mPZB8VP1Z/XPmb8W/1Q3ZD54e9hruehD95MMIaefG78Pcvo7lPyU+Ln2k9qxmzHjs37jPe83zZ89EX/BdTL/P+kPuj/JXRq7N/uv3ZNREzMfpa8Hr6zY63Km+r3i1+1z4ZOvn4ffr7qQ/5H1U+Vn+if+r8HP352dTqL7gvJV+Nv7Z+C/j2cDp9eprPFDBnrQAKGXBSEgBvqgAgxyLeAfHVROk5zzwraM7nzxL4Tzznq2dlB0CVGwCROQAEIh7lEDL0c+a89YxlinADsI2NZPxDwiQb67lcJMR5Yj5OT7/VAADXCsBXwfT01MHp6a9HkWLvAdCWOefVZ4RF/sEUGlLVIWGX5kbwr/oLUnsLGdUoD3UAAAGeaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAv
<p>Others behave very differently, like <code>freezing_tapir</code>, who
downloaded <code>devtools</code>and <em>only</em>
<code>devtools</code>for the whole day, racking up 19,180 downloads
totalling 7.9 gigabytes for that one package alone!</p>
<p><img src="data:image/png;base64,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
<p>Sadly, the app cant tell us any more than thatit cant explain
<em>why</em> these downloaders are behaving this way, nor can it tell us
their street addresses so that we can send ninjas in black RStudio
helicopters to make them stop.</p>
</div>
<div id="the-implementation" class="section level2">
<h2>The implementation</h2>
<p>Now that youve seen what the app does, lets talk about how it was
implemented, then convert it from sync to async.</p>
<div id="user-interface" class="section level3">
<h3>User interface</h3>
<p>The user interface is a pretty typical shinydashboard. Its important
to note that the UI part of the app is entirely agnostic to whether the
server is written in the sync or async style; when we port the app to
async, we wont touch the UI at all.</p>
<p>There are two major pieces of input we need from users: what
<strong>date</strong> to examine (this app only lets us look at one day
at a time) and <strong>how many</strong> of the most prolific
downloaders to look at. Well put these two controls in the dashboard
sidebar.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">dashboardSidebar</span>(</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">dateInput</span>(<span class="st">&quot;date&quot;</span>, <span class="st">&quot;Date&quot;</span>, <span class="at">value =</span> <span class="fu">Sys.Date</span>() <span class="sc">-</span> <span class="dv">2</span>),</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">numericInput</span>(<span class="st">&quot;count&quot;</span>, <span class="st">&quot;Show top N downloaders:&quot;</span>, <span class="dv">6</span>)</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>)</span></code></pre></div>
<p>(We set <code>date</code> to two days ago by default, because theres
some lag between when a day ends and when its logs are published.)</p>
<p>The rest of the UI code is just typical shinydashboard scaffolding,
plus some <code>shinydashboard::valueBoxOutput</code>s and
<code>plotOutputs</code>. These are so trivial that theyre hardly worth
talking about, but Ill include the code here for completeness. Finally,
theres <code>detailViewUI</code>, a <a href="https://shiny.posit.co/r/articles/improve/modules/">Shiny
module</a> that just contains more of the same (value boxes and
plots).</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a> <span class="fu">dashboardBody</span>(</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">fluidRow</span>(</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">tabBox</span>(<span class="at">width =</span> <span class="dv">12</span>,</span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">tabPanel</span>(<span class="st">&quot;All traffic&quot;</span>,</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">fluidRow</span>(</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">valueBoxOutput</span>(<span class="st">&quot;total_size&quot;</span>, <span class="at">width =</span> <span class="dv">4</span>),</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">valueBoxOutput</span>(<span class="st">&quot;total_count&quot;</span>, <span class="at">width =</span> <span class="dv">4</span>),</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">valueBoxOutput</span>(<span class="st">&quot;total_downloaders&quot;</span>, <span class="at">width =</span> <span class="dv">4</span>)</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a> ),</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">plotOutput</span>(<span class="st">&quot;all_hour&quot;</span>)</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> ),</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">tabPanel</span>(<span class="st">&quot;Biggest whales&quot;</span>,</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a> <span class="fu">plotOutput</span>(<span class="st">&quot;downloaders&quot;</span>, <span class="at">height =</span> <span class="dv">500</span>)</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a> ),</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a> <span class="fu">tabPanel</span>(<span class="st">&quot;Whales by hour&quot;</span>,</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="fu">plotOutput</span>(<span class="st">&quot;downloaders_hour&quot;</span>, <span class="at">height =</span> <span class="dv">500</span>)</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> ),</span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> <span class="fu">tabPanel</span>(<span class="st">&quot;Detail view&quot;</span>,</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="fu">detailViewUI</span>(<span class="st">&quot;details&quot;</span>)</span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a> )</span></code></pre></div>
</div>
<div id="server-logic" class="section level3">
<h3>Server logic</h3>
<p>Based on these inputs and outputs, well write a variety of reactive
expressions and output renderers to download, manipulate, and visualize
the relevant log data.</p>
<p>The reactive expressions:</p>
<ul>
<li><code>data</code> (<code>eventReactive</code>): Whenever
<code>input$date</code> changes, the <code>data</code> reactive
downloads the full log for that day from <a href="http://cran-logs.rstudio.com" class="uri">http://cran-logs.rstudio.com</a>, and parses it.</li>
<li><code>whales</code> (<code>reactive</code>): Reads from
<code>data()</code>, tallies the number of downloads performed by each
unique IP, and returns a data frame of the top <code>input$count</code>
most prolific downloaders, along with their download counts.</li>
<li><code>whale_downloads</code> (<code>reactive</code>): Joins the
<code>data()</code> and <code>whales()</code> data frames, to return all
of the details of the cetacean downloads.</li>
</ul>
<p>The <code>whales</code> reactive expression depends on
<code>data</code>, and <code>whale_downloads</code> depends on
<code>data</code> and <code>whales</code>.</p>
<p><img src="data:image/png;base64,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
<p>The outputs in this app are mostly either <code>renderPlot</code>s
that we populate with <code>ggplot2</code>, or
<code>shinydashboard::renderValueBox</code>es. They all rely on one or
more of the reactive expressions we just described. We wont catalog
them all here, as theyre not individually interesting, but we will look
at some archetypes below.</p>
</div>
</div>
<div id="improving-performance-and-scalability" class="section level2">
<h2>Improving performance and scalability</h2>
<p>While this article is specifically about async, this is a good time
to remind you that there are lots of ways to improve the performance of
a Shiny app. Async is just one tool in the toolbox, and before reaching
for that hammer, take a moment to consider your other options:</p>
<ol style="list-style-type: decimal">
<li>Have I used <a href="https://profvis.r-lib.org/">profvis</a> to
<strong>profile my code</strong> and determine whats actually taking so
long? (Human intuition is a notoriously bad profiler!)</li>
<li>Can I perform any <strong>calculations, summarizations, and
aggregations offline</strong>, when my Shiny app isnt even running, and
save the results to .rds files to be read by the app?</li>
<li>Are there any opportunities to <strong>cache</strong>that is, save
the results of my calculations and use them if I get the same request
later? (See <a href="https://cran.r-project.org/package=memoise">memoise</a>, or roll
your own.)</li>
<li>Am I effectively leveraging <a href="https://posit.co/resources/videos/reactivity-pt-1-joe-cheng/">reactive
programming</a> to make sure my reactives are doing as little work as
possible?</li>
<li>When deploying my app, am I load balancing across multiple R
processes and/or multiple servers? (<a href="https://docs.posit.co/shiny-server/">Shiny Server Pro</a>, <a href="https://docs.posit.co/connect/admin/appendix/configuration/">RStudio
Connect</a>, <a href="https://shiny.posit.co/r/articles/improve/scaling-and-tuning/">ShinyApps.io</a>)</li>
</ol>
<p>These options are more generally useful than using async techniques
because they can dramatically speed up the performance of an app even if
only a single user is using it. While it obviously depends on the
particulars of the app itself, a few lines of precomputation or caching
logic can often lead to 10X-100X better performance. Async, on the other
hand, generally doesnt help make a single session faster. Instead, it
helps a single Shiny process support more concurrent sessions without
getting bogged down.</p>
<p>Async can be an essential tool when there is no way around performing
expensive tasks (i.e. taking multiple seconds) while the user waits. For
example, an app that analyzes any user-specified Twitter profile may get
too many unique queries (assuming most people specify their own Twitter
handle) for caching to be much help. And applications that invite users
to upload their own datasets wont have an opportunity to do any offline
summarizing in advance. If you need to run apps like that and support
lots of concurrent users, async can be a huge help.</p>
<p>In that sense, the cranwhales app isnt a perfect example, because it
has lots of opportunities for precomputation and caching that well
willfully ignore today so that I can better illustrate the points I want
to make about async. When youre working on your own app, though, please
think carefully about <em>all</em> of the different techniques you have
for improving performance.</p>
</div>
<div id="converting-to-async" class="section level2">
<h2>Converting to async</h2>
<p>To quote the article <a href="https://rstudio.github.io/promises/articles/shiny.html"><em>Using
promises with Shiny</em></a>, async programming with Shiny boils down to
following a few steps:</p>
<ol style="list-style-type: decimal">
<li>Identify slow operations in your app.</li>
<li>Convert the slow operations into futures.</li>
<li>Any code that relies on the result of those operations (if any),
whether directly or indirectly, now must be converted to promise
handlers that operate on the future object.</li>
</ol>
<p>In this case, the slow operations are easy to identify: the
downloading and parsing that takes place in the <code>data</code>
reactive expression can each take several long seconds.</p>
<p>Converting the download and parsing operations into futures turns out
to be the most complicated part of the process, for reasons well get
into later.</p>
<p>Assuming we do that successfully, the <code>data</code> reactive
expression will no longer return a data frame, but a
<code>promise</code> object (that resolves to a data frame). Since the
<code>whales</code> and <code>whale_downloads</code> reactive
expressions both rely on <code>data</code>, those will both also need to
be converted to read and return <code>promise</code> objects. And
therefore, because the outputs all rely on one or more reactive
expressions, they will all need to know how to deal with
<code>promise</code> objects.</p>
<p>Async code is infectious like that; once you turn the heart of your
app into a promise, everything downstream must become promise-aware as
well, all the way through to the observers and outputs.</p>
<p>With that overview out of the way, lets dive into the code.</p>
<p>In the sections below, well take a look at the code behind some
outputs and reactive expressions. For each element, well look first at
the sync version, then the async version.</p>
<p>In some cases, these code snippets may be slightly abridged. See the
<a href="https://github.com/rstudio/cranwhales">GitHub repository</a>
for the full code.</p>
<p>Until youve received an introduction to the <code>%...&gt;%</code>
operator, the async code below will make no sense, so if you havent
read <a href="https://rstudio.github.io/promises/articles/intro.html"><em>An
informal intro to async programming</em></a> and/or <a href="https://rstudio.github.io/promises/articles/overview.html"><em>Working
with promises in R</em></a>, I highly recommend doing so before
continuing!</p>
<div id="loading-promises-and-future" class="section level3">
<h3>Loading <code>promises</code> and <code>future</code></h3>
<p>The first thing well do is load the basic libraries of async
programming.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(promises)</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(future)</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="fu">plan</span>(multisession)</span></code></pre></div>
<p>I originally used <code>multiprocess</code> but file downloading
inside a future seemed to fail on Mac. (Ive found that its usually not
worth spending a lot of time trying to figure out why
<code>multiprocess</code> doesnt work for some specific code; instead,
just use <code>multisession</code>, since thats probably going to be
the solution anyway.)</p>
</div>
<div id="the-data-reactive-future_promise-all-the-things" class="section level3">
<h3>The <code>data</code> reactive: future_promise() all the things</h3>
<p>The next thing well do is convert the <code>data</code> event
reactive to use <code>future</code> for the expensive bits. The original
code looks lke this:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="co"># SYNCHRONOUS version</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>data <span class="ot">&lt;-</span> <span class="fu">eventReactive</span>(input<span class="sc">$</span>date, {</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a> date <span class="ot">&lt;-</span> input<span class="sc">$</span>date <span class="co"># Example: 2018-05-28</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a> year <span class="ot">&lt;-</span> lubridate<span class="sc">::</span><span class="fu">year</span>(date) <span class="co"># Example: &quot;2018&quot;</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a> url <span class="ot">&lt;-</span> <span class="fu">glue</span>(<span class="st">&quot;http://cran-logs.rstudio.com/{year}/{date}.csv.gz&quot;</span>)</span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a> path <span class="ot">&lt;-</span> <span class="fu">file.path</span>(<span class="st">&quot;data_cache&quot;</span>, <span class="fu">paste0</span>(date, <span class="st">&quot;.csv.gz&quot;</span>))</span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">withProgress</span>(<span class="at">value =</span> <span class="cn">NULL</span>, {</span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="sc">!</span><span class="fu">file.exists</span>(path)) {</span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a> <span class="fu">setProgress</span>(<span class="at">message =</span> <span class="st">&quot;Downloading data...&quot;</span>)</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a> <span class="fu">download.file</span>(url, path)</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a> <span class="fu">setProgress</span>(<span class="at">message =</span> <span class="st">&quot;Parsing data...&quot;</span>)</span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a> <span class="fu">read_csv</span>(path, <span class="at">col_types =</span> <span class="st">&quot;Dti---c-ci&quot;</span>, <span class="at">progress =</span> <span class="cn">FALSE</span>)</span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>(Earlier, I said we wouldnt take advantage of precomputation or
caching. That wasnt entirely true; in the code above, we cache the log
files we download in a <code>data_cache</code> directory. I couldnt
bring myself to put my internet connection through that level of abuse,
as I knew Id be running this code thousands of times as I load tested
it.)</p>
<p>For now, well lose the
<code>withProgress</code>/<code>setProgress</code> reporting, since
doing that correctly requires some more advanced techniques that well
talk about later. Well come back and fix this code later, but for
now:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ASYNCHRONOUS version</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>data <span class="ot">&lt;-</span> <span class="fu">eventReactive</span>(input<span class="sc">$</span>date, {</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a> date <span class="ot">&lt;-</span> input<span class="sc">$</span>date</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a> year <span class="ot">&lt;-</span> lubridate<span class="sc">::</span><span class="fu">year</span>(date)</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a> url <span class="ot">&lt;-</span> <span class="fu">glue</span>(<span class="st">&quot;http://cran-logs.rstudio.com/{year}/{date}.csv.gz&quot;</span>)</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a> path <span class="ot">&lt;-</span> <span class="fu">file.path</span>(<span class="st">&quot;data_cache&quot;</span>, <span class="fu">paste0</span>(date, <span class="st">&quot;.csv.gz&quot;</span>))</span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">future_promise</span>({</span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="sc">!</span><span class="fu">file.exists</span>(path)) {</span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">download.file</span>(url, path)</span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a> <span class="fu">read_csv</span>(path, <span class="at">col_types =</span> <span class="st">&quot;Dti---c-ci&quot;</span>, <span class="at">progress =</span> <span class="cn">FALSE</span>)</span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>Pretty straightforward. This reactive now returns a future (which
counts as a promise), not a data frame.</p>
<p>Remember that we <strong>must</strong> read any reactive values
(including <code>input</code>) and reactive expressions <a href="https://rstudio.github.io/promises/articles/shiny.html#shiny-specific-caveats-and-limitations">from
<strong>outside</strong> the future</a>. (You will get an error if you
attempt to read one from inside the future.)</p>
<p>At this point, since there are no other long-running operations we
want to make asynchronous, were actually done interacting directly with
the <code>future</code> package. The rest of the reactive expressions
will deal with the future returned by <code>data</code> using general
async functions and operators from <code>promises</code>.</p>
</div>
<div id="the-whales-reactive-simple-pipelines-are-simple" class="section level3">
<h3>The <code>whales</code> reactive: simple pipelines are simple</h3>
<p>The <code>whales</code> reactive takes the data frame from
<code>data</code>, and uses dplyr to find the top
<code>input$count</code> most prolific downloaders.</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="co"># SYNCHRONOUS version</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>whales <span class="ot">&lt;-</span> <span class="fu">reactive</span>({</span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">data</span>() <span class="sc">%&gt;%</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">count</span>(ip_id) <span class="sc">%&gt;%</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">arrange</span>(<span class="fu">desc</span>(n)) <span class="sc">%&gt;%</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">head</span>(input<span class="sc">$</span>count)</span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>Since <code>data()</code> now returns a promise, the whole function
needs to be modified to deal with promises.</p>
<p>This is basically a best-case scenario for working with
<code>promises</code>. The whole expression consists of a single
magrittr pipeline. Theres only one object (<code>data()</code>) thats
been converted to a promise. The promise object only appears once, at
the head of the pipeline.</p>
<p>When the stars align like this, converting this code to async is
literally as easy as replacing each <code>%&gt;%</code> with
<code>%...&gt;%</code>:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ASYNCHRONOUS version</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>whales <span class="ot">&lt;-</span> <span class="fu">reactive</span>({</span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">data</span>() <span class="sc">%...&gt;%</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">count</span>(ip_id) <span class="sc">%...&gt;%</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">arrange</span>(<span class="fu">desc</span>(n)) <span class="sc">%...&gt;%</span></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">head</span>(input<span class="sc">$</span>count)</span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>The input (<code>data()</code>) is a promise, the resulting output
object is a promise, each stage of the pipeline returns a promise; but
we can read and write this code almost as easily as the synchronous
version!</p>
<p>An example this simple may seem reductive, but this best-case
scenario happens surprisingly often, if your coding style is influenced
by the tidyverse. In this example app, <strong>59%</strong> of the
reactives, observers, and outputs were converted using nothing more than
replacing <code>%&gt;%</code> with <code>%...&gt;%</code>.</p>
<p>One last thing before we move on. In the last section, I emphasized
that reactive values cannot be read from inside a future. Here, were
using <code>head(input$count)</code> inside a promise-pipeline; since
<code>data()</code> is written using a future, doesnt that mean… well…
isnt this wrong?</p>
<p>Nope—this code is just fine. The prohibition is against reading
reactive values/expressions from <em>inside</em> a future, because code
inside a future is executed in a totally different R process. The steps
in a promise-pipeline arent futures, but promise handlers. These arent
executed in a different process; rather, theyre executed back in the
original R process after a promise is resolved. Were allowed and
expected to access reactive values and expressions from these
handlers.</p>
</div>
<div id="the-whale_downloads-reactive-reading-from-multiple-promises" class="section level3">
<h3>The <code>whale_downloads</code> reactive: reading from multiple
promises</h3>
<p>The <code>whale_downloads</code> reactive is a bit more complicated
case.</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="co"># SYNCHRONOUS version</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>whale_downloads <span class="ot">&lt;-</span> <span class="fu">reactive</span>({</span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">data</span>() <span class="sc">%&gt;%</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">inner_join</span>(<span class="fu">whales</span>(), <span class="st">&quot;ip_id&quot;</span>) <span class="sc">%&gt;%</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">select</span>(<span class="sc">-</span>n)</span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>Looks simple, but we cant just do a simple replacement this time.
Can you see why?</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="co"># BAD VERSION DOESN&#39;T WORK</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>whale_downloads <span class="ot">&lt;-</span> <span class="fu">reactive</span>({</span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">data</span>() <span class="sc">%...&gt;%</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">inner_join</span>(<span class="fu">whales</span>(), <span class="st">&quot;ip_id&quot;</span>) <span class="sc">%...&gt;%</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">select</span>(<span class="sc">-</span>n)</span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>Remember, both <code>data()</code> and <code>whales()</code> now
return a promise object, not a data frame. None of the dplyr verbs know
how to deal with promises natively (and the same is true for almost
every other R function, anywhere in the R universe).</p>
<p>Were able to use <code>%...&gt;%</code> with promises on the
left-hand side and regular dplyr calls on the right-hand side, only
because the <code>%...&gt;%</code> operator “unwraps” the promise object
for us, yielding a regular object (data frame or whatever) to be passed
to dplyr. But in this case, were passing <code>whales()</code>, which a
promise object, directly to <code>inner_join</code>, and
<code>inner_join</code> has no idea what to do with it.</p>
<p>The fundamental thing to pattern-match on here, is that <strong>we
have a block of code that relies on more than one promise
object</strong>, and that means <code>%...&gt;%</code> wont be enough.
This is a pretty common situation as well, and occurs in
<strong>12%</strong> of reactives and outputs in this example app.</p>
<p>Heres what the real solution looks like:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ASYNCHRONOUS version</span></span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a>whale_downloads <span class="ot">&lt;-</span> <span class="fu">reactive</span>({</span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">promise_all</span>(<span class="at">data_df =</span> <span class="fu">data</span>(), <span class="at">whales_df =</span> <span class="fu">whales</span>()) <span class="sc">%...&gt;%</span> <span class="fu">with</span>({</span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a> data_df <span class="sc">%&gt;%</span></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">inner_join</span>(whales_df, <span class="st">&quot;ip_id&quot;</span>) <span class="sc">%&gt;%</span></span>
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">select</span>(<span class="sc">-</span>n)</span>
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<div id="promises-the-gathering" class="section level4">
<h4>Promises: the Gathering</h4>
<p>This solution uses the <a href="https://rstudio.github.io/promises/articles/combining.html#gathering">promise
gathering</a> pattern, which combines <code>promises_all</code>,
<code>%...&gt;%</code>, and <code>with</code>.</p>
<ul>
<li>The <code>promise_all</code> function gathers multiple promise
objects together, and returns a single promise object. This new promise
object doesnt resolve until all the input promise objects are resolved,
and it yields a list of those results.</li>
</ul>
<div class="sourceCode" id="cb12"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="sc">&gt;</span> <span class="fu">promise_all</span>(<span class="at">a =</span> <span class="fu">future_promise</span>(<span class="st">&quot;Hello&quot;</span>), <span class="at">b =</span> <span class="fu">future_promise</span>(<span class="st">&quot;World&quot;</span>)) <span class="sc">%...&gt;%</span> <span class="fu">print</span>()</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="sc">$</span>a</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a>[<span class="dv">1</span>] <span class="st">&quot;Hello&quot;</span></span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="sc">$</span>b</span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a>[<span class="dv">1</span>] <span class="st">&quot;World&quot;</span></span></code></pre></div>
<ul>
<li>The <code>%...&gt;%</code>, as before, “unwraps” the promise object
and passes the result to its right hand side.</li>
<li>The <code>with</code> function (from base R) takes a named list, and
makes it into a sort of virtual parent environment while evaluating a
code block you pass it.</li>
</ul>
<div class="sourceCode" id="cb13"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="sc">&gt;</span> x <span class="sc">+</span> y</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a>Error<span class="sc">:</span> object <span class="st">&#39;x&#39;</span> not found</span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a><span class="sc">&gt;</span> <span class="fu">with</span>(<span class="fu">list</span>(<span class="at">x =</span> <span class="dv">1</span>, <span class="at">y =</span> <span class="dv">2</span>), {</span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a><span class="sc">+</span> x <span class="sc">+</span> y</span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a><span class="sc">+</span> })</span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a>[<span class="dv">1</span>] <span class="dv">3</span></span></code></pre></div>
<p>Lets once again combine the three, with the simplest possible
example of the gathering pattern:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="sc">&gt;</span> <span class="fu">promise_all</span>(<span class="at">x =</span> <span class="fu">future_promise</span>(<span class="st">&quot;Hello&quot;</span>), <span class="at">y =</span> <span class="fu">future_promise</span>(<span class="st">&quot;World&quot;</span>)) <span class="sc">%...&gt;%</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="sc">+</span> <span class="fu">with</span>({ <span class="fu">paste</span>(x, y) }) <span class="sc">%...&gt;%</span></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="sc">+</span> <span class="fu">print</span>()</span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a>[<span class="dv">1</span>] <span class="st">&quot;Hello World&quot;</span></span></code></pre></div>
<p>You can make use of this pattern without remembering exactly how
these pieces combine. Just remember that the arguments to
<code>promise_all</code> provide the promise objects
(<code>future_promise(1)</code> and <code>future_promise(2)</code>),
along with the names you want to use to refer to their yielded values
(<code>x</code> and <code>y</code>); and the code block you put in
<code>with()</code> can refer to those names without worrying about the
fact that they were ever promises to begin with.</p>
</div>
</div>
<div id="the-total_downloaders-value-box-simple-pipelines-are-for-output-too" class="section level3">
<h3>The <code>total_downloaders</code> value box: simple pipelines are
for output, too</h3>
<p><img src="data:image/png;base64,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
<p>All of the value boxes in this app ended up looking a lot like
this:</p>
<div class="sourceCode" id="cb15"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="co"># SYNCHRONOUS version</span></span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a>output<span class="sc">$</span>total_downloaders <span class="ot">&lt;-</span> <span class="fu">renderValueBox</span>({</span>
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">data</span>() <span class="sc">%&gt;%</span></span>
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">pull</span>(ip_id) <span class="sc">%&gt;%</span></span>
<span id="cb15-6"><a href="#cb15-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">unique</span>() <span class="sc">%&gt;%</span></span>
<span id="cb15-7"><a href="#cb15-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">length</span>() <span class="sc">%&gt;%</span></span>
<span id="cb15-8"><a href="#cb15-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">format</span>(<span class="at">big.mark =</span> <span class="st">&quot;,&quot;</span>) <span class="sc">%&gt;%</span></span>
<span id="cb15-9"><a href="#cb15-9" aria-hidden="true" tabindex="-1"></a> <span class="fu">valueBox</span>(<span class="st">&quot;unique downloaders&quot;</span>)</span>
<span id="cb15-10"><a href="#cb15-10" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>This is structurally no different than the <code>whales</code>
best-case scenario reactive. One thing worth pointing out is that an
async <code>renderValueBox</code> means you return a promise that
returns a <code>valueBox</code>; you <em>dont</em> return a
<code>valueBox</code> to whom you have passed a promise.</p>
<p>Meaning, you <em>dont</em> do this:</p>
<div class="sourceCode" id="cb16"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="co"># BAD VERSION DOESN&#39;T WORK</span></span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a>output<span class="sc">$</span>total_downloaders <span class="ot">&lt;-</span> <span class="fu">renderValueBox</span>({</span>
<span id="cb16-4"><a href="#cb16-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">valueBox</span>(</span>
<span id="cb16-5"><a href="#cb16-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">data</span>() <span class="sc">%...&gt;%</span></span>
<span id="cb16-6"><a href="#cb16-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">pull</span>(ip_id) <span class="sc">%...&gt;%</span></span>
<span id="cb16-7"><a href="#cb16-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">unique</span>() <span class="sc">%...&gt;%</span></span>
<span id="cb16-8"><a href="#cb16-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">length</span>() <span class="sc">%...&gt;%</span></span>
<span id="cb16-9"><a href="#cb16-9" aria-hidden="true" tabindex="-1"></a> <span class="fu">format</span>(<span class="at">big.mark =</span> <span class="st">&quot;,&quot;</span>),</span>
<span id="cb16-10"><a href="#cb16-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;unique downloaders&quot;</span></span>
<span id="cb16-11"><a href="#cb16-11" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb16-12"><a href="#cb16-12" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>Instead, you do this:</p>
<div class="sourceCode" id="cb17"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ASYNCHRONOUS version</span></span>
<span id="cb17-2"><a href="#cb17-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb17-3"><a href="#cb17-3" aria-hidden="true" tabindex="-1"></a>output<span class="sc">$</span>total_downloaders <span class="ot">&lt;-</span> <span class="fu">renderValueBox</span>({</span>
<span id="cb17-4"><a href="#cb17-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">data</span>() <span class="sc">%...&gt;%</span></span>
<span id="cb17-5"><a href="#cb17-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">pull</span>(ip_id) <span class="sc">%...&gt;%</span></span>
<span id="cb17-6"><a href="#cb17-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">unique</span>() <span class="sc">%...&gt;%</span></span>
<span id="cb17-7"><a href="#cb17-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">length</span>() <span class="sc">%...&gt;%</span></span>
<span id="cb17-8"><a href="#cb17-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">format</span>(<span class="at">big.mark =</span> <span class="st">&quot;,&quot;</span>) <span class="sc">%...&gt;%</span></span>
<span id="cb17-9"><a href="#cb17-9" aria-hidden="true" tabindex="-1"></a> <span class="fu">valueBox</span>(<span class="st">&quot;unique downloaders&quot;</span>)</span>
<span id="cb17-10"><a href="#cb17-10" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>The other trick worth nothing is the <code>pull</code> verb, which is
used to retrieve a specific column of a data frame as a vector (similar
to <code>$</code> or <code>[[</code>). In this case,
<code>pull(data, ip_id)</code> is equivalent to
<code>data[[&quot;ip_id&quot;]]</code>. Note that <code>pull</code> is part of
dplyr and isnt specific to promises.</p>
</div>
<div id="the-biggest_whales-plot-getting-untidy" class="section level3">
<h3>The <code>biggest_whales</code> plot: getting untidy</h3>
<p>In a cruel twist of API design fate, one of the cornerstone packages
of the tidyverse lacks a tidy API. Im referring, of course, to
<code>ggplot2</code>:</p>
<div class="sourceCode" id="cb18"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="co"># SYNCHRONOUS version</span></span>
<span id="cb18-2"><a href="#cb18-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb18-3"><a href="#cb18-3" aria-hidden="true" tabindex="-1"></a>output<span class="sc">$</span>downloaders <span class="ot">&lt;-</span> <span class="fu">renderPlot</span>({</span>
<span id="cb18-4"><a href="#cb18-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">whales</span>() <span class="sc">%&gt;%</span></span>
<span id="cb18-5"><a href="#cb18-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">ggplot</span>(<span class="fu">aes</span>(ip_name, n)) <span class="sc">+</span></span>
<span id="cb18-6"><a href="#cb18-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">geom_bar</span>(<span class="at">stat =</span> <span class="st">&quot;identity&quot;</span>) <span class="sc">+</span></span>
<span id="cb18-7"><a href="#cb18-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">ylab</span>(<span class="st">&quot;Downloads on this day&quot;</span>)</span>
<span id="cb18-8"><a href="#cb18-8" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>While <code>dplyr</code> and other tidyverse packages are designed to
link calls together with <code>%&gt;%</code>, the older
<code>ggplot2</code> package uses the <code>+</code> operator. This is
mostly a small aesthetic wart when synchronous code, but its a real
problem with async, because the <code>promises</code> package doesnt
currently have a promise-aware replacement for <code>+</code> like it
does for <code>%&gt;%</code>.</p>
<p>Fortunately, theres a pretty good escape hatch for
<code>%&gt;%</code>, and <code>%...&gt;%</code> inherited it too.
Instead of a pipeline stage being a simple function call, you can put a
<code>{</code> and <code>}</code> delimited code block, and inside of
that code block, you can access the “it” value using a period
(<code>.</code>).</p>
<div class="sourceCode" id="cb19"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ASYNCHRONOUS version</span></span>
<span id="cb19-2"><a href="#cb19-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-3"><a href="#cb19-3" aria-hidden="true" tabindex="-1"></a>output<span class="sc">$</span>downloaders <span class="ot">&lt;-</span> <span class="fu">renderPlot</span>({</span>
<span id="cb19-4"><a href="#cb19-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">whales</span>() <span class="sc">%...&gt;%</span> {</span>
<span id="cb19-5"><a href="#cb19-5" aria-hidden="true" tabindex="-1"></a> whale_df <span class="ot">&lt;-</span> .</span>
<span id="cb19-6"><a href="#cb19-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">ggplot</span>(whale_df, <span class="fu">aes</span>(ip_name, n)) <span class="sc">+</span></span>
<span id="cb19-7"><a href="#cb19-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">geom_bar</span>(<span class="at">stat =</span> <span class="st">&quot;identity&quot;</span>) <span class="sc">+</span></span>
<span id="cb19-8"><a href="#cb19-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">ylab</span>(<span class="st">&quot;Downloads on this day&quot;</span>)</span>
<span id="cb19-9"><a href="#cb19-9" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb19-10"><a href="#cb19-10" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p><strong>The importance of this pattern cannot be overstated!</strong>
Using <code>%...&gt;%</code> and simple calls alone, youre restricted
to doing pipeline-compatible operations. But <code>%...&gt;%</code>
together with a curly-brace code block means your handler code can be
any shape or size. Once inside that code block, you have a regular,
non-promise value in <code>.</code> (if you even want to use
it—sometimes you dont, as well see later). You can have zero, one, or
more statements. You can use the <code>.</code> multiple times, in
nested expressions, whatever.</p>
<p>Tip: if you have extensive or complex code to put in a code block,
start the block by creating a properly named variable to store the value
of <code>.</code>. The reason for this is that <code>.</code> may
acquire a different meaning than you intend as you add code to the code
block. For example, if a magrittr pipeline starts with <code>.</code>,
instead of evaluating the pipeline and returning a value, it creates a
function that takes a single argument. So the following code wouldnt
filter the resolved value of <code>whales()</code>, but instead, create
an anonymous function that calls <code>filter(n &gt; 1000)</code> on
whatever you pass it.</p>
<div class="sourceCode" id="cb20"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb20-1"><a href="#cb20-1" aria-hidden="true" tabindex="-1"></a><span class="fu">whales</span>() <span class="sc">%...&gt;%</span> {</span>
<span id="cb20-2"><a href="#cb20-2" aria-hidden="true" tabindex="-1"></a> . <span class="sc">%&gt;%</span> <span class="fu">filter</span>(n <span class="sc">&gt;</span> <span class="dv">1000</span>)</span>
<span id="cb20-3"><a href="#cb20-3" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>This fixes it:</p>
<div class="sourceCode" id="cb21"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb21-1"><a href="#cb21-1" aria-hidden="true" tabindex="-1"></a><span class="fu">whales</span>() <span class="sc">%...&gt;%</span> {</span>
<span id="cb21-2"><a href="#cb21-2" aria-hidden="true" tabindex="-1"></a> whales_df <span class="ot">&lt;-</span> .</span>
<span id="cb21-3"><a href="#cb21-3" aria-hidden="true" tabindex="-1"></a> whales_df <span class="sc">%&gt;%</span> <span class="fu">filter</span>(n <span class="sc">&gt;</span> <span class="dv">1000</span>)</span>
<span id="cb21-4"><a href="#cb21-4" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>There are other ways to work around the above problem as well, but I
like this fix because it doesnt require any thought or care. Just give
the <code>.</code> value a new name, and forget the <code>.</code>
exists.</p>
<p>For untidy code with a single promise object, just remember: pair a
single <code>%...&gt;%</code> with a code block and you should be able
to do almost anything.</p>
</div>
<div id="revisiting-the-data-reactive-progress-support" class="section level3">
<h3>Revisiting the <code>data</code> reactive: progress support</h3>
<p>Now that we have discussed a few techniques for writing async code,
lets come back to our original <code>data</code> event reactive, and
this time do a more faithful async conversion that preserves the
progress reporting functionality of the original.</p>
<p>Again, heres the original sync code:</p>
<div class="sourceCode" id="cb22"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb22-1"><a href="#cb22-1" aria-hidden="true" tabindex="-1"></a><span class="co"># SYNCHRONOUS version</span></span>
<span id="cb22-2"><a href="#cb22-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb22-3"><a href="#cb22-3" aria-hidden="true" tabindex="-1"></a>data <span class="ot">&lt;-</span> <span class="fu">eventReactive</span>(input<span class="sc">$</span>date, {</span>
<span id="cb22-4"><a href="#cb22-4" aria-hidden="true" tabindex="-1"></a> date <span class="ot">&lt;-</span> input<span class="sc">$</span>date <span class="co"># Example: 2018-05-28</span></span>
<span id="cb22-5"><a href="#cb22-5" aria-hidden="true" tabindex="-1"></a> year <span class="ot">&lt;-</span> lubridate<span class="sc">::</span><span class="fu">year</span>(date) <span class="co"># Example: &quot;2018&quot;</span></span>
<span id="cb22-6"><a href="#cb22-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb22-7"><a href="#cb22-7" aria-hidden="true" tabindex="-1"></a> url <span class="ot">&lt;-</span> <span class="fu">glue</span>(<span class="st">&quot;http://cran-logs.rstudio.com/{year}/{date}.csv.gz&quot;</span>)</span>
<span id="cb22-8"><a href="#cb22-8" aria-hidden="true" tabindex="-1"></a> path <span class="ot">&lt;-</span> <span class="fu">file.path</span>(<span class="st">&quot;data_cache&quot;</span>, <span class="fu">paste0</span>(date, <span class="st">&quot;.csv.gz&quot;</span>))</span>
<span id="cb22-9"><a href="#cb22-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb22-10"><a href="#cb22-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">withProgress</span>(<span class="at">value =</span> <span class="cn">NULL</span>, {</span>
<span id="cb22-11"><a href="#cb22-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb22-12"><a href="#cb22-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="sc">!</span><span class="fu">file.exists</span>(path)) {</span>
<span id="cb22-13"><a href="#cb22-13" aria-hidden="true" tabindex="-1"></a> <span class="fu">setProgress</span>(<span class="at">message =</span> <span class="st">&quot;Downloading data...&quot;</span>)</span>
<span id="cb22-14"><a href="#cb22-14" aria-hidden="true" tabindex="-1"></a> <span class="fu">download.file</span>(url, path)</span>
<span id="cb22-15"><a href="#cb22-15" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb22-16"><a href="#cb22-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb22-17"><a href="#cb22-17" aria-hidden="true" tabindex="-1"></a> <span class="fu">setProgress</span>(<span class="at">message =</span> <span class="st">&quot;Parsing data...&quot;</span>)</span>
<span id="cb22-18"><a href="#cb22-18" aria-hidden="true" tabindex="-1"></a> <span class="fu">read_csv</span>(path, <span class="at">col_types =</span> <span class="st">&quot;Dti---c-ci&quot;</span>, <span class="at">progress =</span> <span class="cn">FALSE</span>)</span>
<span id="cb22-19"><a href="#cb22-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb22-20"><a href="#cb22-20" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb22-21"><a href="#cb22-21" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>Progress reporting currently presents two challenges for future.</p>
<p>First, the <code>withProgress({...})</code> function cannot be used
with async. <code>withProgress</code> is designed to wrap a slow
synchronous action, and dismisses its progress dialog when the block of
code it wraps is done executing. Since the call to
<code>future_promise()</code> will return immediately even though the
actual task is far from done, using <code>withProgress</code> wont
work; the progress dialog would be dismissed before the download even
got going.</p>
<p>Its conceivable that <code>withProgress</code> could gain promise
compatibility someday, but its not in Shiny v1.1.0. In the meantime, we
can work around this by using the alternative, <a href="https://shiny.posit.co/r/reference/shiny/latest/progress.html">object-oriented
progress API</a> that Shiny offers. Its a bit more verbose and fiddly
than <code>withProgress</code>/<code>setProgress</code>, but it is
flexible enough to work with futures/promises.</p>
<p>Second, progress messages cant be sent from futures. This is simply
because futures are executed in child processes, which dont have direct
access to the browser like the main Shiny process does.</p>
<p>Its conceivable that <code>future</code> could gain the ability for
child processes to communicate back to their parents, but no good
solution exists at the time of this writing. In the meantime, we can
work around this by taking the one future that does both downloading and
parsing, and splitting it into two separate futures. After the download
future has completed, we can send a progress message that parsing is
beginning, and then start the parsing future.</p>
<p>The regrettably complicated solution is below.</p>
<div class="sourceCode" id="cb23"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb23-1"><a href="#cb23-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ASYNCHRONOUS version</span></span>
<span id="cb23-2"><a href="#cb23-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb23-3"><a href="#cb23-3" aria-hidden="true" tabindex="-1"></a>data <span class="ot">&lt;-</span> <span class="fu">eventReactive</span>(input<span class="sc">$</span>date, {</span>
<span id="cb23-4"><a href="#cb23-4" aria-hidden="true" tabindex="-1"></a> date <span class="ot">&lt;-</span> input<span class="sc">$</span>date</span>
<span id="cb23-5"><a href="#cb23-5" aria-hidden="true" tabindex="-1"></a> year <span class="ot">&lt;-</span> lubridate<span class="sc">::</span><span class="fu">year</span>(date)</span>
<span id="cb23-6"><a href="#cb23-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb23-7"><a href="#cb23-7" aria-hidden="true" tabindex="-1"></a> url <span class="ot">&lt;-</span> <span class="fu">glue</span>(<span class="st">&quot;http://cran-logs.rstudio.com/{year}/{date}.csv.gz&quot;</span>)</span>
<span id="cb23-8"><a href="#cb23-8" aria-hidden="true" tabindex="-1"></a> path <span class="ot">&lt;-</span> <span class="fu">file.path</span>(<span class="st">&quot;data_cache&quot;</span>, <span class="fu">paste0</span>(date, <span class="st">&quot;.csv.gz&quot;</span>))</span>
<span id="cb23-9"><a href="#cb23-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb23-10"><a href="#cb23-10" aria-hidden="true" tabindex="-1"></a> p <span class="ot">&lt;-</span> Progress<span class="sc">$</span><span class="fu">new</span>()</span>
<span id="cb23-11"><a href="#cb23-11" aria-hidden="true" tabindex="-1"></a> p<span class="sc">$</span><span class="fu">set</span>(<span class="at">value =</span> <span class="cn">NULL</span>, <span class="at">message =</span> <span class="st">&quot;Downloading data...&quot;</span>)</span>
<span id="cb23-12"><a href="#cb23-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">future_promise</span>({</span>
<span id="cb23-13"><a href="#cb23-13" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="sc">!</span><span class="fu">file.exists</span>(path)) {</span>
<span id="cb23-14"><a href="#cb23-14" aria-hidden="true" tabindex="-1"></a> <span class="fu">download.file</span>(url, path)</span>
<span id="cb23-15"><a href="#cb23-15" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb23-16"><a href="#cb23-16" aria-hidden="true" tabindex="-1"></a> }) <span class="sc">%...&gt;%</span></span>
<span id="cb23-17"><a href="#cb23-17" aria-hidden="true" tabindex="-1"></a> { p<span class="sc">$</span><span class="fu">set</span>(<span class="at">message =</span> <span class="st">&quot;Parsing data...&quot;</span>) } <span class="sc">%...&gt;%</span></span>
<span id="cb23-18"><a href="#cb23-18" aria-hidden="true" tabindex="-1"></a> { <span class="fu">future_promise</span>(<span class="fu">read_csv</span>(path, <span class="at">col_types =</span> <span class="st">&quot;Dti---c-ci&quot;</span>, <span class="at">progress =</span> <span class="cn">FALSE</span>)) } <span class="sc">%&gt;%</span></span>
<span id="cb23-19"><a href="#cb23-19" aria-hidden="true" tabindex="-1"></a> <span class="fu">finally</span>(<span class="sc">~</span>p<span class="sc">$</span><span class="fu">close</span>())</span>
<span id="cb23-20"><a href="#cb23-20" aria-hidden="true" tabindex="-1"></a>})</span></code></pre></div>
<p>The single future we wrote earlier has now become a pipeline of
promises:</p>
<ol style="list-style-type: decimal">
<li>future (download)</li>
<li>send progress message</li>
<li>future (parse)</li>
<li>dismiss progress dialog</li>
</ol>
<p>Note that neither the R6 call <code>p$set(message = ...)</code> nor
the second <code>future_promise()</code> call are tidy, so they use
curly-brace blocks, as discussed in the above section about
<code>biggest_whales</code>.</p>
<p>The final step of dismissing the progress dialog doesnt use
<code>%...&gt;%</code> at all; because we want the progress dialog to
dismiss whether the download and parse operations succeed or fail, we
use the regular pipe <code>%&gt;%</code> and <code>finally()</code>
function instead. See the relevant section in <a href="https://rstudio.github.io/promises/articles/overview.html#cleaning-up-with-finally"><em>Working
with promises in R</em></a> to learn more.</p>
<p>With these changes in place, weve now covered all of the changes to
the application. You can see the full changes side-by-side via <a href="https://github.com/rstudio/cranwhales/compare/sync...async?diff=split">this
GitHub diff</a>.</p>
</div>
</div>
<div id="measuring-scalability" class="section level2">
<h2>Measuring scalability</h2>
<p>It was a fair amount of work to do the sync-to-async conversion. Now
wed like to know if the conversion to async had the desired effect:
improved responsiveness (i.e. lower latency) when the number of
simultaneous visitors increases.</p>
<div id="load-testing-with-shiny-coming-soon" class="section level3">
<h3>Load testing with Shiny (coming soon)</h3>
<p>At the time of this writing, we are working on a suite of load
testing tools for Shiny that is not publicly available yet, but was
previewed by Sean Lopp during his <a href="https://posit.co/resources/">epic rstudio::conf 2018 talk</a>
about running a Shiny load test with 10,000 simulated concurrent
users.</p>
<p>You use these tools to easily <strong>record</strong> yourself using
your Shiny app, which creates a test script; then <strong>play
back</strong> that test script, but multiplied by
dozens/hundreds/thousands of simulated concurrent users; and finally,
<strong>analyze</strong> the timing data generated during the playback
step to see what kind of latency the simulated users experienced.</p>
<p>To examine the effects of my async refactor, I recorded a simple test
script by loading up the app, waiting for the first tab to appear, then
clicking through each of the other tabs, pausing for several seconds
each time before moving on to the next. When using the app without any
other visitors, the homepage fully loads in less than a second, and the
initial loading of data and rendering of the plot on the default tab
takes about 7 seconds. After that, each tab takes no more than a couple
of seconds to load. Overall, the entire test script, including time
where the user is thinking, takes about 40 seconds under ideal settings
(i.e. only a single concurrent user).</p>
<p>I then used this test script to generate load against the Shiny app
running in my local RStudio. With the settings I chose, the playback
tool introduced one new “user” session every 5 seconds, until 50
sessions total had been launched; then it waited until all the sessions
were complete. I ran this test on both the sync and async versions in
turn, which generated the following results.</p>
</div>
<div id="sync-vs.-async-performance" class="section level3">
<h3>Sync vs. async performance</h3>
<p><img src="data:image/png;base64,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
<p>In this plot, each row represents a single session, and the x
dimension represents time. Each of the rectangles represents a single
“step” in the test script, be it downloading the HTML for the homepage,
fetching one of the two dozen JavaScript/CSS files, or waiting for the
server to update outputs. So the wider a rectangle is, the longer the
user had to wait. (The empty gaps between rectangles represents time the
app is waiting for the user to click an input; their widths are
hard-coded into the test script.)</p>
<p>Of particular importance are the red and pink rectangles, as these
represent the initial page load. While these are taking place, the user
is staring at a blank page, probably wondering if the server is down.
Long waits during this stage are not only undesirable, but surprising
and incomprehensible to the user; whereas the same user is probably
prepared to wait a little while for a complicated visualization to be
rendered in response to an input change.</p>
<p>And as you can see from this plot, the behavior of the async app is
much improved in the critical metric of homepage/JS/CSS loading time.
The sync version of the app starts displaying unacceptably long red/pink
loading times as early as session 15, and by session #44 the maximum
page load time has exceeded one minute. The async version at that point
is showing 25 second load times, which is far from great, but still a
significant step in the right direction.</p>
</div>
<div id="further-optimizations" class="section level3">
<h3>Further optimizations</h3>
<p>I was surprised that the async versions page load times werent even
faster, and even more surprised to see that the blue rectangles were
just as wide as the sync version. Why isnt the async version way
faster? The sync version does all of its work on a single thread, and I
specifically designed this app to be a nightmare for scalability by
having each session kick off by parsing hundreds of megabytes of CSV, an
operation that is quite expensive. The async version gets to spread
these jobs across several workers. Why arent we seeing a greater time
savings?</p>
<p>Mostly, its because calling
<code>future_promise(read_csv(&quot;big_file.csv&quot;))</code> is almost a
worst-case scenario for future and async. <code>read_csv</code> is
generally fast, but because the CRAN log files are so big,
<code>read_csv(&quot;big_file.csv&quot;)</code> is slow. The value it returns is a
very large data frame, that has now been loaded not into the Shiny
process, but a <code>future</code> worker process. In order to return
that data frame to the Shiny process, that data must first be serialized
(I believe <code>future</code> essentially uses <code>saveRDS</code> for
this), transmitted to the Shiny process, and then deserialized; to make
matters worse, the transmitting and deserialization steps happen on the
main R thread that were working so hard to try to keep idle.
<strong>The larger the data we send back and forth to the future, the
more performance suffers,</strong> and in this case were sending back
quite a lot of data.</p>
<p>We can make our code significantly faster by doing more summarizing,
aggregation, and filtering <em>inside</em> the future; not only does
this make more of the work happen in parallel, but by returning the data
in already-processed form, we can have much less data to transfer from
the worker process back to the Shiny process. (For example, the data for
May 31, 2018 weighs 75MB before optimization, and 8.8MB afterwards.)</p>
<p>Compare all three runs in the image below (the newly optimized
version is labelled “async2”). The homepage load times have dropped
further, and the calculation times are now dramatically faster than the
sync code.</p>
<p><img src="data:image/png;base64,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
<p>Looking at the “async2” graph, the leading (bottom-left) edge has the
same shape as before, as thats simply the rate at which the load
testing tool launches new sessions. But notice how much more closely the
trailing (upper-right) edge matches the leading edge! It means that even
as the number of active sessions ramped up, the amount of latency didnt
get dramatically worse, unlike with the “sync” and “async” versions. And
each of the individual blue rectangles in the “async2” are comparatively
tiny, meaning that users never have to wait more than a dozen seconds at
the most for plots to update.</p>
<p>This last plot shows the same data as above, but with the sessions
aligned by start time. You can clearly see how the sessions are both
shorter and less variable in “async2” compared to the others. Ive added
a yellow vertical line at the 10 second mark; if the page load
(red/pink) has not completed at this point, its likely that your
visitor has left in disgust. While “async” does better than “sync”, they
both break through the 10 second mark early and often. In contrast, the
“async2” version just barely peeks over the line three times.</p>
<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA6cAAAPgCAYAAADOQQqkAAAEDWlDQ1BJQ0MgUHJvZmlsZQAAOI2NVV1oHFUUPrtzZyMkzlNsNIV0qD8NJQ2TVjShtLp/3d02bpZJNtoi6GT27s6Yyc44M7v9oU9FUHwx6psUxL+3gCAo9Q/bPrQvlQol2tQgKD60+INQ6Ium65k7M5lpurHeZe58853vnnvuuWfvBei5qliWkRQBFpquLRcy4nOHj4g9K5CEh6AXBqFXUR0rXalMAjZPC3e1W99Dwntf2dXd/p+tt0YdFSBxH2Kz5qgLiI8B8KdVy3YBevqRHz/qWh72Yui3MUDEL3q44WPXw3M+fo1pZuQs4tOIBVVTaoiXEI/MxfhGDPsxsNZfoE1q66ro5aJim3XdoLFw72H+n23BaIXzbcOnz5mfPoTvYVz7KzUl5+FRxEuqkp9G/Ajia219thzg25abkRE/BpDc3pqvphHvRFys2weqvp+krbWKIX7nhDbzLOItiM8358pTwdirqpPFnMF2xLc1WvLyOwTAibpbmvHHcvttU57y5+XqNZrLe3lE/Pq8eUj2fXKfOe3pfOjzhJYtB/yll5SDFcSDiH+hRkH25+L+sdxKEAMZahrlSX8ukqMOWy/jXW2m6M9LDBc31B9LFuv6gVKg/0Szi3KAr1kGq1GMjU/aLbnq6/lRxc4XfJ98hTargX++DbMJBSiYMIe9Ck1YAxFkKEAG3xbYaKmDDgYyFK0UGYpfoWYXG+fAPPI6tJnNwb7ClP7IyF+D+bjOtCpkhz6CFrIa/I6sFtNl8auFXGMTP34sNwI/JhkgEtmDz14ySfaRcTIBInmKPE32kxyyE2Tv+thKbEVePDfW/byMM1Kmm0XdObS7oGD/MypMXFPXrCwOtoYjyyn7BV29/MZfsVzpLDdRtuIZnbpXzvlf+ev8MvYr/Gqk4H/kV/G3csdazLuyTMPsbFhzd1UabQbjFvDRmcWJxR3zcfHkVw9GfpbJmeev9F08WW8uDkaslwX6avlWGU6NRKz0g/SHtCy9J30o/ca9zX3Kfc19zn3BXQKRO8ud477hLnAfc1/G9mrzGlrfexZ5GLdn6ZZrrEohI2wVHhZywjbhUWEy8icMCGNCUdiBlq3r+xafL549HQ5jH+an+1y+LlYBifuxAvRN/lVVVOlwlCkdVm9NOL5BE4wkQ2SMlDZU97hX86EilU/lUmkQUztTE6mx1EEPh7OmdqBtAvv8HdWpbrJS6tJj3n0CWdM6busNzRV3S9KTYhqvNiqWmuroiKgYhshMjmhTh9ptWhsF7970j/SbMrsPE1suR5z7DMC+P/Hs+y7ijrQAlhyAgccjbhjPygfeBTjzhNqy28EdkUh8C+DU9+z2v/oyeH791OncxHOs5y2AtTc7nb/f73TWPkD/qwBnjX8BoJ98VVBg/m8AAEAASURBVHgB7L0JgCRFlf//zbq7+j6np+e+mYPhGHC4BAcFuRERlPW3oisqghf7X/nxx9VdlXVXVxEVFVARAVEQREC5EeSGYTjngrnvvu+u6royfy+quqoyqzKzsu7q7hfQUxnXi4hPRB4vI+OFpJADOybABJgAE2ACTIAJMAEmwASYABNgAmUkYCtj2Vw0E2ACTIAJMAEmwASYABNgAkyACTCBKAFWTnkgMAEmwASYABNgAkyACTABJsAEmEDZCbByWvYu4AowASbABJgAE2ACTIAJMAEmwASYACunPAaYABNgAkyACTABJsAEmAATYAJMoOwEWDktexdwBZgAE2ACTIAJMAEmwASYABNgAkyAlVMeA0yACTABJsAEmAATYAJMgAkwASZQdgKOstcgywr4fD6Mj49nmavAyWUZ3fsO4LGnn0J1QwOOXrO2YAXU11Wjsb5OI29weASDQ6OaMD2PJI2jrfUPlHYdAoH5iSTNjXWoralO+FMPunr64B8PpgZr/AoUzO2YAbvdrgnX8/j84+juHdCLSoQ11Negoa424dc7CASCONTdpxcFm53eq9AuSLKsvxOSx+1Ce1uzbt58AiORCPYe7IJE/wnX2FCL+tqafEQWPK/YHWrvgU6Bp6jOTn2wcN5shEKhopbDws0JuFwuBIPm56+5BI7Nh4AkSXC73dE+kOnewK48BJxOJ1+LyoM+Uaq4Fon7D98TEkhKfiCuR+I5LRwO5122kNPf35+3HCMBCxYsMIri8GlOYNIpp+KECwQCZe22nU88jb1fuAb3HjEDAXczdj42AkXKrLRlrrSCS1Z78Y3PnKVJ+tM/PoU73hyjsJhCpIlUeVrqRvHuLTfhm3cP44GXV8di6Ebx1VNa8LkLPqBKqT38+i2P46UD5pqMpITxhy8eg1XLFmkz6/geemYDvvngLmJiNDGv4FNH1+Dqfz5DJ3cy6Jn1G/Hl32/Oie3aDht+8/ULksIKdLT5vV34+M9fhWKjU4fYfv64BnzlEx8qkPTCiOns7MS5P3gGfsVZGIEGUmxyCM9edw4aKkw5N6julA32er0YGRmZsu2r9IaJB7jGxkb4/X5+SVDGzvJ4PBAvr/kFQfk6obq6Osp/dDTzy/Ty1XJql+xwOCD+CvGcLK5tw8PDUxsYt64iCUw65bQSKEqkdFWFY2/Ihboo25yIOIxnJi3XmZQdyZb+5l2y2RB20OycobIXKyE8oYtE7FUIO2OzkhIpEKK+ps5Gb9mcVaZJHCHrD7+STULE7oJM9dB1ipy5TpQxL7Y2v27RhQhUorxqIckR2KitleiidbSZz0znW29HkG9a+TLk/EyACTABJsAEmAATYAJJAhm0lmRCPmICTIAJMAEmwASYABNgAkyACTABJlAsAqycFossy2UCTIAJMAEmwASYABNgAkyACTABywRYObWMihMyASbABJgAE2ACTIAJMAEmwASYQLEI8JrTHMiGwkF0VcXQyWTFVhiGsY/30m+eFjPJJpFv2JVWI5ksxHp83ZnsIcHjEEaTAGdggNIfpLWwDjLc40Y45E6TKQKEAQ9hSCXiG6L05tZ1xfpKYanWigsHQ3CP9xkbRKJ2jg060d1NbVK5BrJ8LKz9xZ0w0STYSqHk2kZJDsMeyWytOTQcRk9PD1pbW+PiCvYrWLj9ZLGXfkcGvWntiBckDNXU1JTHkq+omyOS5Bavk/i1h32QaN1vJicMWkUcXsNkNjKSxY4JMAEmwASYABNgAkyACRSKACunOZAMBUJwVVVhYbcPTsWPiP8e+KtnwzfrhBykabO8fdCHgYGBqPXHeMypR8zGQ28eQmTCyFE8PPXXXRVTOISC56nyYlW9D1ddfALaW/W3VPnV/c/gnjdJOUUVpTe31msLWbe+F46E4HGTYmz3pFYx4X9sr4QnfvRswi+RYv/lD87Hx89IMnzfqsVkIbiKjCcljQ498sLbuG8jKcm2mBIrtrjRc9v8Ej7x/b/iD1efjba2Nr0kOYUtXTgHd30xgm/f8Sz2B2vw4HYJf1W1Qy30A/OA/7riY+qgkhyL9t52udjeSJ/NN257Bp2hzEpzozSKb128CHV19br1Fsr3wnlzi2pqXrdgDmQCTIAJMAEmwASYABOYkgRYOc2hW6toT7uWvmF8qltBmPSmzS1VCLncGJQacpCmzVLnsKeZwq+vq8MQ6qks7f6n2pyAY2I7mzGpOloXT50HK5cuTE2W8Cuk4PXJtTS7StvgJPW/RLz6QMgWVoOtOLenCkPEQpaMldOoHJXuZJNpeyBRD5XzVnnStq7Z8N4hDIIUXyk5w6rKojkMkvxCbysgTLQffthiSN53MBiasNCsaoe6ArKtPFse2aifVi5doK6K5lj2vInBcGbl1Ou0Y8n8OWhpadHkj3vqaFwKU/PsmAATYAJMgAkwASbABJhAIQhY0zYKURLLYAJMgAkwASbABJgAE2ACTIAJMAEmYECAlVMDMBzMBJgAE2ACTIAJMAEmwASYABNgAqUjwMpp6VhzSUyACTABJsAEmAATYAJMgAkwASZgQIDXnBqAMQsWRngiZKRnzC7R2seJlLTu0Bbxm2WzFCeF9dcpSpAzyo+XL4wLiWM5FDIt
<p>To get a visceral sense for what it feels like to use the app under
load, heres a video that shows what its like to browse the app while
the load test is running at its peak. The left side of the screen shows
“sync”, the right shows “async2”. In both cases, I navigated to the app
when session #40 was started.</p>
<p class="embed-responsive embed-responsive-16by9">
<iframe class="embed-responsive-item" src="data:text/html; charset=utf-8;charset=utf-8,%3C%21DOCTYPE%20html%3E%3Chtml%20lang%3D%22en%22%20dir%3D%22ltr%22%20data%2Dcast%2Dapi%2Denabled%3D%22true%22%3E%3Chead%3E%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice%2Dwidth%2C%20initial%2Dscale%3D1%22%3E%3Cscript%20nonce%3D%22gs%5FNjvqW5Vy60Q2ish%5FMcg%22%3Eif%20%28%27undefined%27%20%3D%3D%20typeof%20Symbol%20%7C%7C%20%27undefined%27%20%3D%3D%20typeof%20Symbol%2Eiterator%29%20%7Bdelete%20Array%2Eprototype%2Eentries%3B%7D%3C%2Fscript%3E%3Cstyle%20name%3D%22www%2Droboto%22%20nonce%3D%22%5F3GHaS1ka29TBozsoQbGpQ%22%3E%40font%2Dface%7Bfont%2Dfamily%3A%27Roboto%27%3Bfont%2Dstyle%3Anormal%3Bfont%2Dweight%3A400%3Bsrc%3Aurl%28%2F%2Ffonts%2Egstatic%2Ecom%2Fs%2Froboto%2Fv18%2FKFOmCnqEu92Fr1Mu4mxP%2Ettf%29format%28%27truetype%27%29%3B%7D%40font%2Dface%7Bfont%2Dfamily%3A%27Roboto%27%3Bfont%2Dstyle%3Anormal%3Bfont%2Dweight%3A500%3Bsrc%3Aurl%28%2F%2Ffonts%2Egstatic%2Ecom%2Fs%2Froboto%2Fv18%2FKFOlCnqEu92Fr1MmEU9fBBc9%2Ettf%29format%28%27truetype%27%29%3B%7D%3C%2Fstyle%3E%3Cscript%20name%3D%22www%2Droboto%22%20nonce%3D%22gs%5FNjvqW5Vy60Q2ish%5FMcg%22%3Eif%20%28document%2Efonts%20%26%26%20document%2Efonts%2Eload%29%20%7Bdocument%2Efonts%2Eload%28%22400%2010pt%20Roboto%22%2C%20%22E%22%29%3B%20document%2Efonts%2Eload%28%22500%2010pt%20Roboto%22%2C%20%22E%22%29%3B%7D%3C%2Fscript%3E%3Clink%20rel%3D%22stylesheet%22%20href%3D%22%2Fs%2Fplayer%2Fb46bb280%2Fwww%2Dplayer%2Ecss%22%20name%3D%22www%2Dplayer%22%20nonce%3D%22%5F3GHaS1ka29TBozsoQbGpQ%22%3E%3Cstyle%20nonce%3D%22%5F3GHaS1ka29TBozsoQbGpQ%22%3Ehtml%20%7Boverflow%3A%20hidden%3B%7Dbody%20%7Bfont%3A%2012px%20Roboto%2C%20Arial%2C%20sans%2Dserif%3B%20background%2Dcolor%3A%20%23000%3B%20color%3A%20%23fff%3B%20height%3A%20100%25%3B%20width%3A%20100%25%3B%20overflow%3A%20hidden%3B%20position%3A%20absolute%3B%20margin%3A%200%3B%20padding%3A%200%3B%7D%23player%20%7Bwidth%3A%20100%25%3B%20height%3A%20100%25%3B%7Dh1%20%7Btext%2Dalign%3A%20center%3B%20color%3A%20%23fff%3B%7Dh3%20%7Bmargin%2Dtop%3A%206px%3B%20margin%2Dbottom%3A%203px%3B%7D%2Eplayer%2Dunavailable%20%7Bposition%3A%20absolute%3B%20top%3A%200%3B%20left%3A%200%3B%20right%3A%200%3B%20bottom%3A%200%3B%20padding%3A%2025px%3B%20font%2Dsize%3A%2013px%3B%20background%3A%20url%28%2Fimg%2Fmeh7%2Epng%29%2050%25%2065%25%20no%2Drepeat%3B%7D%2Eplayer%2Dunavailable%20%2Emessage%20%7Btext%2Dalign%3A%20left%3B%20margin%3A%200%20%2D5px%2015px%3B%20padding%3A%200%205px%2014px%3B%20border%2Dbottom%3A%201px%20solid%20%23888%3B%20font%2Dsize%3A%2019px%3B%20font%2Dweight%3A%20normal%3B%7D%2Eplayer%2Dunavailable%20a%20%7Bcolor%3A%20%23167ac6%3B%20text%2Ddecoration%3A%20none%3B%7D%3C%2Fstyle%3E%3Cscript%20nonce%3D%22gs%5FNjvqW5Vy60Q2ish%5FMcg%22%3Evar%20ytcsi%3D%7Bgt%3Afunction%28n%29%7Bn%3D%28n%7C%7C%22%22%29%2B%22data%5F%22%3Breturn%20ytcsi%5Bn%5D%7C%7C%28ytcsi%5Bn%5D%3D%7Btick%3A%7B%7D%2Cinfo%3A%7B%7D%2Cgel%3A%7BpreLoggedGelInfos%3A%5B%5D%7D%7D%29%7D%2Cnow%3Awindow%2Eperformance%26%26window%2Eperformance%2Etiming%26%26window%2Eperformance%2Enow%26%26window%2Eperformance%2Etiming%2EnavigationStart%3Ffunction%28%29%7Breturn%20window%2Eperformance%2Etiming%2EnavigationStart%2Bwindow%2Eperformance%2Enow%28%29%7D%3Afunction%28%29%7Breturn%28new%20Date%29%2EgetTime%28%29%7D%2Ctick%3Afunction%28l%2Ct%2Cn%29%7Bvar%20ticks%3Dytcsi%2Egt%28n%29%2Etick%3Bvar%20v%3Dt%7C%7Cytcsi%2Enow%28%29%3Bif%28ticks%5Bl%5D%29%7Bticks%5B%22%5F%22%2Bl%5D%3Dticks%5B%22%5F%22%2Bl%5D%7C%7C%5Bticks%5Bl%5D%5D%3Bticks%5B%22%5F%22%2Bl%5D%2Epush%28v%29%7Dticks%5Bl%5D%3D%0Av%7D%2Cinfo%3Afunction%28k%2Cv%2Cn%29%7Bytcsi%2Egt%28n%29%2Einfo%5Bk%5D%3Dv%7D%2CinfoGel%3Afunction%28p%2Cn%29%7Bytcsi%2Egt%28n%29%2Egel%2EpreLoggedGelInfos%2Epush%28p%29%7D%2CsetStart%3Afunction%28t%2Cn%29%7Bytcsi%2Etick%28%22%5Fstart%22%2Ct%2Cn%29%7D%7D%3B%0A%28function%28w%2Cd%29%7Bfunction%20isGecko%28%29%7Bif%28%21w%2Enavigator%29return%20false%3Btry%7Bif%28w%2Enavigator%2EuserAgentData%26%26w%2Enavigator%2EuserAgentData%2Ebrands%26%26w%2Enavigator%2EuserAgentData%2Ebrands%2Elength%29%7Bvar%20brands%3Dw%2Enavigator%2Euser
</iframe>
</p>
<p>Take a look at the <a href="https://github.com/rstudio/cranwhales/compare/async...async2?diff=split">code
diff for async vs. async2</a>. While the code has not changed very
dramatically, it has lost a little elegance and maintainability: the
code for each of the affected outputs now has one foot in the the render
function and one foot in the future. If your apps total audience is a
team of a hundred analysts and execs, you may choose to forgo the extra
performance and stick with the original async (or even sync) code. But
if you have serious scaling needs, the refactoring is probably a small
price to pay.</p>
<p>Lets get real for a second, though. If this werent an example app
written for exposition purposes, but a real production app that was
intended to scale to thousands of concurrent users across dozens of R
processes, we wouldnt download and parse CSV files on the fly. Instead,
wed establish a proper <a href="https://solutions.posit.co/gallery/twitter-etl/">ETL procedure</a>
to run every night and put the results into a properly indexed database
table, or RDS files with just the data we need. As I said <a href="#improving-performance-and-scalability">earlier</a>, a little
precomputation and caching can make a huge difference!</p>
<p>Much of the remaining latency for the async2 branch is from ggplot2
plotting. <a href="https://posit.co/resources/">Seans talk</a> alluded
to some upcoming plot caching features were adding to Shiny, and I
imagine they will have as dramatic an effect for this test as they did
for Sean.</p>
</div>
</div>
<div id="summing-up" class="section level2">
<h2>Summing up</h2>
<p>With async programming, expensive computations and tasks no longer
need to be the scalability killers that they once were for Shiny. Armed
with this and other common techniques like precomputation, caching, and
load balancing, its possible to write responsive and scalable Shiny
applications that can be safely deployed to thousands of concurrent
users.</p>
</div>
<!-- code folding -->
<!-- dynamically load mathjax for compatibility with self-contained -->
<script>
(function () {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
document.getElementsByTagName("head")[0].appendChild(script);
})();
</script>
</body>
</html>