From 8dbf8aa880c00e8ee9bbeba11f17e5ac00dab641 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 1 Jul 2022 22:05:49 +0900 Subject: [PATCH 01/49] Adds Runtime. --- packages/yew/src/platform/mod.rs | 82 ++++++++++++++++---- packages/yew/src/platform/rt_none.rs | 49 ++++++++---- packages/yew/src/platform/rt_tokio.rs | 57 +++++++++----- packages/yew/src/platform/rt_wasm_bindgen.rs | 30 ++++--- packages/yew/src/server_renderer.rs | 26 +++++-- 5 files changed, 176 insertions(+), 68 deletions(-) diff --git a/packages/yew/src/platform/mod.rs b/packages/yew/src/platform/mod.rs index 48b35ddd4b0..31b30f10478 100644 --- a/packages/yew/src/platform/mod.rs +++ b/packages/yew/src/platform/mod.rs @@ -41,6 +41,7 @@ //! `tokio`'s timer, IO and task synchronisation primitives. use std::future::Future; +use std::io::Result; #[cfg(feature = "ssr")] pub(crate) mod io; @@ -70,21 +71,68 @@ where imp::spawn_local(f); } -/// Runs a task with it pinned onto a local worker thread. -/// -/// This can be used to execute non-Send futures without blocking the current thread. -/// -/// It maintains an internal thread pool dedicated to executing local futures. -/// -/// [`spawn_local`] is available with tasks executed with `run_pinned`. -#[inline(always)] -#[cfg(feature = "ssr")] -pub(crate) async fn run_pinned(create_task: F) -> Fut::Output -where - F: FnOnce() -> Fut, - F: Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + 'static, -{ - imp::run_pinned(create_task).await +/// A Runtime Builder. +#[derive(Debug)] +pub struct RuntimeBuilder { + worker_threads: usize, +} + +impl Default for RuntimeBuilder { + fn default() -> Self { + Self { + worker_threads: *imp::DEFAULT_RUNTIME_SIZE, + } + } +} + +impl RuntimeBuilder { + /// Creates a new Runtime Builder. + pub fn new() -> Self { + Self::default() + } + + /// Sets the number of worker threads the Runtime will use. + /// + /// # Default + /// + /// The default number of worker threads is double the number of available CPU cores. + /// + /// # Note + /// + /// This setting has no effect if current platform has no thread support (e.g.: WebAssembly). + pub fn worker_threads(&mut self, val: usize) -> &mut Self { + self.worker_threads = val; + + self + } + + /// Creates a Runtime. + pub fn build(&mut self) -> Result { + Ok(Runtime { + inner: imp::Runtime::new(self.worker_threads)?, + }) + } +} + +/// The Yew Runtime. +#[derive(Debug, Clone, Default)] +pub struct Runtime { + inner: imp::Runtime, +} + +impl Runtime { + /// Runs a task with it pinned to a worker thread. + /// + /// This can be used to execute non-Send futures without blocking the current thread. + /// + /// [`spawn_local`] is available with tasks executed with `run_pinned`. + pub async fn run_pinned(&self, create_task: F) -> Fut::Output + where + F: FnOnce() -> Fut, + F: Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, + { + self.inner.run_pinned(create_task).await + } } diff --git a/packages/yew/src/platform/rt_none.rs b/packages/yew/src/platform/rt_none.rs index 3a8dc6a671f..4fdccab5cff 100644 --- a/packages/yew/src/platform/rt_none.rs +++ b/packages/yew/src/platform/rt_none.rs @@ -1,26 +1,43 @@ use std::future::Future; +use std::io; + +use once_cell::sync::Lazy; + +pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| 0); + +pub static NO_RUNTIME_NOTICE: &str = r#"No runtime configured for this platform, \ + features that requires task spawning can't be used. \ + Either compile with `target_arch = "wasm32", or enable the `tokio` feature."#; #[inline(always)] pub(super) fn spawn_local(_f: F) where F: Future + 'static, { - panic!( - r#"No runtime configured for this platform, features that requires task spawning can't be used. - Either compile with `target_arch = "wasm32", or enable the `tokio` feature."# - ); + panic!("{}", NO_RUNTIME_NOTICE); } -#[cfg(feature = "ssr")] -pub(crate) async fn run_pinned(_create_task: F) -> Fut::Output -where - F: FnOnce() -> Fut, - F: Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + 'static, -{ - panic!( - r#"No runtime configured for this platform, features that requires task spawning can't be used. - Either compile with `target_arch = "wasm32", or enable the `tokio` feature."# - ) +#[derive(Debug, Clone)] +pub(crate) struct Runtime {} + +impl Default for Runtime { + fn default() -> Self { + panic!("{}", NO_RUNTIME_NOTICE); + } +} + +impl Runtime { + pub fn new(_size: usize) -> io::Result { + panic!("{}", NO_RUNTIME_NOTICE); + } + + pub(crate) async fn run_pinned(&self, _create_task: F) -> Fut::Output + where + F: FnOnce() -> Fut, + F: Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, + { + panic!("{}", NO_RUNTIME_NOTICE); + } } diff --git a/packages/yew/src/platform/rt_tokio.rs b/packages/yew/src/platform/rt_tokio.rs index ee8e2251a71..4568d057fc3 100644 --- a/packages/yew/src/platform/rt_tokio.rs +++ b/packages/yew/src/platform/rt_tokio.rs @@ -1,24 +1,10 @@ use std::future::Future; +use std::io; -#[cfg(feature = "ssr")] -pub(super) async fn run_pinned(create_task: F) -> Fut::Output -where - F: FnOnce() -> Fut, - F: Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + 'static, -{ - use once_cell::sync::Lazy; - use tokio_util::task::LocalPoolHandle; - - static POOL_HANDLE: Lazy = - Lazy::new(|| LocalPoolHandle::new(num_cpus::get() * 2)); +use once_cell::sync::Lazy; +use tokio_util::task::LocalPoolHandle; - POOL_HANDLE - .spawn_pinned(create_task) - .await - .expect("future has panicked!") -} +pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| num_cpus::get() * 2); #[inline(always)] pub(super) fn spawn_local(f: F) @@ -27,3 +13,38 @@ where { tokio::task::spawn_local(f); } + +#[derive(Debug, Clone)] +pub(crate) struct Runtime { + pool: LocalPoolHandle, +} + +impl Default for Runtime { + fn default() -> Self { + static DEFAULT_RT: Lazy = + Lazy::new(|| Runtime::new(*DEFAULT_RUNTIME_SIZE).expect("failed to create runtime.")); + + DEFAULT_RT.clone() + } +} + +impl Runtime { + pub fn new(size: usize) -> io::Result { + Ok(Self { + pool: LocalPoolHandle::new(size), + }) + } + + pub(super) async fn run_pinned(&self, create_task: F) -> Fut::Output + where + F: FnOnce() -> Fut, + F: Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, + { + self.pool + .spawn_pinned(create_task) + .await + .expect("future has panicked!") + } +} diff --git a/packages/yew/src/platform/rt_wasm_bindgen.rs b/packages/yew/src/platform/rt_wasm_bindgen.rs index 8307bfd4350..b80671f97ca 100644 --- a/packages/yew/src/platform/rt_wasm_bindgen.rs +++ b/packages/yew/src/platform/rt_wasm_bindgen.rs @@ -1,15 +1,27 @@ #[cfg(feature = "ssr")] use std::future::Future; +use once_cell::sync::Lazy; + +pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| 0); + pub(super) use wasm_bindgen_futures::spawn_local; -#[cfg(feature = "ssr")] -pub(crate) async fn run_pinned(create_task: F) -> Fut::Output -where - F: FnOnce() -> Fut, - F: Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + 'static, -{ - create_task().await +#[derive(Debug, Clone, Default)] +pub(crate) struct Runtime {} + +impl Runtime { + pub fn new(_size: usize) -> io::Result { + Ok(Self {}) + } + + pub(crate) async fn run_pinned(&self, create_task: F) -> Fut::Output + where + F: FnOnce() -> Fut, + F: Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, + { + create_task().await + } } diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index 45c6c79c2e9..c5bab85a31a 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -4,7 +4,7 @@ use futures::stream::{Stream, StreamExt}; use crate::html::{BaseComponent, Scope}; use crate::platform::io::{self, DEFAULT_BUF_SIZE}; -use crate::platform::{run_pinned, spawn_local}; +use crate::platform::{spawn_local, Runtime}; /// A Yew Server-side Renderer that renders on the current thread. #[cfg_attr(documenting, doc(cfg(feature = "ssr")))] @@ -120,6 +120,7 @@ where create_props: Box COMP::Properties>, hydratable: bool, capacity: usize, + rt: Runtime, } impl fmt::Debug for ServerRenderer @@ -170,9 +171,17 @@ where create_props: Box::new(create_props), hydratable: true, capacity: DEFAULT_BUF_SIZE, + rt: Runtime::default(), } } + /// Sets the runtime the ServerRenderer will run the rendering task with. + pub fn with_runtime(mut self, rt: Runtime) -> Self { + self.rt = rt; + + self + } + /// Sets the capacity of renderer buffer. /// /// Default: `8192` @@ -218,14 +227,15 @@ where /// /// Unlike [`LocalServerRenderer::render_stream`], this method is `async fn`. pub async fn render_stream(self) -> impl Stream { - // We use run_pinned to switch to our runtime. - run_pinned(move || async move { - let Self { - create_props, - hydratable, - capacity, - } = self; + let Self { + create_props, + hydratable, + capacity, + rt, + } = self; + // We use run_pinned to switch to our runtime. + rt.run_pinned(move || async move { let props = create_props(); LocalServerRenderer::::with_props(props) From eddec1dbd61aef0c816bd5c5b5dfd62fe9581837 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 1 Jul 2022 22:47:34 +0900 Subject: [PATCH 02/49] A LocalRuntime. --- packages/yew/src/platform/mod.rs | 35 ++++++++++++++++++++ packages/yew/src/platform/rt_none.rs | 17 ++++++++++ packages/yew/src/platform/rt_tokio.rs | 27 +++++++++++++++ packages/yew/src/platform/rt_wasm_bindgen.rs | 17 ++++++++++ 4 files changed, 96 insertions(+) diff --git a/packages/yew/src/platform/mod.rs b/packages/yew/src/platform/mod.rs index 31b30f10478..de3b9a06f72 100644 --- a/packages/yew/src/platform/mod.rs +++ b/packages/yew/src/platform/mod.rs @@ -114,6 +114,36 @@ impl RuntimeBuilder { } } +/// A Yew runtime that runs on the current thread. +#[derive(Debug)] +pub struct LocalRuntime { + inner: imp::LocalRuntime, +} + +impl LocalRuntime { + /// Creates a new LocalRuntime. + pub fn new() -> Result { + Ok(Self { + inner: imp::LocalRuntime::new()?, + }) + } + + /// Runs a Future until completion with current thread blocked. + /// + /// # Panic + /// + /// This method will panic if it is called from within a runtime. + /// If the runtime backend is `wasm-bindgen`, a runtime is started before passing through the + /// WebAssembly boundary and this method will always panic. + pub fn block_on(&self, f: F) -> F::Output + where + F: Future + 'static, + F::Output: 'static, + { + self.inner.block_on(f) + } +} + /// The Yew Runtime. #[derive(Debug, Clone, Default)] pub struct Runtime { @@ -121,6 +151,11 @@ pub struct Runtime { } impl Runtime { + /// Creates a Builder to create a runtime. + pub fn builder() -> RuntimeBuilder { + RuntimeBuilder::new() + } + /// Runs a task with it pinned to a worker thread. /// /// This can be used to execute non-Send futures without blocking the current thread. diff --git a/packages/yew/src/platform/rt_none.rs b/packages/yew/src/platform/rt_none.rs index 4fdccab5cff..88750b950d0 100644 --- a/packages/yew/src/platform/rt_none.rs +++ b/packages/yew/src/platform/rt_none.rs @@ -41,3 +41,20 @@ impl Runtime { panic!("{}", NO_RUNTIME_NOTICE); } } + +#[derive(Debug, Clone)] +pub(crate) struct LocalRuntime {} + +impl LocalRuntime { + pub fn new() -> io::Result { + panic!("{}", NO_RUNTIME_NOTICE); + } + + pub fn block_on(&self, _f: F) -> F::Output + where + F: Future + 'static, + F::Output: 'static, + { + panic!("{}", NO_RUNTIME_NOTICE); + } +} diff --git a/packages/yew/src/platform/rt_tokio.rs b/packages/yew/src/platform/rt_tokio.rs index 4568d057fc3..d6c70c66438 100644 --- a/packages/yew/src/platform/rt_tokio.rs +++ b/packages/yew/src/platform/rt_tokio.rs @@ -2,6 +2,8 @@ use std::future::Future; use std::io; use once_cell::sync::Lazy; +use tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime as TokioRuntime}; +use tokio::task::LocalSet; use tokio_util::task::LocalPoolHandle; pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| num_cpus::get() * 2); @@ -48,3 +50,28 @@ impl Runtime { .expect("future has panicked!") } } + +#[derive(Debug)] +pub(crate) struct LocalRuntime { + local_set: LocalSet, + rt: TokioRuntime, +} + +impl LocalRuntime { + pub fn new() -> io::Result { + Ok(Self { + local_set: LocalSet::new(), + rt: TokioRuntimeBuilder::new_current_thread() + .enable_all() + .build()?, + }) + } + + pub fn block_on(&self, f: F) -> F::Output + where + F: Future + 'static, + F::Output: 'static, + { + self.local_set.block_on(&self.rt, f) + } +} diff --git a/packages/yew/src/platform/rt_wasm_bindgen.rs b/packages/yew/src/platform/rt_wasm_bindgen.rs index b80671f97ca..31d9125e75c 100644 --- a/packages/yew/src/platform/rt_wasm_bindgen.rs +++ b/packages/yew/src/platform/rt_wasm_bindgen.rs @@ -25,3 +25,20 @@ impl Runtime { create_task().await } } + +#[derive(Debug, Clone)] +pub(crate) struct LocalRuntime {} + +impl LocalRuntime { + pub fn new() -> io::Result { + panic!("{}", NO_RUNTIME_NOTICE); + } + + pub fn block_on(&self, _f: F) -> F::Output + where + F: Future + 'static, + F::Output: 'static, + { + panic!("invoked from within a runtime!"); + } +} From a18d7d6aa7b22d676960881876205f57a79f5a63 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 1 Jul 2022 23:17:40 +0900 Subject: [PATCH 03/49] Add note. --- packages/yew/src/server_renderer.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index c5bab85a31a..f8d4cb352e9 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -7,6 +7,15 @@ use crate::platform::io::{self, DEFAULT_BUF_SIZE}; use crate::platform::{spawn_local, Runtime}; /// A Yew Server-side Renderer that renders on the current thread. +/// +/// # Note +/// +/// This renderer does not spawn its own runtime and can only be used when: +/// +/// - `wasm-bindgen` is selected as the backend of Yew runtime. +/// - running within `actix_rt`. +/// - running within a [`LocalRuntime`](crate::platform::LocalRuntime). +/// - running within a tokio [`LocalSet`](tokio::task::LocalSet). #[cfg_attr(documenting, doc(cfg(feature = "ssr")))] #[derive(Debug)] pub struct LocalServerRenderer From 249854204edc4af74c0bce9563a2b9eabe6c87c2 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 00:25:09 +0900 Subject: [PATCH 04/49] Add SSR benchmark. --- Cargo.toml | 7 +++ tools/benchmark-ssr/Cargo.toml | 11 ++++ tools/benchmark-ssr/src/main.rs | 103 ++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 tools/benchmark-ssr/Cargo.toml create mode 100644 tools/benchmark-ssr/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 2b716ffd3b7..13e1e6c40e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,8 +41,15 @@ members = [ # Tools "tools/benchmark-struct", "tools/benchmark-hooks", + "tools/benchmark-ssr", "tools/changelog", "tools/process-benchmark-results", "tools/website-test", ] resolver = "2" + +[profile.bench] +lto = true +codegen-units = 1 +panic = "abort" +opt-level = 3 diff --git a/tools/benchmark-ssr/Cargo.toml b/tools/benchmark-ssr/Cargo.toml new file mode 100644 index 00000000000..27c02386092 --- /dev/null +++ b/tools/benchmark-ssr/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "benchmark-ssr" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +yew = { path = "../../packages/yew", features = ["tokio", "ssr"] } +function_router = { path = "../../examples/function_router" } +tokio = { version = "1.19", features = ["full"] } diff --git a/tools/benchmark-ssr/src/main.rs b/tools/benchmark-ssr/src/main.rs new file mode 100644 index 00000000000..0c2de59b9e8 --- /dev/null +++ b/tools/benchmark-ssr/src/main.rs @@ -0,0 +1,103 @@ +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +use function_router::{ServerApp, ServerAppProps}; +use yew::platform::LocalRuntime; +use yew::prelude::*; + +fn fib(n: u32) -> u32 { + if n <= 1 { + 1 + } else { + fib(n - 1) + fib(n - 2) + } +} + +fn baseline() -> Duration { + let start_time = Instant::now(); + fib(40); + start_time.elapsed() +} + +fn bench_hello_world() -> Duration { + static TOTAL: usize = 1_000_000; + + #[function_component] + fn App() -> Html { + html! {
{"Hello, World!"}
} + } + + let rt = LocalRuntime::new().expect("failed to create runtime."); + + let start_time = Instant::now(); + + rt.block_on(async move { + for _ in 0..TOTAL { + yew::LocalServerRenderer::::new().render().await; + } + }); + + start_time.elapsed() +} + +fn bench_router_app() -> Duration { + static TOTAL: usize = 100_000; + + let start_time = Instant::now(); + + let rt = LocalRuntime::new().expect("failed to create runtime."); + + rt.block_on(async move { + for _ in 0..TOTAL { + yew::LocalServerRenderer::::with_props(ServerAppProps { + url: "/".into(), + queries: HashMap::new(), + }) + .render() + .await; + } + }); + + start_time.elapsed() +} + +fn print_result(name: &str, dur: Duration) { + let dur_millis = i32::try_from(dur.as_micros()).map(f64::from).unwrap() / 1000.0; + + println!("{}: {:.3}ms", name, dur_millis); +} + +fn main() { + let mut baselines = Vec::with_capacity(10); + let mut hello_world_apps = Vec::with_capacity(10); + let mut function_router_apps = Vec::with_capacity(10); + + for _ in 0..10 { + let baseline = baseline(); + baselines.push(baseline); + print_result("Baseline", baseline); + let bench_hello_world = bench_hello_world(); + hello_world_apps.push(bench_hello_world); + print_result("Hello World (1,000,000 runs)", bench_hello_world); + let bench_router_app = bench_router_app(); + function_router_apps.push(bench_router_app); + print_result("Function Router (100,000 runs)", bench_router_app); + } + + print_result( + "Baseline", + baselines.into_iter().fold(Duration::ZERO, |l, r| l + r), + ); + print_result( + "Hello World (10,000,000 runs)", + hello_world_apps + .into_iter() + .fold(Duration::ZERO, |l, r| l + r), + ); + print_result( + "Function Router (1,000,000 runs)", + function_router_apps + .into_iter() + .fold(Duration::ZERO, |l, r| l + r), + ); +} From 7d8a48b24684d59b871a29cb7c5423f68b912495 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 00:53:39 +0900 Subject: [PATCH 05/49] Only create default runtime if no custom runtime is set. --- packages/yew/src/server_renderer.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index f8d4cb352e9..913f10d9a2c 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -129,7 +129,7 @@ where create_props: Box COMP::Properties>, hydratable: bool, capacity: usize, - rt: Runtime, + rt: Option, } impl fmt::Debug for ServerRenderer @@ -180,13 +180,13 @@ where create_props: Box::new(create_props), hydratable: true, capacity: DEFAULT_BUF_SIZE, - rt: Runtime::default(), + rt: None, } } /// Sets the runtime the ServerRenderer will run the rendering task with. pub fn with_runtime(mut self, rt: Runtime) -> Self { - self.rt = rt; + self.rt = Some(rt); self } @@ -243,6 +243,8 @@ where rt, } = self; + let rt = rt.unwrap_or_default(); + // We use run_pinned to switch to our runtime. rt.run_pinned(move || async move { let props = create_props(); From d3121c780efb06ae19bfcc06a547c04e7ced58c7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 00:59:02 +0900 Subject: [PATCH 06/49] Use jemalloc for benchmarking. --- tools/benchmark-ssr/Cargo.toml | 1 + tools/benchmark-ssr/src/main.rs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/tools/benchmark-ssr/Cargo.toml b/tools/benchmark-ssr/Cargo.toml index 27c02386092..e30696d2772 100644 --- a/tools/benchmark-ssr/Cargo.toml +++ b/tools/benchmark-ssr/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" yew = { path = "../../packages/yew", features = ["tokio", "ssr"] } function_router = { path = "../../examples/function_router" } tokio = { version = "1.19", features = ["full"] } +jemallocator = "0.5.0" diff --git a/tools/benchmark-ssr/src/main.rs b/tools/benchmark-ssr/src/main.rs index 0c2de59b9e8..3770f0f4517 100644 --- a/tools/benchmark-ssr/src/main.rs +++ b/tools/benchmark-ssr/src/main.rs @@ -5,6 +5,9 @@ use function_router::{ServerApp, ServerAppProps}; use yew::platform::LocalRuntime; use yew::prelude::*; +#[global_allocator] +static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; + fn fib(n: u32) -> u32 { if n <= 1 { 1 From 6f9b32ad1071b560b838f62b06eafd62774a9f48 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 01:13:39 +0900 Subject: [PATCH 07/49] Remove once_cell for web assembly. --- packages/yew/src/platform/io.rs | 3 +++ packages/yew/src/platform/mod.rs | 2 +- packages/yew/src/platform/rt_none.rs | 6 +++--- packages/yew/src/platform/rt_tokio.rs | 11 ++++++++--- packages/yew/src/platform/rt_wasm_bindgen.rs | 8 ++++---- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/yew/src/platform/io.rs b/packages/yew/src/platform/io.rs index 14e0e11d56e..a51d7c75eba 100644 --- a/packages/yew/src/platform/io.rs +++ b/packages/yew/src/platform/io.rs @@ -60,6 +60,9 @@ impl BufWriter { } fn drain(&mut self) { + if self.buf.is_empty() { + return; + } let _ = self.tx.send(self.buf.drain(..).collect()); self.buf.reserve(self.capacity); } diff --git a/packages/yew/src/platform/mod.rs b/packages/yew/src/platform/mod.rs index de3b9a06f72..b911e676c4c 100644 --- a/packages/yew/src/platform/mod.rs +++ b/packages/yew/src/platform/mod.rs @@ -80,7 +80,7 @@ pub struct RuntimeBuilder { impl Default for RuntimeBuilder { fn default() -> Self { Self { - worker_threads: *imp::DEFAULT_RUNTIME_SIZE, + worker_threads: imp::get_default_runtime_size(), } } } diff --git a/packages/yew/src/platform/rt_none.rs b/packages/yew/src/platform/rt_none.rs index 88750b950d0..871c9b1b203 100644 --- a/packages/yew/src/platform/rt_none.rs +++ b/packages/yew/src/platform/rt_none.rs @@ -1,9 +1,9 @@ use std::future::Future; use std::io; -use once_cell::sync::Lazy; - -pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| 0); +pub(crate) fn get_default_runtime_size() -> usize { + 0 +} pub static NO_RUNTIME_NOTICE: &str = r#"No runtime configured for this platform, \ features that requires task spawning can't be used. \ diff --git a/packages/yew/src/platform/rt_tokio.rs b/packages/yew/src/platform/rt_tokio.rs index d6c70c66438..ae1f831242e 100644 --- a/packages/yew/src/platform/rt_tokio.rs +++ b/packages/yew/src/platform/rt_tokio.rs @@ -6,7 +6,11 @@ use tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime as TokioRuntime}; use tokio::task::LocalSet; use tokio_util::task::LocalPoolHandle; -pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| num_cpus::get() * 2); +pub(crate) fn get_default_runtime_size() -> usize { + pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| num_cpus::get() * 2); + + *DEFAULT_RUNTIME_SIZE +} #[inline(always)] pub(super) fn spawn_local(f: F) @@ -23,8 +27,9 @@ pub(crate) struct Runtime { impl Default for Runtime { fn default() -> Self { - static DEFAULT_RT: Lazy = - Lazy::new(|| Runtime::new(*DEFAULT_RUNTIME_SIZE).expect("failed to create runtime.")); + static DEFAULT_RT: Lazy = Lazy::new(|| { + Runtime::new(get_default_runtime_size()).expect("failed to create runtime.") + }); DEFAULT_RT.clone() } diff --git a/packages/yew/src/platform/rt_wasm_bindgen.rs b/packages/yew/src/platform/rt_wasm_bindgen.rs index 31d9125e75c..80a9ad1c3f2 100644 --- a/packages/yew/src/platform/rt_wasm_bindgen.rs +++ b/packages/yew/src/platform/rt_wasm_bindgen.rs @@ -1,9 +1,9 @@ #[cfg(feature = "ssr")] use std::future::Future; -use once_cell::sync::Lazy; - -pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| 0); +pub(crate) fn get_default_runtime_size() -> usize { + 0 +} pub(super) use wasm_bindgen_futures::spawn_local; @@ -31,7 +31,7 @@ pub(crate) struct LocalRuntime {} impl LocalRuntime { pub fn new() -> io::Result { - panic!("{}", NO_RUNTIME_NOTICE); + Ok(Self {}) } pub fn block_on(&self, _f: F) -> F::Output From 8e72fcce4fadb34a82333c276868d1cf3b1879db Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 01:35:34 +0900 Subject: [PATCH 08/49] Add time. --- packages/yew/Cargo.toml | 4 +-- packages/yew/src/platform/mod.rs | 7 ++--- .../platform/{rt_none.rs => rt_none/mod.rs} | 2 ++ packages/yew/src/platform/rt_none/time.rs | 13 +++++++++ .../platform/{rt_tokio.rs => rt_tokio/mod.rs} | 2 ++ packages/yew/src/platform/rt_tokio/time.rs | 13 +++++++++ .../mod.rs} | 2 ++ .../yew/src/platform/rt_wasm_bindgen/time.rs | 15 +++++++++++ packages/yew/src/platform/time.rs | 27 +++++++++++++++++++ 9 files changed, 80 insertions(+), 5 deletions(-) rename packages/yew/src/platform/{rt_none.rs => rt_none/mod.rs} (98%) create mode 100644 packages/yew/src/platform/rt_none/time.rs rename packages/yew/src/platform/{rt_tokio.rs => rt_tokio/mod.rs} (98%) create mode 100644 packages/yew/src/platform/rt_tokio/time.rs rename packages/yew/src/platform/{rt_wasm_bindgen.rs => rt_wasm_bindgen/mod.rs} (97%) create mode 100644 packages/yew/src/platform/rt_wasm_bindgen/time.rs create mode 100644 packages/yew/src/platform/time.rs diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 29194e2dae6..3feb97357b6 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -26,7 +26,7 @@ slab = "0.4" wasm-bindgen = "0.2" yew-macro = { version = "^0.19.0", path = "../yew-macro" } thiserror = "1.0" -futures = { version = "0.3", optional = true } +futures = "0.3" html-escape = { version = "0.2.9", optional = true } implicit-clone = { version = "0.3", features = ["map"] } base64ct = { version = "1.5.0", features = ["std"], optional = true } @@ -96,7 +96,7 @@ features = [ [features] tokio = ["tokio/rt", "dep:num_cpus", "dep:tokio-util"] -ssr = ["dep:futures", "dep:html-escape", "dep:base64ct", "dep:bincode"] +ssr = ["dep:html-escape", "dep:base64ct", "dep:bincode"] csr = [] hydration = ["csr", "dep:bincode"] nightly = ["yew-macro/nightly"] diff --git a/packages/yew/src/platform/mod.rs b/packages/yew/src/platform/mod.rs index b911e676c4c..55b60c6f017 100644 --- a/packages/yew/src/platform/mod.rs +++ b/packages/yew/src/platform/mod.rs @@ -47,15 +47,16 @@ use std::io::Result; pub(crate) mod io; pub mod sync; +pub mod time; #[cfg(target_arch = "wasm32")] -#[path = "rt_wasm_bindgen.rs"] +#[path = "rt_wasm_bindgen/mod.rs"] mod imp; #[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] -#[path = "rt_tokio.rs"] +#[path = "rt_tokio/mod.rs"] mod imp; #[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio")))] -#[path = "rt_none.rs"] +#[path = "rt_none/mod.rs"] mod imp; /// Spawns a task on current thread. diff --git a/packages/yew/src/platform/rt_none.rs b/packages/yew/src/platform/rt_none/mod.rs similarity index 98% rename from packages/yew/src/platform/rt_none.rs rename to packages/yew/src/platform/rt_none/mod.rs index 871c9b1b203..acf27cda69a 100644 --- a/packages/yew/src/platform/rt_none.rs +++ b/packages/yew/src/platform/rt_none/mod.rs @@ -1,6 +1,8 @@ use std::future::Future; use std::io; +pub(crate) mod time; + pub(crate) fn get_default_runtime_size() -> usize { 0 } diff --git a/packages/yew/src/platform/rt_none/time.rs b/packages/yew/src/platform/rt_none/time.rs new file mode 100644 index 00000000000..9616ea32805 --- /dev/null +++ b/packages/yew/src/platform/rt_none/time.rs @@ -0,0 +1,13 @@ +use std::time::Duration; + +use futures::stream::LocalBoxStream; + +use super::NO_RUNTIME_NOTICE; + +pub(crate) async fn sleep(_dur: Duration) { + panic!("{}", NO_RUNTIME_NOTICE); +} + +pub(crate) fn interval(_dur: Duration) -> LocalBoxStream<'static, ()> { + panic!("{}", NO_RUNTIME_NOTICE); +} diff --git a/packages/yew/src/platform/rt_tokio.rs b/packages/yew/src/platform/rt_tokio/mod.rs similarity index 98% rename from packages/yew/src/platform/rt_tokio.rs rename to packages/yew/src/platform/rt_tokio/mod.rs index ae1f831242e..04cf0998f22 100644 --- a/packages/yew/src/platform/rt_tokio.rs +++ b/packages/yew/src/platform/rt_tokio/mod.rs @@ -6,6 +6,8 @@ use tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime as TokioRuntime}; use tokio::task::LocalSet; use tokio_util::task::LocalPoolHandle; +pub(crate) mod time; + pub(crate) fn get_default_runtime_size() -> usize { pub(crate) static DEFAULT_RUNTIME_SIZE: Lazy = Lazy::new(|| num_cpus::get() * 2); diff --git a/packages/yew/src/platform/rt_tokio/time.rs b/packages/yew/src/platform/rt_tokio/time.rs new file mode 100644 index 00000000000..8cb6458bf8d --- /dev/null +++ b/packages/yew/src/platform/rt_tokio/time.rs @@ -0,0 +1,13 @@ +use std::future::Future; +use std::time::Duration; + +use futures::stream::{Stream, StreamExt}; +use tokio_stream::wrappers::IntervalStream; + +pub(crate) fn sleep(dur: Duration) -> impl Future { + tokio::time::sleep(dur) +} + +pub(crate) fn interval(dur: Duration) -> impl Stream { + IntervalStream::new(tokio::time::interval(dur)).then(|_| async {}) +} diff --git a/packages/yew/src/platform/rt_wasm_bindgen.rs b/packages/yew/src/platform/rt_wasm_bindgen/mod.rs similarity index 97% rename from packages/yew/src/platform/rt_wasm_bindgen.rs rename to packages/yew/src/platform/rt_wasm_bindgen/mod.rs index 80a9ad1c3f2..568450253d1 100644 --- a/packages/yew/src/platform/rt_wasm_bindgen.rs +++ b/packages/yew/src/platform/rt_wasm_bindgen/mod.rs @@ -1,6 +1,8 @@ #[cfg(feature = "ssr")] use std::future::Future; +pub(crate) mod time; + pub(crate) fn get_default_runtime_size() -> usize { 0 } diff --git a/packages/yew/src/platform/rt_wasm_bindgen/time.rs b/packages/yew/src/platform/rt_wasm_bindgen/time.rs new file mode 100644 index 00000000000..ec4f1ea898f --- /dev/null +++ b/packages/yew/src/platform/rt_wasm_bindgen/time.rs @@ -0,0 +1,15 @@ +use std::future::Future; +use std::time::Duration; + +use futures::stream::Stream; + +pub(crate) fn sleep(dur: Duration) -> impl Future { + gloo::timers::future::sleep(dur) +} + +pub(crate) fn interval(dur: Duration) -> impl Stream { + let millis = u32::try_from(dur.as_millis()) + .expect_throw("failed to cast the duration into a u32 with Duration::as_millis."); + + gloo::timers::future::IntervalStream::new(millis) +} diff --git a/packages/yew/src/platform/time.rs b/packages/yew/src/platform/time.rs new file mode 100644 index 00000000000..c7e0bc6680b --- /dev/null +++ b/packages/yew/src/platform/time.rs @@ -0,0 +1,27 @@ +//! Utilities for bridging time and tasks. + +use std::time::Duration; + +use futures::Stream; + +use crate::platform::imp::time as imp; + +/// Waits until duration has elapsed. +/// +/// # Panic +/// +/// On some platforms, if the prodvided duration cannot be converted to u32 in milliseconds, this +/// function will panic. +pub async fn sleep(dur: Duration) { + imp::sleep(dur).await; +} + +/// Creates a Stream that yields an item for after every period has elapsed. +/// +/// # Panic +/// +/// On some platforms, if the prodvided period cannot be converted to u32 in milliseconds, this +/// function will panic. +pub fn interval(period: Duration) -> impl Stream { + imp::interval(period) +} From 7c65632773a9af2dc903c282bfacf74e46ea6c80 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 01:42:50 +0900 Subject: [PATCH 09/49] Fix wasm_bindgen. --- packages/yew/Cargo.toml | 2 +- packages/yew/src/platform/rt_wasm_bindgen/mod.rs | 2 +- packages/yew/src/platform/rt_wasm_bindgen/time.rs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 3feb97357b6..847e92f423d 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -18,7 +18,7 @@ rust-version = "1.60.0" [dependencies] console_error_panic_hook = "0.1" -gloo = "0.8" +gloo = { version = "0.8", features = ["futures"] } gloo-utils = "0.1.0" indexmap = { version = "1", features = ["std"] } js-sys = "0.3" diff --git a/packages/yew/src/platform/rt_wasm_bindgen/mod.rs b/packages/yew/src/platform/rt_wasm_bindgen/mod.rs index 568450253d1..da4ee6a922f 100644 --- a/packages/yew/src/platform/rt_wasm_bindgen/mod.rs +++ b/packages/yew/src/platform/rt_wasm_bindgen/mod.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "ssr")] use std::future::Future; +use std::io; pub(crate) mod time; diff --git a/packages/yew/src/platform/rt_wasm_bindgen/time.rs b/packages/yew/src/platform/rt_wasm_bindgen/time.rs index ec4f1ea898f..ea97fe5dafe 100644 --- a/packages/yew/src/platform/rt_wasm_bindgen/time.rs +++ b/packages/yew/src/platform/rt_wasm_bindgen/time.rs @@ -2,6 +2,7 @@ use std::future::Future; use std::time::Duration; use futures::stream::Stream; +use wasm_bindgen::UnwrapThrowExt; pub(crate) fn sleep(dur: Duration) -> impl Future { gloo::timers::future::sleep(dur) From 75242b52411b1c0a1d453ed6778ef947517aab8a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 01:54:42 +0900 Subject: [PATCH 10/49] Adjust inlining. --- packages/yew/src/platform/rt_tokio/time.rs | 1 + packages/yew/src/platform/rt_wasm_bindgen/time.rs | 1 + packages/yew/src/platform/time.rs | 9 ++++++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/yew/src/platform/rt_tokio/time.rs b/packages/yew/src/platform/rt_tokio/time.rs index 8cb6458bf8d..c6d49abdbe2 100644 --- a/packages/yew/src/platform/rt_tokio/time.rs +++ b/packages/yew/src/platform/rt_tokio/time.rs @@ -4,6 +4,7 @@ use std::time::Duration; use futures::stream::{Stream, StreamExt}; use tokio_stream::wrappers::IntervalStream; +#[inline] pub(crate) fn sleep(dur: Duration) -> impl Future { tokio::time::sleep(dur) } diff --git a/packages/yew/src/platform/rt_wasm_bindgen/time.rs b/packages/yew/src/platform/rt_wasm_bindgen/time.rs index ea97fe5dafe..2e4e7da9636 100644 --- a/packages/yew/src/platform/rt_wasm_bindgen/time.rs +++ b/packages/yew/src/platform/rt_wasm_bindgen/time.rs @@ -4,6 +4,7 @@ use std::time::Duration; use futures::stream::Stream; use wasm_bindgen::UnwrapThrowExt; +#[inline] pub(crate) fn sleep(dur: Duration) -> impl Future { gloo::timers::future::sleep(dur) } diff --git a/packages/yew/src/platform/time.rs b/packages/yew/src/platform/time.rs index c7e0bc6680b..741e22ffb18 100644 --- a/packages/yew/src/platform/time.rs +++ b/packages/yew/src/platform/time.rs @@ -1,5 +1,6 @@ //! Utilities for bridging time and tasks. +use std::future::Future; use std::time::Duration; use futures::Stream; @@ -12,16 +13,18 @@ use crate::platform::imp::time as imp; /// /// On some platforms, if the prodvided duration cannot be converted to u32 in milliseconds, this /// function will panic. -pub async fn sleep(dur: Duration) { - imp::sleep(dur).await; +#[inline(always)] +pub fn sleep(dur: Duration) -> impl Future { + imp::sleep(dur) } -/// Creates a Stream that yields an item for after every period has elapsed. +/// Creates a Stream that yields an item after every period has elapsed. /// /// # Panic /// /// On some platforms, if the prodvided period cannot be converted to u32 in milliseconds, this /// function will panic. +#[inline(always)] pub fn interval(period: Duration) -> impl Stream { imp::interval(period) } From 1d6274c1e99a7a3e03eca7278655a9944e52b345 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 16:30:27 +0900 Subject: [PATCH 11/49] Optimise benchmark output. --- Cargo.toml | 6 + tools/benchmark-ssr/Cargo.toml | 4 + tools/benchmark-ssr/src/main.rs | 189 +++++++++++++++++++++++++------- 3 files changed, 159 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 13e1e6c40e9..cf53e605fcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,12 @@ members = [ ] resolver = "2" +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +opt-level = 3 + [profile.bench] lto = true codegen-units = 1 diff --git a/tools/benchmark-ssr/Cargo.toml b/tools/benchmark-ssr/Cargo.toml index e30696d2772..144629199d6 100644 --- a/tools/benchmark-ssr/Cargo.toml +++ b/tools/benchmark-ssr/Cargo.toml @@ -10,3 +10,7 @@ yew = { path = "../../packages/yew", features = ["tokio", "ssr"] } function_router = { path = "../../examples/function_router" } tokio = { version = "1.19", features = ["full"] } jemallocator = "0.5.0" +once_cell = "1.12.0" +average = "0.13.1" +tabled = "0.7.0" +indicatif = "0.16.2" diff --git a/tools/benchmark-ssr/src/main.rs b/tools/benchmark-ssr/src/main.rs index 3770f0f4517..fc8e4ee9f1d 100644 --- a/tools/benchmark-ssr/src/main.rs +++ b/tools/benchmark-ssr/src/main.rs @@ -1,22 +1,50 @@ use std::collections::HashMap; +use std::env; use std::time::{Duration, Instant}; +use average::Variance; use function_router::{ServerApp, ServerAppProps}; +use indicatif::{ProgressBar, ProgressStyle}; +use once_cell::sync::Lazy; +use tabled::{Style, TableIteratorExt, Tabled}; use yew::platform::LocalRuntime; use yew::prelude::*; #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; -fn fib(n: u32) -> u32 { - if n <= 1 { - 1 - } else { - fib(n - 1) + fib(n - 2) - } +static OUTPUT_JSON: Lazy = Lazy::new(|| { + env::var("BENCH_OUTPUT_JSON") + .map(|m| m == "1") + .unwrap_or(false) +}); + +macro_rules! human_println { + () => { + if !*OUTPUT_JSON { + println!(); + } + }; + ($($arg:tt)*) => { + if !*OUTPUT_JSON { + println!($($arg)*); + } + }; +} + +fn dur_to_millis(dur: Duration) -> f64 { + i32::try_from(dur.as_micros()).map(f64::from).unwrap() / 1000.0 } -fn baseline() -> Duration { +fn bench_baseline() -> Duration { + fn fib(n: u32) -> u32 { + if n <= 1 { + 1 + } else { + fib(n - 1) + fib(n - 2) + } + } + let start_time = Instant::now(); fib(40); start_time.elapsed() @@ -64,43 +92,124 @@ fn bench_router_app() -> Duration { start_time.elapsed() } -fn print_result(name: &str, dur: Duration) { - let dur_millis = i32::try_from(dur.as_micros()).map(f64::from).unwrap() / 1000.0; - - println!("{}: {:.3}ms", name, dur_millis); +#[derive(Debug, Tabled)] +struct Statistics { + #[tabled(rename = "Name")] + name: String, + #[tabled(rename = "Min (ms)")] + min: String, + #[tabled(rename = "Max (ms)")] + max: String, + #[tabled(rename = "Mean (ms)")] + mean: String, + #[tabled(rename = "Standard Deviation")] + std_dev: String, } fn main() { - let mut baselines = Vec::with_capacity(10); - let mut hello_world_apps = Vec::with_capacity(10); - let mut function_router_apps = Vec::with_capacity(10); - - for _ in 0..10 { - let baseline = baseline(); - baselines.push(baseline); - print_result("Baseline", baseline); - let bench_hello_world = bench_hello_world(); - hello_world_apps.push(bench_hello_world); - print_result("Hello World (1,000,000 runs)", bench_hello_world); - let bench_router_app = bench_router_app(); - function_router_apps.push(bench_router_app); - print_result("Function Router (100,000 runs)", bench_router_app); + let mut baseline_results = Vec::new(); + let mut hello_world_results = Vec::new(); + let mut function_router_results = Vec::new(); + + let bar = ProgressBar::new(30); + + { + let bar = bar.downgrade(); + std::thread::spawn(move || { + while let Some(bar) = bar.upgrade() { + bar.tick(); + std::thread::sleep(Duration::from_millis(100)); + } + }); } - print_result( - "Baseline", - baselines.into_iter().fold(Duration::ZERO, |l, r| l + r), - ); - print_result( - "Hello World (10,000,000 runs)", - hello_world_apps - .into_iter() - .fold(Duration::ZERO, |l, r| l + r), - ); - print_result( - "Function Router (1,000,000 runs)", - function_router_apps - .into_iter() - .fold(Duration::ZERO, |l, r| l + r), + bar.set_style( + ProgressStyle::default_bar() + .template( + "{spinner:.green} {prefix} [{elapsed_precise}] [{bar:40.cyan/blue}] round {msg}/10", + ) + // .tick_chars("-\\|/") + .progress_chars("=>-"), ); + + for i in 0..=10 { + bar.set_message(i.to_string()); + if i == 0 { + bar.set_prefix("Warming up"); + } else { + bar.set_prefix("Running "); + } + + let dur = bench_baseline(); + if i > 0 { + baseline_results.push(dur); + bar.inc(1); + } + + let dur = bench_hello_world(); + if i > 0 { + hello_world_results.push(dur); + bar.inc(1); + } + + let dur = bench_router_app(); + if i > 0 { + function_router_results.push(dur); + bar.inc(1); + } + } + + bar.finish_and_clear(); + drop(bar); + + baseline_results.sort(); + hello_world_results.sort(); + function_router_results.sort(); + + let base_var: Variance = baseline_results + .iter() + .cloned() + .map(dur_to_millis) + .collect(); + + let hw_var: Variance = hello_world_results + .iter() + .cloned() + .map(dur_to_millis) + .collect(); + + let fr_var: Variance = function_router_results + .iter() + .cloned() + .map(dur_to_millis) + .collect(); + + let table = [ + Statistics { + name: "Baseline".into(), + min: format!("{:.3}", dur_to_millis(baseline_results[0])), + max: format!("{:.3}", dur_to_millis(baseline_results[9])), + std_dev: format!("{:.3}", base_var.sample_variance().sqrt()), + mean: format!("{:.3}", base_var.mean()), + }, + Statistics { + name: "Hello World".into(), + min: format!("{:.3}", dur_to_millis(hello_world_results[0])), + max: format!("{:.3}", dur_to_millis(hello_world_results[9])), + std_dev: format!("{:.3}", hw_var.sample_variance().sqrt()), + mean: format!("{:.3}", hw_var.mean()), + }, + Statistics { + name: "Function Router".into(), + min: format!("{:.3}", dur_to_millis(function_router_results[0])), + max: format!("{:.3}", dur_to_millis(function_router_results[9])), + std_dev: format!("{:.3}", fr_var.sample_variance().sqrt()), + mean: format!("{:.3}", fr_var.mean()), + }, + ] + .table() + // .with(Header("Statistics")) + .with(Style::rounded()); + + human_println!("{}", table.to_string()); } From 7437a4b15e7350bbedf116a795611151ac02f94a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 20:12:57 +0900 Subject: [PATCH 12/49] Optimise BufWriter. --- packages/yew/src/platform/io.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/yew/src/platform/io.rs b/packages/yew/src/platform/io.rs index a51d7c75eba..9e53991c8a4 100644 --- a/packages/yew/src/platform/io.rs +++ b/packages/yew/src/platform/io.rs @@ -24,7 +24,8 @@ pub(crate) fn buffer(capacity: usize) -> (BufWriter, impl Stream) let (tx, rx) = mpsc::unbounded_channel::(); let tx = BufWriter { - buf: String::with_capacity(capacity), + // We start without allocation so empty strings will not be allocated. + buf: String::new(), tx, capacity, }; @@ -59,12 +60,18 @@ impl BufWriter { self.capacity } + #[inline] fn drain(&mut self) { + if !self.buf.is_empty() { + let _ = self.tx.send(self.buf.split_off(0)); + } + } + + #[inline] + fn reserve(&mut self) { if self.buf.is_empty() { - return; + self.buf.reserve(self.capacity); } - let _ = self.tx.send(self.buf.drain(..).collect()); - self.buf.reserve(self.capacity); } /// Returns `True` if the internal buffer has capacity to fit a string of certain length. @@ -75,6 +82,9 @@ impl BufWriter { /// Writes a string into the buffer, optionally drains the buffer. pub fn write(&mut self, s: Cow<'_, str>) { + // Try to reserve the capacity first. + self.reserve(); + if !self.has_capacity_of(s.len()) { // There isn't enough capacity, we drain the buffer. self.drain(); @@ -97,6 +107,7 @@ impl BufWriter { impl Drop for BufWriter { fn drop(&mut self) { + // We swap the buffer with an empty buffer, this saves an allocation. if !self.buf.is_empty() { let mut buf = String::new(); std::mem::swap(&mut buf, &mut self.buf); From f02d0e20cdeb977045e06f777adcadc8813d0a71 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 21:57:06 +0900 Subject: [PATCH 13/49] Add json output. --- Cargo.toml | 6 -- packages/yew/src/platform/io.rs | 1 - tools/benchmark-ssr/Cargo.toml | 4 +- tools/benchmark-ssr/src/main.rs | 150 ++++++++++++++++++++------------ 4 files changed, 97 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cf53e605fcc..13e1e6c40e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,12 +48,6 @@ members = [ ] resolver = "2" -[profile.release] -lto = true -codegen-units = 1 -panic = "abort" -opt-level = 3 - [profile.bench] lto = true codegen-units = 1 diff --git a/packages/yew/src/platform/io.rs b/packages/yew/src/platform/io.rs index 9e53991c8a4..34546173fb9 100644 --- a/packages/yew/src/platform/io.rs +++ b/packages/yew/src/platform/io.rs @@ -107,7 +107,6 @@ impl BufWriter { impl Drop for BufWriter { fn drop(&mut self) { - // We swap the buffer with an empty buffer, this saves an allocation. if !self.buf.is_empty() { let mut buf = String::new(); std::mem::swap(&mut buf, &mut self.buf); diff --git a/tools/benchmark-ssr/Cargo.toml b/tools/benchmark-ssr/Cargo.toml index 144629199d6..023a6dc2109 100644 --- a/tools/benchmark-ssr/Cargo.toml +++ b/tools/benchmark-ssr/Cargo.toml @@ -10,7 +10,9 @@ yew = { path = "../../packages/yew", features = ["tokio", "ssr"] } function_router = { path = "../../examples/function_router" } tokio = { version = "1.19", features = ["full"] } jemallocator = "0.5.0" -once_cell = "1.12.0" average = "0.13.1" tabled = "0.7.0" indicatif = "0.16.2" +serde = { version = "1.0.138", features = ["derive"] } +serde_json = "1.0.82" +clap = { version = "3.2.8", features = ["derive"] } diff --git a/tools/benchmark-ssr/src/main.rs b/tools/benchmark-ssr/src/main.rs index fc8e4ee9f1d..d6c31ab6d98 100644 --- a/tools/benchmark-ssr/src/main.rs +++ b/tools/benchmark-ssr/src/main.rs @@ -1,11 +1,14 @@ use std::collections::HashMap; -use std::env; +use std::fs; +use std::io::Write; +use std::path::PathBuf; use std::time::{Duration, Instant}; use average::Variance; +use clap::Parser; use function_router::{ServerApp, ServerAppProps}; use indicatif::{ProgressBar, ProgressStyle}; -use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; use tabled::{Style, TableIteratorExt, Tabled}; use yew::platform::LocalRuntime; use yew::prelude::*; @@ -13,23 +16,14 @@ use yew::prelude::*; #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; -static OUTPUT_JSON: Lazy = Lazy::new(|| { - env::var("BENCH_OUTPUT_JSON") - .map(|m| m == "1") - .unwrap_or(false) -}); - -macro_rules! human_println { - () => { - if !*OUTPUT_JSON { - println!(); - } - }; - ($($arg:tt)*) => { - if !*OUTPUT_JSON { - println!($($arg)*); - } - }; +#[derive(Parser)] +struct Args { + /// Disable terminal support. + #[clap(long)] + no_term: bool, + /// Write the report to an output path in json format. + #[clap(long)] + output_path: Option, } fn dur_to_millis(dur: Duration) -> f64 { @@ -92,10 +86,12 @@ fn bench_router_app() -> Duration { start_time.elapsed() } -#[derive(Debug, Tabled)] +#[derive(Debug, Tabled, Serialize, Deserialize)] struct Statistics { #[tabled(rename = "Name")] name: String, + #[tabled(rename = "Round")] + round: String, #[tabled(rename = "Min (ms)")] min: String, #[tabled(rename = "Max (ms)")] @@ -106,60 +102,83 @@ struct Statistics { std_dev: String, } +static ROUND: u16 = 10; + fn main() { + let args = Args::parse(); + let mut baseline_results = Vec::new(); let mut hello_world_results = Vec::new(); let mut function_router_results = Vec::new(); - let bar = ProgressBar::new(30); + let bar = if args.no_term { + None + } else { + // There are 3 items per round. + let bar = ProgressBar::new(u64::from(ROUND * 3)); + // Progress Bar needs to be updated in a different thread. + { + let bar = bar.downgrade(); + std::thread::spawn(move || { + while let Some(bar) = bar.upgrade() { + bar.tick(); + std::thread::sleep(Duration::from_millis(100)); + } + }); + } - { - let bar = bar.downgrade(); - std::thread::spawn(move || { - while let Some(bar) = bar.upgrade() { - bar.tick(); - std::thread::sleep(Duration::from_millis(100)); + bar.set_style( + ProgressStyle::default_bar() + .template(&format!( + "{{spinner:.green}} {{prefix}} [{{elapsed_precise}}] [{{bar:40.cyan/blue}}] \ + round {{msg}}/{}", + ROUND + )) + // .tick_chars("-\\|/") + .progress_chars("=>-"), + ); + + Some(bar) + }; + + for i in 0..=ROUND { + if let Some(ref bar) = bar { + bar.set_message(i.to_string()); + if i == 0 { + bar.set_prefix("Warming up"); + } else { + bar.set_prefix("Running "); } - }); - } - - bar.set_style( - ProgressStyle::default_bar() - .template( - "{spinner:.green} {prefix} [{elapsed_precise}] [{bar:40.cyan/blue}] round {msg}/10", - ) - // .tick_chars("-\\|/") - .progress_chars("=>-"), - ); - - for i in 0..=10 { - bar.set_message(i.to_string()); - if i == 0 { - bar.set_prefix("Warming up"); - } else { - bar.set_prefix("Running "); } let dur = bench_baseline(); if i > 0 { baseline_results.push(dur); - bar.inc(1); + if let Some(ref bar) = bar { + bar.inc(1); + } } let dur = bench_hello_world(); if i > 0 { hello_world_results.push(dur); - bar.inc(1); + if let Some(ref bar) = bar { + bar.inc(1); + } } let dur = bench_router_app(); if i > 0 { function_router_results.push(dur); - bar.inc(1); + if let Some(ref bar) = bar { + bar.inc(1); + } } } - bar.finish_and_clear(); + if let Some(ref bar) = bar { + bar.finish_and_clear(); + } drop(bar); baseline_results.sort(); @@ -184,9 +203,10 @@ fn main() { .map(dur_to_millis) .collect(); - let table = [ + let output = [ Statistics { name: "Baseline".into(), + round: ROUND.to_string(), min: format!("{:.3}", dur_to_millis(baseline_results[0])), max: format!("{:.3}", dur_to_millis(baseline_results[9])), std_dev: format!("{:.3}", base_var.sample_variance().sqrt()), @@ -194,6 +214,7 @@ fn main() { }, Statistics { name: "Hello World".into(), + round: ROUND.to_string(), min: format!("{:.3}", dur_to_millis(hello_world_results[0])), max: format!("{:.3}", dur_to_millis(hello_world_results[9])), std_dev: format!("{:.3}", hw_var.sample_variance().sqrt()), @@ -201,15 +222,32 @@ fn main() { }, Statistics { name: "Function Router".into(), + round: ROUND.to_string(), min: format!("{:.3}", dur_to_millis(function_router_results[0])), max: format!("{:.3}", dur_to_millis(function_router_results[9])), std_dev: format!("{:.3}", fr_var.sample_variance().sqrt()), mean: format!("{:.3}", fr_var.mean()), }, - ] - .table() - // .with(Header("Statistics")) - .with(Style::rounded()); - - human_println!("{}", table.to_string()); + ]; + + println!("{}", output.as_ref().table().with(Style::rounded())); + + if let Some(ref p) = args.output_path { + let mut f = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(p) + .expect("failed to write output."); + + f.write_all( + serde_json::to_string_pretty(&output) + .expect("failed to write output.") + .as_bytes(), + ) + .expect("failed to write output."); + + println!(); + println!("Result has been written to: {}", p.display()); + } } From fa735f717b19f8874e2f8c8896f667cc5499fff4 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Jul 2022 23:04:14 +0900 Subject: [PATCH 14/49] Add Benchmark Workflow. --- .github/workflows/benchmark-ssr.yml | 65 +++++++++++++++++++++ .github/workflows/post-benchmark-ssr.yml | 74 ++++++++++++++++++++++++ .github/workflows/post-size-cmp.yml | 2 +- ci/make_benchmark_ssr_cmt.py | 56 ++++++++++++++++++ tools/benchmark-ssr/src/main.rs | 2 +- 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/benchmark-ssr.yml create mode 100644 .github/workflows/post-benchmark-ssr.yml create mode 100644 ci/make_benchmark_ssr_cmt.py diff --git a/.github/workflows/benchmark-ssr.yml b/.github/workflows/benchmark-ssr.yml new file mode 100644 index 00000000000..dcfb2820325 --- /dev/null +++ b/.github/workflows/benchmark-ssr.yml @@ -0,0 +1,65 @@ +--- +name: Benchmark - SSR + +on: + pull_request: + branches: [master] + paths: + - .github/workflows/benchmark-ssr.yml + - "packages/yew" + - "examples/function_router" + - "tools/benchmark-ssr" + +jobs: + benchmark-ssr: + name: Benchmark - SSR + runs-on: ubuntu-latest + + steps: + - name: Checkout master + uses: actions/checkout@v3 + with: + repository: 'yewstack/yew' + ref: master + path: yew-master + + - name: Checkout pull request + uses: actions/checkout@v3 + with: + path: current-pr + + - name: Setup toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: wasm32-unknown-unknown + profile: minimal + + - name: Restore Rust cache for master + uses: Swatinem/rust-cache@v1 + with: + working-directory: yew-master + key: master + + - name: Restore Rust cache for current pull request + uses: Swatinem/rust-cache@v1 + with: + working-directory: current-pr + key: pr + + - name: Build master examples + run: cargo run --profile=bench --bin benchmark-ssr -- --output-path ./output.json + working-directory: yew-master + + - name: Build pull request examples + run: cargo run --profile=bench --bin benchmark-ssr -- --output-path ./output.json + working-directory: current-pr + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: benchmark-ssr + path: + - "yew-master/output.json" + - "current-pr/output.json" + retention-days: 1 diff --git a/.github/workflows/post-benchmark-ssr.yml b/.github/workflows/post-benchmark-ssr.yml new file mode 100644 index 00000000000..e564e93fb6b --- /dev/null +++ b/.github/workflows/post-benchmark-ssr.yml @@ -0,0 +1,74 @@ +--- +name: Post Comment for Benchmark - SSR + +on: + workflow_run: + workflows: ["Benchmark - SSR"] + types: + - completed + +jobs: + post-benchmark-ssr: + name: Post Comment on Pull Request + runs-on: ubuntu-latest + + steps: + - name: Download Repository + uses: actions/checkout@v2 + + - if: github.event.workflow_run.event == 'pull_request' + name: Download Artifact + uses: Legit-Labs/action-download-artifact@v2 + with: + github_token: "${{ secrets.GITHUB_TOKEN }}" + workflow: benchmark-ssr.yml + run_id: ${{ github.event.workflow_run.id }} + name: benchmark-ssr + path: "benchmark-ssr/" + + - name: Make pull request comment + run: python3 ci/make_benchmark_ssr_cmt.py + + - name: Post Comment + uses: actions/github-script@v6 + with: + script: | + const commentInfo = { + ...context.repo, + issue_number: ${{ env.PR_NUMBER }}, + }; + + const comment = { + ...commentInfo, + body: JSON.parse(process.env.YEW_BENCH_SSR), + }; + + function isCommentByBot(comment) { + return comment.user.type === "Bot" && comment.body.includes("### Benchmark - SSR"); + } + + let commentId = null; + const comments = (await github.rest.issues.listComments(commentInfo)).data; + for (let i = comments.length; i--; ) { + const c = comments[i]; + if (isCommentByBot(c)) { + commentId = c.id; + break; + } + } + + if (commentId) { + try { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: commentId, + body: comment.body, + }); + } catch (e) { + commentId = null; + } + } + + if (!commentId) { + await github.rest.issues.createComment(comment); + } diff --git a/.github/workflows/post-size-cmp.yml b/.github/workflows/post-size-cmp.yml index e1c08f7a2b5..3baba67b990 100644 --- a/.github/workflows/post-size-cmp.yml +++ b/.github/workflows/post-size-cmp.yml @@ -8,7 +8,7 @@ on: - completed jobs: - size-cmp: + post-size-cmp: name: Post Comment on Pull Request runs-on: ubuntu-latest diff --git a/ci/make_benchmark_ssr_cmt.py b/ci/make_benchmark_ssr_cmt.py new file mode 100644 index 00000000000..c2f31939a1d --- /dev/null +++ b/ci/make_benchmark_ssr_cmt.py @@ -0,0 +1,56 @@ +from typing import Dict, List, Optional, Tuple + +import os +import json + + +header = "| Benchmark | Round | Min (ms) | Max (ms) | Mean (ms) | Standard Deviation |" +sep = "| --- | --- | --- | --- | --- | --- |" + + +def write_benchmark(lines: List[str], content: List[Dict[str, str]]) -> None: + lines.append("
") + lines.append("") + lines.append(header) + lines.append(sep) + + for i in content: + lines.append( + "| {i.name} | {i.round} | {i.min} | {i.max} | {i.mean} | {i.std_dev} |" + ) + + lines.append("") + lines.append("
") + + +def main() -> None: + with open("benchmark-ssr/yew-master/output.json") as f: + master_content = json.loads(f.read()) + + with open("benchmark-ssr/current-pr/output.json") as f: + pr_content = json.loads(f.read()) + + lines: List[str] = [] + + lines.append("### Benchmark - SSR") + lines.append("") + + lines.append("#### Yew Master") + lines.append("") + + write_benchmark(lines, master_content) + + lines.append("#### Pull Request") + lines.append("") + + write_benchmark(lines, pr_content) + + output = "\n".join(lines) + + with open(os.environ["GITHUB_ENV"], "a+") as f: + f.write(f"YEW_BENCH_SSR={json.dumps(output)}\n") + f.write(f"PR_NUMBER={issue_number}\n") + + +if __name__ == "__main__": + main() diff --git a/tools/benchmark-ssr/src/main.rs b/tools/benchmark-ssr/src/main.rs index d6c31ab6d98..be36d266006 100644 --- a/tools/benchmark-ssr/src/main.rs +++ b/tools/benchmark-ssr/src/main.rs @@ -88,7 +88,7 @@ fn bench_router_app() -> Duration { #[derive(Debug, Tabled, Serialize, Deserialize)] struct Statistics { - #[tabled(rename = "Name")] + #[tabled(rename = "Benchmark")] name: String, #[tabled(rename = "Round")] round: String, From 74e08762bf92199884505a31935f039705fd46eb Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 3 Jul 2022 02:45:28 +0900 Subject: [PATCH 15/49] Remove local set from tests. --- packages/yew/src/virtual_dom/vsuspense.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 35dd30253a1..01adbd949d6 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -63,15 +63,15 @@ mod ssr_tests { use std::rc::Rc; use std::time::Duration; - use tokio::task::{spawn_local, LocalSet}; use tokio::test; - use tokio::time::sleep; + use yew::platform::spawn_local; + use yew::platform::time::sleep; use crate::prelude::*; use crate::suspense::{Suspension, SuspensionResult}; use crate::ServerRenderer; - #[test(flavor = "multi_thread", worker_threads = 2)] + #[test] async fn test_suspense() { #[derive(PartialEq)] pub struct SleepState { @@ -82,9 +82,7 @@ mod ssr_tests { fn new() -> Self { let (s, handle) = Suspension::new(); - // we use tokio spawn local here. spawn_local(async move { - // we use tokio sleep here. sleep(Duration::from_millis(50)).await; handle.resume(); @@ -137,15 +135,9 @@ mod ssr_tests { } } - let local = LocalSet::new(); - - let s = local - .run_until(async move { - ServerRenderer::::new() - .hydratable(false) - .render() - .await - }) + let s = ServerRenderer::::new() + .hydratable(false) + .render() .await; assert_eq!( From 4db413454dcce7d0bd9fd92a3a7414505b87d1ce Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 3 Jul 2022 12:16:05 +0900 Subject: [PATCH 16/49] Fix Workflow syntax. --- .github/workflows/benchmark-ssr.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark-ssr.yml b/.github/workflows/benchmark-ssr.yml index dcfb2820325..cedc65e31aa 100644 --- a/.github/workflows/benchmark-ssr.yml +++ b/.github/workflows/benchmark-ssr.yml @@ -59,7 +59,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: benchmark-ssr - path: - - "yew-master/output.json" - - "current-pr/output.json" + path: | + yew-master/output.json + current-pr/output.json retention-days: 1 From 2c318a1db19fc89efb0cb7136cfe164ba7e6c472 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 3 Jul 2022 12:27:12 +0900 Subject: [PATCH 17/49] Exclude benchmark from doc tests. --- .github/workflows/benchmark-ssr.yml | 8 ++++---- .github/workflows/main-checks.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/benchmark-ssr.yml b/.github/workflows/benchmark-ssr.yml index cedc65e31aa..24b3baa4417 100644 --- a/.github/workflows/benchmark-ssr.yml +++ b/.github/workflows/benchmark-ssr.yml @@ -47,13 +47,13 @@ jobs: working-directory: current-pr key: pr - - name: Build master examples + - name: Run pull request benchmark run: cargo run --profile=bench --bin benchmark-ssr -- --output-path ./output.json - working-directory: yew-master + working-directory: current-pr - - name: Build pull request examples + - name: Run master benchmark run: cargo run --profile=bench --bin benchmark-ssr -- --output-path ./output.json - working-directory: current-pr + working-directory: yew-master - name: Upload Artifact uses: actions/upload-artifact@v2 diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml index 5333f8ee8d3..b5771373bd1 100644 --- a/.github/workflows/main-checks.yml +++ b/.github/workflows/main-checks.yml @@ -97,7 +97,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --doc --workspace --exclude yew --exclude changelog --exclude website-test --exclude ssr_router --exclude simple_ssr --target wasm32-unknown-unknown + args: --doc --workspace --exclude yew --exclude changelog --exclude website-test --exclude ssr_router --exclude simple_ssr --exclude benchmark-ssr --target wasm32-unknown-unknown - name: Run website code snippet tests uses: actions-rs/cargo@v1 From 214b140adaa765322190bcee7f9ecdd99bcb19ca Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 6 Jul 2022 17:24:50 +0900 Subject: [PATCH 18/49] Adjust feature flags. --- packages/yew/Cargo.toml | 2 +- tools/benchmark-ssr/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 847e92f423d..76f59958790 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -95,7 +95,7 @@ features = [ ] [features] -tokio = ["tokio/rt", "dep:num_cpus", "dep:tokio-util"] +tokio = ["tokio/rt", "tokio/time", "dep:num_cpus", "dep:tokio-util"] ssr = ["dep:html-escape", "dep:base64ct", "dep:bincode"] csr = [] hydration = ["csr", "dep:bincode"] diff --git a/tools/benchmark-ssr/Cargo.toml b/tools/benchmark-ssr/Cargo.toml index 023a6dc2109..df278aa22e4 100644 --- a/tools/benchmark-ssr/Cargo.toml +++ b/tools/benchmark-ssr/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] yew = { path = "../../packages/yew", features = ["tokio", "ssr"] } function_router = { path = "../../examples/function_router" } -tokio = { version = "1.19", features = ["full"] } +tokio = { version = "1.19", features = ["macros", "rt-multi-thread", "parking_lot"] } jemallocator = "0.5.0" average = "0.13.1" tabled = "0.7.0" From f2fcea289095ccf3992c9abb59373a689f309e9b Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Wed, 6 Jul 2022 23:40:14 +0900 Subject: [PATCH 19/49] Adds a pinned channel implementation. --- packages/yew/Cargo.toml | 6 +- packages/yew/src/html/component/lifecycle.rs | 2 +- packages/yew/src/html/component/scope.rs | 6 +- packages/yew/src/platform/{io.rs => fmt.rs} | 81 ++++-- packages/yew/src/platform/mod.rs | 4 +- packages/yew/src/platform/pinned/mod.rs | 7 + packages/yew/src/platform/pinned/mpsc.rs | 273 +++++++++++++++++++ packages/yew/src/platform/pinned/oneshot.rs | 122 +++++++++ packages/yew/src/platform/sync/mod.rs | 5 - packages/yew/src/platform/sync/mpsc.rs | 6 - packages/yew/src/server_renderer.rs | 25 +- packages/yew/src/virtual_dom/mod.rs | 6 +- packages/yew/src/virtual_dom/vcomp.rs | 8 +- packages/yew/src/virtual_dom/vlist.rs | 11 +- packages/yew/src/virtual_dom/vnode.rs | 6 +- packages/yew/src/virtual_dom/vsuspense.rs | 4 +- packages/yew/src/virtual_dom/vtag.rs | 6 +- packages/yew/src/virtual_dom/vtext.rs | 4 +- 18 files changed, 506 insertions(+), 76 deletions(-) rename packages/yew/src/platform/{io.rs => fmt.rs} (74%) create mode 100644 packages/yew/src/platform/pinned/mod.rs create mode 100644 packages/yew/src/platform/pinned/mpsc.rs create mode 100644 packages/yew/src/platform/pinned/oneshot.rs delete mode 100644 packages/yew/src/platform/sync/mod.rs delete mode 100644 packages/yew/src/platform/sync/mpsc.rs diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 76f59958790..3f7f1edc8fc 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -32,8 +32,8 @@ implicit-clone = { version = "0.3", features = ["map"] } base64ct = { version = "1.5.0", features = ["std"], optional = true } bincode = { version = "1.3.3", optional = true } serde = { version = "1", features = ["derive"] } -tokio = { version = "1.19", features = ["sync"] } -tokio-stream = { version = "0.1.9", features = ["sync"] } +tokio = { version = "1.19", features = ["rt", "time"], optional = true } +tokio-stream = { version = "0.1.9", features = ["sync"], optional = true } [dependencies.web-sys] version = "0.3" @@ -95,7 +95,7 @@ features = [ ] [features] -tokio = ["tokio/rt", "tokio/time", "dep:num_cpus", "dep:tokio-util"] +tokio = ["dep:tokio", "dep:tokio-stream", "dep:num_cpus", "dep:tokio-util"] ssr = ["dep:html-escape", "dep:base64ct", "dep:bincode"] csr = [] hydration = ["csr", "dep:bincode"] diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 75841b65904..84c49b7b917 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -41,7 +41,7 @@ pub(crate) enum ComponentRenderState { #[cfg(feature = "ssr")] Ssr { - sender: Option>, + sender: Option>, }, } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 519d7e5a397..05229fad2ac 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -264,15 +264,15 @@ mod feat_ssr { use crate::html::component::lifecycle::{ ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner, }; - use crate::platform::io::BufWriter; - use crate::platform::sync::oneshot; + use crate::platform::fmt::BufWrite; + use crate::platform::pinned::oneshot; use crate::scheduler; use crate::virtual_dom::Collectable; impl Scope { pub(crate) async fn render_into_stream( &self, - w: &mut BufWriter, + w: &mut dyn BufWrite, props: Rc, hydratable: bool, ) { diff --git a/packages/yew/src/platform/io.rs b/packages/yew/src/platform/fmt.rs similarity index 74% rename from packages/yew/src/platform/io.rs rename to packages/yew/src/platform/fmt.rs index 34546173fb9..72d9775655c 100644 --- a/packages/yew/src/platform/io.rs +++ b/packages/yew/src/platform/fmt.rs @@ -5,32 +5,40 @@ use std::borrow::Cow; -use futures::stream::Stream; - -use crate::platform::sync::mpsc::{self, UnboundedReceiverStream, UnboundedSender}; +use crate::platform::pinned; // Same as std::io::BufWriter and futures::io::BufWriter. pub(crate) const DEFAULT_BUF_SIZE: usize = 8 * 1024; -/// A [`futures::io::BufWriter`], but operates over string and yields into a Stream. -pub(crate) struct BufWriter { - buf: String, - tx: UnboundedSender, - capacity: usize, +pub(crate) trait BufSend { + fn buf_send(&self, item: String); } -/// Creates a Buffer pair. -pub(crate) fn buffer(capacity: usize) -> (BufWriter, impl Stream) { - let (tx, rx) = mpsc::unbounded_channel::(); +impl BufSend for pinned::mpsc::UnboundedSender { + fn buf_send(&self, item: String) { + let _ = self.send_now(item); + } +} - let tx = BufWriter { - // We start without allocation so empty strings will not be allocated. - buf: String::new(), - tx, - capacity, - }; +impl BufSend for futures::channel::mpsc::UnboundedSender { + fn buf_send(&self, item: String) { + let _ = self.unbounded_send(item); + } +} - (tx, UnboundedReceiverStream::new(rx)) +pub trait BufWrite { + fn capacity(&self) -> usize; + fn write(&mut self, s: Cow<'_, str>); +} + +/// A [`futures::io::BufWriter`], but operates over string and yields into a Stream. +pub(crate) struct BufWriter +where + S: BufSend, +{ + buf: String, + tx: S, + capacity: usize, } // Implementation Notes: @@ -54,16 +62,22 @@ pub(crate) fn buffer(capacity: usize) -> (BufWriter, impl Stream) // 2. If a fixed buffer is used, the rendering process can become blocked if the buffer is filled. // Using a stream avoids this side effect and allows the renderer to finish rendering // without being actively polled. -impl BufWriter { - #[inline] - pub fn capacity(&self) -> usize { - self.capacity +impl BufWriter +where + S: BufSend, +{ + pub fn new(tx: S, capacity: usize) -> Self { + Self { + buf: String::new(), + tx, + capacity, + } } #[inline] fn drain(&mut self) { if !self.buf.is_empty() { - let _ = self.tx.send(self.buf.split_off(0)); + self.tx.buf_send(self.buf.split_off(0)); } } @@ -79,9 +93,19 @@ impl BufWriter { fn has_capacity_of(&self, next_part_len: usize) -> bool { self.buf.capacity() >= self.buf.len() + next_part_len } +} + +impl BufWrite for BufWriter +where + S: BufSend, +{ + #[inline] + fn capacity(&self) -> usize { + self.capacity + } /// Writes a string into the buffer, optionally drains the buffer. - pub fn write(&mut self, s: Cow<'_, str>) { + fn write(&mut self, s: Cow<'_, str>) { // Try to reserve the capacity first. self.reserve(); @@ -100,17 +124,20 @@ impl BufWriter { // changes if the buffer was drained. If the buffer capacity didn't change, // then it means self.has_capacity_of() has returned true the first time which will be // guaranteed to be matched by the left hand side of this implementation. - let _ = self.tx.send(s.into_owned()); + self.tx.buf_send(s.into_owned()); } } } -impl Drop for BufWriter { +impl Drop for BufWriter +where + S: BufSend, +{ fn drop(&mut self) { if !self.buf.is_empty() { let mut buf = String::new(); std::mem::swap(&mut buf, &mut self.buf); - let _ = self.tx.send(buf); + self.tx.buf_send(buf); } } } diff --git a/packages/yew/src/platform/mod.rs b/packages/yew/src/platform/mod.rs index 55b60c6f017..165d4f6a047 100644 --- a/packages/yew/src/platform/mod.rs +++ b/packages/yew/src/platform/mod.rs @@ -44,9 +44,9 @@ use std::future::Future; use std::io::Result; #[cfg(feature = "ssr")] -pub(crate) mod io; +pub(crate) mod fmt; -pub mod sync; +pub mod pinned; pub mod time; #[cfg(target_arch = "wasm32")] diff --git a/packages/yew/src/platform/pinned/mod.rs b/packages/yew/src/platform/pinned/mod.rs new file mode 100644 index 00000000000..7f870f33df7 --- /dev/null +++ b/packages/yew/src/platform/pinned/mod.rs @@ -0,0 +1,7 @@ +//! Task Synchronisation Primitives for pinned tasks. +//! +//! This module provides task synchronisation for `!Send` futures. + +pub mod mpsc; + +pub mod oneshot; diff --git a/packages/yew/src/platform/pinned/mpsc.rs b/packages/yew/src/platform/pinned/mpsc.rs new file mode 100644 index 00000000000..9ad46784aeb --- /dev/null +++ b/packages/yew/src/platform/pinned/mpsc.rs @@ -0,0 +1,273 @@ +//! A multi-producer single-receiver channel. + +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::rc::Rc; +use std::task::{Poll, Waker}; + +use futures::sink::Sink; +use futures::stream::{FusedStream, Stream}; +use thiserror::Error; + +/// Error returned by [`try_next`](UnboundedReceiver::try_next). +#[derive(Error, Debug)] +#[error("queue is empty")] +pub struct TryRecvError { + _marker: PhantomData<()>, +} + +/// Error returned by [`send`](UnboundedSender::send). +#[derive(Error, Debug)] +#[error("failed to send")] +pub struct SendError { + /// The send value. + pub inner: T, +} + +/// Error returned by [`UnboundedSender`] when used as a [`Sink`]. +#[derive(Error, Debug)] +#[error("failed to send")] +pub struct TrySendError { + _marker: PhantomData<()>, +} + +#[derive(Debug)] +struct Inner { + rx_waker: Option, + closed: bool, + sender_ctr: usize, + items: VecDeque, + + close_wakers: Vec, +} + +impl Inner { + /// Creates a unchecked mutable reference from an immutable reference. + /// + /// SAFETY: You can only use this when: + /// + /// 1. The mutable reference is released at the end of a function call. + /// 2. No parent function has acquired the mutable reference. + /// 3. The caller is not an async function / the mutable reference is released before an await + /// statement. + #[inline] + unsafe fn get_mut_unchecked(&self) -> *mut Self { + self as *const Self as *mut Self + } + + fn close(&mut self) { + self.closed = true; + + if let Some(m) = self.rx_waker.take() { + m.wake(); + } + + for close_waker in self.close_wakers.iter() { + close_waker.wake_by_ref(); + } + } +} + +/// The receiver of a unbounded mpsc channel. +#[derive(Debug)] +pub struct UnboundedReceiver { + inner: Rc>, +} + +impl UnboundedReceiver { + /// Try to read the next value from the channel. + /// + /// This function will return: + /// - `Ok(Some(T))` if a value is ready. + /// - `Ok(None)` if the channel has become closed. + /// - `Err(TryRecvError)` if the channel is not closed and the channel is empty. + pub fn try_next(&self) -> std::result::Result, TryRecvError> { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + match (inner.items.pop_front(), inner.closed) { + (Some(m), _) => Ok(Some(m)), + (None, false) => Ok(None), + (None, true) => Err(TryRecvError { + _marker: PhantomData, + }), + } + } +} + +impl Stream for UnboundedReceiver { + type Item = T; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + match (inner.items.pop_front(), inner.closed) { + (Some(m), _) => Poll::Ready(Some(m)), + (None, false) => { + inner.rx_waker = Some(cx.waker().clone()); + Poll::Pending + } + (None, true) => Poll::Ready(None), + } + } +} + +impl FusedStream for UnboundedReceiver { + fn is_terminated(&self) -> bool { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + inner.items.is_empty() && inner.closed + } +} + +impl Drop for UnboundedReceiver { + fn drop(&mut self) { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + inner.close(); + } +} + +/// The sender of an unbounded mpsc channel. +#[derive(Debug)] +pub struct UnboundedSender { + inner: Rc>, +} + +impl UnboundedSender { + /// Sends a value to the unbounded receiver. + pub fn send_now(&self, item: T) -> Result<(), SendError> { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + if inner.closed { + return Err(SendError { inner: item }); + } + + inner.items.push_back(item); + + if let Some(m) = inner.rx_waker.take() { + m.wake(); + } + + Ok(()) + } + + /// Closes the channel. + pub fn close(&self) { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + inner.close(); + } +} + +impl Clone for UnboundedSender { + fn clone(&self) -> Self { + { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + inner.sender_ctr += 1; + } + + Self { + inner: self.inner.clone(), + } + } +} + +impl Drop for UnboundedSender { + fn drop(&mut self) { + let sender_ctr = { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + inner.sender_ctr -= 1; + + inner.sender_ctr + }; + + if sender_ctr == 0 { + self.close(); + } + } +} + +impl Sink for UnboundedSender { + type Error = TrySendError; + + fn start_send(self: std::pin::Pin<&mut Self>, item: T) -> Result<(), Self::Error> { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + match inner.closed { + false => { + inner.items.push_back(item); + + if let Some(m) = inner.rx_waker.take() { + m.wake(); + } + + Ok(()) + } + true => Err(TrySendError { + _marker: PhantomData, + }), + } + } + + fn poll_ready( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + match inner.closed { + false => Poll::Ready(Ok(())), + true => Poll::Ready(Err(TrySendError { + _marker: PhantomData, + })), + } + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + if inner.closed { + return Poll::Ready(Ok(())); + } + + inner.close_wakers.push(cx.waker().clone()); + Poll::Pending + } +} + +/// Creates an unbounded channel. +/// +/// # Note +/// +/// This channel has an infinite buffer and can run out of memory if the channel is not actively +/// drained. +pub fn unbounded() -> (UnboundedSender, UnboundedReceiver) { + let inner = Rc::new(Inner { + rx_waker: None, + closed: false, + + sender_ctr: 1, + items: VecDeque::new(), + close_wakers: Vec::new(), + }); + + ( + UnboundedSender { + inner: inner.clone(), + }, + UnboundedReceiver { inner }, + ) +} diff --git a/packages/yew/src/platform/pinned/oneshot.rs b/packages/yew/src/platform/pinned/oneshot.rs new file mode 100644 index 00000000000..77364d04fe0 --- /dev/null +++ b/packages/yew/src/platform/pinned/oneshot.rs @@ -0,0 +1,122 @@ +//! A one-time Send - Receive channel. + +use std::future::Future; +use std::rc::Rc; +use std::task::{Poll, Waker}; + +use thiserror::Error; + +/// Error returned by [`send`](Sender::send). +#[derive(Debug, Error)] +#[error("channel has been closed.")] +pub struct SendError { + /// The inner value. + pub inner: T, +} + +#[derive(Debug)] +struct Inner { + rx_waker: Option, + closed: bool, + item: Option, +} + +impl Inner { + /// Creates a unchecked mutable reference from a mutable reference. + /// + /// SAFETY: You can only use this when: + /// + /// 1. The mutable reference is released at the end of a function call. + /// 2. No parent function has acquired the mutable reference. + /// 3. The caller is not an async function / the mutable reference is released before an await + /// statement. + #[inline] + unsafe fn get_mut_unchecked(&self) -> *mut Self { + self as *const Self as *mut Self + } +} + +/// The receiver of a oneshot channel. +#[derive(Debug)] +pub struct Receiver { + inner: Rc>, +} + +impl Future for Receiver { + type Output = Option; + + fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + // Implementation Note: + // + // It might be neater to use a match pattern here. + // However, this will slow down the polling process by 10%. + + if let Some(m) = inner.item.take() { + return Poll::Ready(Some(m)); + } + + if inner.closed { + return Poll::Ready(None); + } + + inner.rx_waker = Some(cx.waker().clone()); + Poll::Pending + } +} + +/// The sender of a oneshot channel. +#[derive(Debug)] +pub struct Sender { + inner: Rc>, +} + +impl Sender { + /// Send an item to the other side of the channel, consumes the sender. + pub fn send(self, item: T) -> Result<(), T> { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + if inner.closed { + return Err(item); + } + + inner.item = Some(item); + + if let Some(ref m) = inner.rx_waker { + m.wake_by_ref(); + } + + Ok(()) + } +} + +impl Drop for Sender { + fn drop(&mut self) { + let inner = unsafe { &mut *self.inner.get_mut_unchecked() }; + + inner.closed = true; + + if inner.item.is_none() { + if let Some(ref m) = inner.rx_waker { + m.wake_by_ref(); + } + } + } +} + +/// Creates a oneshot channel. +pub fn channel() -> (Sender, Receiver) { + let inner = Rc::new(Inner { + rx_waker: None, + closed: false, + item: None, + }); + + ( + Sender { + inner: inner.clone(), + }, + Receiver { inner }, + ) +} diff --git a/packages/yew/src/platform/sync/mod.rs b/packages/yew/src/platform/sync/mod.rs deleted file mode 100644 index 63c99dec41a..00000000000 --- a/packages/yew/src/platform/sync/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! A module that provides task synchronisation primitives. - -#[doc(inline)] -pub use tokio::sync::oneshot; -pub mod mpsc; diff --git a/packages/yew/src/platform/sync/mpsc.rs b/packages/yew/src/platform/sync/mpsc.rs deleted file mode 100644 index de09d342bc9..00000000000 --- a/packages/yew/src/platform/sync/mpsc.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! A multi-producer, single-receiver channel. - -#[doc(inline)] -pub use tokio::sync::mpsc::*; -#[doc(inline)] -pub use tokio_stream::wrappers::{ReceiverStream, UnboundedReceiverStream}; diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index 913f10d9a2c..b42320ce022 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -3,7 +3,7 @@ use std::fmt; use futures::stream::{Stream, StreamExt}; use crate::html::{BaseComponent, Scope}; -use crate::platform::io::{self, DEFAULT_BUF_SIZE}; +use crate::platform::fmt::{BufWriter, DEFAULT_BUF_SIZE}; use crate::platform::{spawn_local, Runtime}; /// A Yew Server-side Renderer that renders on the current thread. @@ -102,7 +102,9 @@ where /// Renders Yew Applications into a string Stream pub fn render_stream(self) -> impl Stream { - let (mut w, r) = io::buffer(self.capacity); + let (tx, rx) = crate::platform::pinned::mpsc::unbounded(); + + let mut w = BufWriter::new(tx, self.capacity); let scope = Scope::::new(None); spawn_local(async move { @@ -111,7 +113,7 @@ where .await; }); - r + rx } } @@ -245,15 +247,22 @@ where let rt = rt.unwrap_or_default(); + let (tx, rx) = futures::channel::mpsc::unbounded(); + let mut w = BufWriter::new(tx, capacity); + // We use run_pinned to switch to our runtime. rt.run_pinned(move || async move { let props = create_props(); + let scope = Scope::::new(None); - LocalServerRenderer::::with_props(props) - .hydratable(hydratable) - .capacity(capacity) - .render_stream() + spawn_local(async move { + scope + .render_into_stream(&mut w, props.into(), hydratable) + .await; + }); }) - .await + .await; + + rx } } diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index e7d9797b18c..99085f73bea 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -112,10 +112,10 @@ pub(crate) use feat_ssr_hydration::*; #[cfg(feature = "ssr")] mod feat_ssr { use super::*; - use crate::platform::io::BufWriter; + use crate::platform::fmt::BufWrite; impl Collectable { - pub(crate) fn write_open_tag(&self, w: &mut BufWriter) { + pub(crate) fn write_open_tag(&self, w: &mut dyn BufWrite) { w.write("".into()); } - pub(crate) fn write_close_tag(&self, w: &mut BufWriter) { + pub(crate) fn write_close_tag(&self, w: &mut dyn BufWrite) { w.write("