feat: support cached ssr

This commit is contained in:
Jonathan Kelley 2022-11-02 01:00:37 -07:00
parent 4a31b29703
commit b6c0bce89c
11 changed files with 433 additions and 178 deletions

View file

@ -3,6 +3,7 @@ use crate::mutations::Mutation::*;
use crate::nodes::VNode;
use crate::nodes::{DynamicNode, DynamicNodeKind, TemplateNode};
use crate::virtualdom::VirtualDom;
use crate::TemplateAttribute;
impl VirtualDom {
/// Create this template and write its mutations
@ -112,10 +113,11 @@ impl VirtualDom {
id,
});
mutations.extend(attrs.into_iter().map(|attr| SetAttribute {
name: attr.name,
value: attr.value,
id,
mutations.extend(attrs.into_iter().filter_map(|attr| match attr {
TemplateAttribute::Static { name, value, .. } => {
Some(SetAttribute { name, value, id })
}
_ => None,
}));
children

View file

@ -1,19 +1,36 @@
use std::fmt::Arguments;
use std::{cell::Cell, fmt::Arguments};
use crate::{innerlude::DynamicNode, LazyNodes, ScopeState, VNode};
use crate::{
arena::ElementId,
innerlude::{DynamicNode, DynamicNodeKind},
LazyNodes, ScopeState, VNode,
};
impl ScopeState {
/// Create some text that's allocated along with the other vnodes
///
pub fn text(&self, args: Arguments) -> DynamicNode {
// let (text, _is_static) = self.raw_text(args);
pub fn text<'a>(&'a self, args: Arguments) -> DynamicNode<'a> {
let (text, _) = self.raw_text(args);
// VNode::Text(self.bump.alloc(VText {
// text,
// id: Default::default(),
// }))
DynamicNode {
kind: DynamicNodeKind::Text {
id: Cell::new(ElementId(0)),
value: text,
},
path: &[0],
}
}
todo!()
pub fn raw_text<'a>(&'a self, args: Arguments) -> (&'a str, bool) {
match args.as_str() {
Some(static_str) => (static_str, true),
None => {
use bumpalo::core_alloc::fmt::Write;
let mut str_buf = bumpalo::collections::String::new_in(self.bump());
str_buf.write_fmt(args).unwrap();
(str_buf.into_bump_str(), false)
}
}
}
pub fn fragment_from_iter<'a, I, F: IntoVnode<'a, I>>(

View file

@ -69,6 +69,8 @@ pub use crate::innerlude::{
// AnyAttributeValue, AnyEvent, Attribute, AttributeValue, Component, Element, ElementId,
Attribute,
AttributeValue,
DynamicNode,
DynamicNodeKind,
Element,
EventPriority,
LazyNodes,
@ -94,8 +96,9 @@ pub use crate::innerlude::{
/// This includes types like [`Scope`], [`Element`], and [`Component`].
pub mod prelude {
pub use crate::innerlude::{
Attribute, Element, EventPriority, LazyNodes, Listener, NodeFactory, Scope, ScopeId,
ScopeState, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent, VNode, VirtualDom,
Attribute, DynamicNode, DynamicNodeKind, Element, EventPriority, LazyNodes, Listener,
NodeFactory, Scope, ScopeId, ScopeState, TaskId, Template, TemplateAttribute, TemplateNode,
UiEvent, VNode, VirtualDom,
};
}

View file

@ -34,8 +34,8 @@ pub enum Mutation<'a> {
},
ReplacePlaceholder {
path: &'static [u8],
m: usize,
path: &'static [u8],
},
AssignId {

View file

@ -1,9 +1,5 @@
use crate::{any_props::AnyProps, arena::ElementId};
use std::{
any::{Any, TypeId},
cell::Cell,
num::NonZeroUsize,
};
use std::{any::Any, cell::Cell, hash::Hasher};
pub type TemplateId = &'static str;
@ -15,7 +11,7 @@ pub struct VNode<'a> {
// When rendered, this template will be linked to its parent
pub parent: Option<(*mut VNode<'static>, usize)>,
pub template: Template,
pub template: Template<'static>,
pub root_ids: &'a [Cell<ElementId>],
@ -26,15 +22,38 @@ pub struct VNode<'a> {
}
#[derive(Debug, Clone, Copy)]
pub struct Template {
pub id: &'static str,
pub roots: &'static [TemplateNode<'static>],
pub struct Template<'a> {
pub id: &'a str,
pub roots: &'a [TemplateNode<'a>],
}
impl<'a> std::hash::Hash for Template<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl PartialEq for Template<'_> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Template<'_> {}
impl PartialOrd for Template<'_> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.id.partial_cmp(other.id)
}
}
impl Ord for Template<'_> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(other.id)
}
}
/// A weird-ish variant of VNodes with way more limited types
#[derive(Debug, Clone, Copy)]
pub enum TemplateNode<'a> {
/// A simple element
Element {
tag: &'a str,
namespace: Option<&'a str>,
@ -62,7 +81,7 @@ pub enum DynamicNodeKind<'a> {
// Comes in with string interpolation or from format_args, include_str, etc
Text {
id: Cell<ElementId>,
value: &'static str,
value: &'a str,
},
// Anything that's coming in as an iterator
@ -72,11 +91,17 @@ pub enum DynamicNodeKind<'a> {
}
#[derive(Debug)]
pub struct TemplateAttribute<'a> {
pub name: &'static str,
pub value: &'a str,
pub namespace: Option<&'static str>,
pub volatile: bool,
pub enum TemplateAttribute<'a> {
Static {
name: &'static str,
value: &'a str,
namespace: Option<&'static str>,
volatile: bool,
},
Dynamic {
name: &'static str,
index: usize,
},
}
pub struct AttributeLocation<'a> {

