Skip to content

feat: subsecond hot-reload support for WarpUI#12911

Open
acarl005 wants to merge 4 commits into
masterfrom
andy/subsecond-hot-reload
Open

feat: subsecond hot-reload support for WarpUI#12911
acarl005 wants to merge 4 commits into
masterfrom
andy/subsecond-hot-reload

Conversation

@acarl005

@acarl005 acarl005 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Description

Integrates Dioxus's subsecond hot-patching engine into WarpUI behind an opt-in hot-reload feature flag. When developing under dx 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. When apply_patch(jump_table) is called, APP_JUMP_TABLE is updated with old_addr → new_addr mappings. Redirection then happens in two ways:

Bare function pointer form (works — call_as_ptr path):

// F is fn() -> ColorU — pointer-sized, so transmute_copy gives the address
// which is looked up directly in APP_JUMP_TABLE
let color = subsecond::call(hot_color);

Closure form (does NOT redirect — call_it path):

// The closure's call_it lives in whatever crate contains this code.
// Only works if that crate is the tip crate being patched.
let color = subsecond::call(|| hot_color());

Key constraint: subsecond::call() integration points must live in the tip crate (the crate containing main.rs). Closures whose call_it is 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 to render_view() in warpui_core is 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 use subsecond::call in their own tip-crate render method:

// In user's tip crate — either pattern works:
fn render(&self, ctx: &AppContext) -> Box<dyn Element> {
// Pattern A: bare fn pointer (simplest for leaf functions)
let color = subsecond::call(hot_color);

// Pattern B: wrap entire body (patches everything in the closure)
subsecond::call(|| {
let color = hot_color();
// ...
})
}

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

File What
crates/warpui/src/hot_reload.rs (new) Standalone devserver WebSocket client. Connects with aslr_reference + build_id + pid, receives JumpTable payloads, calls apply_patch, sets REBUILD_PENDING, calls event-loop wake fn.
crates/warpui_core/src/core/app.rs render_view() wraps view.render(self) in subsecond::call() as an unwind recovery point (not a redirect mechanism).
crates/warpui/src/windowing/winit/app.rs App::run calls hot_reload::connect() on the winit path (Linux/Windows).
crates/warpui/src/platform/mac/app.rs warp_app_will_finish_launching calls hot_reload::connect() on the macOS NSApplication path, with a wake closure that dispatches invalidate_all_views() to the main GCD queue.
crates/warpui/src/lib.rs Re-exports pub mod hot_reload.
crates/warpui/Cargo.toml hot-reload feature + optional subsecond, tungstenite, serde, serde_json deps (non-WASM only).
crates/warpui_core/Cargo.toml hot-reload feature + optional subsecond dep.
Cargo.toml (workspace) subsecond = "0.7" workspace dep + exclude = ["crates/integration"] (dx workaround, see below).
app/Cargo.toml hot-reload feature enabling warpui/hot-reload + optional subsecond dep. Gates integration bin on integration_tests feature.
app/src/workspace/view/vertical_tabs.rs Adds vtabs_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)

1. File saved → dx detects change → compiles patch DLL (~400ms)
2. dx sends JumpTable { lib: patch.dylib, map: { old_addr → new_addr } } over WebSocket
3. hot_reload.rs receives it → subsecond::apply_patch(jump_table) → APP_JUMP_TABLE updated
4. REBUILD_PENDING = true + wake() → dispatch_async(main_queue, invalidate_all_views())
5. invalidate_all_views() → flush_effects → update_windows → request_redraw() → setNeedsDisplayAsync
6. displayLayer: → warp_update_layer → build_scene → render_view
7. render() calls subsecond::call(hot_color) → call_as_ptr → APP_JUMP_TABLE lookup → new hot_color()
8. New color returned → scene rebuilt → visible update ✓

Verification results

Standalone demo (~/warpui-hotreload-demo/) — macOS aarch64

Layer Status Evidence
WebSocket connection [hot-reload] connected OK to ws://127.0.0.1:8080/_dioxus?aslr_reference=...
Patch delivery (~400ms) Hot-patching: src/root_view.rs took 428ms in dx output
apply_patch returns Ok [hot-reload] patch applied — triggering redraw
render() re-called post-patch Confirmed via eprintln diagnostic
Visual update Yellow→orange live, window stays open, no restart

