--- title: "High-Performance Asynchronous Processing with mirai" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{High-Performance Asynchronous Processing with mirai} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") ``` ```{r package-status} RDesk::rdesk_jobs_pending() ``` In a native desktop application, maintaining a highly responsive user interface is a primary design goal. If a user clicks a button to load a large dataset, compile a plot, or run a statistical model, and the user interface freezes while R computes, the user experience degrades. To address this, RDesk implements a non-blocking asynchronous processing engine. Under the hood, RDesk utilizes the **mirai** package to offload intensive computations to persistent background worker processes, ensuring the frontend UI remains interactive at all times. --- ## The Asynchronous Architecture RDesk operates as a parent process managing the main R6 event loop, while launching WebView2 as a native child window. To keep communication fast and secure, RDesk avoids loopback network stacks and HTTP overhead, relying instead on standard I/O pipes. When a long-running task is submitted, RDesk offloads the work to **mirai background daemons**. The diagrams below illustrate how RDesk, standard input/output pipes, and the `mirai` daemon pool interact to process background operations. ### 1. Architectural Component Map This diagram illustrates the process and memory boundaries between the frontend user interface, the main R process event loop, and the pre-warmed background worker processes. ```mermaid graph TD subgraph Core [Main Application Event Flow] Loop[R6 Event Loop] -->|1. Submit Job| Poller[Unresolved Job Poller] Poller -->|2. Offload Task| Workers[mirai Daemon Pool] Workers -->|3. Complete Job| Poller Poller -->|4. Push Results| Loop end UI[Client UI
WebView2 Shell] <-->|stdin / stdout pipes| Loop ``` ### 2. Message Lifecycle Sequence This sequence diagram outlines the chronological flow of a message, from a user action in the UI, through the stdin pipe to R, submission to a mirai background daemon, non-blocking state polling, and the final return of results to the frontend. ```mermaid sequenceDiagram autonumber participant UI as WebView2 UI (HTML/JS) participant Main as Main R Process (Event Loop) participant Workers as mirai Daemon Pool UI->>Main: 1. Send action with payload Note over Main: Receives message envelope
and schedules handler Main->>Workers: 2. Offload computation Note over Workers: Worker process executes task
in the background (non-blocking) loop Event Loop Main->>Main: 3. Non-blocking job polling end Workers-->>Main: 4. Complete task and return results Main->>UI: 5. Push results back to UI ``` --- ## Persistent Daemons vs On-Demand Processes RDesk implements a **dual-backend** async mechanism that automatically selects the best available engine: 1. **mirai (Persistent Daemons - Default)**: Starts a pool of pre-warmed background workers at application launch using `mirai::daemons()`. When an asynchronous task is submitted, it is dispatched to an existing worker instantly. 2. **callr (On-Demand Processes - Fallback)**: Spins up a fresh R process for each task and terminates it upon completion. This is utilized in headless CI environments or systems where background daemons are unavailable. ### Why mirai is the preferred backend: * **Zero Startup Latency**: Pre-warmed background workers start executing tasks in milliseconds, whereas spawning a fresh R process on demand via `callr` adds 1–2 seconds of process-creation overhead for each execution. * **Resource Preservation**: A fixed pool of persistent workers ensures background memory usage remains flat and predictable, avoiding spikes from concurrent process launches. * **Automatic Lifecycle Handling**: Daemons are automatically spun up on `App$run()` startup and cleanly terminated on application shutdown, preventing orphaned zombie processes. --- ## Under the Hood: The Developer Experience For the application developer, RDesk abstracts this multi-process coordination into the standard `async()` wrapper. ```r # In R/server.R app$on_message("run_model", async(function(payload) { # This code runs entirely in an isolated mirai worker. # The UI remains 100% interactive and responsive. Sys.sleep(3) # Simulate a heavy statistical modeling task result <- kmeans(mtcars, centers = payload$centers) list(centers = result$centers, size = result$size) }, app = app, loading_message = "Running K-Means...")) ``` ### Critical Rules for Async Workers: Because the `async()` handler runs in an isolated `mirai` background process: * **No `app$` Access**: Background workers cannot reach the main R process. Do not call `app$send()`, `app$toast()`, or `app$dialog_*` inside the worker closure. Return the final data as a list, and the wrapper handles routing. * **Scoped Closures**: Ensure any package dependencies are explicitly imported (using `library()` inside the worker, or listed in the `DESCRIPTION` Imports) and variables are passed via the payload or explicitly bound in the local closure.