
High-Performance Asynchronous Processing with mirai
Source:vignettes/mirai-integration.Rmd
mirai-integration.RmdIn 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.
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 <br/> 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.
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<br/>and schedules handler
Main->>Workers: 2. Offload computation
Note over Workers: Worker process executes task<br/>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:
-
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. - 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
callradds 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.
# 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.