on master help sign in
Add a `Cached<R>` render combinator to cache results of rendering
commita44bfa7a6750b00000ff4151c290733968829f4d authorNick Fitzgerald <fitzgen@gmail.com> dateWed 23 Jan 2019, 10:18 (8 years ago) parente604deb
5 files changed+403−172
src/cached.rs+161 src/lib.rs+2 tests/web/cached.rs+51 tests/web/main.rs+187 tests/web.rs → tests/web/render.rs+2−172
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+2
@@ -4,10 +4,12 @@ use bumpalo::Bump;
44#[doc(hidden)]
55pub mod change_list;
66
7+mod cached;
78mod node;
89mod vdom;
910
1011// Re-export items at the top level.
12+pub use self::cached::Cached;
1113pub use self::node::{Attribute, ElementNode, Node, TextNode};
1214pub use self::vdom::Vdom;
1315
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−172
@@ -1,136 +1,7 @@
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;
1+use super::{assert_rendered, before_after, create_element, RenderFn};
2+use dodrio::{Attribute, Node, Vdom};
93use wasm_bindgen_test::*;
104
11−wasm_bindgen_test_configure!(run_in_browser);
12−
13−fn window() -> web_sys::Window {
14− web_sys::window().expect("no global `window` exists")
15−}
16−
17−fn document() -> web_sys::Document {
18− window()
19− .document()
20− .expect("should have a document on window")
21−}
22−
23−fn create_element(tag: &str) -> web_sys::Element {
24− document()
25− .create_element(tag)
26− .expect("should create element OK")
27−}
28−
29−fn init_logging() {
30− use std::sync::Once;
31− static START: Once = Once::new();
32− START.call_once(|| {
33− console_log::init_with_level(Level::Trace).expect("could not initialize console_log");
34− });
35−}
36−
37−fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
38− init_logging();
39− let bump = &Bump::new();
40− let node = r.render(bump);
41− let child = container
42− .first_child()
43− .expect("container does not have anything rendered into it?");
44− check_node(&child, &node);
45−
46− fn stringify_actual_node(n: &web_sys::Node) -> String {
47− if let Some(el) = n.dyn_ref::<web_sys::Element>() {
48− el.outer_html()
49− } else {
50− format!("#text({:?})", n.text_content())
51− }
52− }
53−
54− fn check_node(actual: &web_sys::Node, expected: &Node) {
55− debug!("check_render:");
56− debug!(" actual = {}", stringify_actual_node(&actual));
57− debug!(" expected = {:#?}", expected);
58− match expected {
59− Node::Text(text_node) => {
60− assert_eq!(
61− actual.node_name().to_uppercase(),
62− "#TEXT",
63− "actual.node_name() == #TEXT"
64− );
65− assert_eq!(
66− actual.text_content().unwrap_or_default(),
67− text_node.text(),
68− "actual.text_content() == expected.text()"
69− );
70− }
71− Node::Element(elem) => {
72− assert_eq!(
73− actual.node_name().to_uppercase(),
74− elem.tag_name().to_uppercase(),
75− "actual.node_name() == expected.tag_name()"
76− );
77− let actual = actual
78− .dyn_ref::<web_sys::Element>()
79− .expect("`actual` should be an `Element`");
80− check_attributes(actual.attributes(), elem.attributes());
81− check_children(actual.child_nodes(), elem.children());
82− }
83− }
84− }
85−
86− fn check_attributes(actual: web_sys::NamedNodeMap, expected: &[Attribute]) {
87− assert_eq!(
88− actual.length(),
89− expected.len() as u32,
90− "actual's number of attributes == expected's number of attributes"
91− );
92− for attr in expected {
93− let actual_attr = actual
94− .get_named_item(attr.name)
95− .expect(&format!("should have attribute \"{}\"", attr.name));
96− assert_eq!(
97− actual_attr.value(),
98− attr.value,
99− "actual attr value == expected attr value for attr \"{}\"",
100− attr.name
101− );
102− }
103− }
104−
105− fn check_children(actual: web_sys::NodeList, expected: &[Node]) {
106− assert_eq!(
107− actual.length(),
108− expected.len() as u32,
109− "actual children length == expected children length"
110− );
111− for (i, child) in expected.iter().enumerate() {
112− let actual_child = actual.item(i as u32).unwrap();
113− check_node(&actual_child, child);
114− }
115− }
116−}
117−
118−struct RenderFn<F>(F)
119−where
120− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>;
121−
122−impl<F> Render for RenderFn<F>
123−where
124− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>,
125−{
126− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
127− where
128− 'a: 'bump,
129− {
130− (self.0)(bump)
131− }
132−}
133−
1345#[wasm_bindgen_test]
1356fn render_initial_text() {
1367 let hello = RenderFn(|_bump| Node::text("hello"));
@@ -162,47 +33,6 @@ fn render_initial_node() {
16233 assert_rendered(&container, &hello);
16334}
16435
165−fn assert_before_after<R, S>(before: R, after: S)
166−where
167− R: Render,
168− S: Render,
169−{
170− let container = create_element("div");
171−
172− debug!("====== Rendering the *before* DOM into the physical DOM ======");
173− let mut vdom = Vdom::new(&container, &before);
174− debug!("====== Checking the *before* DOM against the physical DOM ======");
175− assert_rendered(&container, &before);
176−
177− debug!("====== Rendering the *after* DOM into the physical DOM ======");
178− vdom.render(&after);
179− debug!("====== Checking the *after* DOM against the physical DOM ======");
180− assert_rendered(&container, &after);
181−}
182−
183−macro_rules! before_after {
184− ( $(
185− $name:ident {
186− before($before_bump:ident) {
187− $( $before:tt )*
188− }
189− after($after_bump:ident) {
190− $( $after:tt )*
191− }
192− }
193− )* ) => {
194− $(
195− #[wasm_bindgen_test]
196− fn $name() {
197− assert_before_after(
198− RenderFn(|$before_bump| { $( $before )* }),
199− RenderFn(|$after_bump| { $( $after )* })
200− );
201− }
202− )*
203− }
204−}
205−
20636before_after! {
20737 same_text {
20838 before(_bump) {