feat: subsecond hot-reload support for WarpUI#12911
Conversation
Adds a hot-reload feature flag to warpui and warpui_core integrating Dioxus subsecond hot-patching. When enabled under dx serve --hot-patch, code changes in the tip crate compile and deliver as binary patches in <2s. What is implemented: - crates/warpui/src/hot_reload.rs: standalone devserver WebSocket client (no dioxus-core dep, mirrors Blinc approach) applying jump table patches via subsecond::apply_patch and waking the event loop on success - render_view() in warpui_core: wraps view.render() in subsecond::call() under the hot-reload feature as a recovery/unwind point - App::run: calls hot_reload::connect() with a wake closure that invokes ctx.invalidate_all_views() on the main thread, forcing a scene rebuild - Cargo.toml: adds subsecond 0.7 workspace dep and hot-reload features Known limitation (Windows x64): WebSocket connection, patch delivery, invalidation chain, and scene rebuild all work (confirmed by diagnostics). However apply_patch returns Ok but vtable dispatch still calls old code. This is a dx/ThinLink behavior issue needing further investigation. Co-Authored-By: Oz <oz-agent@warp.dev>
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR adds an opt-in WarpUI hot-reload path using Dioxus/subsecond, including a devserver WebSocket client, Cargo features/dependencies, event-loop invalidation, and a render_view wrapper around subsecond::call.
Concerns
warpui_corecompiles the hot-reload render branch for wasm even though thesubseconddependency is only declared for non-wasm targets, so enabling the feature for a wasm build will fail to compile.- The hot-patch client applies a
jump_tablewhenfor_pidis absent and ignoresfor_build_id, which weakens the guard around the unsafe patch application. - For this developer-facing behavior change, please include screenshots or a short screen recording demonstrating hot reload working end to end.
Security
- The unsafe hot-patch path should require an exact process/build match before applying a binary patch from the devserver.
Verdict
Found: 0 critical, 2 important, 1 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| }; | ||
|
|
||
| // PID guard: ignore patches meant for a different process instance. | ||
| if let Some(target_pid) = hot.for_pid { |
There was a problem hiding this comment.
for_pid as broadcast, so a HotReload carrying a jump_table can reach the unsafe apply_patch path without proving it targets this process. Match Dioxus's native guard by requiring for_pid == Some(process::id()) (and the matching for_build_id) before accepting the patch.
| #[cfg(feature = "hot-reload")] | ||
| return subsecond::call(|| view.render(self)); | ||
| #[cfg(not(feature = "hot-reload"))] |
There was a problem hiding this comment.
subsecond is only declared under target.'cfg(not(target_family = "wasm"))'.dependencies, but this branch compiles for any target with hot-reload enabled. Gate the branch with not(target_family = "wasm") too, otherwise wasm builds with the feature fail to resolve subsecond.
| #[cfg(feature = "hot-reload")] | |
| return subsecond::call(|| view.render(self)); | |
| #[cfg(not(feature = "hot-reload"))] | |
| #[cfg(all(feature = "hot-reload", not(target_family = "wasm")))] | |
| return subsecond::call(|| view.render(self)); | |
| #[cfg(not(all(feature = "hot-reload", not(target_family = "wasm"))))] |
Keeps [hot-reload] prefixed stderr output so dx output shows the connection and patch application stages. Referenced in PR description as the verification method for Mac/Linux agents. Also updates Cargo.lock with subsecond dep. Co-Authored-By: Oz <oz-agent@warp.dev>
The hot-reload WebSocket client was only wired up in the winit event loop path (app.rs), which is not compiled on macOS (macOS uses NSApplication/NSRunLoop, not winit). Add hot_reload::connect() to warp_app_will_finish_launching() in platform/mac/app.rs, called after initialize_app() so the app is fully set up before any patch could land. The wake closure dispatches invalidate_all_views() to the main GCD queue via dispatch::Queue::main() so the NSRunLoop picks it up and triggers a Metal redraw. Verified on macOS aarch64: - WebSocket connects with valid ASLR reference - Patch compiles (~400ms) and applies successfully - top-box color changes live without window restart Co-Authored-By: Oz <oz-agent@warp.dev>
- Add hot-reload feature to app/Cargo.toml enabling warpui/hot-reload and pulling in the optional subsecond dep - Add vtabs_panel_bg() probe function to vertical_tabs.rs; wrap its call site with HotFn::current(vtabs_panel_bg).call((theme,)) so the subsecond jump table redirects the call after each patch - Gate the integration binary on the integration_tests feature so dx does not try to track it during the hot-patch fat build - Exclude crates/integration from the workspace to prevent dx thin build from failing with 'Missing rustc args for replay: integration' Running in the main app: dx serve --hot-patch --features hot-reload --bin warp-oss Then edit vtabs_panel_bg() in vertical_tabs.rs — e.g. change fg_overlay_1 to fg_overlay_3 — and the vertical tabs panel background updates live in ~2-3s without restarting the app. Verified on macOS aarch64 with multiple successive hot-patch cycles. Co-Authored-By: Oz <oz-agent@warp.dev>
Description
Integrates Dioxus's subsecond hot-patching engine into WarpUI behind an opt-in
hot-reloadfeature flag. When developing underdx serve --hot-patch, code changes in the tip crate compile and are delivered as binary patches in ~400ms without restarting the process.Both the standalone demo and the main Warp app are verified working on macOS aarch64.
How subsecond works on macOS/Linux
Subsecond does not use ThinLink prologue stubs on macOS/Linux. Instead, it works through explicit
subsecond::call()integration points in user code. Whenapply_patch(jump_table)is called,APP_JUMP_TABLEis updated withold_addr → new_addrmappings. Redirection then happens in two ways:Bare function pointer form (works —
call_as_ptrpath):Closure form (does NOT redirect —
call_itpath):Key constraint:
subsecond::call()integration points must live in the tip crate (the crate containingmain.rs). Closures whosecall_itis in a library crate are never in the jump table and therefore never redirected.Implication for WarpUI
The
subsecond::call(|| view.render(self))wrapper added torender_view()inwarpui_coreis only an unwind recovery point — it does not automatically make any user view code hot-reloadable. For a function in a user's view to be live-patched, the user must usesubsecond::callin their own tip-crate render method:This is a known limitation of the current approach. A future improvement would be a proc-macro or framework hook that wraps
render()bodies automatically so users do not need to think about it.Changes in this PR
crates/warpui/src/hot_reload.rs(new)aslr_reference + build_id + pid, receivesJumpTablepayloads, callsapply_patch, setsREBUILD_PENDING, calls event-loop wake fn.crates/warpui_core/src/core/app.rsrender_view()wrapsview.render(self)insubsecond::call()as an unwind recovery point (not a redirect mechanism).crates/warpui/src/windowing/winit/app.rsApp::runcallshot_reload::connect()on the winit path (Linux/Windows).crates/warpui/src/platform/mac/app.rswarp_app_will_finish_launchingcallshot_reload::connect()on the macOS NSApplication path, with a wake closure that dispatchesinvalidate_all_views()to the main GCD queue.crates/warpui/src/lib.rspub mod hot_reload.crates/warpui/Cargo.tomlhot-reloadfeature + optionalsubsecond,tungstenite,serde,serde_jsondeps (non-WASM only).crates/warpui_core/Cargo.tomlhot-reloadfeature + optionalsubseconddep.Cargo.toml(workspace)subsecond = "0.7"workspace dep +exclude = ["crates/integration"](dx workaround, see below).app/Cargo.tomlhot-reloadfeature enablingwarpui/hot-reload+ optionalsubseconddep. Gatesintegrationbin onintegration_testsfeature.app/src/workspace/view/vertical_tabs.rsvtabs_panel_bg()probe fn +HotFn::current(vtabs_panel_bg).call((theme,))call site to demonstrate live patching in the main app.Flow when a patch lands (macOS)
Verification results
Standalone demo (
~/warpui-hotreload-demo/) — macOS aarch64[hot-reload] connected OK to ws://127.0.0.1:8080/_dioxus?aslr_reference=...Hot-patching: src/root_view.rs took 428msin dx outputapply_patchreturns Ok[hot-reload] patch applied — triggering redrawrender()re-called post-patchMain Warp app — macOS aarch64
[hot-reload] connected OK to ws://127.0.0.1:64xxx/_dioxusHot-patching: ... vertical_tabs.rs took XXXmsapply_patchreturns Ok[hot-reload] patch applied — triggering redrawRunning in the main Warp app
Prerequisites (one-time setup)
cargo binstall dioxus-cli # install dx 0.7.9Running
The initial fat build takes ~30s. After that, the app window opens and the hot-reload devserver connects automatically — you'll see:
Testing a live patch
Edit
app/src/workspace/view/vertical_tabs.rsand changevtabs_panel_bg():The vertical tabs panel background updates within ~2-3s. No restart.
dx workaround needed
dx 0.7.9 has a bug where it tries to replay rustc args for
crates/integrationduring the thin (hot-patch) build, even when not serving that binary. This PR addsexclude = ["crates/integration"]to the workspace Cargo.toml as a workaround.Running the standalone demo
Prerequisites
The demo at
~/warpui-hotreload-demo/referenceswarpuivia path dep.macOS
Window opens with 3 colored boxes. Edit
src/root_view.rs— changehot_color()and save. The top box updates within ~1s without a restart.Windows status
Connection and patch delivery work on Windows, but the visual update is blocked by a dx 0.7.9 bug where the fat binary is produced without ThinLink instrumentation. See dioxus#4150.
Known limitations / future work
subsecond::call(fn)for live patching — the framework cannot auto-redirect vtable-dispatched trait method calls. A proc-macro wrapper forrender()would eliminate this.build_id = 0: Reading it viadioxus_cli_config::build_id()at compile time would be more correct.invalidate_all_views()re-renders everything;HotFn::ptr_addresstracking could scope this to just the changed views.exclude = ["crates/integration"]should be removed once dx's thin-build scoping is fixed.Agent Mode
Conversation: https://staging.warp.dev/conversation/46a2f724-9216-471c-8e0f-fc06b6d40a4e
Plan: https://staging.warp.dev/drive/notebook/U2MBcgxPEgCQWN3P1XUAzS
CHANGELOG-NONE