on master help sign in
Re-introduce `Cached<R>`

Now, instead of reusing the same `Bump` arena inside the `Cached<R>`, the vdom maintains a set of `Bump` arenas and cached `Node`s that are the results of rendering `Cached<R>`s. The entries in the set are immutable after insertion, and invalidating a `Cached<R>` causes it to request a new `Bump` arena from the vdom instead of resetting the old `Bump` that it allocated into last. But how to clean up this set of cached immutable `Bump` arenas? We have a tiny, specialized garbage collector for just these arenas. Because they are immutable, their outgoing edges never change, so we compute and store the set of transitive edges when inserting new arenas into the set. This means that the "mark" phase of our collector is super quick and easy, since it doesn't need to recursively mark things or maintain a mark stack. We garbage collect the arenas after each render. Additionally, there is a new `NodeKind::Cached` variant. This contains the id of its results in the set of cached nodes and their `Bump` arenas. When diffing, if we see that the old and new virtual dom point to the same cached subtree, then we avoid diffing them. This gives us really nice speed ups, and makes the new `Cached<R>` even better than the old version, even ignoring the bugs that the old version had. Fixes #31.

commit52d9f3eb069dba8525f2bf5d67349643924d7912 authorNick Fitzgerald <fitzgen@gmail.com> dateThu 28 Mar 2019, 16:00 (8 years ago) parent6bc6a45
11 files changed+1019−272
benches/benches.rs+6−3 src/cached.rs+160 src/cached_set.rs+152 src/diff.rs+310 src/events.rs+37−7 src/lib.rs+13 src/node.rs+29−1 src/render_context.rs+28−17 src/vdom.rs+32−237 tests/web/cached.rs+234 tests/web/main.rs+18−7
benches/benches.rs+6−3
@@ -10,8 +10,9 @@ use criterion::{
1010use dodrio::{
1111 builder::*,
1212 bumpalo::{self, Bump},
13− Node, Render, RenderContext, Vdom,
13+ CachedSet, Node, Render, RenderContext, Vdom,
1414};
15+use std::cell::RefCell;
1516
1617/// The simplest thing we can render: `<div/>`.
1718struct Empty;
@@ -44,9 +45,10 @@ fn criterion_benchmark(c: &mut Criterion) {
4445 "render",
4546 Benchmark::new("empty", {
4647 let mut bump = Bump::new();
48+ let cached_set = RefCell::new(CachedSet::default());
4749 move |b| {
4850 bump.reset();
49− let mut cx = RenderContext::new(&bump);
51+ let mut cx = RenderContext::new(&bump, &cached_set);
5052 b.iter(|| {
5153 black_box(Empty.render(&mut cx));
5254 })
@@ -60,9 +62,10 @@ fn criterion_benchmark(c: &mut Criterion) {
6062 "list",
6163 {
6264 let mut bump = Bump::new();
65+ let cached_set = RefCell::new(CachedSet::default());
6366 move |b, n| {
6467 bump.reset();
65− let mut cx = RenderContext::new(&bump);
68+ let mut cx = RenderContext::new(&bump, &cached_set);
6669 b.iter(|| {
6770 black_box(SimpleList(*n).render(&mut cx));
6871 })
src/cached.rsnew file+160
@@ -0,0 +1,160 @@
1+use crate::{
2+ cached_set::{CacheId, CachedSet},
3+ Node, Render, RenderContext,
4+};
5+use std::cell::Cell;
6+use std::ops::{Deref, DerefMut};
7+
8+/// A renderable that supports caching for when rendering is expensive but can
9+/// generate the same DOM tree.
10+#[derive(Clone, Debug)]
11+pub struct Cached<R> {
12+ inner: R,
13+ cached: Cell<Option<CacheId>>,
14+}
15+
16+impl<R> Cached<R> {
17+ /// Construct a new `Cached<R>` of an inner `R`.
18+ ///
19+ /// # Example
20+ ///
21+ /// ```
22+ /// use dodrio::{Cached, Node, Render, RenderContext};
23+ ///
24+ /// pub struct Counter {
25+ /// count: u32,
26+ /// }
27+ ///
28+ /// impl Render for Counter {
29+ /// fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
30+ /// // ...
31+ /// # unimplemented!()
32+ /// }
33+ /// }
34+ ///
35+ /// // Create a render-able counter.
36+ /// let counter = Counter { count: 0 };
37+ ///
38+ /// // And cache its rendering!
39+ /// let cached_counter = Cached::new(counter);
40+ /// ```
41+ #[inline]
42+ pub fn new(inner: R) -> Cached<R> {
43+ let cached = Cell::new(None);
44+ Cached { inner, cached }
45+ }
46+
47+ /// Invalidate the cached rendering.
48+ ///
49+ /// This method should be called whenever the inner `R` must be re-rendered,
50+ /// and the cached `Node` from the last time `R::render` was invoked can no
51+ /// longer be re-used.
52+ ///
53+ /// # Example
54+ ///
55+ /// The `Cached<Hello>` component must have its cache invalidated whenever
56+ /// the `who` string is changed, or else the cached rendering will keep
57+ /// displaying greetings to old `who`s.
58+ ///
59+ /// ```
60+ /// use dodrio::{bumpalo, Cached, Node, Render, RenderContext};
61+ ///
62+ /// /// A component that renders to "<p>Hello, {who}!</p>"
63+ /// pub struct Hello {
64+ /// who: String
65+ /// }
66+ ///
67+ /// impl Render for Hello {
68+ /// fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
69+ /// use dodrio::builder::*;
70+ /// let greeting = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
71+ /// p(&cx)
72+ /// .children([text(greeting.into_bump_str())])
73+ /// .finish()
74+ /// }
75+ /// }
76+ ///
77+ /// /// Whenever a `Cached<Hello>`'s `who` is updated, we need to invalidate the
78+ /// /// cache so that we don't keep displaying greetings to old `who`s.
79+ /// pub fn set_who(hello: &mut Cached<Hello>, who: String) {
80+ /// hello.who = who;
81+ /// Cached::invalidate(hello);
82+ /// }
83+ /// ```
84+ #[inline]
85+ pub fn invalidate(cached: &Self) {
86+ cached.cached.set(None);
87+ }
88+
89+ /// Convert a `Cached<R>` back into a plain `R`.
90+ #[inline]
91+ pub fn into_inner(cached: Self) -> R {
92+ cached.inner
93+ }
94+}
95+
96+impl<R> Deref for Cached<R> {
97+ type Target = R;
98+
99+ fn deref(&self) -> &R {
100+ &self.inner
101+ }
102+}
103+
104+impl<R> DerefMut for Cached<R> {
105+ fn deref_mut(&mut self) -> &mut R {
106+ &mut self.inner
107+ }
108+}
109+
110+impl<R> Render for Cached<R>
111+where
112+ R: Render,
113+{
114+ fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
115+ let id = match self.cached.get() {
116+ // This does-the-cache-contain-this-id check is necessary because
117+ // the same `Cached<R>` instance can be rendered into vdom A, which
118+ // will save the results into A's cached set and yield id X. Then,
119+ // it can be rendered *again* into a second vdom B, and B's cached
120+ // set does not have the saved results for id X. If we didn't have
121+ // this check, instead of a cache miss, we would have a panic.
122+ //
123+ // This scenario should basically never happen in the real world,
124+ // however. If we ever find that it is worth sharing a cached render
125+ // component between multiple vdoms, and want to avoid these
126+ // "unnecessary" cache misses, we can do this:
127+ //
128+ // * Make each `Cached<R>` have an instance id and generation
129+ // counter.
130+ //
131+ // * On invalidation, bump the generation counter.
132+ //
133+ // * Add a generation member to the `CacheEntry`.
134+ //
135+ // * Each vdom maintains a map from `Cached<R>` instance id to
136+ // `CacheEntry`s.
137+ //
138+ // * We only re-use the cached results if the `Cached<R>`'s
139+ // generation counter matches the entry in the vdom's cached set.
140+ //
141+ // This is all do-able but is a bit more book keeping than we really
142+ // want to do unless it is well motivated.
143+ Some(id)
144+ if {
145+ let cached_set = cx.cached_set.borrow();
146+ cached_set.contains(id)
147+ } =>
148+ {
149+ id
150+ }
151+ _ => {
152+ let id = CachedSet::insert(cx, |nested_cx| self.inner.render(nested_cx));
153+ self.cached.set(Some(id));
154+ id
155+ }
156+ };
157+
158+ Node::cached(id)
159+ }
160+}
src/cached_set.rsnew file+152
@@ -0,0 +1,152 @@
1+use crate::{
2+ events::EventsRegistry,
3+ node::{Node, NodeKind},
4+ render_context::RenderContext,
5+};
6+use bumpalo::Bump;
7+use fxhash::{FxHashMap, FxHashSet};
8+use std::sync::atomic::{AtomicUsize, Ordering};
9+use wasm_bindgen::prelude::*;
10+
11+static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
12+
13+pub_unstable_internal! {
14+ #[derive(Debug, Default)]
15+ pub(crate) struct CachedSet {
16+ items: FxHashMap<CacheId, CacheEntry>,
17+ }
18+}
19+
20+pub_unstable_internal! {
21+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
22+ pub(crate) struct CacheId(u32);
23+}
24+
25+#[derive(Debug)]
26+pub(crate) struct CacheEntry {
27+ bump: Bump,
28+
29+ // Actually a reference into `bump`.
30+ node: *const Node<'static>,
31+
32+ // All the other cached entries that `node` references (including transitive
33+ // edges). By eagerly following transitive edges and recording them the
34+ // once, we don't have to repeat the tracing every time we want to garbage
35+ // collect cache entries.
36+ edges: FxHashSet<CacheId>,
37+}
38+
39+impl CachedSet {
40+ pub(crate) fn gc(&mut self, registry: &mut EventsRegistry, roots: &[CacheId]) {
41+ // TODO: We need to port a hash set over in `bumpalo::collections` so
42+ // that we can use a temporary bump arena for this set.
43+ let mut mark_bits = FxHashSet::default();
44+ mark_bits.reserve(self.items.len());
45+
46+ for root in roots {
47+ if mark_bits.insert(*root) {
48+ let entry = self
49+ .items
50+ .get(root)
51+ .expect_throw("CachedSet::gc: should have root in cached set");
52+
53+ // NB: no need to recurse here because `edges` already contains
54+ // transitive edges!
55+ mark_bits.extend(entry.edges.iter().cloned());
56+ }
57+ }
58+
59+ self.items.retain(|id, entry| {
60+ let keep = mark_bits.contains(id);
61+ if !keep {
62+ debug!("CachedSet::gc: removing {:?}", id);
63+ let node: &Node = unsafe { &*entry.node };
64+ registry.remove_subtree(node);
65+ }
66+ keep
67+ });
68+ }
69+
70+ // Trace all the transitive edges to other cached entries that the given
71+ // node has.
72+ fn trace(&self, node: &Node) -> FxHashSet<CacheId> {
73+ let mut edges = FxHashSet::default();
74+
75+ let bump = Bump::new();
76+ let mut stack = bumpalo::collections::Vec::with_capacity_in(64, &bump);
77+ stack.push(node);
78+
79+ while let Some(node) = stack.pop() {
80+ match &node.kind {
81+ NodeKind::Text(_) => continue,
82+ NodeKind::Cached(c) => {
83+ debug_assert!(self.items.contains_key(&c.id));
84+ edges.insert(c.id);
85+ edges.extend(
86+ self.items
87+ .get(&c.id)
88+ .expect_throw("CachedSet::trace: should have c.id in cached set")
89+ .edges
90+ .iter()
91+ .cloned(),
92+ );
93+ }
94+ NodeKind::Element(el) => {
95+ stack.extend(el.children);
96+ }
97+ }
98+ }
99+
100+ edges
101+ }
102+
103+ fn next_id(&mut self) -> CacheId {
104+ CacheId(ID_COUNTER.fetch_add(1, Ordering::AcqRel) as u32)
105+ }
106+
107+ pub(crate) fn insert<F>(cx: &mut RenderContext, f: F) -> CacheId
108+ where
109+ F: for<'a> FnOnce(&mut RenderContext<'a>) -> Node<'a>,
110+ {
111+ let bump = Bump::new();
112+ let set = cx.cached_set;
113+ let mut nested_cx = RenderContext::new(&bump, set);
114+ let node = f(&mut nested_cx);
115+ let node = bump.alloc(node);
116+ let edges = {
117+ let set = set.borrow();
118+ set.trace(node)
119+ };
120+ let node = node as *mut Node<'_> as usize as *const Node<'static>;
121+ let entry = CacheEntry { bump, node, edges };
122+
123+ let mut set = set.borrow_mut();
124+ let id = set.next_id();
125+ debug!("CachedSet::insert: id = {:?}; entry = {:?}", id, entry);
126+ set.items.insert(id, entry);
127+ id
128+ }
129+
130+ /// Does the cached set contain a cached node with the given id?
131+ pub fn contains(&self, id: CacheId) -> bool {
132+ self.items.contains_key(&id)
133+ }
134+
135+ /// Get the node for the given cache id.
136+ pub fn get(&self, mut id: CacheId) -> Node {
137+ debug!("CachedSet::get: id = {:?}", id);
138+ loop {
139+ let entry = self
140+ .items
141+ .get(&id)
142+ .expect_throw("CachedSet::get: should have id in set");
143+ let node: &Node = unsafe { &*entry.node };
144+ if let NodeKind::Cached(ref c) = node.kind {
145+ id = c.id;
146+ continue;
147+ } else {
148+ return node.clone();
149+ }
150+ }
151+ }
152+}
src/diff.rsnew file+310
@@ -0,0 +1,310 @@
1+use crate::{
2+ cached_set::{CacheId, CachedSet},
3+ change_list::ChangeList,
4+ events::EventsRegistry,
5+ node::{Attribute, ElementNode, Listener, Node, NodeKind, TextNode},
6+};
7+use std::cmp;
8+
9+pub(crate) fn diff(
10+ cached_set: &CachedSet,
11+ change_list: &mut ChangeList,
12+ registry: &mut EventsRegistry,
13+ old: Node,
14+ new: Node,
15+ cached_roots: &mut bumpalo::collections::Vec<CacheId>,
16+) {
17+ match (&new.kind, &old.kind) {
18+ (
19+ &NodeKind::Text(TextNode { text: new_text }),
20+ &NodeKind::Text(TextNode { text: old_text }),
21+ ) => {
22+ debug!(" both are text nodes");
23+ if new_text != old_text {
24+ debug!(" text needs updating");
25+ change_list.emit_set_text(new_text);
26+ }
27+ }
28+
29+ (&NodeKind::Text(_), &NodeKind::Element(_)) => {
30+ debug!(" replacing a text node with an element");
31+ create(cached_set, change_list, registry, new, cached_roots);
32+ registry.remove_subtree(&old);
33+ change_list.emit_replace_with();
34+ }
35+
36+ (&NodeKind::Element(_), &NodeKind::Text(_)) => {
37+ debug!(" replacing an element with a text node");
38+ create(cached_set, change_list, registry, new, cached_roots);
39+ // Note: text nodes cannot have event listeners, so we don't need to
40+ // remove the old node's listeners from our registry her.
41+ change_list.emit_replace_with();
42+ }
43+
44+ (
45+ &NodeKind::Element(ElementNode {
46+ tag_name: new_tag_name,
47+ listeners: new_listeners,
48+ attributes: new_attributes,
49+ children: new_children,
50+ namespace: new_namespace,
51+ }),
52+ &NodeKind::Element(ElementNode {
53+ tag_name: old_tag_name,
54+ listeners: old_listeners,
55+ attributes: old_attributes,
56+ children: old_children,
57+ namespace: old_namespace,
58+ }),
59+ ) => {
60+ debug!(" updating an element");
61+ if new_tag_name != old_tag_name || new_namespace != old_namespace {
62+ debug!(" different tag names or namespaces; creating new element and replacing old element");
63+ create(cached_set, change_list, registry, new, cached_roots);
64+ registry.remove_subtree(&old);
65+ change_list.emit_replace_with();
66+ return;
67+ }
68+ diff_listeners(change_list, registry, old_listeners, new_listeners);
69+ diff_attributes(change_list, old_attributes, new_attributes);
70+ diff_children(
71+ cached_set,
72+ change_list,
73+ registry,
74+ old_children,
75+ new_children,
76+ cached_roots,
77+ );
78+ }
79+
80+ // Both the new and old nodes are cached.
81+ (&NodeKind::Cached(ref new), &NodeKind::Cached(ref old)) => {
82+ cached_roots.push(new.id);
83+
84+ if new.id == old.id {
85+ // This is the same cached node, so nothing has changed!
86+ return;
87+ }
88+
89+ let new = cached_set.get(new.id);
90+ let old = cached_set.get(old.id);
91+ diff(cached_set, change_list, registry, old, new, cached_roots);
92+ }
93+
94+ // New cached node when the old node was not cached. In this scenario,
95+ // we assume that they are pretty different, and it isn't worth diffing
96+ // the subtrees, so we just create the new cached node afresh.
97+ (&NodeKind::Cached(ref c), _) => {
98+ cached_roots.push(c.id);
99+ let new = cached_set.get(c.id);
100+ create(cached_set, change_list, registry, new, cached_roots);
101+ registry.remove_subtree(&old);
102+ change_list.emit_replace_with();
103+ }
104+
105+ // Old cached node and new non-cached node. Again, assume that they are
106+ // probably pretty different and create the new non-cached node afresh.
107+ (_, &NodeKind::Cached(_)) => {
108+ create(cached_set, change_list, registry, new, cached_roots);
109+ registry.remove_subtree(&old);
110+ change_list.emit_replace_with();
111+ }
112+ }
113+}
114+
115+fn diff_listeners(
116+ change_list: &mut ChangeList,
117+ registry: &mut EventsRegistry,
118+ old: &[Listener],
119+ new: &[Listener],
120+) {
121+ debug!(" updating event listeners");
122+
123+ 'outer1: for new_l in new {
124+ unsafe {
125+ // Safety relies on removing `new_l` from the registry manually at
126+ // the end of its lifetime. This happens below in the `'outer2`
127+ // loop, and elsewhere in diffing when removing old dom trees.
128+ registry.add(new_l);
129+ }
130+
131+ for old_l in old {
132+ if new_l.event == old_l.event {
133+ change_list.emit_update_event_listener(new_l);
134+ continue 'outer1;
135+ }
136+ }
137+
138+ change_list.emit_new_event_listener(new_l);
139+ }
140+
141+ 'outer2: for old_l in old {
142+ registry.remove(old_l);
143+
144+ for new_l in new {
145+ if new_l.event == old_l.event {
146+ continue 'outer2;
147+ }
148+ }
149+ change_list.emit_remove_event_listener(old_l.event);
150+ }
151+}
152+
153+fn diff_attributes(change_list: &mut ChangeList, old: &[Attribute], new: &[Attribute]) {
154+ debug!(" updating attributes");
155+
156+ // Do O(n^2) passes to add/update and remove attributes, since
157+ // there are almost always very few attributes.
158+ 'outer: for new_attr in new {
159+ if new_attr.is_volatile() {
160+ change_list.emit_set_attribute(new_attr.name, new_attr.value);
161+ } else {
162+ for old_attr in old {
163+ if old_attr.name == new_attr.name {
164+ if old_attr.value != new_attr.value {
165+ change_list.emit_set_attribute(new_attr.name, new_attr.value);
166+ }
167+ continue 'outer;
168+ }
169+ }
170+ change_list.emit_set_attribute(new_attr.name, new_attr.value);
171+ }
172+ }
173+
174+ 'outer2: for old_attr in old {
175+ for new_attr in new {
176+ if old_attr.name == new_attr.name {
177+ continue 'outer2;
178+ }
179+ }
180+ change_list.emit_remove_attribute(old_attr.name);
181+ }
182+}
183+
184+fn diff_children(
185+ cached_set: &CachedSet,
186+ change_list: &mut ChangeList,
187+ registry: &mut EventsRegistry,
188+ old: &[Node],
189+ new: &[Node],
190+ cached_roots: &mut bumpalo::collections::Vec<CacheId>,
191+) {
192+ debug!(" updating children shared by old and new");
193+
194+ let num_children_to_diff = cmp::min(new.len(), old.len());
195+ let mut new_children = new.iter();
196+ let mut old_children = old.iter();
197+ let mut pushed = false;
198+
199+ for (i, (new_child, old_child)) in new_children
200+ .by_ref()
201+ .zip(old_children.by_ref())
202+ .take(num_children_to_diff)
203+ .enumerate()
204+ {
205+ if i == 0 {
206+ change_list.emit_push_first_child();
207+ pushed = true;
208+ } else {
209+ debug_assert!(pushed);
210+ change_list.emit_pop_push_next_sibling();
211+ }
212+
213+ diff(
214+ cached_set,
215+ change_list,
216+ registry,
217+ old_child.clone(),
218+ new_child.clone(),
219+ cached_roots,
220+ );
221+ }
222+
223+ if old_children.next().is_some() {
224+ debug!(" removing extra old children");
225+ debug_assert!(new_children.next().is_none());
226+ if !pushed {
227+ change_list.emit_push_first_child();
228+ } else {
229+ change_list.emit_pop_push_next_sibling();
230+ }
231+ change_list.emit_remove_self_and_next_siblings();
232+ pushed = false;
233+ } else {
234+ debug!(" creating new children");
235+ for (i, new_child) in new_children.enumerate() {
236+ if i == 0 && pushed {
237+ change_list.emit_pop();
238+ pushed = false;
239+ }
240+ create(
241+ cached_set,
242+ change_list,
243+ registry,
244+ new_child.clone(),
245+ cached_roots,
246+ );
247+ change_list.emit_append_child();
248+ }
249+ }
250+
251+ debug!(" done updating children");
252+ if pushed {
253+ change_list.emit_pop();
254+ }
255+}
256+
257+fn create(
258+ cached_set: &CachedSet,
259+ change_list: &mut ChangeList,
260+ registry: &mut EventsRegistry,
261+ node: Node,
262+ cached_roots: &mut bumpalo::collections::Vec<CacheId>,
263+) {
264+ match node.kind {
265+ NodeKind::Text(TextNode { text }) => {
266+ change_list.emit_create_text_node(text);
267+ }
268+ NodeKind::Element(ElementNode {
269+ tag_name,
270+ listeners,
271+ attributes,
272+ children,
273+ namespace,
274+ }) => {
275+ if let Some(namespace) = namespace {
276+ change_list.emit_create_element_ns(tag_name, namespace);
277+ } else {
278+ change_list.emit_create_element(tag_name);
279+ }
280+ for l in listeners {
281+ unsafe {
282+ registry.add(l);
283+ }
284+ change_list.emit_new_event_listener(l);
285+ }
286+ for attr in attributes {
287+ if namespace.is_none() || attr.name.starts_with("xmlns") {
288+ change_list.emit_set_attribute(&attr.name, &attr.value);
289+ } else {
290+ change_list.emit_set_attribute_ns(&attr.name, &attr.value);
291+ }
292+ }
293+ for child in children {
294+ create(
295+ cached_set,
296+ change_list,
297+ registry,
298+ child.clone(),
299+ cached_roots,
300+ );
301+ change_list.emit_append_child();
302+ }
303+ }
304+ NodeKind::Cached(c) => {
305+ cached_roots.push(c.id);
306+ let node = cached_set.get(c.id);
307+ create(cached_set, change_list, registry, node, cached_roots)
308+ }
309+ }
310+}
src/events.rs+37−7
@@ -1,5 +1,4 @@
1−use crate::vdom::VdomInner;
2−use crate::Listener;
1+use crate::{node::Node, vdom::VdomInner, Listener};
32use std::cell::RefCell;
43use std::rc::{Rc, Weak};
54
@@ -14,11 +13,17 @@ cfg_if::cfg_if! {
1413 ) {
1514 (Rc::new(RefCell::new(EventsRegistry {})), ())
1615 }
16+ pub(crate) fn remove(&mut self, _listener: &Listener) {}
17+ pub(crate) fn remove_subtree(&mut self, _node: &Node) {}
1718 pub(crate) unsafe fn add<'a>(&mut self, _listener: &'a Listener<'a>) {}
1819 pub(crate) fn clear_active_listeners(&mut self) {}
1920 }
2021 } else {
21− use crate::{node::ListenerCallback, vdom::VdomWeak};
22+ use bumpalo::Bump;
23+ use crate::{
24+ node::{ElementNode, ListenerCallback, NodeKind},
25+ vdom::VdomWeak,
26+ };
2227 use fxhash::FxHashMap;
2328 use std::fmt;
2429 use std::mem;
@@ -88,14 +93,39 @@ cfg_if::cfg_if! {
8893 (registry, closure)
8994 }
9095
96+ pub(crate) fn remove(&mut self, listener: &Listener) {
97+ let id = listener.get_callback_parts();
98+ debug!("EventsRegistry::remove(0x{:x}, 0x{:x})", id.0, id.1);
99+ debug_assert!(id.0 != 0);
100+ self.active.remove(&id);
101+ }
102+
103+ pub(crate) fn remove_subtree(&mut self, node: &Node) {
104+ let bump = Bump::new();
105+ let mut stack = bumpalo::collections::Vec::with_capacity_in(64, &bump);
106+ stack.push(node);
107+
108+ while let Some(node) = stack.pop() {
109+ match node.kind {
110+ NodeKind::Cached(_) | NodeKind::Text(_) => continue,
111+ NodeKind::Element(ElementNode {listeners, children, ..}) => {
112+ for l in listeners {
113+ self.remove(l);
114+ }
115+ stack.extend(children);
116+ }
117+ }
118+ }
119+ }
120+
91121 /// Add an event listener to the registry, exposing to JS.
92122 ///
93123 /// # Unsafety
94124 ///
95− /// The listener's lifetime is extended to `'static` and it is the caller's
96− /// responsibility to ensure that the listener is not kept in the registry
97− /// after it is dropped. This is typically done via clearing the whole
98− /// registry on each render and repopulating it during diffing.
125+ /// The listener's lifetime is extended to `'static` and it is the
126+ /// caller's responsibility to ensure that the listener is not kept
127+ /// in the registry after it is dropped. This is maintained during
128+ /// diffing.
99129 pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
100130 let id = listener.get_callback_parts();
101131 debug!("EventsRegistry::add(0x{:x}, 0x{:x})", id.0, id.1);
src/lib.rs+13
@@ -85,6 +85,9 @@ cfg_if::cfg_if! {
8585#[doc(hidden)]
8686pub mod change_list;
8787
88+mod cached;
89+mod cached_set;
90+mod diff;
8891mod events;
8992mod node;
9093mod render;
@@ -94,11 +97,20 @@ mod vdom;
9497pub mod builder;
9598
9699// Re-export items at the top level.
100+pub use self::cached::Cached;
97101pub use self::node::{Attribute, Listener, Node};
98102pub use self::render::{Render, RootRender};
99103pub use self::render_context::RenderContext;
100104pub use self::vdom::{Vdom, VdomWeak};
101105
106+cfg_if::cfg_if! {
107+ if #[cfg(all(target_arch = "wasm32", not(feature = "xxx-unstable-internal-use-only")))] {
108+ use wasm_bindgen::__rt::WasmRefCell as RefCell;
109+ } else {
110+ use std::cell::RefCell;
111+ }
112+}
113+
102114// Polyfill some Web stuff for benchmarking...
103115cfg_if::cfg_if! {
104116 if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
@@ -116,6 +128,7 @@ cfg_if::cfg_if! {
116128
117129cfg_if::cfg_if! {
118130 if #[cfg(feature = "xxx-unstable-internal-use-only")] {
131+ pub use self::cached_set::{CachedSet};
119132 pub use self::node::{ElementNode, NodeKind, TextNode};
120133 }
121134}
src/node.rs+29−1
@@ -1,7 +1,8 @@
1−use crate::{RootRender, VdomWeak};
1+use crate::{cached_set::CacheId, RootRender, VdomWeak};
22use bumpalo::Bump;
33use std::fmt;
44use std::iter;
5+use std::marker::PhantomData;
56use std::mem;
67
78/// A virtual DOM node.
@@ -24,6 +25,10 @@ pub_unstable_internal! {
2425
2526 /// An element potentially with attributes and children.
2627 Element(ElementNode<'a>),
28+
29+ /// A node in the vdom's `CachedSet`. This allows us to avoid
30+ /// re-rendering and re-diffing subtrees.
31+ Cached(CachedNode<'a>),
2732 }
2833}
2934
@@ -49,6 +54,18 @@ pub_unstable_internal! {
4954 }
5055}
5156
57+pub_unstable_internal! {
58+ /// A cached node exists in an arena that is internal to the `Vdom`. It
59+ /// allows us to avoid both re-rendering a sub-tree and re-diffing
60+ /// it. `CachedNode`s cannot contain cycles, since they can only contain
61+ /// other `CachedNode`s that already exist when they are being constructed.
62+ #[derive(Debug, Clone)]
63+ pub(crate) struct CachedNode<'a> {
64+ pub id: CacheId,
65+ pub _phantom: PhantomData<&'a Node<'a>>,
66+ }
67+}
68+
5269/// An event listener callback function.
5370///
5471/// It takes three parameters:
@@ -156,6 +173,17 @@ impl<'a> Node<'a> {
156173 kind: NodeKind::Text(TextNode { text }),
157174 }
158175 }
176+
177+ /// Construct a new cached node with the given id.
178+ #[inline]
179+ pub(crate) fn cached(id: CacheId) -> Node<'a> {
180+ Node {
181+ kind: NodeKind::Cached(CachedNode {
182+ id,
183+ _phantom: PhantomData,
184+ }),
185+ }
186+ }
159187}
160188
161189/// A node can become an iterator that yields the node itself once.
src/render_context.rs+28−17
@@ -1,11 +1,12 @@
1+use crate::cached_set::CachedSet;
12use bumpalo::Bump;
3+use std::fmt;
24
35/// Common context available to all `Render` implementations.
46///
57/// Notably, the `RenderContext` gives access to the bump arena that the virtual
68/// DOM should be allocated within. This is available via the `bump` field.
7−#[derive(Debug)]
8−pub struct RenderContext<'bump> {
9+pub struct RenderContext<'a> {
910 /// The underlying bump arena that virtual DOMs are rendered into.
1011 ///
1112 /// ## Example
@@ -18,35 +19,45 @@ pub struct RenderContext<'bump> {
1819 /// cx.bump.alloc(42)
1920 /// }
2021 /// ```
21− pub bump: &'bump Bump,
22+ pub bump: &'a Bump,
2223
23− // Prevent exhaustive matching on the rendering context, so we can add more
24− // members in a semver-compatible way.
24+ pub(crate) cached_set: &'a crate::RefCell<CachedSet>,
25+
26+ // Prevent exhaustive matching on the rendering context, so we can always
27+ // add more members in a semver-compatible way.
2528 _non_exhaustive: (),
2629}
2730
28−impl<'bump> RenderContext<'bump> {
29− // Don't use this! For internal use only! Subject to change in breaking ways
30− // without a breaking version bump!
31− #[doc(hidden)]
32− pub fn new(bump: &'bump Bump) -> Self {
33− RenderContext {
34− bump,
35− _non_exhaustive: (),
31+impl fmt::Debug for RenderContext<'_> {
32+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33+ f.debug_struct("RenderContext")
34+ .field("bump", &self.bump)
35+ .finish()
36+ }
37+}
38+
39+impl<'a> RenderContext<'a> {
40+ pub_unstable_internal! {
41+ pub(crate) fn new(bump: &'a Bump, cached_set: &'a crate::RefCell<CachedSet>) -> Self {
42+ RenderContext {
43+ bump,
44+ cached_set,
45+ _non_exhaustive: (),
46+ }
3647 }
3748 }
3849}
3950
40−impl<'a, 'bump> From<&'a RenderContext<'bump>> for &'bump Bump {
51+impl<'a, 'b> From<&'b RenderContext<'a>> for &'a Bump {
4152 #[inline]
42− fn from(cx: &'a RenderContext<'bump>) -> &'bump Bump {
53+ fn from(cx: &'b RenderContext<'a>) -> &'a Bump {
4354 cx.bump
4455 }
4556}
4657
47−impl<'a, 'b, 'bump> From<&'a &'b mut RenderContext<'bump>> for &'bump Bump {
58+impl<'a, 'b, 'c> From<&'c &'b mut RenderContext<'a>> for &'a Bump {
4859 #[inline]
49− fn from(cx: &'a &'b mut RenderContext<'bump>) -> &'bump Bump {
60+ fn from(cx: &'c &'b mut RenderContext<'a>) -> &'a Bump {
5061 cx.bump
5162 }
5263}
src/vdom.rs+32−237
@@ -1,13 +1,13 @@
11use super::change_list::ChangeList;
2−use super::node::{Attribute, ElementNode, Listener, Node, NodeKind, TextNode};
32use super::RootRender;
3+use crate::cached_set::CachedSet;
44use crate::events::EventsRegistry;
5+use crate::node::Node;
56use crate::RenderContext;
67use bumpalo::Bump;
78use futures::future::Future;
89use std::cell::Cell;
910use std::cell::RefCell;
10−use std::cmp;
1111use std::fmt;
1212use std::mem;
1313use std::mem::ManuallyDrop;
@@ -55,10 +55,11 @@ pub(crate) struct VdomInnerExclusive {
5555 // implementation.
5656 component: Option<Box<RootRender>>,
5757
58− dom_buffers: [Bump; 2],
58+ dom_buffers: Option<[Bump; 2]>,
5959 change_list: ManuallyDrop<ChangeList>,
6060 container: crate::Element,
6161 events_registry: Option<Rc<RefCell<EventsRegistry>>>,
62+ cached_set: crate::RefCell<CachedSet>,
6263
6364 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
6465 // caching renders, into `self.component`'s bump.
@@ -176,11 +177,12 @@ impl Vdom {
176177 },
177178 exclusive: RefCell::new(VdomInnerExclusive {
178179 component: Some(component),
179− dom_buffers,
180+ dom_buffers: Some(dom_buffers),
180181 change_list,
181182 container,
182183 current_root,
183184 events_registry: None,
185+ cached_set: crate::RefCell::new(Default::default()),
184186 }),
185187 });
186188
@@ -250,33 +252,42 @@ impl VdomInnerExclusive {
250252 pub(crate) fn render(&mut self) {
251253 unsafe {
252254 let events_registry = self.events_registry.take().unwrap();
253−
254255 {
255− // All the old listeners are no longer active. We will build a new
256− // set of active listeners when diffing.
257− //
258− // NB: if we end up avoiding diffing cached renders (instead of just
259− // avoiding re-rendering them) then we will need to maintain cached
260− // active listeners, and can't just clear all active listeners and
261− // rebuild them here.
262256 let mut registry = events_registry.borrow_mut();
263− registry.clear_active_listeners();
264257
265258 // Reset the inactive bump arena's pointer.
266− self.dom_buffers[1].reset();
259+ let mut dom_buffers = self.dom_buffers.take().unwrap_throw();
260+ dom_buffers[1].reset();
267261
268262 // Render the new current contents into the inactive bump arena.
269− let mut cx = RenderContext::new(&self.dom_buffers[1]);
263+ let mut cx = RenderContext::new(&dom_buffers[1], &self.cached_set);
270264 let new_contents = self.component.as_ref().unwrap_throw().render(&mut cx);
271265 let new_contents = extend_node_lifetime(new_contents);
272266
273267 // Diff the old contents with the new contents.
274268 let old_contents = self.current_root.take().unwrap();
275− self.diff(&mut registry, old_contents, new_contents.clone());
269+ let mut cache_roots = bumpalo::collections::Vec::new_in(&dom_buffers[1]);
270+ {
271+ let cached_set = self.cached_set.borrow();
272+ crate::diff::diff(
273+ &cached_set,
274+ &mut self.change_list,
275+ &mut registry,
276+ old_contents,
277+ new_contents.clone(),
278+ &mut cache_roots,
279+ );
280+ }
281+
282+ {
283+ // Clean up unused cached renders.
284+ let mut cached_set = self.cached_set.borrow_mut();
285+ cached_set.gc(&mut registry, &cache_roots);
286+ }
276287
277288 // Swap the buffers to make the bump arena with the new contents the
278289 // active arena, and the old one into the inactive arena.
279− self.swap_buffers();
290+ self.swap_buffers(dom_buffers);
280291 self.set_current_root(new_contents);
281292 }
282293
@@ -290,233 +301,17 @@ impl VdomInnerExclusive {
290301 }
291302 }
292303
293− fn swap_buffers(&mut self) {
294− let (first, second) = self.dom_buffers.as_mut().split_at_mut(1);
304+ fn swap_buffers(&mut self, mut dom_buffers: [Bump; 2]) {
305+ debug_assert!(self.dom_buffers.is_none());
306+ let (first, second) = dom_buffers.as_mut().split_at_mut(1);
295307 mem::swap(&mut first[0], &mut second[0]);
308+ self.dom_buffers = Some(dom_buffers);
296309 }
297310
298311 unsafe fn set_current_root(&mut self, current: Node<'static>) {
299312 debug_assert!(self.current_root.is_none());
300313 self.current_root = Some(current);
301314 }
302−
303− fn diff<'a>(&mut self, registry: &mut EventsRegistry, old: Node<'a>, new: Node<'a>) {
304− match (&new.kind, old.kind) {
305− (
306− &NodeKind::Text(TextNode { text: new_text }),
307− NodeKind::Text(TextNode { text: old_text }),
308− ) => {
309− debug!(" both are text nodes");
310− if new_text != old_text {
311− debug!(" text needs updating");
312− self.change_list.emit_set_text(new_text);
313− }
314− }
315− (&NodeKind::Text(_), NodeKind::Element(_)) => {
316− debug!(" replacing a text node with an element");
317− self.create(registry, new);
318− self.change_list.emit_replace_with();
319− }
320− (&NodeKind::Element(_), NodeKind::Text(_)) => {
321− debug!(" replacing an element with a text node");
322− self.create(registry, new);
323− self.change_list.emit_replace_with();
324− }
325− (
326− &NodeKind::Element(ElementNode {
327− tag_name: new_tag_name,
328− listeners: new_listeners,
329− attributes: new_attributes,
330− children: new_children,
331− namespace: new_namespace,
332− }),
333− NodeKind::Element(ElementNode {
334− tag_name: old_tag_name,
335− listeners: old_listeners,
336− attributes: old_attributes,
337− children: old_children,
338− namespace: old_namespace,
339− }),
340− ) => {
341− debug!(" updating an element");
342− if new_tag_name != old_tag_name || new_namespace != old_namespace {
343− debug!(" different tag names or namespaces; creating new element and replacing old element");
344− self.create(registry, new);
345− self.change_list.emit_replace_with();
346− return;
347− }
348− self.diff_listeners(registry, old_listeners, new_listeners);
349− self.diff_attributes(old_attributes, new_attributes);
350− self.diff_children(registry, old_children, new_children);
351− }
352− }
353− }
354−
355− fn diff_listeners<'a>(
356− &mut self,
357− registry: &mut EventsRegistry,
358− old: &'a [Listener<'a>],
359− new: &'a [Listener<'a>],
360− ) {
361− debug!(" updating event listeners");
362−
363− 'outer1: for new_l in new {
364− unsafe {
365− // Safety relies on removing `new_l` from the registry manually
366− // at the end of its lifetime. This happens when we invoke
367− // `clear_active_listeners` at the start of a new rendering
368− // phase.
369− registry.add(new_l);
370− }
371− for old_l in old {
372− if new_l.event == old_l.event {
373− self.change_list.emit_update_event_listener(new_l);
374− continue 'outer1;
375− }
376− }
377− self.change_list.emit_new_event_listener(new_l);
378− }
379−
380− 'outer2: for old_l in old {
381− for new_l in new {
382− if new_l.event == old_l.event {
383− continue 'outer2;
384− }
385− }
386− self.change_list.emit_remove_event_listener(old_l.event);
387− }
388− }
389−
390− fn diff_attributes(&mut self, old: &[Attribute], new: &[Attribute]) {
391− debug!(" updating attributes");
392−
393− // Do O(n^2) passes to add/update and remove attributes, since
394− // there are almost always very few attributes.
395− 'outer: for new_attr in new {
396− if new_attr.is_volatile() {
397− self.change_list
398− .emit_set_attribute(new_attr.name, new_attr.value);
399− } else {
400− for old_attr in old {
401− if old_attr.name == new_attr.name {
402− if old_attr.value != new_attr.value {
403− self.change_list
404− .emit_set_attribute(new_attr.name, new_attr.value);
405− }
406− continue 'outer;
407− }
408− }
409− self.change_list
410− .emit_set_attribute(new_attr.name, new_attr.value);
411− }
412− }
413−
414− 'outer2: for old_attr in old {
415− for new_attr in new {
416− if old_attr.name == new_attr.name {
417− continue 'outer2;
418− }
419− }
420− self.change_list.emit_remove_attribute(old_attr.name);
421− }
422− }
423−
424− fn diff_children<'a>(
425− &mut self,
426− registry: &mut EventsRegistry,
427− old: &'a [Node<'a>],
428− new: &'a [Node<'a>],
429− ) {
430− debug!(" updating children shared by old and new");
431−
432− let num_children_to_diff = cmp::min(new.len(), old.len());
433− let mut new_children = new.iter();
434− let mut old_children = old.iter();
435− let mut pushed = false;
436−
437− for (i, (new_child, old_child)) in new_children
438− .by_ref()
439− .zip(old_children.by_ref())
440− .take(num_children_to_diff)
441− .enumerate()
442− {
443− if i == 0 {
444− self.change_list.emit_push_first_child();
445− pushed = true;
446− } else {
447− debug_assert!(pushed);
448− self.change_list.emit_pop_push_next_sibling();
449− }
450−
451− self.diff(registry, old_child.clone(), new_child.clone());
452− }
453−
454− if old_children.next().is_some() {
455− debug!(" removing extra old children");
456− debug_assert!(new_children.next().is_none());
457− if !pushed {
458− self.change_list.emit_push_first_child();
459− } else {
460− self.change_list.emit_pop_push_next_sibling();
461− }
462− self.change_list.emit_remove_self_and_next_siblings();
463− pushed = false;
464− } else {
465− debug!(" creating new children");
466− for (i, new_child) in new_children.enumerate() {
467− if i == 0 && pushed {
468− self.change_list.emit_pop();
469− pushed = false;
470− }
471− self.create(registry, new_child.clone());
472− self.change_list.emit_append_child();
473− }
474− }
475−
476− debug!(" done updating children");
477− if pushed {
478− self.change_list.emit_pop();
479− }
480− }
481−
482− fn create<'a>(&mut self, registry: &mut EventsRegistry, node: Node<'a>) {
483− match node.kind {
484− NodeKind::Text(TextNode { text }) => {
485− self.change_list.emit_create_text_node(text);
486− }
487− NodeKind::Element(ElementNode {
488− tag_name,
489− listeners,
490− attributes,
491− children,
492− namespace,
493− }) => {
494− if let Some(namespace) = namespace {
495− self.change_list.emit_create_element_ns(tag_name, namespace);
496− } else {
497− self.change_list.emit_create_element(tag_name);
498− }
499− for l in listeners {
500− unsafe {
501− registry.add(l);
502− }
503− self.change_list.emit_new_event_listener(l);
504− }
505− for attr in attributes {
506− if namespace.is_none() || attr.name.starts_with("xmlns") {
507− self.change_list.emit_set_attribute(&attr.name, &attr.value);
508− } else {
509− self.change_list
510− .emit_set_attribute_ns(&attr.name, &attr.value);
511− }
512− }
513− for child in children {
514− self.create(registry, child.clone());
515− self.change_list.emit_append_child();
516− }
517− }
518− }
519− }
520315}
521316
522317fn request_animation_frame(f: &Closure<FnMut()>) {
tests/web/cached.rsnew file+234
@@ -0,0 +1,234 @@
1+use super::{assert_rendered, before_after, create_element, RenderFn};
2+use dodrio::{builder::*, bumpalo, Cached, Node, Render, RenderContext, Vdom};
3+use futures::prelude::*;
4+use std::cell::Cell;
5+use std::rc::Rc;
6+use wasm_bindgen::*;
7+use wasm_bindgen_test::*;
8+
9+pub struct CountRenders {
10+ render_count: Cell<usize>,
11+}
12+
13+impl CountRenders {
14+ pub fn new() -> CountRenders {
15+ CountRenders {
16+ render_count: Cell::new(0),
17+ }
18+ }
19+}
20+
21+impl Render for CountRenders {
22+ fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
23+ let count = self.render_count.get() + 1;
24+ self.render_count.set(count);
25+
26+ let s = bumpalo::format!(in cx.bump, "{}", count);
27+ text(s.into_bump_str())
28+ }
29+}
30+
31+#[wasm_bindgen_test(async)]
32+fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
33+ use dodrio::builder::*;
34+
35+ let cached = Cached::new(CountRenders::new());
36+
37+ let container0 = create_element("div");
38+ let container1 = container0.clone();
39+ let container2 = container0.clone();
40+ let container3 = container0.clone();
41+ let container4 = container0.clone();
42+
43+ let vdom0 = Rc::new(Vdom::new(&container0, cached));
44+ let vdom1 = vdom0.clone();
45+ let vdom2 = vdom0.clone();
46+ let vdom3 = vdom0.clone();
47+ let vdom4 = vdom0.clone();
48+ let vdom5 = vdom0.clone();
49+ let vdom6 = vdom0.clone();
50+ let vdom7 = vdom0.clone();
51+
52+ vdom0
53+ .weak()
54+ // We render, populate the cache, and get "1".
55+ .render()
56+ .and_then(move |_| {
57+ vdom0.weak().with_component(move |comp| {
58+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
59+ assert_eq!(comp.render_count.get(), 1);
60+ assert_rendered(&container1, &RenderFn(|_| text("1")));
61+ })
62+ })
63+ // We re-render, re-use the cached node, and get "1" again.
64+ .and_then(move |_| vdom1.weak().render())
65+ .and_then(move |_| {
66+ vdom2.weak().with_component(move |comp| {
67+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
68+ assert_eq!(comp.render_count.get(), 1);
69+ assert_rendered(&container2, &RenderFn(|_| text("1")));
70+ })
71+ })
72+ // We invalidate the cache, re-render, re-populate the cache, and should
73+ // now have "2".
74+ .and_then(move |_| {
75+ vdom3.weak().with_component(move |comp| {
76+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
77+ Cached::invalidate(comp);
78+ })
79+ })
80+ .and_then(move |_| vdom4.weak().render())
81+ .and_then(move |_| {
82+ vdom5.weak().with_component(move |comp| {
83+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
84+ assert_eq!(comp.render_count.get(), 2);
85+ assert_rendered(&container3, &RenderFn(|_| text("2")));
86+ })
87+ })
88+ // We re-render, re-use the cached node, and get "2" again.
89+ .and_then(move |_| vdom6.weak().render())
90+ .and_then(move |_| {
91+ vdom7.weak().with_component(move |comp| {
92+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
93+ assert_eq!(comp.render_count.get(), 2);
94+ assert_rendered(&container4, &RenderFn(|_| text("2")));
95+ })
96+ })
97+ .map_err(|e| JsValue::from(e.to_string()))
98+}
99+
100+struct Id(&'static str);
101+impl Render for Id {
102+ fn render<'a>(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
103+ text(self.0)
104+ }
105+}
106+
107+thread_local! {
108+ static WARM_CHEESE: Rc<Cached<Id>> = Rc::new(Cached::new(Id("cheese")));
109+ static WARM_CHEESIER: Rc<Cached<Id>> = Rc::new(Cached::new(Id("cheesier")));
110+}
111+
112+fn warm_cheese<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
113+ WARM_CHEESE.with(|c| {
114+ let _ = c.render(cx);
115+ c.render(cx)
116+ })
117+}
118+
119+fn warm_cheesier<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
120+ WARM_CHEESIER.with(|c| {
121+ let _ = c.render(cx);
122+ c.render(cx)
123+ })
124+}
125+
126+before_after! {
127+ cold_cache_and_not_cached {
128+ before(cx) {
129+ Cached::new(Id("cheese")).render(cx)
130+ }
131+ after(cx) {
132+ Id("ravioli").render(cx)
133+ }
134+ }
135+
136+ not_cached_and_cold_cache {
137+ before(cx) {
138+ Id("ravioli").render(cx)
139+ }
140+ after(cx) {
141+ Cached::new(Id("cheese")).render(cx)
142+ }
143+ }
144+
145+ cold_cache_and_cold_cache_same {
146+ before(cx) {
147+ Cached::new(Id("cheese")).render(cx)
148+ }
149+ after(cx) {
150+ Cached::new(Id("cheese")).render(cx)
151+ }
152+ }
153+
154+ cold_cache_and_cold_cache_different {
155+ before(cx) {
156+ Cached::new(Id("cheese")).render(cx)
157+ }
158+ after(cx) {
159+ Cached::new(Id("ravioli")).render(cx)
160+ }
161+ }
162+
163+ warm_cache_and_not_cached {
164+ before(cx) {
165+ warm_cheese(cx)
166+ }
167+ after(cx) {
168+ Id("ravioli").render(cx)
169+ }
170+ }
171+
172+ not_cached_and_warm_cache {
173+ before(cx) {
174+ Id("ravioli").render(cx)
175+ }
176+ after(cx) {
177+ warm_cheese(cx)
178+ }
179+ }
180+
181+ warm_cache_and_warm_cache_same {
182+ before(cx) {
183+ warm_cheese(cx)
184+ }
185+ after(cx) {
186+ warm_cheese(cx)
187+ }
188+ }
189+
190+ warm_cache_and_warm_cache_different {
191+ before(cx) {
192+ warm_cheese(cx)
193+ }
194+ after(cx) {
195+ warm_cheesier(cx)
196+ }
197+ }
198+
199+ cold_cache_and_warm_cache_same {
200+ before(cx) {
201+ Cached::new(Id("cheese")).render(cx)
202+ }
203+ after(cx) {
204+ warm_cheese(cx)
205+ }
206+ }
207+
208+ cold_cache_and_warm_cache_different {
209+ before(cx) {
210+ Cached::new(Id("cheese")).render(cx)
211+ }
212+ after(cx) {
213+ warm_cheesier(cx)
214+ }
215+ }
216+
217+ warm_cache_and_cold_cache_same {
218+ before(cx) {
219+ warm_cheese(cx)
220+ }
221+ after(cx) {
222+ Cached::new(Id("cheese")).render(cx)
223+ }
224+ }
225+
226+ warm_cache_and_cold_cache_different {
227+ before(cx) {
228+ warm_cheesier(cx)
229+ }
230+ after(cx) {
231+ Cached::new(Id("cheese")).render(cx)
232+ }
233+ }
234+}
tests/web/main.rs+18−7
@@ -3,9 +3,12 @@
33#![cfg(all(feature = "xxx-unstable-internal-use-only", target_arch = "wasm32"))]
44
55use bumpalo::Bump;
6−use dodrio::{Attribute, ElementNode, Node, NodeKind, Render, RenderContext, TextNode, Vdom};
6+use dodrio::{
7+ Attribute, CachedSet, ElementNode, Node, NodeKind, Render, RenderContext, TextNode, Vdom,
8+};
79use futures::prelude::*;
810use log::*;
11+use std::cell::RefCell;
912use std::rc::Rc;
1013use wasm_bindgen::prelude::*;
1114use wasm_bindgen::JsCast;
@@ -13,6 +16,7 @@ use wasm_bindgen_test::*;
1316
1417wasm_bindgen_test_configure!(run_in_browser);
1518
19+pub mod cached;
1620pub mod events;
1721pub mod js_api;
1822pub mod render;
@@ -48,13 +52,16 @@ pub fn init_logging() {
4852pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
4953 init_logging();
5054
55+ let cached_set = &RefCell::new(CachedSet::default());
5156 let bump = &Bump::new();
52− let cx = &mut RenderContext::new(bump);
57+ let cx = &mut RenderContext::new(bump, cached_set);
5358 let node = r.render(cx);
5459 let child = container
5560 .first_child()
5661 .expect("container does not have anything rendered into it?");
57− check_node(&child, &node);
62+
63+ let cached_set = cached_set.borrow();
64+ check_node(&cached_set, &child, &node);
5865
5966 fn stringify_actual_node(n: &web_sys::Node) -> String {
6067 if let Some(el) = n.dyn_ref::<web_sys::Element>() {
@@ -64,7 +71,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
6471 }
6572 }
6673
67− fn check_node(actual: &web_sys::Node, expected: &Node) {
74+ fn check_node(cached_set: &CachedSet, actual: &web_sys::Node, expected: &Node) {
6875 debug!("check_render:");
6976 debug!(" actual = {}", stringify_actual_node(&actual));
7077 debug!(" expected = {:#?}", expected);
@@ -97,11 +104,15 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
97104 .dyn_ref::<web_sys::Element>()
98105 .expect("`actual` should be an `Element`");
99106 check_attributes(actual.attributes(), attributes);
100− check_children(actual.child_nodes(), children);
107+ check_children(cached_set, actual.child_nodes(), children);
101108 if let Some(namespace) = namespace {
102109 assert_eq!(actual.namespace_uri(), Some(namespace.into()))
103110 }
104111 }
112+ NodeKind::Cached(ref c) => {
113+ let expected = cached_set.get(c.id);
114+ check_node(cached_set, actual, &expected);
115+ }
105116 }
106117 }
107118
@@ -124,7 +135,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
124135 }
125136 }
126137
127− fn check_children(actual: web_sys::NodeList, expected: &[Node]) {
138+ fn check_children(cached_set: &CachedSet, actual: web_sys::NodeList, expected: &[Node]) {
128139 assert_eq!(
129140 actual.length(),
130141 expected.len() as u32,
@@ -132,7 +143,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
132143 );
133144 for (i, child) in expected.iter().enumerate() {
134145 let actual_child = actual.item(i as u32).unwrap();
135− check_node(&actual_child, child);
146+ check_node(cached_set, &actual_child, child);
136147 }
137148 }
138149}