View file

@ -15,7 +15,7 @@ use slab::Slab;
use std::collections::{BTreeSet, HashMap};
pub struct VirtualDom {
pub(crate) templates: HashMap<TemplateId, Template>,
pub(crate) templates: HashMap<TemplateId, Template<'static>>,
pub(crate) elements: Slab<ElementPath>,
pub(crate) scopes: Slab<ScopeState>,
pub(crate) scope_stack: Vec<ScopeId>,
@ -82,6 +82,14 @@ impl VirtualDom {
///
/// This is cancel safe, so if the future is dropped, you can push events into the virtualdom
pub async fn wait_for_work(&mut self) {}
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
self.scopes.get(id.0)
}
pub fn base_scope(&self) -> &ScopeState {
self.scopes.get(0).unwrap()
}
}
impl Drop for VirtualDom {

View file

@ -30,6 +30,9 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
fn basic_template(cx: Scope) -> Element {
cx.render(rsx! {
div {
(0..2).map(|i| rsx! {
div { "asd" }
}),
(0..2).map(|i| rsx! {
div { "asd" }
})
@ -42,7 +45,10 @@ fn basic_prints() {
let mut dom = VirtualDom::new(basic_template);
let mut edits = Vec::new();
dom.rebuild(&mut edits);
dbg!(edits);
let mut edits = Vec::new();
dom.rebuild(&mut edits);
dbg!(edits);

View file

@ -21,6 +21,8 @@ mod ifmt;
mod node;
mod template;
use std::collections::HashMap;
// Re-export the namespaces into each other
pub use component::*;
pub use element::*;
@ -39,6 +41,9 @@ use syn::{
#[derive(Default)]
pub struct CallBody {
pub roots: Vec<BodyNode>,
// set this after
pub inline_cx: bool,
}
impl Parse for CallBody {
@ -55,7 +60,10 @@ impl Parse for CallBody {
roots.push(node);
}
Ok(Self { roots })
Ok(Self {
roots,
inline_cx: false,
})
}
}
@ -66,14 +74,20 @@ impl ToTokens for CallBody {
// We'll use the size of the vecs to determine the index of the dynamic node in the final
struct DynamicContext<'a> {
dynamic_nodes: Vec<&'a BodyNode>,
dynamic_attributes: Vec<&'a ElementAttrNamed>,
dynamic_listeners: Vec<&'a ElementAttrNamed>,
dynamic_attributes: HashMap<Vec<u8>, AttrLocation<'a>>,
current_path: Vec<u8>,
}
#[derive(Default)]
struct AttrLocation<'a> {
attrs: Vec<&'a ElementAttrNamed>,
listeners: Vec<&'a ElementAttrNamed>,
}
let mut context = DynamicContext {
dynamic_nodes: vec![],
dynamic_attributes: vec![],
dynamic_listeners: vec![],
dynamic_attributes: HashMap::new(),
current_path: vec![],
};
fn render_static_node<'a>(root: &'a BodyNode, cx: &mut DynamicContext<'a>) -> TokenStream2 {
@ -81,10 +95,14 @@ impl ToTokens for CallBody {
BodyNode::Element(el) => {
let el_name = &el.name;
let children = {
let children = el.children.iter().map(|root| render_static_node(root, cx));
quote! { #(#children),* }
};
let children = el.children.iter().enumerate().map(|(idx, root)| {
cx.current_path.push(idx as u8);
let out = render_static_node(root, cx);
cx.current_path.pop();
out
});
let children = quote! { #(#children),* };
let attrs = el.attributes.iter().filter_map(|attr| {
//
@ -92,7 +110,7 @@ impl ToTokens for CallBody {
ElementAttr::AttrText { name, value } if value.is_static() => {
let value = value.source.as_ref().unwrap();
Some(quote! {
::dioxus::core::TemplateAttribute {
::dioxus::core::TemplateAttribute::Static {
name: dioxus_elements::#el_name::#name.0,
namespace: dioxus_elements::#el_name::#name.1,
volatile: dioxus_elements::#el_name::#name.2,
@ -104,7 +122,7 @@ impl ToTokens for CallBody {
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
let value = value.source.as_ref().unwrap();
Some(quote! {
::dioxus::core::TemplateAttribute {
::dioxus::core::TemplateAttribute::Static {
name: dioxus_elements::#el_name::#name.0,
namespace: dioxus_elements::#el_name::#name.1,
volatile: dioxus_elements::#el_name::#name.2,
@ -117,16 +135,21 @@ impl ToTokens for CallBody {
| ElementAttr::AttrText { .. }
| ElementAttr::CustomAttrText { .. }
| ElementAttr::CustomAttrExpression { .. } => {
// todo!();
let ct = cx.dynamic_attributes.len();
cx.dynamic_attributes.push(attr);
// cx.dynamic_attributes.push(attr);
// quote! {}
None
// quote! { ::dioxus::core::TemplateAttribute::Dynamic(#ct) }
// None
Some(quote! { ::dioxus::core::TemplateAttribute::Dynamic {
name: "asd",
index: #ct
} })
}
ElementAttr::EventTokens { .. } => {
let ct = cx.dynamic_listeners.len();
cx.dynamic_listeners.push(attr);
// todo!();
// let ct = cx.dynamic_listeners.len();
// cx.dynamic_listeners.push(attr);
// quote! {}
None
}
@ -156,55 +179,46 @@ impl ToTokens for CallBody {
}
}
let root_printer = self
.roots
.iter()
.map(|root| render_static_node(root, &mut context));
let root_printer = self.roots.iter().enumerate().map(|(idx, root)| {
context.current_path.push(idx as u8);
let out = render_static_node(root, &mut context);
context.current_path.pop();
out
});
// Render and release the mutable borrow on context
let roots = quote! { #( #root_printer ),* };
let node_printer = &context.dynamic_nodes;
let attr_printer = context.dynamic_attributes.iter();
let listener_printer = context.dynamic_listeners.iter();
out_tokens.append_all(quote! {
// LazyNodes::new(move | __cx: ::dioxus::core::NodeFactory| -> ::dioxus::core::VNode {
// __cx.template_ref(
// ::dioxus::core::Template {
// id: ::dioxus::core::get_line_num!(),
// roots: &[ #roots ]
// },
// __cx.bump().alloc([
// #( #node_printer ),*
// ]),
// __cx.bump().alloc([
// #( #attr_printer ),*
// ]),
// __cx.bump().alloc([
// #( #listener_printer ),*
// ]),
// None
// )
// })
let body = quote! {
static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template {
id: ::dioxus::core::get_line_num!(),
roots: &[ #roots ]
};
::dioxus::core::VNode {
node_id: Default::default(),
parent: None,
template: TEMPLATE,
root_ids: __cx.bump().alloc([]),
dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
dynamic_attrs: __cx.bump().alloc([]),
}
};
::dioxus::core::LazyNodes::new( move | __cx: ::dioxus::core::NodeFactory| -> ::dioxus::core::VNode {
static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template {
id: ::dioxus::core::get_line_num!(),
roots: &[ #roots ]
};
::dioxus::core::VNode {
node_id: Default::default(),
parent: None,
template: TEMPLATE,
root_ids: __cx.bump().alloc([]),
dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
dynamic_attrs: __cx.bump().alloc([]),
if self.inline_cx {
out_tokens.append_all(quote! {
{
let __cx = cx;
#body
}
})
})
} else {
out_tokens.append_all(quote! {
::dioxus::core::LazyNodes::new( move | __cx: ::dioxus::core::NodeFactory| -> ::dioxus::core::VNode {
#body
})
})
}
}
}

View file

@ -1,9 +1,10 @@
#![doc = include_str!("../README.md")]
use std::fmt::{Display, Formatter, Write};
mod template;
use dioxus_core::exports::bumpalo;
use dioxus_core::IntoVNode;
use dioxus_core::*;
fn app(_cx: Scope) -> Element {
@ -25,9 +26,7 @@ impl SsrRenderer {
pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String {
let scope = self.vdom.base_scope();
let factory = NodeFactory::new(scope);
let root = f.into_vnode(factory);
let root = f.call(scope);
format!(
"{:}",
TextRenderer {
@ -55,8 +54,7 @@ pub fn render_lazy<'a>(f: LazyNodes<'a, '_>) -> String {
// Therefore, we cast our local bump allocator to the right lifetime. This is okay because our usage of the bump
// arena is *definitely* shorter than the <'a> lifetime, and we return *owned* data - not borrowed data.
let scope = unsafe { &*scope };
let root = f.into_vnode(NodeFactory::new(scope));
let root = f.call(scope);
let vdom = Some(&vdom);
@ -148,99 +146,99 @@ impl<'a: 'c, 'c> TextRenderer<'a, '_, 'c> {
il: u16,
last_node_was_text: &mut bool,
) -> std::fmt::Result {
match &node {
VNode::Text(text) => {
if *last_node_was_text {
write!(f, "<!--spacer-->")?;
}
// match &node {
// VNode::Text(text) => {
// if *last_node_was_text {
// write!(f, "<!--spacer-->")?;
// }
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
}
}
// if self.cfg.indent {
// for _ in 0..il {
// write!(f, " ")?;
// }
// }
*last_node_was_text = true;
// *last_node_was_text = true;
write!(f, "{}", text.text)?
}
VNode::Element(el) => {
*last_node_was_text = false;
// write!(f, "{}", text.text)?
// }
// VNode::Element(el) => {
// *last_node_was_text = false;
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
}
}
// if self.cfg.indent {
// for _ in 0..il {
// write!(f, " ")?;
// }
// }
write!(f, "<{}", el.tag)?;
// write!(f, "<{}", el.tag)?;
let inner_html = render_attributes(el.attributes.iter(), f)?;
// let inner_html = render_attributes(el.attributes.iter(), f)?;
match self.cfg.newline {
true => writeln!(f, ">")?,
false => write!(f, ">")?,
}
// match self.cfg.newline {
// true => writeln!(f, ">")?,
// false => write!(f, ">")?,
// }
if let Some(inner_html) = inner_html {
write!(f, "{}", inner_html)?;
} else {
let mut last_node_was_text = false;
for child in el.children {
self.html_render(child, f, il + 1, &mut last_node_was_text)?;
}
}
// if let Some(inner_html) = inner_html {
// write!(f, "{}", inner_html)?;
// } else {
// let mut last_node_was_text = false;
// for child in el.children {
// self.html_render(child, f, il + 1, &mut last_node_was_text)?;
// }
// }
if self.cfg.newline {
writeln!(f)?;
}
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
}
}
// if self.cfg.newline {
// writeln!(f)?;
// }
// if self.cfg.indent {
// for _ in 0..il {
// write!(f, " ")?;
// }
// }
write!(f, "</{}>", el.tag)?;
if self.cfg.newline {
writeln!(f)?;
}
}
VNode::Fragment(frag) => match frag.children.len() {
0 => {
*last_node_was_text = false;
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
}
}
write!(f, "<!--placeholder-->")?;
}
_ => {
for child in frag.children {
self.html_render(child, f, il + 1, last_node_was_text)?;
}
}
},
VNode::Component(vcomp) => {
let idx = vcomp.scope.get().unwrap();
// write!(f, "</{}>", el.tag)?;
// if self.cfg.newline {
// writeln!(f)?;
// }
// }
// VNode::Fragment(frag) => match frag.children.len() {
// 0 => {
// *last_node_was_text = false;
// if self.cfg.indent {
// for _ in 0..il {
// write!(f, " ")?;
// }
// }
// write!(f, "<!--placeholder-->")?;
// }
// _ => {
// for child in frag.children {
// self.html_render(child, f, il + 1, last_node_was_text)?;
// }
// }
// },
// VNode::Component(vcomp) => {
// let idx = vcomp.scope.get().unwrap();
if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
let new_node = vdom.get_scope(idx).unwrap().root_node();
self.html_render(new_node, f, il + 1, last_node_was_text)?;
} else {
}
}
VNode::Template(t) => {
if let Some(vdom) = self.vdom {
todo!()
} else {
panic!("Cannot render template without vdom");
}
}
VNode::Placeholder(_) => {
todo!()
}
}
// if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
// let new_node = vdom.get_scope(idx).unwrap().root_node();
// self.html_render(new_node, f, il + 1, last_node_was_text)?;
// } else {
// }
// }
// VNode::Template(t) => {
// if let Some(vdom) = self.vdom {
// todo!()
// } else {
// panic!("Cannot render template without vdom");
// }
// }
// VNode::Placeholder(_) => {
// todo!()
// }
// }
Ok(())
}

View file

@ -0,0 +1,182 @@
use dioxus_core::prelude::*;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::rc::Rc;
/// A virtualdom renderer that caches the templates it has seen for faster rendering
#[derive(Default)]
pub struct SsrRender {
template_cache: RefCell<HashMap<Template<'static>, Rc<StringCache>>>,
}
struct StringCache {
segments: Vec<Segment>,
}
#[derive(Default)]
struct StringChain {
segments: Vec<Segment>,
}
#[derive(Debug, Clone)]
enum Segment {
Attr(usize),
Dyn(usize),
PreRendered(String),
}
impl std::fmt::Write for StringChain {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
match self.segments.last_mut() {
Some(Segment::PreRendered(s2)) => s2.push_str(s),
_ => self.segments.push(Segment::PreRendered(s.to_string())),
}
Ok(())
}
}
impl StringCache {
fn from_template(template: &VNode) -> Result<Self, std::fmt::Error> {
let mut chain = StringChain::default();
let mut cur_path = vec![];
for (root_idx, root) in template.template.roots.iter().enumerate() {
Self::recurse(root, &mut cur_path, root_idx, &mut chain)?;
}
Ok(Self {
segments: chain.segments,
})
}
fn recurse(
root: &TemplateNode,
cur_path: &mut Vec<usize>,
root_idx: usize,
chain: &mut StringChain,
) -> Result<(), std::fmt::Error> {
match root {
TemplateNode::Element {
tag,
attrs,
children,
..
} => {
cur_path.push(root_idx);
write!(chain, "<{}", tag)?;
for attr in *attrs {
match attr {
TemplateAttribute::Static { name, value, .. } => {
write!(chain, " {}=\"{}\"", name, value)?;
}
TemplateAttribute::Dynamic { index, .. } => {
chain.segments.push(Segment::Attr(*index))
}
}
}
if children.len() == 0 {
write!(chain, "/>")?;
} else {
write!(chain, ">")?;
for child in *children {
Self::recurse(child, cur_path, root_idx, chain)?;
}
write!(chain, "</{}>", tag)?;
}
cur_path.pop();
}
TemplateNode::Text(text) => write!(chain, "{}", text)?,
TemplateNode::Dynamic(idx) => chain.segments.push(Segment::Dyn(*idx)),
TemplateNode::DynamicText(idx) => chain.segments.push(Segment::Dyn(*idx)),
}
Ok(())
}
}
impl SsrRender {
fn render_vdom(&mut self, dom: &VirtualDom) -> String {
let scope = dom.base_scope();
let root = scope.root_node();
let mut out = String::new();
self.render_template(&mut out, root).unwrap();
out
}
fn render_template(&self, buf: &mut String, template: &VNode) -> std::fmt::Result {
let entry = self
.template_cache
.borrow_mut()
.entry(template.template)
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
.clone();
for segment in entry.segments.iter() {
match segment {
Segment::Attr(idx) => {
todo!("properly implement attrs in the macro");
// let loc = &template.dynamic_attrs[*idx];
// for attr in loc.attrs.iter() {
// write!(buf, " {}=\"{}\"", attr.name, attr.value)?;
// }
}
Segment::Dyn(idx) => match &template.dynamic_nodes[*idx].kind {
DynamicNodeKind::Text { value, .. } => write!(buf, "{}", value)?,
DynamicNodeKind::Fragment { children } => {
for child in *children {
self.render_template(buf, child)?;
}
//
}
DynamicNodeKind::Component { .. } => {
//
}
},
Segment::PreRendered(text) => buf.push_str(&text),
}
}
Ok(())
}
}
#[test]
fn to_string_works() {
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
let dynamic = 123;
cx.render(rsx! {
div { class: "asdasdasd", class: "asdasdasd",
"Hello world 1 -->"
"{dynamic}"
"<-- Hello world 2"
div { "nest 1" }
div {}
div { "nest 2" }
(0..5).map(|i| rsx! {
div { "finalize {i}" }
})
}
})
}
let mut dom = VirtualDom::new(app);
let mut mutations = Vec::new();
dom.rebuild(&mut mutations);
let cache = StringCache::from_template(&dom.base_scope().root_node()).unwrap();
dbg!(cache.segments);
let mut renderer = SsrRender::default();
dbg!(renderer.render_vdom(&dom));
}

View file