on master help sign in
Merge pull request #10 from fitzgen/cached-rendering
e457eebNick Fitzgerald
examples/counter/crate/src/lib.rs+1−1
@@ -22,7 +22,7 @@ impl Counter {
22}
22}
23
23
24impl Render for Counter {
24impl Render for Counter {
25− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::node::Node<'bump>
25+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
26 where
26 where
27 'a: 'bump,
27 'a: 'bump,
28 {
28 {
examples/hello-world/crate/src/lib.rs+1−1
@@ -11,7 +11,7 @@ impl<'who> HelloWorld<'who> {
11}
11}
12
12
13impl<'who> Render for HelloWorld<'who> {
13impl<'who> Render for HelloWorld<'who> {
14− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::node::Node<'bump>
14+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
15 where
15 where
16 'a: 'bump,
16 'a: 'bump,
17 {
17 {
examples/input-form/crate/src/lib.rs+1−1
@@ -19,7 +19,7 @@ impl SayHelloTo {
19}
19}
20
20
21impl Render for SayHelloTo {
21impl Render for SayHelloTo {
22− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::node::Node<'bump>
22+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
23 where
23 where
24 'a: 'bump,
24 'a: 'bump,
25 {
25 {
src/cached.rsnew file+161
@@ -0,0 +1,161 @@
1+use crate::{Node, Render};
2+use bumpalo::Bump;
3+use std::cell::RefCell;
4+use std::mem;
5+use std::ops::{Deref, DerefMut};
6+
7+// TODO: This implementation will only cache the rendering (generation of the
8+// virtual DOM) but not the diffing of the cached subtree. We could skip diffing
9+// for cached tree by adding `fn is_cached(&self) -> bool` to `Node` that we can
10+// check during diffing. This comes at the cost of bloating `Node`'s size (since
11+// we don't have any padding to sneak an extra `bool` field into). We should
12+// investigate whether it is worth adding this or not.
13+
14+/// A renderable that supports caching for when rendering is expensive but can
15+/// generate the same DOM tree.
16+pub struct Cached<R> {
17+ inner: R,
18+ bump: bumpalo::Bump,
19+ // Actually a self-reference into `self.bump`. Safe because we ensure that
20+ // whenever we hand out a cached node, we use a lifetime that cannot outlive
21+ // its owning `Cached<R>`.
22+ cached: RefCell<Option<Node<'static>>>,
23+}
24+
25+impl<R> Cached<R> {
26+ /// Construct a new `Cached<R>` of an inner `R`.
27+ ///
28+ /// # Example
29+ ///
30+ /// ```
31+ /// use dodrio::{Cached, Node, Render};
32+ ///
33+ /// pub struct Counter {
34+ /// count: u32,
35+ /// }
36+ ///
37+ /// impl Render for Counter {
38+ /// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
39+ /// where
40+ /// 'a: 'bump
41+ /// {
42+ /// // ...
43+ /// # unimplemented!()
44+ /// }
45+ /// }
46+ ///
47+ /// // Create a render-able counter.
48+ /// let counter = Counter { count: 0 };
49+ ///
50+ /// // And cache its rendering!
51+ /// let cached_counter = Cached::new(counter);
52+ /// ```
53+ #[inline]
54+ pub fn new(inner: R) -> Cached<R> {
55+ let bump = Bump::new();
56+ let cached = RefCell::new(None);
57+ Cached {
58+ inner,
59+ bump,
60+ cached,
61+ }
62+ }
63+
64+ /// Invalidate the cached rendering.
65+ ///
66+ /// This method should be called whenever the inner `R` must be re-rendered,
67+ /// and the cached `Node` from the last time `R::render` was invoked can no
68+ /// longer be re-used.
69+ ///
70+ /// # Example
71+ ///
72+ /// The `Cached<Hello>` component must have its cache invalidated whenever
73+ /// the `who` string is changed, or else the cached rendering whill keep
74+ /// displaying greetings to old `who`s.
75+ ///
76+ /// ```
77+ /// use dodrio::{Cached, Node, Render};
78+ ///
79+ /// /// A component that renders to "<p>Hello, {who}!</p>"
80+ /// pub struct Hello {
81+ /// who: String
82+ /// }
83+ ///
84+ /// impl Render for Hello {
85+ /// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
86+ /// where
87+ /// 'a: 'bump,
88+ /// {
89+ /// Node::element(bump, "p", [], [
90+ /// Node::text("Hello, "),
91+ /// Node::text(&self.who),
92+ /// Node::text("!"),
93+ /// ])
94+ /// }
95+ /// }
96+ ///
97+ /// /// Whenever a `Cached<Hello>`'s `who` is updated, we need to invalidate the
98+ /// /// cache so that we don't keep displaying greetings to old `who`s.
99+ /// pub fn set_who(hello: &mut Cached<Hello>, who: String) {
100+ /// hello.who = who;
101+ /// Cached::invalidate(hello);
102+ /// }
103+ /// ```
104+ #[inline]
105+ pub fn invalidate(cached: &mut Self) {
106+ *cached.cached.borrow_mut() = None;
107+ }
108+
109+ /// Convert a `Cached<R>` back into a plain `R`.
110+ #[inline]
111+ pub fn into_inner(cached: Cached<R>) -> R {
112+ cached.inner
113+ }
114+}
115+
116+impl<R> Deref for Cached<R> {
117+ type Target = R;
118+
119+ fn deref(&self) -> &R {
120+ &self.inner
121+ }
122+}
123+
124+impl<R> DerefMut for Cached<R> {
125+ fn deref_mut(&mut self) -> &mut R {
126+ &mut self.inner
127+ }
128+}
129+
130+unsafe fn extend_node_lifetime<'a>(node: Node<'a>) -> Node<'static> {
131+ mem::transmute(node)
132+}
133+
134+impl<R: Render> Render for Cached<R> {
135+ fn render<'a, 'bump>(&'a self, _: &'bump Bump) -> Node<'bump>
136+ where
137+ 'a: 'bump,
138+ {
139+ let mut cached = self.cached.borrow_mut();
140+
141+ if let Some(cached) = cached.as_ref() {
142+ // The cached node is actually a self-reference, so it has the `'a`
143+ // lifetime.
144+ let cached: Node<'a> = cached.clone();
145+
146+ // But the `'a` lifetime outlives `'bump`, so we can safely convert
147+ // it to the narrower `'bump` lifetime.
148+ let cached: Node<'bump> = cached;
149+
150+ // Return the cached rendering!
151+ return cached;
152+ }
153+
154+ // We don't have a cached node. Render into our own `Bump`, cache it for
155+ // future renders, and return it. Same lifetimes as above.
156+ let node: Node<'a> = self.inner.render(&self.bump);
157+ *cached = Some(unsafe { extend_node_lifetime(node.clone()) });
158+ let node: Node<'bump> = node;
159+ node
160+ }
161+}
src/lib.rs+8−2
@@ -1,9 +1,15 @@
1use bumpalo::Bump;
1use bumpalo::Bump;
2
2
3+// Only `pub` so that the wasm-bindgen bindings work.
4+#[doc(hidden)]
3pub mod change_list;
5pub mod change_list;
4−pub mod node;
5−pub mod vdom;
6
6
7+mod cached;
8+mod node;
9+mod vdom;
10+
11+// Re-export items at the top level.
12+pub use self::cached::Cached;
7pub use self::node::{Attribute, ElementNode, Node, TextNode};
13pub use self::node::{Attribute, ElementNode, Node, TextNode};
8pub use self::vdom::Vdom;
14pub use self::vdom::Vdom;
9
15
tests/web/cached.rsnew file+51
@@ -0,0 +1,51 @@
1+use super::{assert_rendered, create_element, RenderFn};
2+use bumpalo::Bump;
3+use dodrio::{Cached, Node, Render, Vdom};
4+use std::cell::Cell;
5+use wasm_bindgen_test::*;
6+
7+pub struct CountRenders {
8+ render_count: Cell<usize>,
9+}
10+
11+impl CountRenders {
12+ pub fn new() -> CountRenders {
13+ CountRenders {
14+ render_count: Cell::new(0),
15+ }
16+ }
17+}
18+
19+impl Render for CountRenders {
20+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
21+ where
22+ 'a: 'bump,
23+ {
24+ let count = self.render_count.get() + 1;
25+ self.render_count.set(count);
26+
27+ let s = bumpalo::format!(in bump, "{}", count);
28+ Node::text(s.into_bump_str())
29+ }
30+}
31+
32+#[wasm_bindgen_test]
33+fn uses_cached_render() {
34+ let container = create_element("div");
35+ let mut cached = Cached::new(CountRenders::new());
36+ let mut vdom = Vdom::new(&container, &cached);
37+
38+ for _ in 0..10 {
39+ vdom.render(&cached);
40+ assert_eq!(cached.render_count.get(), 1);
41+ assert_rendered(&container, &RenderFn(|_| Node::text("1")))
42+ }
43+
44+ Cached::invalidate(&mut cached);
45+
46+ for _ in 0..10 {
47+ vdom.render(&cached);
48+ assert_eq!(cached.render_count.get(), 2);
49+ assert_rendered(&container, &RenderFn(|_| Node::text("2")))
50+ }
51+}
tests/web/main.rsnew file+187
@@ -0,0 +1,187 @@
1+//! Test suite for the Web and headless browsers.
2+
3+#![cfg(target_arch = "wasm32")]
4+
5+use bumpalo::Bump;
6+use dodrio::{Attribute, Node, Render, Vdom};
7+use log::*;
8+use wasm_bindgen::JsCast;
9+use wasm_bindgen_test::*;
10+
11+wasm_bindgen_test_configure!(run_in_browser);
12+
13+pub mod cached;
14+pub mod render;
15+
16+pub fn window() -> web_sys::Window {
17+ web_sys::window().expect("no global `window` exists")
18+}
19+
20+pub fn document() -> web_sys::Document {
21+ window()
22+ .document()
23+ .expect("should have a document on window")
24+}
25+
26+pub fn create_element(tag: &str) -> web_sys::Element {
27+ document()
28+ .create_element(tag)
29+ .expect("should create element OK")
30+}
31+
32+/// Ensure that logs go to the devtools console.
33+pub fn init_logging() {
34+ use std::sync::Once;
35+ static START: Once = Once::new();
36+ START.call_once(|| {
37+ console_log::init_with_level(Level::Trace).expect("could not initialize console_log");
38+ });
39+}
40+
41+/// Assert that the `container` contains the physical DOM tree that matches
42+/// `r`'s rendered virtual DOM.
43+pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
44+ init_logging();
45+
46+ let bump = &Bump::new();
47+ let node = r.render(bump);
48+ let child = container
49+ .first_child()
50+ .expect("container does not have anything rendered into it?");
51+ check_node(&child, &node);
52+
53+ fn stringify_actual_node(n: &web_sys::Node) -> String {
54+ if let Some(el) = n.dyn_ref::<web_sys::Element>() {
55+ el.outer_html()
56+ } else {
57+ format!("#text({:?})", n.text_content())
58+ }
59+ }
60+
61+ fn check_node(actual: &web_sys::Node, expected: &Node) {
62+ debug!("check_render:");
63+ debug!(" actual = {}", stringify_actual_node(&actual));
64+ debug!(" expected = {:#?}", expected);
65+ match expected {
66+ Node::Text(text_node) => {
67+ assert_eq!(
68+ actual.node_name().to_uppercase(),
69+ "#TEXT",
70+ "actual.node_name() == #TEXT"
71+ );
72+ assert_eq!(
73+ actual.text_content().unwrap_or_default(),
74+ text_node.text(),
75+ "actual.text_content() == expected.text()"
76+ );
77+ }
78+ Node::Element(elem) => {
79+ assert_eq!(
80+ actual.node_name().to_uppercase(),
81+ elem.tag_name().to_uppercase(),
82+ "actual.node_name() == expected.tag_name()"
83+ );
84+ let actual = actual
85+ .dyn_ref::<web_sys::Element>()
86+ .expect("`actual` should be an `Element`");
87+ check_attributes(actual.attributes(), elem.attributes());
88+ check_children(actual.child_nodes(), elem.children());
89+ }
90+ }
91+ }
92+
93+ fn check_attributes(actual: web_sys::NamedNodeMap, expected: &[Attribute]) {
94+ assert_eq!(
95+ actual.length(),
96+ expected.len() as u32,
97+ "actual's number of attributes == expected's number of attributes"
98+ );
99+ for attr in expected {
100+ let actual_attr = actual
101+ .get_named_item(attr.name)
102+ .expect(&format!("should have attribute \"{}\"", attr.name));
103+ assert_eq!(
104+ actual_attr.value(),
105+ attr.value,
106+ "actual attr value == expected attr value for attr \"{}\"",
107+ attr.name
108+ );
109+ }
110+ }
111+
112+ fn check_children(actual: web_sys::NodeList, expected: &[Node]) {
113+ assert_eq!(
114+ actual.length(),
115+ expected.len() as u32,
116+ "actual children length == expected children length"
117+ );
118+ for (i, child) in expected.iter().enumerate() {
119+ let actual_child = actual.item(i as u32).unwrap();
120+ check_node(&actual_child, child);
121+ }
122+ }
123+}
124+
125+/// Use the function `F` to render.
126+pub struct RenderFn<F>(F)
127+where
128+ F: for<'bump> Fn(&'bump Bump) -> Node<'bump>;
129+
130+impl<F> Render for RenderFn<F>
131+where
132+ F: for<'bump> Fn(&'bump Bump) -> Node<'bump>,
133+{
134+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
135+ where
136+ 'a: 'bump,
137+ {
138+ (self.0)(bump)
139+ }
140+}
141+
142+/// Assert that if we start by rendering the `before` virtual DOM tree into a
143+/// physical DOM tree, and then diff it with the `after` virtual DOM tree, then
144+/// the physical DOM tree correctly matches `after`.
145+pub fn assert_before_after<R, S>(before: R, after: S)
146+where
147+ R: Render,
148+ S: Render,
149+{
150+ let container = create_element("div");
151+
152+ debug!("====== Rendering the *before* DOM into the physical DOM ======");
153+ let mut vdom = Vdom::new(&container, &before);
154+ debug!("====== Checking the *before* DOM against the physical DOM ======");
155+ assert_rendered(&container, &before);
156+
157+ debug!("====== Rendering the *after* DOM into the physical DOM ======");
158+ vdom.render(&after);
159+ debug!("====== Checking the *after* DOM against the physical DOM ======");
160+ assert_rendered(&container, &after);
161+}
162+
163+/// A helper macro for decalaring a bunch of `assert_before_after` tests.
164+#[macro_export]
165+macro_rules! before_after {
166+ ( $(
167+ $name:ident {
168+ before($before_bump:ident) {
169+ $( $before:tt )*
170+ }
171+ after($after_bump:ident) {
172+ $( $after:tt )*
173+ }
174+ }
175+ )* ) => {
176+ $(
177+ #[wasm_bindgen_test]
178+ fn $name() {
179+ use crate::{assert_before_after, RenderFn};
180+ assert_before_after(
181+ RenderFn(|$before_bump| { $( $before )* }),
182+ RenderFn(|$after_bump| { $( $after )* })
183+ );
184+ }
185+ )*
186+ }
187+}
tests/web.rs → tests/web/render.rsrenamed+2−176
@@ -1,140 +1,7 @@
1−//! Test suite for the Web and headless browsers.
1+use super::{assert_rendered, before_after, create_element, RenderFn};
2−
3−#![cfg(target_arch = "wasm32")]
4−
5−use bumpalo::Bump;
6−use dodrio::{
7− node::{Attribute, Node},
2+use dodrio::{Attribute, Node, Vdom};
8− vdom::Vdom,
9− Render,
10−};
11−use log::*;
12−use wasm_bindgen::JsCast;
13use wasm_bindgen_test::*;
3use wasm_bindgen_test::*;
14
4
15−wasm_bindgen_test_configure!(run_in_browser);
16−
17−fn window() -> web_sys::Window {
18− web_sys::window().expect("no global `window` exists")
19−}
20−
21−fn document() -> web_sys::Document {
22− window()
23− .document()
24− .expect("should have a document on window")
25−}
26−
27−fn create_element(tag: &str) -> web_sys::Element {
28− document()
29− .create_element(tag)
30− .expect("should create element OK")
31−}
32−
33−fn init_logging() {
34− use std::sync::Once;
35− static START: Once = Once::new();
36− START.call_once(|| {
37− console_log::init_with_level(Level::Trace).expect("could not initialize console_log");
38− });
39−}
40−
41−fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
42− init_logging();
43− let bump = &Bump::new();
44− let node = r.render(bump);
45− let child = container
46− .first_child()
47− .expect("container does not have anything rendered into it?");
48− check_node(&child, &node);
49−
50− fn stringify_actual_node(n: &web_sys::Node) -> String {
51− if let Some(el) = n.dyn_ref::<web_sys::Element>() {
52− el.outer_html()
53− } else {
54− format!("#text({:?})", n.text_content())
55− }
56− }
57−
58− fn check_node(actual: &web_sys::Node, expected: &Node) {
59− debug!("check_render:");
60− debug!(" actual = {}", stringify_actual_node(&actual));
61− debug!(" expected = {:#?}", expected);
62− match expected {
63− Node::Text(text_node) => {
64− assert_eq!(
65− actual.node_name().to_uppercase(),
66− "#TEXT",
67− "actual.node_name() == #TEXT"
68− );
69− assert_eq!(
70− actual.text_content().unwrap_or_default(),
71− text_node.text(),
72− "actual.text_content() == expected.text()"
73− );
74− }
75− Node::Element(elem) => {
76− assert_eq!(
77− actual.node_name().to_uppercase(),
78− elem.tag_name().to_uppercase(),
79− "actual.node_name() == expected.tag_name()"
80− );
81− let actual = actual
82− .dyn_ref::<web_sys::Element>()
83− .expect("`actual` should be an `Element`");
84− check_attributes(actual.attributes(), elem.attributes());
85− check_children(actual.child_nodes(), elem.children());
86− }
87− }
88− }
89−
90− fn check_attributes(actual: web_sys::NamedNodeMap, expected: &[Attribute]) {
91− assert_eq!(
92− actual.length(),
93− expected.len() as u32,
94− "actual's number of attributes == expected's number of attributes"
95− );
96− for attr in expected {
97− let actual_attr = actual
98− .get_named_item(attr.name)
99− .expect(&format!("should have attribute \"{}\"", attr.name));
100− assert_eq!(
101− actual_attr.value(),
102− attr.value,
103− "actual attr value == expected attr value for attr \"{}\"",
104− attr.name
105− );
106− }
107− }
108−
109− fn check_children(actual: web_sys::NodeList, expected: &[Node]) {
110− assert_eq!(
111− actual.length(),
112− expected.len() as u32,
113− "actual children length == expected children length"
114− );
115− for (i, child) in expected.iter().enumerate() {
116− let actual_child = actual.item(i as u32).unwrap();
117− check_node(&actual_child, child);
118− }
119− }
120−}
121−
122−struct RenderFn<F>(F)
123−where
124− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>;
125−
126−impl<F> Render for RenderFn<F>
127−where
128− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>,
129−{
130− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
131− where
132− 'a: 'bump,
133− {
134− (self.0)(bump)
135− }
136−}
137−
138#[wasm_bindgen_test]
5#[wasm_bindgen_test]
139fn render_initial_text() {
6fn render_initial_text() {
140 let hello = RenderFn(|_bump| Node::text("hello"));
7 let hello = RenderFn(|_bump| Node::text("hello"));
@@ -166,47 +33,6 @@ fn render_initial_node() {
166 assert_rendered(&container, &hello);
33 assert_rendered(&container, &hello);
167}
34}
168
35
169−fn assert_before_after<R, S>(before: R, after: S)
170−where
171− R: Render,
172− S: Render,
173−{
174− let container = create_element("div");
175−
176− debug!("====== Rendering the *before* DOM into the physical DOM ======");
177− let mut vdom = Vdom::new(&container, &before);
178− debug!("====== Checking the *before* DOM against the physical DOM ======");
179− assert_rendered(&container, &before);
180−
181− debug!("====== Rendering the *after* DOM into the physical DOM ======");
182− vdom.render(&after);
183− debug!("====== Checking the *after* DOM against the physical DOM ======");
184− assert_rendered(&container, &after);
185−}
186−
187−macro_rules! before_after {
188− ( $(
189− $name:ident {
190− before($before_bump:ident) {
191− $( $before:tt )*
192− }
193− after($after_bump:ident) {
194− $( $after:tt )*
195− }
196− }
197− )* ) => {
198− $(
199− #[wasm_bindgen_test]
200− fn $name() {
201− assert_before_after(
202− RenderFn(|$before_bump| { $( $before )* }),
203− RenderFn(|$after_bump| { $( $after )* })
204− );
205− }
206− )*
207− }
208−}
209−
210before_after! {
36before_after! {
211 same_text {
37 same_text {
212 before(_bump) {
38 before(_bump) {