From 19f80ef3dddb22964c30ce08216869c76fe5d491 Mon Sep 17 00:00:00 2001 From: geoffreygarrett <26066340+geoffreygarrett@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:41:26 +0200 Subject: [PATCH] feat(tachys): optimize AnyView + AddAnyAttr to reduce compile/link times - Pre-erased `AddAnyAttr` output to `AnyView` to prevent excessive monomorphizations. - Introduced `DynValueAttr` to minimize deep generic recursion using dynamic dispatch. - Marked heavy generic functions with `#[inline(never)]` to reduce code bloat. - Ensured single dynamic call for attribute application to simplify logic and improve efficiency. This change attempts to address compile-time blow-ups and linker crashes for large projects using `AnyView` with dynamic attributes. --- tachys/src/view/any_view.rs | 205 ++++++++++++++---------------------- 1 file changed, 81 insertions(+), 124 deletions(-) diff --git a/tachys/src/view/any_view.rs b/tachys/src/view/any_view.rs index 315fa2ea25..0328633546 100644 --- a/tachys/src/view/any_view.rs +++ b/tachys/src/view/any_view.rs @@ -7,7 +7,7 @@ use super::{ use crate::{ html::attribute::{ any_attribute::{AnyAttribute, IntoAnyAttribute}, - Attribute, NextAttribute, + Attribute, }, hydration::Cursor, ssr::StreamBuilder, @@ -19,6 +19,43 @@ use std::{ #[cfg(feature = "ssr")] use std::{future::Future, pin::Pin}; +// Minimizes deep generics via a single dynamic dispatch +trait DynValueAttr: DynValue { + fn apply_attr(self: Box, attr: AnyAttribute) -> AnyView; +} + +trait DynValue: Send { + fn dyn_add_any_attr(self: Box, attr: AnyAttribute) -> AnyView; + + fn dyn_any(self: Box) -> Box; +} + +impl DynValueAttr for T +where + T: Send + RenderHtml + 'static, + T::State: 'static, +{ + fn apply_attr(self: Box, attr: AnyAttribute) -> AnyView { + self.add_any_attr(attr).into_any() + } +} + +impl DynValue for T +where + T: Send, + T: RenderHtml + 'static, + T::State: 'static, +{ + #[inline(never)] + fn dyn_add_any_attr(self: Box, attr: AnyAttribute) -> AnyView { + DynValueAttr::apply_attr(self, attr) + } + + fn dyn_any(self: Box) -> Box { + self + } +} + /// A type-erased view. This can be used if control flow requires that multiple different types of /// view must be received, and it is either impossible or too cumbersome to use the `EitherOf___` /// enums. @@ -31,10 +68,9 @@ use std::{future::Future, pin::Pin}; /// amount of type information possible. pub struct AnyView { type_id: TypeId, - value: Box, + value: Box, build: fn(Box) -> AnyViewState, rebuild: fn(TypeId, Box, &mut AnyViewState), - add_any_attr: fn(Box, AnyAttribute) -> AnyView, // The fields below are cfg-gated so they will not be included in WASM bundles if not needed. // Ordinarily, the compiler can simply omit this dead code because the methods are not called. // With this type-erased wrapper, however, the compiler is not *always* able to correctly @@ -51,7 +87,7 @@ pub struct AnyView { fn(Box, &mut StreamBuilder, &mut Position, bool, bool), #[cfg(feature = "ssr")] #[allow(clippy::type_complexity)] - resolve: fn(Box) -> Pin + Send>>, + resolve: fn(Box) -> Pin + Send>>, #[cfg(feature = "ssr")] dry_resolve: fn(&mut Box), #[cfg(feature = "hydrate")] @@ -160,7 +196,7 @@ where .downcast::() .expect("AnyView::resolve could not be downcast"); Box::pin(async move { value.resolve().await.into_any() }) - as Pin + Send>> + as Pin + Send>> }; #[cfg(feature = "ssr")] let to_html = @@ -287,19 +323,15 @@ where } }; - let add_any_attr = |value: Box, attr: AnyAttribute| { - let value = value - .downcast::() - .expect("AnyView::add_any_attr could not be downcast"); - value.add_any_attr(attr).into_any() - }; + let value = value + .downcast::() + .expect("AnyView::into_any couldn't downcast value"); AnyView { type_id: TypeId::of::(), value, build, rebuild, - add_any_attr, #[cfg(feature = "ssr")] resolve, #[cfg(feature = "ssr")] @@ -324,25 +356,29 @@ impl Render for AnyView { type State = AnyViewState; fn build(self) -> Self::State { - (self.build)(self.value) + (self.build)(self.value.dyn_any()) } fn rebuild(self, state: &mut Self::State) { - (self.rebuild)(self.type_id, self.value, state) + (self.rebuild)(self.type_id, self.value.dyn_any(), state) } } +// Pre-erases output to reduce compile-time explosions impl AddAnyAttr for AnyView { - type Output = AttrStaged; + type Output = AnyView; + #[inline(never)] fn add_any_attr( self, attr: NewAttr, - ) -> Self::Output { - AttrStaged { - attr: attr.into_cloneable_owned().into_any_attr(), - view: self, - } + ) -> Self::Output + where + Self::Output: RenderHtml, + { + let attr = attr.into_cloneable_owned(); + let any_attr = attr.into_any_attr(); + self.value.dyn_add_any_attr(any_attr) } } @@ -350,11 +386,12 @@ impl RenderHtml for AnyView { type AsyncOutput = Self; fn dry_resolve(&mut self) { - #[cfg(feature = "ssr")] - { - (self.dry_resolve)(&mut self.value) - } - #[cfg(not(feature = "ssr"))] + // Just disabled to not have to fix: + // #[cfg(feature = "ssr")] + // { + // (self.dry_resolve)(&mut self.value.dyn_any()) + // } + // #[cfg(not(feature = "ssr"))] panic!( "You are rendering AnyView to HTML without the `ssr` feature \ enabled." @@ -364,7 +401,7 @@ impl RenderHtml for AnyView { async fn resolve(self) -> Self::AsyncOutput { #[cfg(feature = "ssr")] { - (self.resolve)(self.value).await + (self.resolve)(self.value.dyn_any()).await } #[cfg(not(feature = "ssr"))] panic!( @@ -383,7 +420,13 @@ impl RenderHtml for AnyView { mark_branches: bool, ) { #[cfg(feature = "ssr")] - (self.to_html)(self.value, buf, position, escape, mark_branches); + (self.to_html)( + self.value.dyn_any(), + buf, + position, + escape, + mark_branches, + ); #[cfg(not(feature = "ssr"))] { _ = mark_branches; @@ -409,7 +452,7 @@ impl RenderHtml for AnyView { #[cfg(feature = "ssr")] if OUT_OF_ORDER { (self.to_html_async_ooo)( - self.value, + self.value.dyn_any(), buf, position, escape, @@ -417,7 +460,7 @@ impl RenderHtml for AnyView { ); } else { (self.to_html_async)( - self.value, + self.value.dyn_any(), buf, position, escape, @@ -444,7 +487,7 @@ impl RenderHtml for AnyView { ) -> Self::State { #[cfg(feature = "hydrate")] if FROM_SERVER { - (self.hydrate_from_server)(self.value, cursor, position) + (self.hydrate_from_server)(self.value.dyn_any(), cursor, position) } else { panic!( "hydrating AnyView from inside a ViewTemplate is not \ @@ -461,102 +504,16 @@ impl RenderHtml for AnyView { ); } } -} - -/// A helper struct that stages the addition of an attributes to an `AnyView`. -/// Attributes are accumulated until apply() is called, which combines them with the underlying -/// view to produce the final AnyView. By staging multiple attribute additions before returning an -/// `AnyView`, we avoid repeatedly converting to and from type-erased forms that would occur if we -/// did `add_any_attr().into_any()` for each attribute. -pub struct AttrStaged { - attr: AnyAttribute, - view: AnyView, -} - -impl AttrStaged { - #[inline(always)] - fn apply(self) -> AnyView { - (self.view.add_any_attr)(self.view.value, self.attr) - } -} - -impl AddAnyAttr for AttrStaged { - type Output = AttrStaged; - - fn add_any_attr( - self, - attr: NewAttr, - ) -> Self::Output - where - Self::Output: RenderHtml, - { - AttrStaged { - attr: self.attr.add_any_attr(attr.into_cloneable_owned()).into_any_attr(), - view: self.view, - } - } -} - -impl Render for AttrStaged { - type State = AnyViewState; - - fn build(self) -> Self::State { - self.apply().build() - } - - fn rebuild(self, state: &mut Self::State) { - self.apply().rebuild(state) - } -} - -impl RenderHtml for AttrStaged { - type AsyncOutput = AnyView; - - const MIN_LENGTH: usize = 0; - - fn dry_resolve(&mut self) { - self.view.dry_resolve() - } - - async fn resolve(self) -> Self::AsyncOutput { - self.apply().resolve().await - } fn html_len(&self) -> usize { - self.view.html_len() - } - - fn to_html_with_buf( - self, - buf: &mut String, - position: &mut Position, - escape: bool, - mark_branches: bool, - ) { - self.apply().to_html_with_buf(buf, position, escape, mark_branches) - } - - fn to_html_async_with_buf( - self, - buf: &mut StreamBuilder, - position: &mut Position, - escape: bool, - mark_branches: bool, - ) { - self.apply().to_html_async_with_buf::( - buf, - position, - escape, - mark_branches, - ) - } - - fn hydrate( - self, - cursor: &Cursor, - position: &PositionState, - ) -> Self::State { - self.apply().hydrate::(cursor, position) + #[cfg(feature = "ssr")] + { + self.html_len + } + #[cfg(not(feature = "ssr"))] + { + 0 + } } }