Skip to contents

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.

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:

  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.

# 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.