diff --git a/BENCHMARKS.md b/BENCHMARKS.md index e2edc629..6482ad2d 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -90,12 +90,17 @@ Reports per (path × scale): - **bytes/run** — exact bytes requested from the allocator - **wall µs/run** — `Instant::elapsed()` per iteration - **user µs/run** — `getrusage(RUSAGE_SELF).ru_utime` delta +- **sys µs/run** — `getrusage(RUSAGE_SELF).ru_stime` delta - **process RSS** — `ru_maxrss` high-water mark This is the **only** bench in the suite that gives you exact allocation numbers. Use it to verify "zero per-write allocation" claims and to detect allocation-pressure regressions. +On non-Unix targets, `getrusage` is unavailable; the benchmark still runs and +reports allocation and wall-clock data, while user/sys CPU and RSS counters are +reported as zero. + ### `streaming-e2e-ttfb` (HTTP-level) `crates/webui/examples/streaming_e2e_ttfb_bench.rs` spawns a real diff --git a/DESIGN.md b/DESIGN.md index 8f952685..06d81f32 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -489,7 +489,7 @@ pub enum ExpressionError { ### Core API ```rust pub struct WebUIHandler { - plugin: Option>, + plugin_factory: Option Box>, } /// Options controlling how the handler renders a protocol. @@ -519,10 +519,10 @@ impl<'a> RenderOptions<'a> { impl WebUIHandler { pub fn new() -> Self; - pub fn with_plugin(plugin: Box) -> Self; + pub fn with_plugin(factory: fn() -> Box) -> Self; pub fn handle( - &mut self, + &self, protocol: &WebUIProtocol, state: &Value, options: &RenderOptions<'_>, @@ -531,6 +531,14 @@ impl WebUIHandler { } ``` +Programmatic Rust hosts should normally import the handler surface through the +top-level `microsoft-webui` crate (library name `webui`). It re-exports +`HandlerResult`, `RenderOptions`, `ResponseWriter`, `WebUIHandler`, +`HandlerPlugin`, `FastV3HydrationPlugin`, `WebUIHydrationPlugin`, parser +configuration types, and `WebUIProtocol` so applications do not need direct +dependencies on the internal handler, parser, or protocol crates for common +build/render use. + #### ProtocolIndex `ProtocolIndex` is a pre-computed index over a `WebUIProtocol` that accelerates @@ -761,6 +769,10 @@ pub trait HandlerPlugin { fn pop_scope(&mut self); fn on_binding_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>; fn on_binding_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>; + fn on_for_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>; + fn on_for_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>; + fn on_if_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>; + fn on_if_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>; fn on_repeat_item_start(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>; fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>; fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()>; @@ -769,29 +781,50 @@ pub trait HandlerPlugin { state: &serde_json::Value, writer: &mut dyn ResponseWriter, ) -> Result<()>; + fn emit_templates( + &self, + protocol: &WebUIProtocol, + components: &HashSet, + nonce: Option<&str>, + writer: &mut dyn ResponseWriter, + ) -> Result<()>; + fn collect_template_js( + &self, + protocol: &WebUIProtocol, + components: &HashSet, + ) -> Option>; } ``` **Hook invocation points:** - **Signal**: `on_binding_start` before, `on_binding_end` after (same scope) -- **For loop**: `on_binding_start/end` around entire loop; `on_repeat_item_start/end` + `push_scope/pop_scope` per item -- **If condition**: `on_binding_start/end` around condition; `push_scope/pop_scope` if condition is true +- **For loop**: `on_for_start/end` around entire loop; `on_repeat_item_start/end` + `push_scope/pop_scope` per item +- **If condition**: `on_if_start/end` around condition; `push_scope/pop_scope` if condition is true - **Component**: `push_scope/pop_scope` around component body - **Plugin fragment**: `on_element_data` with parser-produced hydration bytes from protocol - **Matched route component**: `write_route_component_state` before the opening tag closes +- **Template emission**: `emit_templates` writes component templates during SSR; + `collect_template_js` lets plugins that store executable JS templates merge + them into the consolidated `window.__webui` script block. **Selecting handler plugins** -The CLI and host APIs select handler plugins by name (passed as a string). No plugin -is loaded by default; output is plain SSR HTML unless a plugin is selected. +The CLI selects parser/handler plugin pairs by name. Rust hosts pass a +zero-capture plugin factory to `WebUIHandler::with_plugin`; each render creates +a fresh plugin instance from the factory so the handler remains shareable. No +plugin is loaded by default; output is plain SSR HTML unless a plugin is +selected. -The set of available plugin names is implementation-defined; refer to the CLI and -crate documentation for the current list. Each plugin emits its own framework-specific -hydration markers and attributes; WebUI itself does not interpret them. +The top-level `webui` library re-exports the built-in handler plugin types used +by Rust hosts, including `FastV3HydrationPlugin` and `WebUIHydrationPlugin`. +Each plugin emits its own framework-specific hydration markers and attributes; +WebUI itself does not interpret them. **Usage:** ```rust -let handler = WebUIHandler::with_plugin(|| Box::new(MyHydrationPlugin::new())); +use webui::{FastV3HydrationPlugin, WebUIHandler}; + +let handler = WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new())); handler.handle(&protocol, &state, &options, &mut writer)?; ``` ### Fragment Processing @@ -1486,9 +1519,11 @@ webui-ffi ──────► webui-handler ◄────── webui-wasm ( ``` The `webui` library crate is the primary API surface for programmatic use. -It re-exports `WebUIHandler`, `ResponseWriter`, and `WebUIProtocol` from their -respective crates and provides `build()`, `build_to_disk()`, and `inspect()` -functions with `BuildStats` (duration, fragment/component/CSS counts, protocol size). +It re-exports `WebUIHandler`, `RenderOptions`, `ResponseWriter`, `HandlerResult`, +`HandlerPlugin`, `FastV3HydrationPlugin`, `WebUIHydrationPlugin`, +`WebUIProtocol`, and parser configuration types from their respective crates and +provides `build()`, `build_to_disk()`, and `inspect()` functions with +`BuildStats` (duration, fragment/component/CSS counts, protocol size). ### WASM Distribution diff --git a/README.md b/README.md index 4f2ca70a..59e89bc3 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,17 @@ Or install the Rust CLI: cargo install microsoft-webui-cli ``` +For the Rust library API: + +```bash +cargo add microsoft-webui +``` + +Cargo imports the library as `webui`. The crate re-exports the core handler +API (`WebUIHandler`, `RenderOptions`, `ResponseWriter`) and built-in hydration +plugins (`FastV3HydrationPlugin`, `WebUIHydrationPlugin`) so Rust hosts can +build and render through one dependency. + For .NET server-side bindings: ```bash diff --git a/crates/webui-press/src/build.rs b/crates/webui-press/src/build.rs index 5d4852df..f8a4d95f 100644 --- a/crates/webui-press/src/build.rs +++ b/crates/webui-press/src/build.rs @@ -806,7 +806,7 @@ fn bundle_components( let out_file = site_dir.join("components.js"); - let status = std::process::Command::new("npx") + let status = std::process::Command::new(npx_command()) .arg("esbuild") .arg("--bundle") .arg("--format=esm") @@ -836,6 +836,16 @@ fn bundle_components( Ok(ts_files.len()) } +#[cfg(windows)] +fn npx_command() -> &'static str { + "npx.cmd" +} + +#[cfg(not(windows))] +fn npx_command() -> &'static str { + "npx" +} + const PRE_BLOCK_MARKER_PREFIX: &str = ""; @@ -1093,4 +1103,10 @@ mod tests { assert_eq!(fxhash("abc"), fxhash("abc")); assert_ne!(fxhash("abc"), fxhash("abd")); } + + #[test] + fn npx_command_uses_platform_shim() { + let expected = if cfg!(windows) { "npx.cmd" } else { "npx" }; + assert_eq!(npx_command(), expected); + } } diff --git a/crates/webui/README.md b/crates/webui/README.md index e0083326..335726d3 100644 --- a/crates/webui/README.md +++ b/crates/webui/README.md @@ -5,9 +5,11 @@ Programmatic Rust API for the [WebUI](https://github.com/microsoft/webui) build- ## Install ```bash -cargo add webui +cargo add microsoft-webui ``` +The package is imported as `webui` in Rust code. + ## Quick Start ```rust @@ -83,9 +85,9 @@ handler.handle(&protocol, &state, &RenderOptions::new("index.html", "/"), &mut w With a hydration plugin enabled (the `webui` plugin shown here; see the WebUI documentation for the available plugin identifiers): ```rust -use webui::{WebUIHandler, HandlerPlugin}; -use webui_handler::plugin::webui::WebUIHydrationPlugin; +use webui::{FastV3HydrationPlugin, WebUIHandler, WebUIHydrationPlugin}; +let fast_handler = WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new())); let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new())); ``` @@ -125,6 +127,8 @@ let partial = route_handler::render_partial( | `WebUIHandler` | Rendering engine (stateless, thread-safe) | | `RenderOptions` | Render configuration (entry_id, request_path) | | `ResponseWriter` | Trait for streaming rendered output | +| `HandlerPlugin` | Trait for custom handler-side hydration plugins | +| `FastV3HydrationPlugin` / `WebUIHydrationPlugin` | Built-in handler plugins re-exported by this crate | | `CssStrategy` | CSS delivery mode (Link or Style) | | `WebUIError` | Error type for build/inspect operations | diff --git a/crates/webui/benches/README.md b/crates/webui/benches/README.md index 6b841cbc..c3f54cf8 100644 --- a/crates/webui/benches/README.md +++ b/crates/webui/benches/README.md @@ -16,9 +16,10 @@ Two criterion benches in this directory: Two **examples** (in `crates/webui/examples/`) round out the suite: * **`streaming_resource_bench.rs`** — exact allocation count, bytes - allocated, getrusage CPU time, and peak RSS via a custom + allocated, getrusage user/system CPU time, and peak RSS via a custom `GlobalAlloc`. The only bench in the workspace that gives exact - allocation numbers. + allocation numbers. On non-Unix targets, getrusage-backed CPU/RSS + counters report zero while allocation and wall-clock measurements still run. * **`streaming_e2e_ttfb_bench.rs`** — HTTP-level TTFB through a real actix-web server. diff --git a/crates/webui/examples/streaming_resource_bench.rs b/crates/webui/examples/streaming_resource_bench.rs index 5954912e..61e74fec 100644 --- a/crates/webui/examples/streaming_resource_bench.rs +++ b/crates/webui/examples/streaming_resource_bench.rs @@ -20,13 +20,16 @@ //! //! * **allocations** — count of `alloc` calls (custom GlobalAlloc) //! * **bytes allocated** — total bytes requested -//! * **CPU user time** — `getrusage(RUSAGE_SELF).ru_utime` delta -//! * **peak RSS** — `ru_maxrss` high-water mark +//! * **CPU user time** — `getrusage(RUSAGE_SELF).ru_utime` delta on Unix +//! * **CPU system time** — `getrusage(RUSAGE_SELF).ru_stime` delta on Unix +//! * **peak RSS** — `ru_maxrss` high-water mark on Unix //! //! Unlike criterion (which only reports wall-clock), this gives a //! direct allocator-level view useful for verifying that the streaming //! writer's "zero per-write allocation" claim actually holds in the -//! production path. +//! production path. On non-Unix targets, CPU and RSS counters are reported +//! as zero because `getrusage` is unavailable; allocation counts and wall-clock +//! time are still reported. //! //! Usage: //! @@ -118,6 +121,7 @@ struct Rusage { } impl Rusage { + #[cfg(unix)] fn now() -> Self { let mut usage: libc::rusage = unsafe { std::mem::zeroed() }; // SAFETY: `usage` is a valid mutable pointer to a fully-initialised @@ -131,15 +135,27 @@ impl Rusage { } } + #[cfg(not(unix))] + fn now() -> Self { + Self { + user_cpu: Duration::ZERO, + sys_cpu: Duration::ZERO, + max_rss_raw: 0, + } + } + fn max_rss_bytes(&self) -> i64 { if cfg!(target_os = "macos") { self.max_rss_raw - } else { + } else if cfg!(unix) { self.max_rss_raw * 1024 + } else { + self.max_rss_raw } } } +#[cfg(unix)] fn timeval_to_duration(tv: libc::timeval) -> Duration { let secs = tv.tv_sec as u64; let usecs = tv.tv_usec as u32; @@ -791,6 +807,8 @@ fn main() { println!("Notes:"); println!(" * `allocs/run` and `bytes/run` are exact (custom GlobalAlloc)."); println!(" * `user µs/run` is `getrusage(RUSAGE_SELF).ru_utime` delta / iters."); + println!(" * `sys µs/run` is `getrusage(RUSAGE_SELF).ru_stime` delta / iters."); + println!(" * On non-Unix targets, getrusage-backed user/sys/RSS values are zero."); println!(" * `process RSS` is the high-water mark for the whole process at"); println!(" phase end. Per-iteration RSS is not directly observable; use"); println!(" `bytes/run` to compare per-render heap pressure across paths."); diff --git a/crates/webui/src/lib.rs b/crates/webui/src/lib.rs index c7399acd..59f663ae 100644 --- a/crates/webui/src/lib.rs +++ b/crates/webui/src/lib.rs @@ -5,6 +5,9 @@ //! //! This crate provides the core build, render, and inspection APIs //! that power the `webui` CLI, Node.js bindings, and WASM module. +//! It also re-exports the primary handler, parser, protocol, and built-in +//! handler plugin types so Rust hosts can depend on `microsoft-webui` alone for +//! common build/render integration. //! //! # Example //! @@ -19,6 +22,16 @@ //! //! println!("Built {} fragments in {:?}", result.stats.fragment_count, result.stats.duration); //! ``` +//! +//! # Hydration plugins +//! +//! Built-in handler plugins are available from this crate root: +//! +//! ```rust,no_run +//! use webui::{FastV3HydrationPlugin, WebUIHandler}; +//! +//! let _handler = WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new())); +//! ``` mod error; pub mod server; @@ -26,16 +39,17 @@ pub mod streaming; pub use error::WebUIError; -// Re-export core types from downstream crates +// Re-export core types from downstream crates. +pub use webui_handler::plugin::{ + fast_v3::FastV3HydrationPlugin, webui::WebUIHydrationPlugin, HandlerPlugin, +}; pub use webui_handler::route_handler::{ encode_inventory, get_needed_components, get_needed_components_for_request, parse_inventory, ProtocolIndex, }; pub use webui_handler::route_matcher::CompiledRouteCache; pub use webui_handler::Result as HandlerResult; -pub use webui_handler::{ - plugin::HandlerPlugin, HandlerError, RenderOptions, ResponseWriter, WebUIHandler, -}; +pub use webui_handler::{HandlerError, RenderOptions, ResponseWriter, WebUIHandler}; pub use webui_parser::CssStrategy; pub use webui_parser::Diagnostic; pub use webui_parser::DomStrategy; diff --git a/crates/webui/src/streaming.rs b/crates/webui/src/streaming.rs index 84835fed..f92a472d 100644 --- a/crates/webui/src/streaming.rs +++ b/crates/webui/src/streaming.rs @@ -55,8 +55,8 @@ //! zero scan cost and no risk of mis-firing on `` literals //! appearing inside HTML comments, `