From 89986705217ffb15036856acd91910c55a4538a5 Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Thu, 23 Apr 2026 16:45:45 +0200 Subject: [PATCH 01/10] Block requests until active rebuilds finish --- Cargo.toml | 5 ++++- src/dev_server.rs | 56 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c95170b..d259b07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ walkdir = "2.3.2" # NOTE: we don't depend on this crate but we need to activate this feature otherwise it's super slow walrus = { version = "0.26.1", features = ["parallel"] } wasm-bindgen-cli-support = "0.2.100" -xtask-watch = "0.3.2" +xtask-watch = { version = "0.3.2" } [target.'cfg(unix)'.dependencies] libc = "0.2.112" @@ -52,3 +52,6 @@ rustdoc-args = ["--cfg", "docsrs"] [workspace] members = ["xtask-wasm-run-example"] resolver = "2" + +[patch.crates-io] +xtask-watch = { path = "../xtask-watch" } diff --git a/src/dev_server.rs b/src/dev_server.rs index 98fee69..d17db4e 100644 --- a/src/dev_server.rs +++ b/src/dev_server.rs @@ -10,7 +10,7 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream}, path::{Path, PathBuf}, process, - sync::Arc, + sync::{Arc, Condvar, Mutex}, thread, }; @@ -352,6 +352,9 @@ impl DevServer { } let dist_dir = self.dist_dir.clone().unwrap(); + // Track how many build batches are currently active. + let active_builds = Arc::new((Mutex::new(0), Condvar::new())); + let watch_process = { // mem::take so we can pass &self to build_command while the fields are empty. let pre_hooks = std::mem::take(&mut self.pre_hooks); @@ -373,9 +376,33 @@ impl DevServer { format!("cannot create dist directory `{}`", dist_dir.display()) })?; let watch = self.watch.exclude_path(&dist_dir); - let handle = std::thread::spawn(|| match watch.run(commands) { - Ok(()) => log::trace!("Starting to watch"), - Err(err) => log::error!("an error occurred when starting to watch: {err}"), + + let active_builds_watch = Arc::clone(&active_builds); + let handle = std::thread::spawn(move || { + match watch.run_with_hooks( + commands, + { + let b = Arc::clone(&active_builds_watch); + move || { + let mut count = b.0.lock().unwrap(); + *count += 1; + b.1.notify_all(); + } + }, + { + let b = Arc::clone(&active_builds_watch); + move || { + let mut count = b.0.lock().unwrap(); + if *count > 0 { + *count -= 1; + } + b.1.notify_all(); + } + }, + ) { + Ok(()) => log::trace!("Starting to watch"), + Err(err) => log::error!("an error occurred when starting to watch: {err}"), + } }); Some(handle) @@ -385,8 +412,15 @@ impl DevServer { }; if let Some(handler) = self.request_handler { - serve(self.ip, self.port, dist_dir, self.not_found_path, handler) - .context("an error occurred when starting to serve")?; + serve( + self.ip, + self.port, + dist_dir, + self.not_found_path, + handler, + active_builds, + ) + .context("an error occurred when starting to serve")?; } else { serve( self.ip, @@ -394,6 +428,7 @@ impl DevServer { dist_dir, self.not_found_path, Arc::new(default_request_handler), + active_builds, ) .context("an error occurred when starting to serve")?; } @@ -428,6 +463,7 @@ fn serve( dist_dir: PathBuf, not_found_path: Option, handler: RequestHandler, + active_builds: Arc<(Mutex, Condvar)>, ) -> Result<()> { let address = SocketAddr::new(ip, port); let listener = TcpListener::bind(address).context("cannot bind to the given address")?; @@ -450,7 +486,15 @@ fn serve( let handler = handler.clone(); let dist_dir = dist_dir.clone(); let not_found_path = not_found_path.clone(); + let active_builds = Arc::clone(&active_builds); thread::spawn(move || { + // Block until all active build batches are done. + let (lock, cvar) = &*active_builds; + drop( + cvar.wait_while(lock.lock().unwrap(), |count| *count > 0) + .unwrap(), + ); + let header = warn_not_fail!(read_header(&stream)); let request = Request { stream: &mut stream, From 72baf7f6ee7e142557fc52db3ab4b66e294c878f Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Thu, 23 Apr 2026 18:11:34 +0200 Subject: [PATCH 02/10] Use `run_with_lock` instead --- Cargo.toml | 3 --- src/dev_server.rs | 49 ++++++++++++----------------------------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d259b07..92231d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,3 @@ rustdoc-args = ["--cfg", "docsrs"] [workspace] members = ["xtask-wasm-run-example"] resolver = "2" - -[patch.crates-io] -xtask-watch = { path = "../xtask-watch" } diff --git a/src/dev_server.rs b/src/dev_server.rs index d17db4e..efba075 100644 --- a/src/dev_server.rs +++ b/src/dev_server.rs @@ -10,7 +10,7 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream}, path::{Path, PathBuf}, process, - sync::{Arc, Condvar, Mutex}, + sync::{Arc, Mutex}, thread, }; @@ -352,8 +352,8 @@ impl DevServer { } let dist_dir = self.dist_dir.clone().unwrap(); - // Track how many build batches are currently active. - let active_builds = Arc::new((Mutex::new(0), Condvar::new())); + // Shared critical section between build execution and request serving. + let section_lock = Arc::new(Mutex::new(())); let watch_process = { // mem::take so we can pass &self to build_command while the fields are empty. @@ -377,29 +377,9 @@ impl DevServer { })?; let watch = self.watch.exclude_path(&dist_dir); - let active_builds_watch = Arc::clone(&active_builds); + let section_lock_watch = Arc::clone(§ion_lock); let handle = std::thread::spawn(move || { - match watch.run_with_hooks( - commands, - { - let b = Arc::clone(&active_builds_watch); - move || { - let mut count = b.0.lock().unwrap(); - *count += 1; - b.1.notify_all(); - } - }, - { - let b = Arc::clone(&active_builds_watch); - move || { - let mut count = b.0.lock().unwrap(); - if *count > 0 { - *count -= 1; - } - b.1.notify_all(); - } - }, - ) { + match watch.run_with_lock(commands, section_lock_watch) { Ok(()) => log::trace!("Starting to watch"), Err(err) => log::error!("an error occurred when starting to watch: {err}"), } @@ -418,7 +398,7 @@ impl DevServer { dist_dir, self.not_found_path, handler, - active_builds, + section_lock, ) .context("an error occurred when starting to serve")?; } else { @@ -428,7 +408,7 @@ impl DevServer { dist_dir, self.not_found_path, Arc::new(default_request_handler), - active_builds, + section_lock, ) .context("an error occurred when starting to serve")?; } @@ -463,7 +443,7 @@ fn serve( dist_dir: PathBuf, not_found_path: Option, handler: RequestHandler, - active_builds: Arc<(Mutex, Condvar)>, + section_lock: Arc>, ) -> Result<()> { let address = SocketAddr::new(ip, port); let listener = TcpListener::bind(address).context("cannot bind to the given address")?; @@ -486,20 +466,15 @@ fn serve( let handler = handler.clone(); let dist_dir = dist_dir.clone(); let not_found_path = not_found_path.clone(); - let active_builds = Arc::clone(&active_builds); + let section_lock = Arc::clone(§ion_lock); thread::spawn(move || { - // Block until all active build batches are done. - let (lock, cvar) = &*active_builds; - drop( - cvar.wait_while(lock.lock().unwrap(), |count| *count > 0) - .unwrap(), - ); - let header = warn_not_fail!(read_header(&stream)); + let path = warn_not_fail!(parse_request_path(&header)); + let _guard = section_lock.lock().expect("not poisoned"); let request = Request { stream: &mut stream, header: header.as_ref(), - path: warn_not_fail!(parse_request_path(&header)), + path, dist_dir: dist_dir.as_ref(), not_found_path: not_found_path.as_deref(), }; From 22feb28cd6e46b04d0ac9763214e74fa07224b5b Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Thu, 23 Apr 2026 20:14:25 +0200 Subject: [PATCH 03/10] Use `xtask_watch::Lock` type --- src/dev_server.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/dev_server.rs b/src/dev_server.rs index efba075..78d9b3c 100644 --- a/src/dev_server.rs +++ b/src/dev_server.rs @@ -10,9 +10,10 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream}, path::{Path, PathBuf}, process, - sync::{Arc, Mutex}, + sync::Arc, thread, }; +use xtask_watch::Lock; type RequestHandler = Arc Result<()> + Send + Sync + 'static>; @@ -353,7 +354,7 @@ impl DevServer { let dist_dir = self.dist_dir.clone().unwrap(); // Shared critical section between build execution and request serving. - let section_lock = Arc::new(Mutex::new(())); + let section_lock = Lock::new(); let watch_process = { // mem::take so we can pass &self to build_command while the fields are empty. @@ -377,7 +378,7 @@ impl DevServer { })?; let watch = self.watch.exclude_path(&dist_dir); - let section_lock_watch = Arc::clone(§ion_lock); + let section_lock_watch = section_lock.clone(); let handle = std::thread::spawn(move || { match watch.run_with_lock(commands, section_lock_watch) { Ok(()) => log::trace!("Starting to watch"), @@ -443,7 +444,7 @@ fn serve( dist_dir: PathBuf, not_found_path: Option, handler: RequestHandler, - section_lock: Arc>, + section_lock: Lock, ) -> Result<()> { let address = SocketAddr::new(ip, port); let listener = TcpListener::bind(address).context("cannot bind to the given address")?; @@ -466,11 +467,11 @@ fn serve( let handler = handler.clone(); let dist_dir = dist_dir.clone(); let not_found_path = not_found_path.clone(); - let section_lock = Arc::clone(§ion_lock); + let section_lock = section_lock.clone(); thread::spawn(move || { let header = warn_not_fail!(read_header(&stream)); let path = warn_not_fail!(parse_request_path(&header)); - let _guard = section_lock.lock().expect("not poisoned"); + let _guard = section_lock.lock(); let request = Request { stream: &mut stream, header: header.as_ref(), From 112239d3c3d2b336b81c8dad72138d10dcd81de0 Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Thu, 23 Apr 2026 20:43:22 +0200 Subject: [PATCH 04/10] Clean up --- Cargo.toml | 2 +- src/dev_server.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92231d7..c95170b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ walkdir = "2.3.2" # NOTE: we don't depend on this crate but we need to activate this feature otherwise it's super slow walrus = { version = "0.26.1", features = ["parallel"] } wasm-bindgen-cli-support = "0.2.100" -xtask-watch = { version = "0.3.2" } +xtask-watch = "0.3.2" [target.'cfg(unix)'.dependencies] libc = "0.2.112" diff --git a/src/dev_server.rs b/src/dev_server.rs index 78d9b3c..e415652 100644 --- a/src/dev_server.rs +++ b/src/dev_server.rs @@ -470,12 +470,11 @@ fn serve( let section_lock = section_lock.clone(); thread::spawn(move || { let header = warn_not_fail!(read_header(&stream)); - let path = warn_not_fail!(parse_request_path(&header)); let _guard = section_lock.lock(); let request = Request { stream: &mut stream, header: header.as_ref(), - path, + path: warn_not_fail!(parse_request_path(&header)), dist_dir: dist_dir.as_ref(), not_found_path: not_found_path.as_deref(), }; From 323c0b322f065c2ae6b2c194ccfa15bd350a8008 Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Thu, 23 Apr 2026 23:13:54 +0200 Subject: [PATCH 05/10] Replace `lock()` by `read()` --- src/dev_server.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/dev_server.rs b/src/dev_server.rs index e415652..d2f6bff 100644 --- a/src/dev_server.rs +++ b/src/dev_server.rs @@ -470,7 +470,14 @@ fn serve( let section_lock = section_lock.clone(); thread::spawn(move || { let header = warn_not_fail!(read_header(&stream)); - let _guard = section_lock.lock(); + let _guard = match section_lock.read() { + Ok(guard) => guard, + Err(err) => { + let _ = stream.write("HTTP/1.1 500 INTERNAL SERVER ERROR\r\n\r\n".as_bytes()); + log::error!("could not acquire read lock: {err}"); + return; + } + }; let request = Request { stream: &mut stream, header: header.as_ref(), From 13b7af1ee62db4a1f210c280dffaff539199be9e Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Fri, 24 Apr 2026 03:30:34 +0200 Subject: [PATCH 06/10] Use `WatchLock` --- src/dev_server.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/dev_server.rs b/src/dev_server.rs index d2f6bff..286c9a8 100644 --- a/src/dev_server.rs +++ b/src/dev_server.rs @@ -13,7 +13,7 @@ use std::{ sync::Arc, thread, }; -use xtask_watch::Lock; +use xtask_watch::WatchLock; type RequestHandler = Arc Result<()> + Send + Sync + 'static>; @@ -353,8 +353,7 @@ impl DevServer { } let dist_dir = self.dist_dir.clone().unwrap(); - // Shared critical section between build execution and request serving. - let section_lock = Lock::new(); + let watch_lock = self.watch.lock(); let watch_process = { // mem::take so we can pass &self to build_command while the fields are empty. @@ -378,12 +377,9 @@ impl DevServer { })?; let watch = self.watch.exclude_path(&dist_dir); - let section_lock_watch = section_lock.clone(); - let handle = std::thread::spawn(move || { - match watch.run_with_lock(commands, section_lock_watch) { - Ok(()) => log::trace!("Starting to watch"), - Err(err) => log::error!("an error occurred when starting to watch: {err}"), - } + let handle = std::thread::spawn(move || match watch.run(commands) { + Ok(()) => log::trace!("Starting to watch"), + Err(err) => log::error!("an error occurred when starting to watch: {err}"), }); Some(handle) @@ -399,7 +395,7 @@ impl DevServer { dist_dir, self.not_found_path, handler, - section_lock, + watch_lock, ) .context("an error occurred when starting to serve")?; } else { @@ -409,7 +405,7 @@ impl DevServer { dist_dir, self.not_found_path, Arc::new(default_request_handler), - section_lock, + watch_lock, ) .context("an error occurred when starting to serve")?; } @@ -444,7 +440,7 @@ fn serve( dist_dir: PathBuf, not_found_path: Option, handler: RequestHandler, - section_lock: Lock, + watch_lock: WatchLock, ) -> Result<()> { let address = SocketAddr::new(ip, port); let listener = TcpListener::bind(address).context("cannot bind to the given address")?; @@ -467,10 +463,10 @@ fn serve( let handler = handler.clone(); let dist_dir = dist_dir.clone(); let not_found_path = not_found_path.clone(); - let section_lock = section_lock.clone(); + let watch_lock = watch_lock.clone(); thread::spawn(move || { let header = warn_not_fail!(read_header(&stream)); - let _guard = match section_lock.read() { + let _guard = match watch_lock.read() { Ok(guard) => guard, Err(err) => { let _ = stream.write("HTTP/1.1 500 INTERNAL SERVER ERROR\r\n\r\n".as_bytes()); From a8556f9d45387f4bab122ea437f97f3db171b7c2 Mon Sep 17 00:00:00 2001 From: yozhgoor Date: Fri, 24 Apr 2026 13:38:59 +0200 Subject: [PATCH 07/10] Replace `read` by `acquire` --- src/dev_server.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/dev_server.rs b/src/dev_server.rs index 286c9a8..7af95d4 100644 --- a/src/dev_server.rs +++ b/src/dev_server.rs @@ -466,14 +466,7 @@ fn serve( let watch_lock = watch_lock.clone(); thread::spawn(move || { let header = warn_not_fail!(read_header(&stream)); - let _guard = match watch_lock.read() { - Ok(guard) => guard, - Err(err) => { - let _ = stream.write("HTTP/1.1 500 INTERNAL SERVER ERROR\r\n\r\n".as_bytes()); - log::error!("could not acquire read lock: {err}"); - return; - } - }; + let _guard = watch_lock.acquire(); let request = Request { stream: &mut stream, header: header.as_ref(), From d95971bf77e52eeff51bb30ad1f3063fa0b36827 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Sat, 25 Apr 2026 17:27:44 +0200 Subject: [PATCH 08/10] Re-export WatchLock and WatchLockGuard from xtask-wasm --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index aa7c260..4e11e17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -306,6 +306,7 @@ use std::process::Command; #[cfg(not(target_arch = "wasm32"))] pub use xtask_watch::{ anyhow, cargo_metadata, cargo_metadata::camino, clap, metadata, package, xtask_command, Watch, + WatchLock, WatchLockGuard, }; #[cfg(not(target_arch = "wasm32"))] From 25ab949230a6ccd482668addecc58880b6a0cb1a Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Sat, 25 Apr 2026 17:27:58 +0200 Subject: [PATCH 09/10] Comment why read_header is intentionally called before acquiring the watch lock --- src/dev_server.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dev_server.rs b/src/dev_server.rs index 7af95d4..b25b13d 100644 --- a/src/dev_server.rs +++ b/src/dev_server.rs @@ -465,6 +465,10 @@ fn serve( let not_found_path = not_found_path.clone(); let watch_lock = watch_lock.clone(); thread::spawn(move || { + // Read the request header *before* acquiring the watch lock so that connections + // can be accepted and parsed while a rebuild is in progress. This reduces + // perceived latency: the response is dispatched immediately once the build + // finishes rather than having to re-parse the header afterward. let header = warn_not_fail!(read_header(&stream)); let _guard = watch_lock.acquire(); let request = Request { From d43478765573704d74a52fc397d33f48132d363d Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Sat, 25 Apr 2026 18:43:08 +0200 Subject: [PATCH 10/10] chore: bump xtask-watch to 0.3.3 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c95170b..b7f189b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ walkdir = "2.3.2" # NOTE: we don't depend on this crate but we need to activate this feature otherwise it's super slow walrus = { version = "0.26.1", features = ["parallel"] } wasm-bindgen-cli-support = "0.2.100" -xtask-watch = "0.3.2" +xtask-watch = "0.3.3" [target.'cfg(unix)'.dependencies] libc = "0.2.112"