From 1f6a603dc44797e9dae9c3ae8ce5da35507a8b03 Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Mon, 24 Aug 2026 10:58:15 -0700 Subject: [PATCH 1/2] perf(handler): centralize HTML attribute writing in ResponseWriter Add hidden `write_attribute`/`write_boolean_attribute` methods to `ResponseWriter` with `write()`-based defaults, plus centralized append helpers and macros that let buffered hosts emit attributes with a direct buffer append instead of five virtual dispatches. Adopt the macros across every buffered host writer, removing six duplicated `ResponseWriter` implementations in webui-ffi, webui-python, webui-press, webui-cli, webui-wasm, webui, webui-node, and the demo server. Streaming and callback writers override the methods directly so their flush and forwarding semantics are preserved. Output is byte-identical: the default trait methods emit the same sequence as before, and the macro-generated methods write the same bytes without routing through `write()`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- DESIGN.md | 9 ++ crates/webui-cli/src/commands/serve.rs | 31 +--- crates/webui-ffi/src/lib.rs | 28 +--- .../benches/bootstrap_state_bench.rs | 2 + crates/webui-handler/benches/handler_bench.rs | 2 + .../benches/streaming_hydration_bench.rs | 2 + crates/webui-handler/src/lib.rs | 59 ++++++- crates/webui-handler/src/response_writer.rs | 148 ++++++++++++++++++ crates/webui-handler/src/streaming/mod.rs | 20 +++ crates/webui-handler/src/streaming/owned.rs | 10 ++ crates/webui-node/src/lib.rs | 47 +++--- crates/webui-press/src/build.rs | 28 +--- crates/webui-python/src/lib.rs | 29 +--- crates/webui-wasm/src/handler.rs | 40 ++--- crates/webui/benches/contact_book_bench.rs | 4 +- crates/webui/src/server.rs | 27 +--- crates/webui/src/streaming.rs | 22 +++ examples/demo/server/src/shell.rs | 23 +-- 18 files changed, 335 insertions(+), 196 deletions(-) create mode 100644 crates/webui-handler/src/response_writer.rs diff --git a/DESIGN.md b/DESIGN.md index 3f7bf038c..823b7049e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1007,6 +1007,15 @@ pub trait ResponseWriter { } ``` +The handler owns quoted and boolean attribute formatting in centralized string +and byte helpers. Normal buffered hosts use the hidden +`define_string_response_writer!` / `define_bytes_response_writer!` macros, or +the corresponding method macros for writers with additional fields. The macros +expand local, inlinable methods without duplicating formatter source in every +host. Generic writers retain byte-identical `write()`-based fallback behavior. +Only writers with distinct semantics - threshold flushing, stream forwarding, +or profiling counters - implement the hidden attribute methods directly. + ### Streaming Response Writers (`webui::streaming`) Hosts that support HTTP response streaming can render directly into a diff --git a/crates/webui-cli/src/commands/serve.rs b/crates/webui-cli/src/commands/serve.rs index a9fa5385a..bbe15395e 100644 --- a/crates/webui-cli/src/commands/serve.rs +++ b/crates/webui-cli/src/commands/serve.rs @@ -33,6 +33,8 @@ use crate::utils::error::CliError; use crate::utils::output; #[cfg(test)] use metafile::temp_directory as metafile_temp_directory; + +webui_handler::define_string_response_writer!(MemoryWriter, buf); use metafile::write_atomic; #[derive(Args)] @@ -230,30 +232,6 @@ struct SharedState { entry: String, } -/// In-memory writer implementing `ResponseWriter` for the handler. -struct MemoryWriter { - buf: String, -} - -impl MemoryWriter { - fn with_capacity(cap: usize) -> Self { - Self { - buf: String::with_capacity(cap), - } - } -} - -impl ResponseWriter for MemoryWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.buf.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} - /// SSE endpoint path. Root-relative so the script works under any /// `` and across sub-path deployments. const HMR_ENDPOINT: &str = "/__webui/livereload"; @@ -564,9 +542,10 @@ fn build_and_render( &mut writer, )?; + let output = writer.buf; let html = match livereload { - Some(lr) => lr.inject(&writer.buf), - None => writer.buf, + Some(lr) => lr.inject(&output), + None => output, }; if let Some(path) = &config.metafile { diff --git a/crates/webui-ffi/src/lib.rs b/crates/webui-ffi/src/lib.rs index 1a12ede67..412631f44 100644 --- a/crates/webui-ffi/src/lib.rs +++ b/crates/webui-ffi/src/lib.rs @@ -28,7 +28,7 @@ use webui_handler::plugin::fast_v3::FastV3HydrationPlugin; use webui_handler::plugin::webui::WebUIHydrationPlugin; use webui_handler::{ BoundaryDescriptor, BoundaryInstanceId, BoundaryKey, BoundaryMode, Protocol, RenderOptions, - ResponseWriter, SessionOptions, StreamStep, StreamingSession, WebUIHandler, + SessionOptions, StreamStep, StreamingSession, WebUIHandler, }; /// Opaque C handle for a loaded WebUI protocol. @@ -81,29 +81,7 @@ struct ProtocolContext { protocol: Arc, } -/// A simple string buffer for collecting rendered output. -struct StringResponseWriter { - content: String, -} - -impl StringResponseWriter { - fn new() -> Self { - Self { - content: String::new(), - } - } -} - -impl ResponseWriter for StringResponseWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.content.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} +webui_handler::define_string_response_writer!(StringResponseWriter, content); // --------------------------------------------------------------------------- // FFI: error reporting @@ -414,7 +392,7 @@ unsafe fn render_decoded_protocol( options = options.with_nonce(nonce); } - let mut writer = StringResponseWriter::new(); + let mut writer = StringResponseWriter::with_capacity(0); match context .handler .render(protocol, &data, &options, &mut writer) diff --git a/crates/webui-handler/benches/bootstrap_state_bench.rs b/crates/webui-handler/benches/bootstrap_state_bench.rs index bfe66f0c4..498ab2b72 100644 --- a/crates/webui-handler/benches/bootstrap_state_bench.rs +++ b/crates/webui-handler/benches/bootstrap_state_bench.rs @@ -89,6 +89,8 @@ impl ResponseWriter for BenchWriter { Ok(()) } + webui_handler::string_response_writer_methods!(output); + fn end(&mut self) -> webui_handler::Result<()> { Ok(()) } diff --git a/crates/webui-handler/benches/handler_bench.rs b/crates/webui-handler/benches/handler_bench.rs index 4392913c0..2fca20ad1 100644 --- a/crates/webui-handler/benches/handler_bench.rs +++ b/crates/webui-handler/benches/handler_bench.rs @@ -37,6 +37,8 @@ impl ResponseWriter for BenchWriter { Ok(()) } + webui_handler::string_response_writer_methods!(output); + fn end(&mut self) -> webui_handler::Result<()> { Ok(()) } diff --git a/crates/webui-handler/benches/streaming_hydration_bench.rs b/crates/webui-handler/benches/streaming_hydration_bench.rs index 9588d9ff4..077cc8e8b 100644 --- a/crates/webui-handler/benches/streaming_hydration_bench.rs +++ b/crates/webui-handler/benches/streaming_hydration_bench.rs @@ -59,6 +59,8 @@ impl ResponseWriter for BenchWriter { Ok(()) } + webui_handler::string_response_writer_methods!(output); + fn end(&mut self) -> webui_handler::Result<()> { Ok(()) } diff --git a/crates/webui-handler/src/lib.rs b/crates/webui-handler/src/lib.rs index ffbfd208b..e7a50e490 100644 --- a/crates/webui-handler/src/lib.rs +++ b/crates/webui-handler/src/lib.rs @@ -9,11 +9,17 @@ pub mod css_module; pub(crate) mod html_encode; pub mod plugin; +mod response_writer; pub mod route_handler; pub mod route_matcher; pub(crate) mod route_renderer; pub(crate) mod streaming; +#[doc(hidden)] +pub use response_writer::{ + append_attribute_to_bytes, append_attribute_to_string, append_boolean_attribute_to_bytes, + append_boolean_attribute_to_string, +}; pub use route_handler::Protocol; /// Minimal HTML escaper for the 6 XSS-critical characters @@ -192,6 +198,23 @@ pub trait ResponseWriter { /// Write content to the output fn write(&mut self, content: &str) -> Result<()>; + /// Write one complete quoted HTML attribute. + #[doc(hidden)] + fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> { + self.write(" ")?; + self.write(name)?; + self.write("=\"")?; + self.write(value)?; + self.write("\"") + } + + /// Write one complete boolean HTML attribute. + #[doc(hidden)] + fn write_boolean_attribute(&mut self, name: &str) -> Result<()> { + self.write(" ")?; + self.write(name) + } + /// Finalize the output fn end(&mut self) -> Result<()>; @@ -2939,11 +2962,7 @@ impl Default for WebUIHandler { /// Write ` name="value"` to the writer without allocating a format string. fn write_attr(writer: &mut dyn ResponseWriter, name: &str, value: &str) -> Result<()> { - writer.write(" ")?; - writer.write(name)?; - writer.write("=\"")?; - writer.write(value)?; - writer.write("\"") + writer.write_attribute(name, value) } #[cfg(test)] @@ -3011,6 +3030,36 @@ mod tests { } } + #[test] + fn generated_string_writer_methods_preserve_exact_attribute_output() { + struct AttributeWriter { + output: String, + } + + impl ResponseWriter for AttributeWriter { + fn write(&mut self, content: &str) -> Result<()> { + self.output.push_str(content); + Ok(()) + } + + crate::string_response_writer_methods!(output); + + fn end(&mut self) -> Result<()> { + Ok(()) + } + } + + let mut writer = AttributeWriter { + output: String::new(), + }; + write_attr(&mut writer, "data-id", "42") + .unwrap_or_else(|error| panic!("attribute write failed: {error}")); + writer + .write_boolean_attribute("disabled") + .unwrap_or_else(|error| panic!("boolean attribute write failed: {error}")); + assert_eq!(writer.output, " data-id=\"42\" disabled"); + } + #[test] fn test_handle_raw() { // Create a simple protocol diff --git a/crates/webui-handler/src/response_writer.rs b/crates/webui-handler/src/response_writer.rs new file mode 100644 index 000000000..f60232ecb --- /dev/null +++ b/crates/webui-handler/src/response_writer.rs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// Append one quoted HTML attribute to an existing string allocation. +#[doc(hidden)] +#[inline] +pub fn append_attribute_to_string(output: &mut String, name: &str, value: &str) { + output.push(' '); + output.push_str(name); + output.push_str("=\""); + output.push_str(value); + output.push('"'); +} + +/// Append one boolean HTML attribute to an existing string allocation. +#[doc(hidden)] +#[inline] +pub fn append_boolean_attribute_to_string(output: &mut String, name: &str) { + output.push(' '); + output.push_str(name); +} + +/// Append one quoted HTML attribute to an existing byte allocation. +#[doc(hidden)] +#[inline] +pub fn append_attribute_to_bytes(output: &mut Vec, name: &str, value: &str) { + output.push(b' '); + output.extend_from_slice(name.as_bytes()); + output.extend_from_slice(b"=\""); + output.extend_from_slice(value.as_bytes()); + output.push(b'"'); +} + +/// Append one boolean HTML attribute to an existing byte allocation. +#[doc(hidden)] +#[inline] +pub fn append_boolean_attribute_to_bytes(output: &mut Vec, name: &str) { + output.push(b' '); + output.extend_from_slice(name.as_bytes()); +} + +/// Generate optimized attribute methods for a string-backed `ResponseWriter`. +#[doc(hidden)] +#[macro_export] +macro_rules! string_response_writer_methods { + ($field:ident) => { + fn write_attribute(&mut self, name: &str, value: &str) -> $crate::Result<()> { + self.$field.push(' '); + self.$field.push_str(name); + self.$field.push_str("=\""); + self.$field.push_str(value); + self.$field.push('"'); + Ok(()) + } + + fn write_boolean_attribute(&mut self, name: &str) -> $crate::Result<()> { + self.$field.push(' '); + self.$field.push_str(name); + Ok(()) + } + }; +} + +/// Generate optimized attribute methods for a byte-backed `ResponseWriter`. +#[doc(hidden)] +#[macro_export] +macro_rules! bytes_response_writer_methods { + ($field:ident) => { + fn write_attribute(&mut self, name: &str, value: &str) -> $crate::Result<()> { + self.$field.push(b' '); + self.$field.extend_from_slice(name.as_bytes()); + self.$field.extend_from_slice(b"=\""); + self.$field.extend_from_slice(value.as_bytes()); + self.$field.push(b'"'); + Ok(()) + } + + fn write_boolean_attribute(&mut self, name: &str) -> $crate::Result<()> { + self.$field.push(b' '); + self.$field.extend_from_slice(name.as_bytes()); + Ok(()) + } + }; +} + +/// Define a private string-backed writer with local, inlinable methods. +#[doc(hidden)] +#[macro_export] +macro_rules! define_string_response_writer { + ($name:ident, $field:ident) => { + struct $name { + $field: String, + } + + impl $name { + fn with_capacity(capacity: usize) -> Self { + Self { + $field: String::with_capacity(capacity), + } + } + } + + impl $crate::ResponseWriter for $name { + fn write(&mut self, content: &str) -> $crate::Result<()> { + self.$field.push_str(content); + Ok(()) + } + + $crate::string_response_writer_methods!($field); + + fn end(&mut self) -> $crate::Result<()> { + Ok(()) + } + } + }; +} + +/// Define a private byte-backed writer with local, inlinable methods. +#[doc(hidden)] +#[macro_export] +macro_rules! define_bytes_response_writer { + ($name:ident, $field:ident) => { + struct $name { + $field: Vec, + } + + impl $name { + fn with_capacity(capacity: usize) -> Self { + Self { + $field: Vec::with_capacity(capacity), + } + } + } + + impl $crate::ResponseWriter for $name { + fn write(&mut self, content: &str) -> $crate::Result<()> { + self.$field.extend_from_slice(content.as_bytes()); + Ok(()) + } + + $crate::bytes_response_writer_methods!($field); + + fn end(&mut self) -> $crate::Result<()> { + Ok(()) + } + } + }; +} diff --git a/crates/webui-handler/src/streaming/mod.rs b/crates/webui-handler/src/streaming/mod.rs index d66d1511d..b95515896 100644 --- a/crates/webui-handler/src/streaming/mod.rs +++ b/crates/webui-handler/src/streaming/mod.rs @@ -146,6 +146,26 @@ impl ResponseWriter for StreamingSink<'_, W> { self.transport.write(content) } + fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> { + if let Some(opening) = self.component_opening.as_mut() { + crate::response_writer::append_attribute_to_string(&mut opening.bytes, name, value); + return Ok(()); + } + self.written = self + .written + .wrapping_add(name.len().wrapping_add(value.len()).wrapping_add(4)); + self.transport.write_attribute(name, value) + } + + fn write_boolean_attribute(&mut self, name: &str) -> Result<()> { + if let Some(opening) = self.component_opening.as_mut() { + crate::response_writer::append_boolean_attribute_to_string(&mut opening.bytes, name); + return Ok(()); + } + self.written = self.written.wrapping_add(name.len().wrapping_add(1)); + self.transport.write_boolean_attribute(name) + } + fn end(&mut self) -> Result<()> { self.transport.end() } diff --git a/crates/webui-handler/src/streaming/owned.rs b/crates/webui-handler/src/streaming/owned.rs index ac1c76eab..ceada76aa 100644 --- a/crates/webui-handler/src/streaming/owned.rs +++ b/crates/webui-handler/src/streaming/owned.rs @@ -38,6 +38,16 @@ impl ResponseWriter for BufferSink { Ok(()) } + fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> { + crate::append_attribute_to_bytes(&mut self.bytes, name, value); + Ok(()) + } + + fn write_boolean_attribute(&mut self, name: &str) -> Result<()> { + crate::append_boolean_attribute_to_bytes(&mut self.bytes, name); + Ok(()) + } + fn end(&mut self) -> Result<()> { Ok(()) } diff --git a/crates/webui-node/src/lib.rs b/crates/webui-node/src/lib.rs index acf039152..a30c93bef 100644 --- a/crates/webui-node/src/lib.rs +++ b/crates/webui-node/src/lib.rs @@ -557,28 +557,7 @@ fn create_handler(plugin: Option) -> napi::Result { }) } -struct BufferedWriter { - output: String, -} - -impl BufferedWriter { - fn new() -> Self { - Self { - output: String::with_capacity(4096), - } - } -} - -impl ResponseWriter for BufferedWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.output.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} +webui_handler::define_string_response_writer!(BufferedWriter, output); fn render_to_string( handler: &WebUIHandler, @@ -586,7 +565,7 @@ fn render_to_string( state: &Value, options: &RenderOptions<'_>, ) -> napi::Result { - let mut writer = BufferedWriter::new(); + let mut writer = BufferedWriter::with_capacity(4096); handler .render(protocol, state, options, &mut writer) .map_err(|e| NapiError::from_reason(format!("Render error: {e}")))?; @@ -644,6 +623,28 @@ where Ok(()) } + fn write_attribute(&mut self, name: &str, value: &str) -> webui_handler::Result<()> { + if self.error.is_some() { + return Err(callback_writer_error()); + } + webui_handler::append_attribute_to_string(&mut self.buffer, name, value); + if self.buffer.len() >= STREAM_CHUNK_SIZE { + self.flush()?; + } + Ok(()) + } + + fn write_boolean_attribute(&mut self, name: &str) -> webui_handler::Result<()> { + if self.error.is_some() { + return Err(callback_writer_error()); + } + webui_handler::append_boolean_attribute_to_string(&mut self.buffer, name); + if self.buffer.len() >= STREAM_CHUNK_SIZE { + self.flush()?; + } + Ok(()) + } + fn end(&mut self) -> webui_handler::Result<()> { self.flush() } diff --git a/crates/webui-press/src/build.rs b/crates/webui-press/src/build.rs index 0027c5d76..90c6551cb 100644 --- a/crates/webui-press/src/build.rs +++ b/crates/webui-press/src/build.rs @@ -14,7 +14,7 @@ use console::style; use rayon::prelude::*; use serde_json::{Map, Value}; use webui::BuildOptions; -use webui_handler::{Protocol, RenderOptions, ResponseWriter, WebUIHandler}; +use webui_handler::{Protocol, RenderOptions, WebUIHandler}; use webui_tokens::TokenFile; use crate::bundler::{ @@ -29,6 +29,8 @@ use crate::markdown::Highlighter; use crate::state::{load_render_states, merge_page_state}; use crate::types::{BuildStats, DocsConfig}; +webui_handler::define_string_response_writer!(StringWriter, buf); + /// Persistent state held by the dev server across rebuilds. The dev /// server always performs a full rebuild on every watcher tick — the /// previous incremental machinery proved too complex for the marginal @@ -217,30 +219,6 @@ fn json_obj(entries: [(&str, Value); N]) -> Value { Value::Object(map) } -/// A writer that collects rendered HTML into a String buffer. -struct StringWriter { - buf: String, -} - -impl StringWriter { - fn with_capacity(cap: usize) -> Self { - Self { - buf: String::with_capacity(cap), - } - } -} - -impl ResponseWriter for StringWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.buf.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} - /// Build a documentation site from the given configuration. /// /// `config_dir` is the directory containing `config.json`. It is used to diff --git a/crates/webui-python/src/lib.rs b/crates/webui-python/src/lib.rs index 28c681bb8..483e179c0 100644 --- a/crates/webui-python/src/lib.rs +++ b/crates/webui-python/src/lib.rs @@ -15,7 +15,7 @@ use webui_handler::plugin::fast_v3::FastV3HydrationPlugin; use webui_handler::plugin::webui::WebUIHydrationPlugin; use webui_handler::{ BoundaryDescriptor, BoundaryInstanceId, BoundaryKey, BoundaryMode, HandlerError, Protocol, - RenderOptions, ResponseWriter, SessionOptions, StreamStep as HandlerStreamStep, + RenderOptions, SessionOptions, StreamStep as HandlerStreamStep, StreamingSession as HandlerStreamingSession, WebUIHandler, }; @@ -44,6 +44,8 @@ type PyRenderOptions = ( type PyPartialOptions = (String, String, String); type PyTemplateOptions = (Vec, String); +webui_handler::define_bytes_response_writer!(BytesWriter, bytes); + enum JsonInput { Text(PyBackedStr), Bytes(PyBackedBytes), @@ -182,29 +184,6 @@ impl BindingError { } } -struct BytesWriter { - bytes: Vec, -} - -impl BytesWriter { - fn new() -> Self { - Self { - bytes: Vec::with_capacity(4096), - } - } -} - -impl ResponseWriter for BytesWriter { - fn write(&mut self, content: &str) -> Result<(), HandlerError> { - self.bytes.extend_from_slice(content.as_bytes()); - Ok(()) - } - - fn end(&mut self) -> Result<(), HandlerError> { - Ok(()) - } -} - #[pyclass(name = "_Renderer", frozen, module = "microsoft_webui._native")] struct NativeRenderer { protocol: Arc, @@ -338,7 +317,7 @@ impl NativeRenderer { ) -> Result, BindingError> { let state = serde_json::from_slice::(input.as_bytes()) .map_err(|error| BindingError::state(format!("failed to parse state JSON: {error}")))?; - let mut writer = BytesWriter::new(); + let mut writer = BytesWriter::with_capacity(4096); self.handler .render(&self.protocol, &state, &options.borrowed(), &mut writer) .map_err(render_binding_error)?; diff --git a/crates/webui-wasm/src/handler.rs b/crates/webui-wasm/src/handler.rs index 56e09f7e8..b91c17e70 100644 --- a/crates/webui-wasm/src/handler.rs +++ b/crates/webui-wasm/src/handler.rs @@ -21,29 +21,7 @@ use webui_protocol::WebUIProtocol; const STREAM_CHUNK_SIZE: usize = 16 * 1024; -/// A string buffer for collecting rendered output. -struct StringWriter { - content: String, -} - -impl StringWriter { - fn with_capacity(cap: usize) -> Self { - Self { - content: String::with_capacity(cap), - } - } -} - -impl ResponseWriter for StringWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.content.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} +webui_handler::define_string_response_writer!(StringWriter, content); /// A writer that batches rendered fragments before crossing into JavaScript. struct CallbackWriter<'a> { @@ -81,6 +59,22 @@ impl ResponseWriter for CallbackWriter<'_> { Ok(()) } + fn write_attribute(&mut self, name: &str, value: &str) -> webui_handler::Result<()> { + webui_handler::append_attribute_to_string(&mut self.buffer, name, value); + if self.buffer.len() >= STREAM_CHUNK_SIZE { + self.flush()?; + } + Ok(()) + } + + fn write_boolean_attribute(&mut self, name: &str) -> webui_handler::Result<()> { + webui_handler::append_boolean_attribute_to_string(&mut self.buffer, name); + if self.buffer.len() >= STREAM_CHUNK_SIZE { + self.flush()?; + } + Ok(()) + } + fn end(&mut self) -> webui_handler::Result<()> { self.flush() } diff --git a/crates/webui/benches/contact_book_bench.rs b/crates/webui/benches/contact_book_bench.rs index 8b8813a86..ff7be52fb 100644 --- a/crates/webui/benches/contact_book_bench.rs +++ b/crates/webui/benches/contact_book_bench.rs @@ -6,7 +6,7 @@ //! benchmark time, then measures protocol parsing and handler rendering at //! different data scales (10 / 100 / 1,000 contacts). //! -//! Run with: `cargo bench -p webui --bench contact_book_bench` +//! Run with: `cargo bench -p microsoft-webui --bench contact_book_bench` use criterion::{criterion_group, BenchmarkId, Criterion, Throughput}; use serde_json::{json, Value}; @@ -96,6 +96,8 @@ impl ResponseWriter for BenchWriter { Ok(()) } + webui_handler::string_response_writer_methods!(output); + fn end(&mut self) -> webui_handler::Result<()> { Ok(()) } diff --git a/crates/webui/src/server.rs b/crates/webui/src/server.rs index 13b7d4c3b..ab8f2a4d3 100644 --- a/crates/webui/src/server.rs +++ b/crates/webui/src/server.rs @@ -27,10 +27,12 @@ //! } //! ``` -use crate::{Protocol, ResponseWriter, WebUIHandler}; +use crate::{Protocol, WebUIHandler}; use webui_handler::route_handler; use webui_handler::RenderOptions; +webui_handler::define_string_response_writer!(MemWriter, buf); + /// A server request to be handled by [`serve_request`]. pub struct ServeRequest<'a> { /// The URL path (e.g., `"/email/thread-5"`, `"/folder/sent"`). @@ -108,29 +110,6 @@ pub fn serve_request( } } -struct MemWriter { - buf: String, -} - -impl MemWriter { - fn with_capacity(cap: usize) -> Self { - Self { - buf: String::with_capacity(cap), - } - } -} - -impl ResponseWriter for MemWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.buf.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/webui/src/streaming.rs b/crates/webui/src/streaming.rs index 0af8e2076..4bca74015 100644 --- a/crates/webui/src/streaming.rs +++ b/crates/webui/src/streaming.rs @@ -618,6 +618,28 @@ impl ResponseWriter for StreamingWriter { Ok(()) } + fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> { + if let Some(cause) = self.terminated { + return Err(cause.into()); + } + webui_handler::append_attribute_to_bytes(&mut self.buf, name, value); + if self.buf.len() >= self.chunk_target { + self.flush_buf()?; + } + Ok(()) + } + + fn write_boolean_attribute(&mut self, name: &str) -> Result<()> { + if let Some(cause) = self.terminated { + return Err(cause.into()); + } + webui_handler::append_boolean_attribute_to_bytes(&mut self.buf, name); + if self.buf.len() >= self.chunk_target { + self.flush_buf()?; + } + Ok(()) + } + fn end(&mut self) -> Result<()> { // Surface the final-flush error so the caller can distinguish // "fully delivered" from "client gave up at the very last diff --git a/examples/demo/server/src/shell.rs b/examples/demo/server/src/shell.rs index f422b067f..694b1f859 100644 --- a/examples/demo/server/src/shell.rs +++ b/examples/demo/server/src/shell.rs @@ -15,10 +15,12 @@ use std::sync::Arc; use webui::{build, BuildOptions, Plugin, Protocol}; use webui_handler::plugin::webui::WebUIHydrationPlugin; -use webui_handler::{RenderOptions, ResponseWriter, WebUIHandler}; +use webui_handler::{RenderOptions, WebUIHandler}; use crate::registry::AppEntry; +webui_handler::define_string_response_writer!(StringWriter, buf); + /// Shared state for the shell renderer: the compiled protocol and the /// directory containing client-side assets (`dist/`). pub(crate) struct ShellState { @@ -97,21 +99,6 @@ fn build_state(apps: &[AppEntry], current_index: usize) -> serde_json::Value { }) } -struct StringWriter { - buf: String, -} - -impl ResponseWriter for StringWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.buf.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} - /// Serves the shell page at `/`. Picks the initial current app via the /// `?app=` query parameter when present. pub(crate) async fn shell_page( @@ -136,9 +123,7 @@ pub(crate) async fn shell_page( let state = build_state(&apps, current_index); - let mut writer = StringWriter { - buf: String::with_capacity(8 * 1024), - }; + let mut writer = StringWriter::with_capacity(8 * 1024); let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new())); let opts = RenderOptions::new("index.html", "/"); From 55626e8d16348b72fb02518c9e3bbeeeb8700e46 Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Mon, 24 Aug 2026 11:25:08 -0700 Subject: [PATCH 2/2] style(cli): group serve imports before writer macro Move `use metafile::write_atomic;` into the contiguous import block so the `define_string_response_writer!` invocation, which defines the `MemoryWriter` type, sits after all imports instead of between them. Addresses PR #467 review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfb1dbef-d6a7-4d85-93c4-90d9b61cfc3c --- crates/webui-cli/src/commands/serve.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/webui-cli/src/commands/serve.rs b/crates/webui-cli/src/commands/serve.rs index bbe15395e..0ea5b19f1 100644 --- a/crates/webui-cli/src/commands/serve.rs +++ b/crates/webui-cli/src/commands/serve.rs @@ -33,9 +33,9 @@ use crate::utils::error::CliError; use crate::utils::output; #[cfg(test)] use metafile::temp_directory as metafile_temp_directory; +use metafile::write_atomic; webui_handler::define_string_response_writer!(MemoryWriter, buf); -use metafile::write_atomic; #[derive(Args)] pub struct ServeArgs {