Técnicas y Mejores Prácticas
Para ver más posiciones: https://www.appsilon.com/careers
¿Qué es una computadora? La interacción de 3 componentes principales
¡Depende de la necesidad!
En general, pensemos en tiempo (CPU) o espacio (memory/storage). El dinero es un eje secreto.
https://www.appsilon.com/post/optimize-shiny-app-performance
Prueba la app y anota cuánto tiempo te toma ver la información para:
3 ciudades diferentes
3 edades máximas diferentes
Enlace: https://01933b4a-2e76-51f9-79f4-629808c48a59.share.connect.posit.cloud/
El profiling es una técnica utilizada para identificar cuellos de botella en el rendimiento de tu código:
{profvis}
Es una herramienta interactiva que proporciona una visualización detallada del tiempo de ejecución de tu código.
Una herramienta que usa Javascript para calcular el tiempo que toman las acciones en la app, desde el punto de vista del navegador.
Es super fácil de añadir a una app.
Ejecutar cualquiera de estas operaciones en la consola de Javascript.
// Print out all measurements
showAllMeasurements()
// To download all measurements as a CSV file
exportMeasurements()
// To print out summarised measurements (slowest rendering output, slowest server computation)
showSummarisedMeasurements()
// To export an html file that visualizes measurements on a timeline
await exportHtmlReport()
Muchos navegadores cuentan con herramientas de desarrollador donde puedes encontrar una mientras tu app está corriendo.
Ubicar la herramienta en Rstudio
La consola de R mostrará el botón “Stop profiling”. Esto significa que el profiler está activado.
Corre tu shiny app e interactúa con ella. Luego, puedes detener la app y el profiler.
El panel de edición de Rstudio te mostrará una nueva vista.
La parte superior hace profiling de cada línea de código, la parte inferior muestra un FlameGraph, que indica el tiempo requerido por cada operación.
También puede accederse a la pestaña “Data”.
Esta indica cuánto tiempo y memoria se ha requerido por cada operación. Nos da un resumen de la medición.
Para una revisión más exhaustiva del uso de {profvis}
, puedes consultar la documentación oficial:
Realiza el profiling del archivo “app.R”.
Toma en cuenta que estás probando esto para un solo usuario.
¡Puedes combinar todo!
NO ejecutar durante el workshop porque toma tiempo en correr
suppressMessages(
microbenchmark::microbenchmark(
read.csv = read.csv("data/personal.csv"),
read_csv = readr::read_csv("data/personal.csv"),
vroom = vroom::vroom("data/personal.csv"),
fread = data.table::fread("data/personal.csv")
)
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval
#> read.csv 1891.3824 2007.2517 2113.5217 2082.6016 2232.7825 2442.6901 100
#> read_csv 721.9287 820.4181 873.4603 866.7321 897.3488 1165.5929 100
#> vroom 176.7522 189.8111 205.2099 197.9027 206.2619 495.2784 100
#> fread 291.9581 370.8261 410.3995 398.9489 439.7827 638.0363 100
NO ejecutar durante el workshop porque toma tiempo en correr
suppressMessages(
microbenchmark::microbenchmark(
read.csv = read.csv("data/personal.csv"),
fst = fst::read_fst("data/personal.fst"),
parquet = arrow::read_parquet("data/personal.parquet"),
rds = readRDS("data/personal.rds")
)
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval
#> read.csv 1911.2919 2075.26525 2514.29114 2308.57325 2658.03690 4130.748 100
#> fst 201.1500 267.85160 339.73881 308.24680 357.19565 834.646 100
#> parquet 64.5013 67.29655 84.48485 70.70505 87.81995 405.147 100
#> rds 558.5518 644.32460 782.37898 695.07300 860.85075 1379.519 100
Es, en esencia, caching. Personalmente, mi estrategia favorita.
Difícil de usar si se requiere calcular en vivo, real-time (stock exchange, streaming data), o la data no puede ser guardada en cualquier lugar (seguridad, privacidad).
Algunos ejemplos notables son SQLite, MySQL, PostgreSQL, DuckDB.
freeCodeCamp tiene un buen curso para principiantes.
Implementa una estrategia de optimización
Del lado de shiny, optimizar consiste básicamente en hacer que la app (en realidad, el procesador) haga el menor trabajo posible.
server <- function(input, output, session) {
output$table <- renderTable({
survey |>
filter(region == input$region) |>
filter(age <= input$age)
})
output$histogram <- renderPlot({
survey |>
filter(region == input$region) |>
filter(age <= input$age) |>
ggplot(aes(temps_trajet_en_heures)) +
geom_histogram(bins = 20) +
theme_light()
})
}
reactive()
al rescate
server <- function(input, output, session) {
filtered <- reactive({
survey |>
filter(region == input$region) |>
filter(age <= input$age)
})
output$table <- renderTable({
filtered()
})
output$histogram <- renderPlot({
filtered() |>
ggplot(aes(temps_trajet_en_heures)) +
geom_histogram(bins = 20) +
theme_light()
})
}
Puedes encadenar bindEvent()
a un reactive()
u observe()
.
ui <- page_sidebar(
sidebar = sidebar(
selectInput(inputId = "region", ...),
sliderInput(inputId = "age", ...),
actionButton(inputId = "compute", label = "Calcular")
),
...
)
server <- function(input, output, session) {
filtered <- reactive({
survey |>
filter(region == input$region) |>
filter(age <= input$age)
}) |>
bindEvent(input$compute, ignoreNULL = FALSE)
}
Ahora filtered()
solo se actualizará cuando haya interacción con input$compute
.
bindCache()
nos permite guardar cómputos al vuelo en base a ciertas keys.
Cuando una combinación vuelva a aparecer, se leerá el valor en lugar de recalcularlo.
cache = "app"
(default)cache = "session"
cache object + opciones
Por defecto, se usará un máximo de 200 Mb de caché.
bindEvent()
)Es posible delegar ciertos cálculos al navegador. Por ejemplo, renderizar un gráfico con {plotly}
en lugar de {ggplot2}
.
Con ello, se manda la “receta” del gráfico en lugar del gráfico mismo. Al recibir la receta, el navegador se encarga de renderizarla.
Estas funciones traducen sintaxis de ggplot2
a sintaxis de plotly.js
de manera bastante eficiente. Tiene soporte para muchos tipos de gráficos.
Pero no te confíes, en muchos casos, el código va a necesitar retoques. Especialmente al usar extensiones de ggplot2
.
Otros paquetes similares:
En el caso de las tablas, el server-side processing permite paginar el resultado y enviar al navegador solo la página que está siendo mostrada en el momento.
El paquete {DT}
es una opción solida.
Otra opcion:
{reactable}
en conjunto con {reactable.extras}
(by Appsilon).Implementa alguna de las optimizaciones mencionadas.
Ejemplo: Una cocina con una hornilla. Si empecé a freir pollo, no puedo freir nada hasta terminar de freir el pollo.
Ejemplo: Una cocina con múltiples hornillas. Si empecé a freir pollo en una hornilla, puedo freir otra cosa en una hornilla diferente.
Hornilla == Proceso en la PC
Cuidado
A más hornillas, también es más fácil quemar la comida!
Nota
ExtendedTask es un recurso bastante nuevo. También es posible usar solo future()
o future_promise()
dentro de un reactive para lograr un efecto similar, aunque con menos ventajas.
Esto le dice al proceso que corre la app que los futures creados se resuelvan en sesiones paralelas.
ExtendedTask
.Cambiamos el actionButton()
por bslib::input_task_button()
. Este botón tendrá un comportamiento especial.
server <- function(input, output, session) {
filter_task <- ExtendedTask$new(function(p_survey, p_region, p_age) {
future_promise({
p_survey |>
dplyr::filter(region == p_region) |>
dplyr::filter(age <= p_age)
})
}) |>
bind_task_button("compute")
observe(filter_task$invoke(survey, input$region, input$age)) |>
bindEvent(input$compute, ignoreNULL = FALSE)
filtered <- reactive(filter_task$result())
output$table <- DT::renderDT(filtered())
...
}
Paso 1: Se creó un ExtendedTask
que envuelve a una función.
Dentro de la función, tenemos la lógica de nuestro cálculo envuelta en un future_promise()
. La función asume una sesión en blanco.
Paso 2: Se hizo bind a un task button
Nota
bind_task_button()
requiere el mismo id que input_task_button()
. bindEvent()
acepta cualquier reactive.
Paso 3: Invocar la tarea con ExtendedTask$invoke()
.
server <- function(input, output, session) {
filter_task <- ExtendedTask$new(function(p_survey, p_region, p_age) {
...
}) |>
bind_task_button("compute")
observe(filter_task$invoke(survey, input$region, input$age)) |>
bindEvent(input$compute, ignoreNULL = FALSE)
filtered <- reactive(filter_task$result())
output$table <- DT::renderDT(filtered())
...
}
Se le provee la data necesaria para trabajar. Tomar en cuenta que invoke()
no tiene valor de retorno (es un side-effect).
Paso 4: Recuperar los resultados con ExtendedTask$result()
.
server <- function(input, output, session) {
filter_task <- ExtendedTask$new(function(...) {
...
}) |>
bind_task_button("compute")
observe(filter_task$invoke(...)) |>
bindEvent(input$compute, ignoreNULL = FALSE)
filtered <- reactive(filter_task$result())
output$table <- DT::renderDT(filtered())
...
}
result()
se comporta como cualquier reactive.
server <- function(input, output, session) {
filter_task <- ExtendedTask$new(function(p_survey, p_region, p_age) {
future_promise({
p_survey |>
dplyr::filter(region == p_region) |>
dplyr::filter(age <= p_age)
})
}) |>
bind_task_button("compute")
observe(filter_task$invoke(survey, input$region, input$age)) |>
bindEvent(input$compute, ignoreNULL = FALSE)
filtered <- reactive(filter_task$result())
output$table <- DT::renderDT(filtered())
...
}
¡Perdimos el cache! ExtendedTask()
no es 100% compatible con las estrategias de caching vistas.
Acá se desplegó la app con todas las mejoras vistas en los ejercicios. Además, survey
utiliza la data completa, en lugar de una muestra por región.
Enlace: https://01933e23-3162-29f7-ec09-ce351b4b4615.share.connect.posit.cloud/-
¡{rhino}
tiene todo esto!
LatinR: 2024-11-18