fix: hotpatching fullstack (#4730)
* fix: hotpatching fullstack * add hot-patching fullstack test * add caching to playwright apt-get * only run playwright on beta * install binutils? * dont commit lockfile * ignore lockfiles * make sure ld is in path
This commit is contained in:
1 parent
a4aef33369
commit
bee89b795d
15 files changed
+265
-38
No files matched your search
@@ -224,14 +224,18 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- { toolchain: 1.86.0 }
|
||||
- { toolchain: beta }
|
||||
# - { toolchain: 1.86.0 }
|
||||
steps:
|
||||
# Do our best to cache the toolchain and node install steps
|
||||
- uses: actions/checkout@v5
|
||||
- name: Free Disk Space
|
||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
|
||||
uses: ./.github/actions/free-disk-space
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold
|
||||
version: 1.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -254,6 +258,7 @@ jobs:
|
||||
# The hot patch test requires incremental compilation
|
||||
CARGO_INCREMENTAL: 1
|
||||
run: |
|
||||
export PATH=$PATH:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin
|
||||
npm ci
|
||||
npm install -D @playwright/test
|
||||
npx playwright install --with-deps
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
/packages/playwright-tests/fullstack/dist
|
||||
/packages/playwright-tests/test-results
|
||||
/packages/playwright-tests/web-hot-patch-temp
|
||||
/packages/playwright-tests/web-hot-patch-fullstack-temp
|
||||
/packages/playwright-tests/web-hot-patch/Cargo.lock
|
||||
/packages/playwright-tests/web-hot-patch-fullstack/Cargo.lock
|
||||
/dist
|
||||
.DS_Store
|
||||
/examples/assets/test_video.mp4
|
||||
|
||||
@@ -715,7 +715,7 @@ impl AppBuilder {
|
||||
|
||||
let changed_file = changed_files.first().unwrap();
|
||||
tracing::info!(
|
||||
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#} ({})",
|
||||
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
|
||||
changed_file
|
||||
.display()
|
||||
.to_string()
|
||||
@@ -723,8 +723,7 @@ impl AppBuilder {
|
||||
SystemTime::now()
|
||||
.duration_since(res.time_start)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
triple
|
||||
.as_millis()
|
||||
);
|
||||
|
||||
self.patches.push(jump_table.clone());
|
||||
|
||||
@@ -175,27 +175,18 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Resu
|
||||
}
|
||||
BuilderUpdate::BuildReady { bundle } => match bundle.mode {
|
||||
BuildMode::Thin { ref cache, .. } => {
|
||||
let elapsed =
|
||||
bundle.time_end.duration_since(bundle.time_start).unwrap();
|
||||
match builder.hotpatch(&bundle, id, cache).await {
|
||||
Ok(jumptable) => {
|
||||
let pid = match id {
|
||||
BuildId::CLIENT => builder.client.pid,
|
||||
_ => builder.server.as_ref().and_then(|s| s.pid),
|
||||
};
|
||||
devserver.send_patch(jumptable, elapsed, id, pid).await
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to hot-patch app: {err}");
|
||||
if let Err(err) =
|
||||
builder.hotpatch(&bundle, id, cache, &mut devserver).await
|
||||
{
|
||||
tracing::error!("Failed to hot-patch app: {err}");
|
||||
|
||||
if let Some(_patching) =
|
||||
err.downcast_ref::<crate::build::PatchError>()
|
||||
{
|
||||
tracing::info!("Starting full rebuild: {err}");
|
||||
builder.full_rebuild().await;
|
||||
devserver.send_reload_start().await;
|
||||
devserver.start_build().await;
|
||||
}
|
||||
if let Some(_patching) =
|
||||
err.downcast_ref::<crate::build::PatchError>()
|
||||
{
|
||||
tracing::info!("Starting full rebuild: {err}");
|
||||
builder.full_rebuild().await;
|
||||
devserver.send_reload_start().await;
|
||||
devserver.start_build().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use subsecond_types::JumpTable;
|
||||
use syn::spanned::Spanned;
|
||||
use tokio::process::Command;
|
||||
|
||||
@@ -53,7 +52,7 @@ pub(crate) struct AppServer {
|
||||
pub(crate) watcher_rx: UnboundedReceiver<notify::Event>,
|
||||
|
||||
// Tracked state related to open builds and hot reloading
|
||||
pub(crate) applied_hot_reload_message: HotReloadMsg,
|
||||
pub(crate) applied_client_hot_reload_message: HotReloadMsg,
|
||||
pub(crate) file_map: HashMap<PathBuf, CachedFile>,
|
||||
|
||||
// Resolved args related to how we go about processing the rebuilds and logging
|
||||
@@ -185,7 +184,7 @@ impl AppServer {
|
||||
// Create the runner
|
||||
let mut runner = Self {
|
||||
file_map: Default::default(),
|
||||
applied_hot_reload_message: Default::default(),
|
||||
applied_client_hot_reload_message: Default::default(),
|
||||
automatic_rebuilds: true,
|
||||
watch_fs,
|
||||
use_hotpatch_engine,
|
||||
@@ -709,27 +708,66 @@ impl AppServer {
|
||||
|
||||
pub(crate) async fn hotpatch(
|
||||
&mut self,
|
||||
res: &BuildArtifacts,
|
||||
bundle: &BuildArtifacts,
|
||||
id: BuildId,
|
||||
cache: &HotpatchModuleCache,
|
||||
) -> Result<JumpTable> {
|
||||
devserver: &mut WebServer,
|
||||
) -> Result<()> {
|
||||
let elapsed = bundle
|
||||
.time_end
|
||||
.duration_since(bundle.time_start)
|
||||
.unwrap_or_default();
|
||||
|
||||
let jump_table = match id {
|
||||
BuildId::CLIENT => self.client.hotpatch(res, cache).await,
|
||||
BuildId::CLIENT => self.client.hotpatch(bundle, cache).await,
|
||||
BuildId::SERVER => {
|
||||
self.server
|
||||
.as_mut()
|
||||
.context("Server not found")?
|
||||
.hotpatch(res, cache)
|
||||
.hotpatch(bundle, cache)
|
||||
.await
|
||||
}
|
||||
_ => bail!("Invalid build id"),
|
||||
}?;
|
||||
|
||||
if id == BuildId::CLIENT {
|
||||
self.applied_hot_reload_message.jump_table = self.client.patches.last().cloned();
|
||||
self.applied_client_hot_reload_message.jump_table = self.client.patches.last().cloned();
|
||||
}
|
||||
|
||||
Ok(jump_table)
|
||||
// If no server, just send the patch immediately
|
||||
let Some(server) = self.server.as_mut() else {
|
||||
devserver
|
||||
.send_patch(jump_table, elapsed, id, self.client.pid)
|
||||
.await;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// If we have a server, we need to wait until both the client and server are ready
|
||||
// Otherwise we end up with an annoying race condition where the client can't actually load the patch
|
||||
if self.client.stage == BuildStage::Success && server.stage == BuildStage::Success {
|
||||
let client_jump_table = self
|
||||
.client
|
||||
.patches
|
||||
.last()
|
||||
.cloned()
|
||||
.context("Missing client jump table")?;
|
||||
|
||||
let server_jump_table = server
|
||||
.patches
|
||||
.last()
|
||||
.cloned()
|
||||
.context("Missing server jump table")?;
|
||||
|
||||
devserver
|
||||
.send_patch(server_jump_table, elapsed, BuildId::SERVER, server.pid)
|
||||
.await;
|
||||
|
||||
devserver
|
||||
.send_patch(client_jump_table, elapsed, BuildId::CLIENT, self.client.pid)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_build(&self, id: BuildId) -> Option<&AppBuilder> {
|
||||
@@ -751,7 +789,7 @@ impl AppServer {
|
||||
|
||||
/// Get any hot reload changes that have been applied since the last full rebuild
|
||||
pub(crate) fn applied_hot_reload_changes(&mut self, build: BuildId) -> HotReloadMsg {
|
||||
let mut msg = self.applied_hot_reload_message.clone();
|
||||
let mut msg = self.applied_client_hot_reload_message.clone();
|
||||
|
||||
if build == BuildId::CLIENT {
|
||||
msg.jump_table = self.client.patches.last().cloned();
|
||||
@@ -773,7 +811,7 @@ impl AppServer {
|
||||
|
||||
/// Clear the hot reload changes. This should be called any time a new build is starting
|
||||
pub(crate) fn clear_hot_reload_changes(&mut self) {
|
||||
self.applied_hot_reload_message = Default::default();
|
||||
self.applied_client_hot_reload_message = Default::default();
|
||||
}
|
||||
|
||||
pub(crate) fn clear_patches(&mut self) {
|
||||
@@ -836,7 +874,7 @@ impl AppServer {
|
||||
|
||||
/// Store the hot reload changes for any future clients that connect
|
||||
fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) {
|
||||
let applied = &mut self.applied_hot_reload_message;
|
||||
let applied = &mut self.applied_client_hot_reload_message;
|
||||
|
||||
// Merge the assets, unknown files, and templates
|
||||
// We keep the newer change if there is both a old and new change
|
||||
|
||||
@@ -207,5 +207,13 @@ module.exports = defineConfig({
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
command:
|
||||
'rm -rf ./web-hot-patch-fullstack-temp && cp -r ./web-hot-patch-fullstack ./web-hot-patch-fullstack-temp && cd web-hot-patch-fullstack-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --web --addr "127.0.0.1" --port 9981 --hot-patch --exit-on-error',
|
||||
port: 9981,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
target
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "dioxus-playwright-fullstack-patch-test"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
description = "Playwright test for Dioxus Web"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { path = "../../dioxus", features = ["fullstack"] }
|
||||
|
||||
[features]
|
||||
web = ["dioxus/web"]
|
||||
server = ["dioxus/server"]
|
||||
|
||||
[workspace]
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: rgb(100, 100, 100);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#main {
|
||||
background-color: red;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,36 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const CSS: Asset = asset!("/assets/style.css");
|
||||
const IMAGE: Asset = asset!("/assets/toasts.png");
|
||||
|
||||
fn app() -> Element {
|
||||
let mut num = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
document::Link {
|
||||
href: CSS,
|
||||
rel: "stylesheet",
|
||||
}
|
||||
img {
|
||||
id: "toasts",
|
||||
src: IMAGE,
|
||||
}
|
||||
button {
|
||||
id: "increment-button",
|
||||
onclick: move |_| async move {
|
||||
let increment_amount = get_count().await.unwrap();
|
||||
*num.write() += increment_amount;
|
||||
},
|
||||
"Click me! Count: {num}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/get_count")]
|
||||
async fn get_count() -> Result<i32> {
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
@@ -7,6 +7,6 @@ license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { path = "../../dioxus", features = ["web"]}
|
||||
dioxus = { path = "../../dioxus", features = ["web"] }
|
||||
|
||||
[workspace]
|
||||
[workspace]
|
||||
@@ -0,0 +1,113 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require("@playwright/test");
|
||||
|
||||
const hotPatchTimeout = {
|
||||
timeout: 1000 * 60 * 2, // 2 minute
|
||||
};
|
||||
|
||||
test("button click", async ({ page }) => {
|
||||
const fs = require("fs");
|
||||
const mainPath = "web-hot-patch-fullstack-temp/src/main.rs";
|
||||
var mainContent = fs.readFileSync(mainPath, "utf8");
|
||||
const stylePath = "web-hot-patch-fullstack-temp/assets/style.css";
|
||||
var styleContent = fs.readFileSync(stylePath, "utf8");
|
||||
|
||||
// Reset any changes made to the main.rs and style.css files.
|
||||
mainContent = mainContent.replace(/Ok\(\s*2\s*\)/g, "Ok(1)");
|
||||
mainContent = mainContent.replace("Click button! Count:", "Click me! Count:");
|
||||
mainContent = mainContent.replace("asset!('/assets/alternative-style.css')", "asset!('/assets/style.css')");
|
||||
fs.writeFileSync(mainPath, mainContent);
|
||||
styleContent = styleContent.replace(
|
||||
"background-color: blue;",
|
||||
"background-color: red;"
|
||||
);
|
||||
fs.writeFileSync(stylePath, styleContent);
|
||||
|
||||
await page.goto("http://localhost:9981");
|
||||
|
||||
|
||||
// wait a sec for the serverfn to process
|
||||
// give the page time to finish loading resources
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// ** First test make sure the initial fat build is working **
|
||||
// Expect the page to contain the counter text.
|
||||
const main = page.locator("#main");
|
||||
await expect(main).toContainText("Click me! Count: 0");
|
||||
|
||||
// Click the increment button.
|
||||
let button = page.locator("button#increment-button");
|
||||
await button.click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Expect the page to contain the updated counter text.
|
||||
await expect(main).toContainText("Click me! Count: 1");
|
||||
|
||||
// Make sure the css is applied correctly.
|
||||
await expect(main).toHaveCSS("background-color", "rgb(255, 0, 0)");
|
||||
|
||||
// Make sure the image is loaded.
|
||||
const headerImage = page.locator("#toasts");
|
||||
// expect the attribute src to start with /assets/toasts-
|
||||
await expect(headerImage).toHaveAttribute("src", /\/assets\/toasts-/);
|
||||
|
||||
// ** Then make sure the hot patch is working **
|
||||
// Then change the file to increment by 2.
|
||||
const updatedContent = mainContent.replace(/Ok\(\s*1\s*\)/g, "Ok(2)");
|
||||
// Change the click me text to reflect the new increment.
|
||||
const updatedContentWithText = updatedContent.replace(
|
||||
"Click me! Count:",
|
||||
"Click button! Count:"
|
||||
);
|
||||
fs.writeFileSync(mainPath, updatedContentWithText);
|
||||
|
||||
// Wait for the page to update and show the new text.
|
||||
await expect(main).toContainText("Click button! Count: 1", hotPatchTimeout);
|
||||
|
||||
// Now click the button again.
|
||||
await button.click();
|
||||
|
||||
// wait a sec for the serverfn to process
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Expect the count to update by 2.
|
||||
await expect(main).toContainText("Click button! Count: 3");
|
||||
|
||||
// Next change just the css file to change the background color to blue.
|
||||
const updatedStyleContent = styleContent.replace(
|
||||
"background-color: red;",
|
||||
"background-color: blue;"
|
||||
);
|
||||
fs.writeFileSync(stylePath, updatedStyleContent);
|
||||
|
||||
// Wait for the page to update the background color.
|
||||
await expect(main).toHaveCSS(
|
||||
"background-color",
|
||||
"rgb(0, 0, 255)",
|
||||
hotPatchTimeout
|
||||
);
|
||||
|
||||
// Make sure the image is still loaded.
|
||||
// expect the attribute src to start with /assets/toasts-
|
||||
await expect(headerImage).toHaveAttribute("src", /\/assets\/toasts-/);
|
||||
|
||||
// ** Then add a new asset to the page **
|
||||
// Switch from style to alternative-style
|
||||
const updatedContentWithAlternativeStyle = updatedContentWithText.replace(
|
||||
'asset!("/assets/style.css")',
|
||||
'asset!("/assets/alternative-style.css")'
|
||||
);
|
||||
fs.writeFileSync(mainPath, updatedContentWithAlternativeStyle);
|
||||
|
||||
// Assert the page has the new alternative style applied.
|
||||
const body = page.locator("body");
|
||||
// Log the page content to debug if needed.
|
||||
console.log(await page.content());
|
||||
await expect(body).toHaveCSS(
|
||||
"background-color",
|
||||
"rgb(100, 100, 100)",
|
||||
hotPatchTimeout
|
||||
);
|
||||
});
|
||||
@@ -573,6 +573,16 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> {
|
||||
|
||||
// Wait for the fetch to complete - we need the wasm module size in bytes to reserve in the memory
|
||||
let response: web_sys::Response = JsFuture::from(response).await.unwrap().unchecked_into();
|
||||
|
||||
// If the status is not success, we bail
|
||||
if !response.ok() {
|
||||
panic!(
|
||||
"Failed to patch wasm module at {} - response failed with: {}",
|
||||
path,
|
||||
response.status_text()
|
||||
);
|
||||
}
|
||||
|
||||
let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
|
||||
Reference in new issue
Block a user