Main Warp app — macOS aarch64

Layer Status Evidence
WebSocket connection [hot-reload] connected OK to ws://127.0.0.1:64xxx/_dioxus
Patch delivery (~2-3s) Hot-patching: ... vertical_tabs.rs took XXXms
apply_patch returns Ok [hot-reload] patch applied — triggering redraw
Visual update Vertical tabs panel background changes live, multiple successive cycles verified

Running in the main Warp app

Prerequisites (one-time setup)

cargo binstall dioxus-cli   # install dx 0.7.9

Running

cd ~/warp/warp
dx serve --hot-patch --features hot-reload --bin warp-oss

The initial fat build takes ~30s. After that, the app window opens and the hot-reload devserver connects automatically — you'll see:

[hot-reload] connecting to ws://127.0.0.1:XXXXX/_dioxus
[hot-reload] connected OK to ws://127.0.0.1:XXXXX/_dioxus?aslr_reference=...

Testing a live patch

Edit app/src/workspace/view/vertical_tabs.rs and change vtabs_panel_bg():

// Before — very subtle overlay (default):
fn vtabs_panel_bg(warp_theme: &WarpTheme) -> WarpThemeFill {
internal_colors::fg_overlay_1(warp_theme)
}

// After — noticeably darker (change this and save):
fn vtabs_panel_bg(warp_theme: &WarpTheme) -> WarpThemeFill {
internal_colors::fg_overlay_3(warp_theme)
}

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/integration during the thin (hot-patch) build, even when not serving that binary. This PR adds exclude = ["crates/integration"] to the workspace Cargo.toml as a workaround.

Note for reviewers: This exclude prevents crates/integration from being a workspace member. Standard development workflows (cargo nextest run --workspace) are unaffected since crates/integration was already excluded from default-members. The integration test runner must be compiled explicitly: cargo build -p integration --features integration_tests. This exclusion should be revisited or removed once dx is updated.


Running the standalone demo

Prerequisites

cargo binstall dioxus-cli
mkdir -p ~/warpui-hotreload-demo/assets  # required by rust-embed
export MACOSX_DEPLOYMENT_TARGET=14.0    # macOS only

The demo at ~/warpui-hotreload-demo/ references warpui via path dep.

macOS

cd ~/warpui-hotreload-demo
MACOSX_DEPLOYMENT_TARGET=14.0 dx serve --hot-patch --features hot-reload

Window opens with 3 colored boxes. Edit src/root_view.rs — change hot_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

  • User opt-in required: Views must explicitly use subsecond::call(fn) for live patching — the framework cannot auto-redirect vtable-dispatched trait method calls. A proc-macro wrapper for render() would eliminate this.
  • Windows blocked by dx bug: fat-binary instrumentation silently skipped in dx 0.7.9 on Windows.
  • build_id = 0: Reading it via dioxus_cli_config::build_id() at compile time would be more correct.
  • Coarse invalidation: invalidate_all_views() re-renders everything; HotFn::ptr_address tracking could scope this to just the changed views.
  • dx workspace workaround: The exclude = ["crates/integration"] should be removed once dx's thin-build scoping is fixed.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

Conversation: https://staging.warp.dev/conversation/46a2f724-9216-471c-8e0f-fc06b6d40a4e
Plan: https://staging.warp.dev/drive/notebook/U2MBcgxPEgCQWN3P1XUAzS

CHANGELOG-NONE

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>
@cla-bot cla-bot Bot added the cla-signed label Jun 22, 2026
@oz-for-oss

oz-for-oss Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@acarl005

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 /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_core compiles the hot-reload render branch for wasm even though the subsecond dependency 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_table when for_pid is absent and ignores for_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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] [SECURITY] This treats a missing 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.

Comment on lines +4739 to +4741
#[cfg(feature = "hot-reload")]
return subsecond::call(|| view.render(self));
#[cfg(not(feature = "hot-reload"))]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] 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.

Suggested change
#[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"))))]

acarl005 and others added 3 commits June 22, 2026 10:27
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant