Skip to content

Commit

Permalink
feat(tachys): optimize AnyView + AddAnyAttr to reduce compile/link times
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
geoffreygarrett committed Jan 17, 2025
1 parent 7fc0c41 commit 19f80ef
Showing 1 changed file with 81 additions and 124 deletions.
205 changes: 81 additions & 124 deletions tachys/src/view/any_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use super::{
use crate::{
html::attribute::{
any_attribute::{AnyAttribute, IntoAnyAttribute},
Attribute, NextAttribute,
Attribute,
},
hydration::Cursor,
ssr::StreamBuilder,
Expand All @@ -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<Self>, attr: AnyAttribute) -> AnyView;
}

trait DynValue: Send {
fn dyn_add_any_attr(self: Box<Self>, attr: AnyAttribute) -> AnyView;

fn dyn_any(self: Box<Self>) -> Box<dyn Any + Send>;
}

impl<T> DynValueAttr for T
where
T: Send + RenderHtml + 'static,
T::State: 'static,
{
fn apply_attr(self: Box<Self>, attr: AnyAttribute) -> AnyView {
self.add_any_attr(attr).into_any()
}
}

impl<T> DynValue for T
where
T: Send,
T: RenderHtml + 'static,
T::State: 'static,
{
#[inline(never)]
fn dyn_add_any_attr(self: Box<Self>, attr: AnyAttribute) -> AnyView {
DynValueAttr::apply_attr(self, attr)
}

fn dyn_any(self: Box<Self>) -> Box<dyn Any + Send> {
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.
Expand All @@ -31,10 +68,9 @@ use std::{future::Future, pin::Pin};
/// amount of type information possible.
pub struct AnyView {
type_id: TypeId,
value: Box<dyn Any + Send>,
value: Box<dyn DynValue>,
build: fn(Box<dyn Any>) -> AnyViewState,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
add_any_attr: fn(Box<dyn Any>, 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
Expand All @@ -51,7 +87,7 @@ pub struct AnyView {
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
#[cfg(feature = "ssr")]
#[allow(clippy::type_complexity)]
resolve: fn(Box<dyn Any>) -> Pin<Box<dyn Future<Output=AnyView> + Send>>,
resolve: fn(Box<dyn Any>) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
#[cfg(feature = "ssr")]
dry_resolve: fn(&mut Box<dyn Any + Send>),

Check warning on line 92 in tachys/src/view/any_view.rs

View workflow job for this annotation

GitHub Actions / autofix

field `dry_resolve` is never read
#[cfg(feature = "hydrate")]
Expand Down Expand Up @@ -160,7 +196,7 @@ where
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any() })
as Pin<Box<dyn Future<Output=AnyView> + Send>>
as Pin<Box<dyn Future<Output = AnyView> + Send>>
};
#[cfg(feature = "ssr")]
let to_html =
Expand Down Expand Up @@ -287,19 +323,15 @@ where
}
};

let add_any_attr = |value: Box<dyn Any>, attr: AnyAttribute| {
let value = value
.downcast::<T>()
.expect("AnyView::add_any_attr could not be downcast");
value.add_any_attr(attr).into_any()
};
let value = value
.downcast::<T>()
.expect("AnyView::into_any couldn't downcast value");

AnyView {
type_id: TypeId::of::<T>(),
value,
build,
rebuild,
add_any_attr,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
Expand All @@ -324,37 +356,42 @@ 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<SomeNewAttr: Attribute> = AttrStaged;
type Output<SomeNewAttr: Attribute> = AnyView;

#[inline(never)]
fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr> {
AttrStaged {
attr: attr.into_cloneable_owned().into_any_attr(),
view: self,
}
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
let attr = attr.into_cloneable_owned();
let any_attr = attr.into_any_attr();
self.value.dyn_add_any_attr(any_attr)
}
}

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."
Expand All @@ -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!(
Expand All @@ -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;
Expand All @@ -409,15 +452,15 @@ 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,
mark_branches,
);
} else {
(self.to_html_async)(
self.value,
self.value.dyn_any(),
buf,
position,
escape,
Expand All @@ -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 \
Expand All @@ -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<SomeNewAttr: Attribute> = AttrStaged;

fn add_any_attr<NewAttr: Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: 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<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
) {
self.apply().to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
mark_branches,
)
}

fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
self.apply().hydrate::<FROM_SERVER>(cursor, position)
#[cfg(feature = "ssr")]
{
self.html_len
}
#[cfg(not(feature = "ssr"))]
{
0
}
}
}

Expand Down

0 comments on commit 19f80ef

Please sign in to comment.