From e5b76a7ece9625f40feaf4f1f9d50687070bafa6 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 23 May 2022 01:28:24 +0900 Subject: [PATCH] BufWriter. --- packages/yew/src/html/component/scope.rs | 18 +++---- packages/yew/src/server_renderer.rs | 63 +++++++++++++++++++++-- packages/yew/src/virtual_dom/mod.rs | 36 +++++-------- packages/yew/src/virtual_dom/vcomp.rs | 14 ++--- packages/yew/src/virtual_dom/vlist.rs | 23 +++++---- packages/yew/src/virtual_dom/vnode.rs | 20 ++++--- packages/yew/src/virtual_dom/vsuspense.rs | 11 ++-- packages/yew/src/virtual_dom/vtag.rs | 57 +++++++++----------- packages/yew/src/virtual_dom/vtext.rs | 6 +-- 9 files changed, 141 insertions(+), 107 deletions(-) diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index d59dcd30730..2d33150d6b4 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -260,7 +260,6 @@ impl Scope { #[cfg(feature = "ssr")] mod feat_ssr { - use futures::channel::mpsc::UnboundedSender; use futures::channel::oneshot; use super::*; @@ -268,19 +267,18 @@ mod feat_ssr { ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner, }; use crate::scheduler; + use crate::server_renderer::BufWriter; use crate::virtual_dom::Collectable; impl Scope { pub(crate) async fn render_into_stream( &self, - tx: &mut UnboundedSender, + w: &mut BufWriter, props: Rc, hydratable: bool, ) { - let (html_tx, html_rx) = oneshot::channel(); - let state = ComponentRenderState::Ssr { - sender: Some(html_tx), - }; + let (tx, rx) = oneshot::channel(); + let state = ComponentRenderState::Ssr { sender: Some(tx) }; scheduler::push_component_create( self.id, @@ -298,17 +296,17 @@ mod feat_ssr { let collectable = Collectable::for_component::(); if hydratable { - collectable.write_open_tag(tx); + collectable.write_open_tag(w); } - let html = html_rx.await.unwrap(); + let html = rx.await.unwrap(); let self_any_scope = AnyScope::from(self.clone()); - html.render_into_stream(tx, &self_any_scope, hydratable) + html.render_into_stream(w, &self_any_scope, hydratable) .await; if hydratable { - collectable.write_close_tag(tx); + collectable.write_close_tag(w); } scheduler::push_component_destroy(Box::new(DestroyRunner { diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index 97dc8eb8c1e..fbb67a44214 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -1,9 +1,66 @@ -use futures::channel::mpsc; +use std::borrow::Cow; + +use futures::channel::mpsc::{self, UnboundedSender}; use futures::stream::{Stream, StreamExt}; use crate::html::{BaseComponent, Scope}; use crate::platform::{run_pinned, spawn_local}; +pub(crate) struct BufWriter { + buf: String, + tx: UnboundedSender, +} + +impl BufWriter { + pub fn new() -> (Self, impl Stream) { + let (tx, rx) = mpsc::unbounded::(); + + let this = Self { + buf: String::with_capacity(4096), + tx, + }; + + (this, rx) + } + + /// Writes a string into the buffer, optionally drains the buffer. + pub fn write(&mut self, s: Cow<'_, str>) { + if s.len() > 4096 { + // if the next chunk is more than 4096, we drain the buffer and the next + // chunk. + if !self.buf.is_empty() { + let mut buf = String::with_capacity(4096); + std::mem::swap(&mut buf, &mut self.buf); + let _ = self.tx.unbounded_send(buf); + } + + let _ = self.tx.unbounded_send(s.into_owned()); + } else if self.buf.len() + s.len() < 4096 { + // The length of current chunk and the next part is less than 4096, we push + // it on to the buffer. + self.buf.push_str(&s); + } else { + // The length of current chunk and the next part is more than 4096, we send + // the current buffer and make the next chunk the new buffer. + let mut buf = s.into_owned(); + + buf.reserve(4096); + std::mem::swap(&mut buf, &mut self.buf); + let _ = self.tx.unbounded_send(buf); + } + } +} + +impl Drop for BufWriter { + fn drop(&mut self) { + if !self.buf.is_empty() { + let mut buf = "".to_string(); + std::mem::swap(&mut buf, &mut self.buf); + let _ = self.tx.unbounded_send(buf); + } + } +} + /// A Yew Server-side Renderer that renders on the current thread. #[cfg_attr(documenting, doc(cfg(feature = "ssr")))] #[derive(Debug)] @@ -82,12 +139,12 @@ where // Whilst not required to be async here, this function is async to keep the same function // signature as the ServerRenderer. pub async fn render_stream(self) -> impl Stream { - let (mut tx, rx) = mpsc::unbounded::(); + let (mut w, rx) = BufWriter::new(); let scope = Scope::::new(None); spawn_local(async move { scope - .render_into_stream(&mut tx, self.props.into(), self.hydratable) + .render_into_stream(&mut w, self.props.into(), self.hydratable) .await; }); diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index fad826bd973..0eb099172ce 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -265,46 +265,36 @@ pub(crate) use feat_ssr_hydration::*; #[cfg(feature = "ssr")] mod feat_ssr { - use futures::channel::mpsc::UnboundedSender; - use super::*; + use crate::server_renderer::BufWriter; impl Collectable { - pub fn write_open_tag(&self, tx: &mut UnboundedSender) { - // - let mut w = String::with_capacity(11); - - w.push_str(""); - - let _ = tx.unbounded_send(w); + w.write(self.end_mark().into()); + w.write("-->".into()); } - pub fn write_close_tag(&self, tx: &mut UnboundedSender) { - // - let mut w = String::with_capacity(12); - w.push_str(""); - - let _ = tx.unbounded_send(w); + w.write(self.end_mark().into()); + w.write("-->".into()); } } } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 9716cb2df3a..b8ee8df4da9 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -4,8 +4,6 @@ use std::any::TypeId; use std::fmt; use std::rc::Rc; -#[cfg(feature = "ssr")] -use futures::channel::mpsc::UnboundedSender; #[cfg(feature = "ssr")] use futures::future::{FutureExt, LocalBoxFuture}; #[cfg(feature = "csr")] @@ -21,6 +19,8 @@ use crate::html::Scoped; #[cfg(any(feature = "ssr", feature = "csr"))] use crate::html::{AnyScope, Scope}; use crate::html::{BaseComponent, NodeRef}; +#[cfg(feature = "ssr")] +use crate::server_renderer::BufWriter; /// A virtual component. pub struct VComp { @@ -71,7 +71,7 @@ pub(crate) trait Mountable { #[cfg(feature = "ssr")] fn render_into_stream<'a>( &'a self, - w: &'a mut UnboundedSender, + w: &'a mut BufWriter, parent_scope: &'a AnyScope, hydratable: bool, ) -> LocalBoxFuture<'a, ()>; @@ -129,7 +129,7 @@ impl Mountable for PropsWrapper { #[cfg(feature = "ssr")] fn render_into_stream<'a>( &'a self, - tx: &'a mut UnboundedSender, + w: &'a mut BufWriter, parent_scope: &'a AnyScope, hydratable: bool, ) -> LocalBoxFuture<'a, ()> { @@ -137,7 +137,7 @@ impl Mountable for PropsWrapper { async move { scope - .render_into_stream(tx, self.props.clone(), hydratable) + .render_into_stream(w, self.props.clone(), hydratable) .await; } .boxed_local() @@ -246,13 +246,13 @@ mod feat_ssr { #[inline] pub(crate) async fn render_into_stream( &self, - tx: &mut UnboundedSender, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { self.mountable .as_ref() - .render_into_stream(tx, parent_scope, hydratable) + .render_into_stream(w, parent_scope, hydratable) .await; } } diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 99bdfab88c0..3e40a11284b 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -146,23 +146,23 @@ mod test { #[cfg(feature = "ssr")] mod feat_ssr { - use futures::channel::mpsc::{self, UnboundedSender}; use futures::stream::{FuturesOrdered, StreamExt}; use super::*; use crate::html::AnyScope; + use crate::server_renderer::BufWriter; impl VList { pub(crate) async fn render_into_stream( &self, - tx: &mut UnboundedSender, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { if self.children.len() < 2 { match self.children.first() { Some(m) => { - m.render_into_stream(tx, parent_scope, hydratable).await; + m.render_into_stream(w, parent_scope, hydratable).await; } None => {} @@ -176,18 +176,19 @@ mod feat_ssr { .children .iter() .map(|m| async move { - let (mut tx, rx) = mpsc::unbounded(); + let (mut w, rx) = BufWriter::new(); - m.render_into_stream(&mut tx, parent_scope, hydratable) - .await; - drop(tx); + m.render_into_stream(&mut w, parent_scope, hydratable).await; + drop(w); - let s: String = rx.collect().await; - s + rx }) .collect(); - while let Some(m) = children.next().await { - let _ = tx.unbounded_send(m); + + while let Some(mut rx) = children.next().await { + while let Some(next_chunk) = rx.next().await { + w.write(next_chunk.into()); + } } } } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 6b5a7ad39e1..40d862f7fc4 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -147,37 +147,35 @@ impl PartialEq for VNode { #[cfg(feature = "ssr")] mod feat_ssr { - use futures::channel::mpsc::UnboundedSender; use futures::future::{FutureExt, LocalBoxFuture}; use super::*; use crate::html::AnyScope; + use crate::server_renderer::BufWriter; impl VNode { pub(crate) fn render_into_stream<'a>( &'a self, - tx: &'a mut UnboundedSender, + w: &'a mut BufWriter, parent_scope: &'a AnyScope, hydratable: bool, ) -> LocalBoxFuture<'a, ()> { async fn render_into_stream_( this: &VNode, - tx: &mut UnboundedSender, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { match this { - VNode::VTag(vtag) => { - vtag.render_into_stream(tx, parent_scope, hydratable).await - } + VNode::VTag(vtag) => vtag.render_into_stream(w, parent_scope, hydratable).await, VNode::VText(vtext) => { - vtext.render_into_stream(tx, parent_scope, hydratable).await + vtext.render_into_stream(w, parent_scope, hydratable).await } VNode::VComp(vcomp) => { - vcomp.render_into_stream(tx, parent_scope, hydratable).await + vcomp.render_into_stream(w, parent_scope, hydratable).await } VNode::VList(vlist) => { - vlist.render_into_stream(tx, parent_scope, hydratable).await + vlist.render_into_stream(w, parent_scope, hydratable).await } // We are pretty safe here as it's not possible to get a web_sys::Node without // DOM support in the first place. @@ -191,13 +189,13 @@ mod feat_ssr { VNode::VPortal(_) => {} VNode::VSuspense(vsuspense) => { vsuspense - .render_into_stream(tx, parent_scope, hydratable) + .render_into_stream(w, parent_scope, hydratable) .await } } } - async move { render_into_stream_(self, tx, parent_scope, hydratable).await } + async move { render_into_stream_(self, w, parent_scope, hydratable).await } .boxed_local() } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 7e54428dd2b..1c9479e492f 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -26,32 +26,31 @@ impl VSuspense { #[cfg(feature = "ssr")] mod feat_ssr { - use futures::channel::mpsc::UnboundedSender; - use super::*; use crate::html::AnyScope; + use crate::server_renderer::BufWriter; use crate::virtual_dom::Collectable; impl VSuspense { pub(crate) async fn render_into_stream( &self, - tx: &mut UnboundedSender, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { let collectable = Collectable::Suspense; if hydratable { - collectable.write_open_tag(tx); + collectable.write_open_tag(w); } // always render children on the server side. self.children - .render_into_stream(tx, parent_scope, hydratable) + .render_into_stream(w, parent_scope, hydratable) .await; if hydratable { - collectable.write_close_tag(tx); + collectable.write_close_tag(w); } } } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 503a65ae3c4..6895e72d189 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -428,12 +428,9 @@ impl PartialEq for VTag { #[cfg(feature = "ssr")] mod feat_ssr { - use std::fmt::Write; - - use futures::channel::mpsc::UnboundedSender; - use super::*; use crate::html::AnyScope; + use crate::server_renderer::BufWriter; use crate::virtual_dom::VText; // Elements that cannot have any child elements. @@ -445,59 +442,53 @@ mod feat_ssr { impl VTag { pub(crate) async fn render_into_stream( &self, - tx: &mut UnboundedSender, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { - // Preallocate a String that is big enough for most elements. - let mut start_tag = String::with_capacity(64); - start_tag.push('<'); - start_tag.push_str(self.tag()); - - let write_attr = |w: &mut String, name: &str, val: Option<&str>| { - match val { - Some(val) => { - write!( - w, - "{}=\"{}\"", - name, - html_escape::encode_double_quoted_attribute(val) - ) - } - None => { - write!(w, " {}", name) - } + w.write("<".into()); + w.write(self.tag().into()); + + let write_attr = |w: &mut BufWriter, name: &str, val: Option<&str>| match val { + Some(val) => w.write( + format!( + "{}=\"{}\"", + name, + html_escape::encode_double_quoted_attribute(val) + ) + .into(), + ), + None => { + w.write(name.into()); } - .unwrap(); }; if let VTagInner::Input(_) = self.inner { if let Some(m) = self.value() { - write_attr(&mut start_tag, "value", Some(m)); + write_attr(w, "value", Some(m)); } if self.checked() { - write_attr(&mut start_tag, "checked", None); + write_attr(w, "checked", None); } } for (k, v) in self.attributes.iter() { - write_attr(&mut start_tag, k, Some(v)); + write_attr(w, k, Some(v)); } - start_tag.push('>'); - let _ = tx.unbounded_send(start_tag); + w.write(">".into()); match self.inner { VTagInner::Input(_) => {} VTagInner::Textarea { .. } => { if let Some(m) = self.value() { VText::new(m.to_owned()) - .render_into_stream(tx, parent_scope, hydratable) + .render_into_stream(w, parent_scope, hydratable) .await; } - let _ = tx.unbounded_send("".into()); + w.write("".into()); } VTagInner::Other { ref tag, @@ -506,10 +497,10 @@ mod feat_ssr { } => { if !VOID_ELEMENTS.contains(&tag.as_ref()) { children - .render_into_stream(tx, parent_scope, hydratable) + .render_into_stream(w, parent_scope, hydratable) .await; - let _ = tx.unbounded_send(format!("", tag)); + w.write(format!("", tag).into()); } else { // We don't write children of void elements nor closing tags. debug_assert!(children.is_empty(), "{} cannot have any children!", tag); diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 9ac1dae7d97..f0bcb2fb684 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -34,22 +34,22 @@ impl PartialEq for VText { #[cfg(feature = "ssr")] mod feat_ssr { - use futures::channel::mpsc::UnboundedSender; use super::*; use crate::html::AnyScope; + use crate::server_renderer::BufWriter; impl VText { pub(crate) async fn render_into_stream( &self, - tx: &mut UnboundedSender, + w: &mut BufWriter, _parent_scope: &AnyScope, _hydratable: bool, ) { let mut s = String::with_capacity(self.text.len()); html_escape::encode_text_to_string(&self.text, &mut s); - let _ = tx.unbounded_send(s); + w.write(s.into()); } } }