makepad/isolate.md
andodeki d6d1f99ca9 Update fork to upstream dev 5d4483f
- Sync with latest upstream dev branch
- Include all map improvements: 2D/3D toggle, shadows, labels, overlays
- Include platform updates: location API, audio echo cancellation
- Preserve fork-specific re-exports (gltf, csg, test)
2026-07-31 18:47:03 +00:00

228 lines
7.4 KiB
Markdown

# Splash UI-Thread Isolate Changes
## Target
Run each inline `Splash` widget in its own script VM while still creating and mutating native widgets on the UI thread.
The MVP is not a worker-thread sandbox. It is UI-thread VM isolation with sampled wall-clock guards so Splash scripts cannot monopolize the UI loop indefinitely.
## VM Identity
Use one VM id type everywhere:
```rust
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
struct SplashVmId(u64);
const MAIN_SPLASH_VM_ID: SplashVmId = SplashVmId(0);
```
Rules:
- `SplashVmId(0)` is the existing app VM.
- New Splash isolates allocate ids from `1` upward.
- Widget/script routing carries `SplashVmId`, not a `ScriptVmOwner` enum.
- Unknown app widgets default to `MAIN_SPLASH_VM_ID`.
## Cx Storage
Add isolated VM storage under `CxScriptData`:
```rust
struct IsolatedScriptVms {
next_id: u64,
vms: HashMap<SplashVmId, IsolatedSplashVm>,
widget_owners: HashMap<WidgetUid, SplashVmId>,
}
struct IsolatedSplashVm {
std: ScriptStd,
vm: Option<Box<ScriptVmBase>>,
initialized: bool,
generation: u64,
wake_pending: bool,
consecutive_over_budget: u32,
}
```
Add helpers:
- `Cx::with_script_vm_id(vm_id, f)`: routes id `0` to the app VM, non-zero ids to isolated VMs.
- `Cx::with_splash_vm(id, f)`: non-zero isolate helper.
- subtree owner registration/unregistration for widgets produced by a Splash view.
## Splash Eval
Change `Splash::eval_body` from app-VM eval to isolate eval:
```text
Splash::set_text
-> allocate or reuse SplashVmId
-> initialize restricted Splash VM once
-> eval_with_append_source under wall-clock budget
-> View::script_from_value in the same VM
-> register produced widget subtree with that SplashVmId
-> replace the native View
-> redraw
```
The restricted Splash VM registers the widget/runtime modules needed for normal widget DSL, but does not inherit host app script globals. Filesystem, process spawning, and arbitrary network remain disabled by default.
## Widget Message Routing
Make widget async routing VM-id-aware.
Update widget-to-script requests:
```rust
struct WidgetToScriptCallRequest {
vm_id: SplashVmId,
target_uid: WidgetUid,
// existing callback/function/arg fields
}
```
Dispatch:
- `vm_id == MAIN_SPLASH_VM_ID`: use existing app `cx.with_vm`.
- non-zero `vm_id`: use `cx.with_splash_vm`.
Update script-to-widget async calls the same way:
- pending returns keyed by `(SplashVmId, ScriptThreadId)`;
- `ui` handle injection is per VM;
- `View.render()` callbacks and returned objects apply in the same VM that requested them.
Move `TextInput` callback execution onto the same VM-id-aware queue instead of directly calling `cx.with_vm`.
## UI Loop Pumping
Use the existing UI service points:
- after `Cx::call_event_handler`, pump app tasks, then pump runnable Splash isolates;
- on `Event::Signal`, pump isolates with `wake_pending` or queued work;
- on `TimerEvent`, route script timers by `SplashVmId`;
- on network responses, only route to Splash isolates if network is explicitly enabled.
Scheduling policy:
- round-robin non-zero Splash ids;
- per-isolate soft wall-clock slice;
- aggregate per-frame Splash wall-clock cap;
- one wake request per isolate while work is pending, using `wake_pending` to avoid signal storms.
## Tasks And Async
Each isolate owns its own `ScriptStd`, including `ScriptTasks`.
Add a budgeted task pump:
```rust
fn handle_script_tasks_budgeted(
host: &mut dyn Any,
std: &mut ScriptStd,
script_vm: &mut Option<Box<ScriptVmBase>>,
budget: &mut ScriptRunBudget,
) -> ScriptPumpOutcome;
```
Stop pumping when:
- no work progresses;
- the isolate time slice is exhausted;
- the frame-wide Splash time budget is exhausted;
- the isolate hits hard overrun policy.
Do not discard unfinished work on a soft time yield. Leave pending resumes, task queues, and pump-hook work in that isolate's `ScriptStd`.
`widget_async` task hooks must be installed per `ScriptStd` or per `SplashVmId`, not behind one global app-only boolean.
## Timers
Make script timers VM-id-aware:
```rust
struct CxScriptTimer {
vm_id: SplashVmId,
id: LiveId,
repeat: bool,
timer: Timer,
callback: ScriptFnRef,
}
```
`std.start_timeout`, `std.start_interval`, and `std.stop_timer` operate on the current VM id. `stop_timer` only removes timers owned by that VM id.
Timer callbacks run through `Cx::with_script_vm_id` under the timer callback wall-clock budget. On repeated interval overruns, stop the timer and report an isolate diagnostic.
## Sampled Wall-Clock Guard
Add a wall-clock budget checked inside `ScriptVm::run_core`, but only sample time every N instructions:
```rust
pub struct ScriptRunBudget {
pub soft_deadline: Instant,
pub hard_deadline: Instant,
pub sample_interval_instructions: u32,
pub instructions_until_sample: u32,
pub reason: ScriptBudgetReason,
}
```
Deadline meaning:
- `soft_deadline`: cooperative yield point. The current script thread stays resumable, the isolate is queued for a later signal/frame, and this is not treated as an error.
- `hard_deadline`: runaway failure point. The current script execution is bailed, pending work for that isolate generation is cleared, and a diagnostic is reported.
Interpreter loop behavior:
```text
each opcode/direct value
-> instructions_until_sample -= 1
-> if zero:
instructions_until_sample = sample_interval_instructions
now = monotonic_time()
if now >= hard_deadline: hard time-budget failure
if now >= soft_deadline: soft time-budget yield
```
Do not reuse `ScriptTrapOn::Pause` for budget yield. Add a distinct outcome/trap, for example `TimeBudgetYield`, so async pauses still mean "waiting for external work".
Soft time yield:
- preserve thread state and instruction pointer;
- queue the isolate for a later signal/frame;
- return control to the UI loop without treating it as an error.
Hard time failure:
- bail the current thread;
- clear pending timers/tasks/callbacks for that isolate generation;
- keep the previous valid Splash view if possible;
- show/log a diagnostic.
Native Rust calls cannot be preempted mid-call. For Splash VMs, sample immediately before and after native calls so execution stops after an overlong native call returns.
## Implementation Order
1. Add `SplashVmId`, id `0` main routing, and isolated VM registry storage.
2. Add `Cx::with_script_vm_id` and isolate initialization.
3. Add sampled wall-clock budget support to VM entry points and `run_core`.
4. Move `Splash::eval_body` to isolated VM eval and subtree owner registration.
5. Make widget async requests, returns, `ui`, `View.render()`, and `TextInput` VM-id-aware.
6. Make task hooks and task pumping per VM id and budgeted.
7. Make script timers VM-id-aware and budgeted.
8. Add isolate pumping to event, signal, timer, and optional network paths.
9. Add stale subtree unregistering and isolate generation cleanup.
## Tests
- Two Splash widgets keep independent state and callbacks.
- Splash callback updates only its own subtree.
- `TextInput.on_return` runs in the owning Splash VM.
- `View.render()` callback and result apply in the owning Splash VM.
- Infinite loop yields or is killed by wall-clock policy while the app UI remains responsive.
- Requeueing task is limited by per-isolate pump budget.
- Interval callback cannot run in app VM and is stopped after repeated overruns.
- `stop_timer` from one VM cannot cancel another VM's timer.
- Replacing Splash body invalidates stale callbacks from the previous generation.