Comparing changes
Open a pull request
- 1 commit
- 3 files changed
- 0 comments
- 1 contributor
- +1 −1 README.md
- +72 −59 app.R
- +41 −36 modules/detail.R
| @@ -2,7 +2,7 @@ | |||
|
|
|
||
| This is a Shiny example application, meant to demonstrate how to convert synchronous (traditional) Shiny apps to asynchronous ones. | This is a Shiny example application, meant to demonstrate how to convert synchronous (traditional) Shiny apps to asynchronous ones. | ||
|
|
|
||
| This is the **synchronous** version. The (naive) asynchronous version is [here](https://github.com/jcheng5/cranwhales/tree/async) ([diff](https://github.com/jcheng5/cranwhales/compare/sync...async?diff=split)). The optimized asynchronous version is [here](https://github.com/rstudio/cranwhales/tree/async2) ([diff](https://github.com/jcheng5/cranwhales/compare/async...async2?diff=split)). | This is the (naive) **asynchronous** version. The synchronous version is [here](https://github.com/jcheng5/cranwhales) ([diff](https://github.com/jcheng5/cranwhales/compare/sync...async?diff=split)). The optimized asynchronous version is [here](https://github.com/rstudio/cranwhales/tree/async2) ([diff](https://github.com/jcheng5/cranwhales/compare/async...async2?diff=split)). | ||
|
|
|
||
| To learn more about asynchronous programming in R and Shiny, see https://rstudio.github.io/promises/. | To learn more about asynchronous programming in R and Shiny, see https://rstudio.github.io/promises/. | ||
|
|
|
||
| @@ -8,6 +8,11 @@ library(glue) | |||
| library(lubridate) | library(lubridate) | ||
| library(gdata) # for gdata::humanReadable | library(gdata) # for gdata::humanReadable | ||
|
|
|
||
| library(promises) | |||
| library(future) | |||
| # Leave one core for Shiny itself | |||
| plan(multisession(workers = availableCores() - 1)) | |||
|
|
|||
| source("random-names.R") | source("random-names.R") | ||
| source("modules/detail.R") | source("modules/detail.R") | ||
|
|
|
||
| @@ -59,26 +64,26 @@ server <- function(input, output, session) { | |||
| url <- glue("http://cran-logs.rstudio.com/{year}/{date}.csv.gz") | url <- glue("http://cran-logs.rstudio.com/{year}/{date}.csv.gz") | ||
| path <- file.path("data_cache", paste0(date, ".csv.gz")) | path <- file.path("data_cache", paste0(date, ".csv.gz")) | ||
|
|
|
||
| withProgress(value = NULL, { | p <- Progress$new() | ||
|
|
p$set(value = NULL, message = "Downloading data...") | ||
| future({ | |||
| # Download to a temporary file path, then rename to the real | # Download to a temporary file path, then rename to the real | ||
| # path when the download is complete. We do this so other | # path when the download is complete. We do this so other | ||
| # processes/sessions don't use partially downloaded files. | # processes/sessions don't use partially downloaded files. | ||
| if (!file.exists(path)) { | if (!file.exists(path)) { | ||
| tmppath <- paste0(path, "-", Sys.getpid()) | tmppath <- paste0(path, "-", Sys.getpid()) | ||
| setProgress(message = "Downloading data...") | |||
| download.file(url, tmppath) | download.file(url, tmppath) | ||
| if (!file.exists(path)) { | if (!file.exists(path)) { | ||
| file.rename(tmppath, path) | file.rename(tmppath, path) | ||
| } else { | } else { | ||
| file.remove(tmppath) | file.remove(tmppath) | ||
| } | } | ||
| } | } | ||
| }) %...>% | |||
| setProgress(message = "Parsing data...") | { p$set(message = "Parsing data...") } %...>% | ||
| read_csv(path, col_types = "Dti---c-ci", progress = FALSE) %>% | { future(read_csv(path, col_types = "Dti---c-ci", progress = FALSE) %>% | ||
| filter(!is.na(package)) | filter(!is.na(package))) } %>% | ||
| }) | finally(~p$close()) | ||
| }) | }) | ||
|
|
|
||
| # Returns a data frame of just the top `input$count` downloaders of the day, | # Returns a data frame of just the top `input$count` downloaders of the day, | ||
| @@ -93,21 +98,24 @@ server <- function(input, output, session) { | |||
| need(input$count > 0, "Too few downloaders"), | need(input$count > 0, "Too few downloaders"), | ||
| need(input$count <= 25, "Too many downloaders; 25 or fewer please") | need(input$count <= 25, "Too many downloaders; 25 or fewer please") | ||
| ) | ) | ||
| data() %>% | data() %...>% | ||
| count(ip_id, country) %>% | count(ip_id, country) %...>% | ||
| arrange(desc(n)) %>% | arrange(desc(n)) %...>% | ||
| head(input$count) %>% | head(input$count) %...>% | ||
| mutate(ip_name = factor(ip_id, levels = ip_id, | mutate(ip_name = factor(ip_id, levels = ip_id, | ||
| labels = glue("{random_name(length(ip_id), input$date)} [{country}]"))) %>% | labels = glue("{random_name(length(ip_id), input$date)} [{country}]"))) %...>% | ||
| select(-country) | select(-country) | ||
| }) | }) | ||
|
|
|
||
| # data(), filtered down to the downloads that are by the top `input$count` | # data(), filtered down to the downloads that are by the top `input$count` | ||
| # downloaders | # downloaders | ||
| whale_downloads <- reactive({ | whale_downloads <- reactive({ | ||
| data() %>% | promise_all(data = data(), whales = whales()) %...>% | ||
| inner_join(whales(), "ip_id") %>% | with({ | ||
| select(-n) | data %>% | ||
| inner_join(whales, "ip_id") %>% | |||
| select(-n) | |||
| }) | |||
| }) | }) | ||
|
|
|
||
|
|
|
||
| @@ -116,77 +124,82 @@ server <- function(input, output, session) { | |||
| #### "All traffic" tab ---------------------------------------- | #### "All traffic" tab ---------------------------------------- | ||
|
|
|
||
| output$total_size <- renderValueBox({ | output$total_size <- renderValueBox({ | ||
| data() %>% | data() %...>% | ||
| pull(size) %>% | pull(size) %...>% | ||
| as.numeric() %>% # Cast from integer to numeric to avoid overflow warning | as.numeric() %...>% # Cast from integer to numeric to avoid overflow warning | ||
| sum() %>% | sum() %...>% | ||
| humanReadable() %>% | humanReadable() %...>% | ||
| valueBox("bandwidth consumed") | valueBox("bandwidth consumed") | ||
| }) | }) | ||
|
|
|
||
| output$total_count <- renderValueBox({ | output$total_count <- renderValueBox({ | ||
| data() %>% | data() %...>% | ||
| nrow() %>% | nrow() %...>% | ||
| format(big.mark = ",") %>% | format(big.mark = ",") %...>% | ||
| valueBox("files downloaded") | valueBox("files downloaded") | ||
| }) | }) | ||
|
|
|
||
| output$total_uniques <- renderValueBox({ | output$total_uniques <- renderValueBox({ | ||
| data() %>% | data() %...>% | ||
| pull(package) %>% | pull(package) %...>% | ||
| unique() %>% | unique() %...>% | ||
| length() %>% | length() %...>% | ||
| format(big.mark = ",") %>% | format(big.mark = ",") %...>% | ||
| valueBox("unique packages") | valueBox("unique packages") | ||
| }) | }) | ||
|
|
|
||
| output$total_downloaders <- renderValueBox({ | output$total_downloaders <- renderValueBox({ | ||
| data() %>% | data() %...>% | ||
| pull(ip_id) %>% | pull(ip_id) %...>% | ||
| unique() %>% | unique() %...>% | ||
| length() %>% | length() %...>% | ||
| format(big.mark = ",") %>% | format(big.mark = ",") %...>% | ||
| valueBox("unique downloaders") | valueBox("unique downloaders") | ||
| }) | }) | ||
|
|
|
||
| output$all_hour <- renderPlot({ | output$all_hour <- renderPlot({ | ||
| whale_ip <- whales()$ip_id | promise_all(data = data(), whales = whales()) %...>% | ||
|
|
with({ | ||
| data() %>% | whale_ip <- whales$ip_id | ||
| mutate( |
|
||
| time = hms::trunc_hms(time, 60*60), | data %>% | ||
| is_whale = ip_id %in% whale_ip | mutate( | ||
| ) %>% | time = hms::trunc_hms(time, 60*60), | ||
| count(time, is_whale) %>% | is_whale = ip_id %in% whale_ip | ||
| ggplot(aes(time, n, fill = is_whale)) + | ) %>% | ||
| geom_bar(stat = "identity") + | count(time, is_whale) %>% | ||
| scale_fill_manual(values = c("#666666", "#88FF99"), | ggplot(aes(time, n, fill = is_whale)) + | ||
| labels = c("no", "yes")) + | geom_bar(stat = "identity") + | ||
| ylab("Downloads") + | scale_fill_manual(values = c("#666666", "#88FF99"), | ||
| xlab("Hour") + | labels = c("no", "yes")) + | ||
| scale_y_continuous(labels = scales::comma) | ylab("Downloads") + | ||
| xlab("Hour") + | |||
| scale_y_continuous(labels = scales::comma) | |||
| }) | |||
| }) | }) | ||
|
|
|
||
| #### "Biggest whales" tab ------------------------------------- | #### "Biggest whales" tab ------------------------------------- | ||
|
|
|
||
| output$downloaders <- renderPlot({ | output$downloaders <- renderPlot({ | ||
| whales() %>% | whales() %...>% { | ||
| ggplot(aes(ip_name, n)) + | ggplot(., aes(ip_name, n)) + | ||
| geom_bar(stat = "identity") + | geom_bar(stat = "identity") + | ||
| ylab("Downloads on this day") | ylab("Downloads on this day") | ||
| } | |||
| }) | }) | ||
|
|
|
||
| #### "Whales by hour" tab ------------------------------------- | #### "Whales by hour" tab ------------------------------------- | ||
|
|
|
||
| output$downloaders_hour <- renderPlot({ | output$downloaders_hour <- renderPlot({ | ||
| whale_downloads() %>% | whale_downloads() %...>% | ||
| mutate(time = hms::trunc_hms(time, 60*60)) %>% | mutate(time = hms::trunc_hms(time, 60*60)) %...>% | ||
| count(time, ip_name) %>% | count(time, ip_name) %...>% { | ||
| ggplot(aes(time, n)) + | ggplot(., aes(time, n)) + | ||
| geom_bar(stat = "identity") + | geom_bar(stat = "identity") + | ||
| facet_wrap(~ip_name) + | facet_wrap(~ip_name) + | ||
| ylab("Downloads") + | ylab("Downloads") + | ||
| xlab("Hour") | xlab("Hour") | ||
| } | |||
| }) | }) | ||
|
|
|
||
| #### "Detail view" tab ---------------------------------------- | #### "Detail view" tab ---------------------------------------- | ||
| @@ -16,61 +16,66 @@ detailViewUI <- function(id) { | |||
|
|
|
||
| detailView <- function(input, output, session, whales, whale_downloads) { | detailView <- function(input, output, session, whales, whale_downloads) { | ||
|
|
|
||
| # When whales() changes, update the selectInput with their names | # When whales() changes, update the selectInput with their names. | ||
| observeEvent(try(silent=TRUE, whales()), { | # The gross try(... %>% catch(~{})) is necessary to swallow both sync and | ||
| choices <- tryCatch( | # async errors, because either will cause the session to be immediately and | ||
| whales()$ip_name, | # unceremoniously ended. Something to improve in Shiny v1.2. | ||
| error = function(err) { character(0) } | observeEvent(try(whales() %>% catch(~{})), { | ||
| ) | whales() %...>% | ||
| updateSelectInput(session, "detail_ip_name", | pull(ip_name) %...!% | ||
| choices = choices, | { character(0) } %...>% | ||
| selected = if (input$detail_ip_name %in% choices) | { | ||
| input$detail_ip_name | updateSelectInput(session, "detail_ip_name", | ||
| else | choices = ., | ||
| character(0)) | selected = if (input$detail_ip_name %in% .) | ||
| freezeReactiveValue(input, "detail_ip_name") | input$detail_ip_name | ||
| else | |||
| character(0)) | |||
| freezeReactiveValue(input, "detail_ip_name") | |||
| } | |||
| }) | }) | ||
|
|
|
||
| detail_downloads <- reactive({ | detail_downloads <- reactive({ | ||
| req(input$detail_ip_name, nzchar(input$detail_ip_name)) | req(input$detail_ip_name, nzchar(input$detail_ip_name)) | ||
| whale_downloads() %>% | whale_downloads() %...>% | ||
| filter(ip_name == input$detail_ip_name) %>% | filter(ip_name == input$detail_ip_name) %...>% | ||
| arrange(time) %>% | arrange(time) %...>% | ||
| mutate(package = factor(package, levels = rev(unique(package)), ordered = TRUE)) | mutate(package = factor(package, levels = rev(unique(package)), ordered = TRUE)) | ||
| }) | }) | ||
|
|
|
||
| output$detail_size <- renderValueBox({ | output$detail_size <- renderValueBox({ | ||
| detail_downloads() %>% | detail_downloads() %...>% | ||
| pull(size) %>% | pull(size) %...>% | ||
| as.numeric() %>% # Cast from integer to numeric to avoid overflow warning | as.numeric() %...>% # Cast from integer to numeric to avoid overflow warning | ||
| sum() %>% | sum() %...>% | ||
| humanReadable() %>% | humanReadable() %...>% | ||
| valueBox("bandwidth consumed") | valueBox("bandwidth consumed") | ||
| }) | }) | ||
|
|
|
||
| output$detail_count <- renderValueBox({ | output$detail_count <- renderValueBox({ | ||
| detail_downloads() %>% | detail_downloads() %...>% | ||
| nrow() %>% | nrow() %...>% | ||
| format(big.mark = ",") %>% | format(big.mark = ",") %...>% | ||
| valueBox("files downloaded") | valueBox("files downloaded") | ||
| }) | }) | ||
|
|
|
||
| output$detail_uniques <- renderValueBox({ | output$detail_uniques <- renderValueBox({ | ||
| detail_downloads() %>% | detail_downloads() %...>% | ||
| pull(package) %>% | pull(package) %...>% | ||
| unique() %>% | unique() %...>% | ||
| length() %>% | length() %...>% | ||
| format(big.mark = ",") %>% | format(big.mark = ",") %...>% | ||
| valueBox("unique packages") | valueBox("unique packages") | ||
| }) | }) | ||
|
|
|
||
| # Show every single download from the selected downloader | # Show every single download from the selected downloader | ||
| output$detail <- renderPlot({ | output$detail <- renderPlot({ | ||
|
|
|
||
| validate(need(input$detail_ip_name, "Select a downloader from the list above")) | validate(need(input$detail_ip_name, "Select a downloader from the list above")) | ||
| pkg <- levels(detail_downloads()$package) |
|
||
|
|
detail_downloads() %...>% { | ||
| detail_downloads() %>% { | pkg <- levels(.$package) | ||
|
|
|||
| ggplot(., aes(time, package)) + | ggplot(., aes(time, package)) + | ||
| geom_point() + | geom_point() + | ||
| scale_x_time(breaks = seq(hms::hms(0,0,0), by = 60*60*3, length.out = 9), | scale_x_time(breaks = seq(hms::hms(0,0,0), by = 60*60*3, length.out = 9), | ||
| @@ -83,12 +88,12 @@ detailView <- function(input, output, session, whales, whale_downloads) { | |||
| # Show the downloads that are brushed on output$detail | # Show the downloads that are brushed on output$detail | ||
| output$detail_table <- renderDT({ | output$detail_table <- renderDT({ | ||
| req(input$detail_brush) | req(input$detail_brush) | ||
| detail_downloads() %>% | detail_downloads() %...>% | ||
| brushedPoints(input$detail_brush) %>% | brushedPoints(input$detail_brush) %...>% | ||
| mutate( | mutate( | ||
| time = as.character(time), | time = as.character(time), | ||
| size = humanReadable(size) | size = humanReadable(size) | ||
| ) %>% | ) %...>% | ||
| select(-ip_id, -ip_name, -country) | select(-ip_id, -ip_name, -country) | ||
| }) | }) | ||
| } | } | ||