Class UiRuntime

java.lang.Object
limn.concurrent.UiRuntime
All Implemented Interfaces:
AutoCloseable

public final class UiRuntime extends Object implements AutoCloseable
The concurrency runtime behind Ui: a single UI thread fed by a thread-safe (MPSC) task queue, plus a worker pool for the application's heavy background work.

The owning backend binds the UI thread (bindToCurrentThread()), drains the queue once per frame (drain()) and sleeps in the native event wait between frames, never busy-waiting. Posting triggers the UiRuntime.Waker, which the LWJGL backend maps to glfwPostEmptyEvent() so the sleeping loop wakes up.

Drain semantics: drain() runs only the tasks that were queued when it started (a snapshot). Tasks posted while draining run on the next frame, which keeps a self-reposting task from live-locking the loop. The loop must therefore consult nanosUntilNextDeadline() before sleeping: 0 means "don't sleep, there is pending work".

Every task is timed; tasks exceeding the slow-task budget (default 8 ms) are logged as warnings, making "my click handler does blocking I/O" contract violations visible during development.

  • Constructor Details

  • Method Details

    • create

      public static UiRuntime create(UiRuntime.Waker waker)
      Creates a runtime with the real clock and an owned daemon worker pool.
      Parameters:
      waker - invoked (from arbitrary threads) whenever work is posted from outside the UI thread
    • bindToCurrentThread

      public void bindToCurrentThread()
      Declares the calling thread as the UI thread. Called once by the backend.
    • isUiThread

      public boolean isUiThread()
      Returns:
      true iff called on the bound UI thread
    • checkUiThread

      public void checkUiThread()
      Enforces thread confinement: throws unless called on the UI thread.
      Throws:
      IllegalStateException - if called from any other thread, or before a UI thread was bound
    • post

      public void post(Runnable action)
      Enqueues action to run on the UI thread on the next frame. Safe to call from any thread; wakes the native loop when needed.

      A task that mutates widget or scene state is responsible for invalidating what it touched: Widget.invalidate(), Widget.markNeedsLayout() or Scene.requestRender(). Running a task buys no frame of its own, so a mutation nothing invalidates stays unpainted until something else asks for one. Every widget setter and every tree change invalidates already; a task that writes a field behind a setter's back, or that changes what a custom onPaint reads, does not.

      The UiRuntime.Waker is called on every post, this thread's included; see there for why the runtime does not decide whether the wake is needed.

    • postDelayed

      public Subscription postDelayed(Runnable action, long delayMillis)
      Enqueues action to run on the UI thread once delayMillis have elapsed. Safe to call from any thread.

      A task that mutates widget or scene state is responsible for invalidating what it touched: see post(java.lang.Runnable). Firing buys no frame of its own, which is what makes a timer the cheap way to watch something that rarely changes: a poll that re-reads a value and finds it unchanged costs a wake-up and nothing else, where a ticker asks for a frame every frame it stays registered.

      Returns:
      a handle that withdraws the task if it has not run, from any thread
    • setDelayClock

      public void setDelayClock(@Nullable LongSupplier clock)
      Measures delayed tasks on clock from now on, or on this runtime's own clock again when it is null. A task already waiting keeps what was left of its delay: due in 300 ms on the old clock, it is due in 300 ms on the new one, and the queue keeps its order.

      For a harness that renders on a clock of its own. The site's gallery films each scene on a clock that advances a fixed step per frame, and a delayed task measured on the wall -- a scroll bar's hold, a caret's blink, a tooltip's dwell -- fell due on whichever frame the render speed put it on, so a warm JVM and a cold one filmed different frames. Measured on the film's clock it falls due on the same frame on any machine.

      nanosUntilNextDeadline() answers on that clock too, so a clock that moves only when its owner moves it holds its tasks until then, however long the loop sleeps. That is the point for a harness that renders every frame it can, and the reason an application has no use for this. The slow-task budget stays on the runtime's own clock: it measures how long a task took, and a film's clock stands still while one runs.

      Parameters:
      clock - what delayed tasks read as now, or null for this runtime's own clock
    • async

      public <T extends @Nullable Object> CompletableFuture<T> async(Supplier<T> work)
      Runs work on the worker pool and completes the returned future on the UI thread. The future's default async executor is the UI thread too, so thenAccept/whenComplete callbacks land on the UI thread, the canonical "click → fetch → update label" path.

      Precisely: dependents registered before completion (the normal case, chaining right after this call) and all *Async dependents run on the UI thread. A non-async dependent attached from a background thread after the future already completed runs inline on the attaching thread, and cancel() completes the future on the cancelling thread; in those corners, use thenAcceptAsync(fn) (the default executor is already the UI thread) or attach from the UI thread.

      A dependent that mutates widget or scene state is responsible for invalidating what it touched: see post(java.lang.Runnable). Completing on the UI thread buys no frame; label.setText(data) asks for the frame that shows it, a field written directly does not.

    • work

      public <T extends @Nullable Object> Work<T> work(Work.Body<T> body)
      Describes background work with a lifecycle: cancellable, able to report progress, and able to decline delivery when whoever asked has gone away. Nothing runs until Work.start().

      Reach for this over async(java.util.function.Supplier<T>) whenever the request can be superseded: a cancelled job delivers nothing, so a view that starts one per keystroke shows the answer to the question last asked, where competing async calls show whichever answer happened to finish last.

    • uiExecutor

      public Executor uiExecutor()
      Returns:
      an Executor that posts to the UI thread (post(java.lang.Runnable))
    • workerPool

      public ExecutorService workerPool()
      Returns:
      the worker pool used by async(java.util.function.Supplier<T>)
    • drain

      public int drain()
      Runs due tasks on the UI thread: first promotes expired delayed tasks, then runs the tasks queued at the moment this call started (snapshot; see class docs). A task that throws is logged and does not abort the drain, and a task over the slow-task budget is logged as a warning.

      Draining schedules no repaint. Each task invalidates what it mutated (see post(java.lang.Runnable)); one that threw did not finish doing so, which is what drain(Runnable) exists for.

      Returns:
      the number of tasks executed
    • drain

      public int drain(Runnable onTaskCrash)
      drain(), plus a hook for the state a crashed task left behind.
      Parameters:
      onTaskCrash - run on the UI thread, inside the drain, once per task that threw, after the crash is logged and reported. A task that throws part-way has applied part of its mutation and invalidated none of it, and how much is unknowable from here, so the caller that owns the surfaces settles them: the LWJGL backend repaints every window. Called on no other path; a task that returns normally is trusted to have invalidated its own work. Runs inside the drain, so it must not throw (an exception from it aborts the remaining tasks).
      Returns:
      the number of tasks executed, crashed ones included
    • hasPendingTasks

      public boolean hasPendingTasks()
      Returns:
      true if immediate tasks are queued right now
    • nanosUntilNextDeadline

      public long nanosUntilNextDeadline()
      How long the native loop may sleep: 0 if immediate work is pending, -1 if it may sleep indefinitely, otherwise the nanoseconds until the earliest delayed task is due, on the clock delayed tasks are measured on (setDelayClock(java.util.function.LongSupplier)).
    • setSlowTaskBudgetMillis

      public void setSlowTaskBudgetMillis(long millis)
      Budget above which a drained task is reported as slow. Default: 8 ms.
    • close

      public void close()
      Shuts down the worker pool if this runtime created it.
      Specified by:
      close in interface AutoCloseable