on master help sign in
Automatically create "templates" for `Cached<R>`

This adds a `R: Default` trait bound to `Cached<R>`, and the first time any instance of `Cached<R>` is rendered, we first render the default `R` instance and save it as a "template" for all future `Cached<R>` renders. The subsequent renders will call `cloneNode` (with the "deep" parameter set to true) on the template and then diff against the cloned node, rather than creating the whole subtree from scratch. This cuts down on DOM method invocations and traversal instructions.

commitf9901e6abdc905016f51480638a71c3a4628c612 authorNick Fitzgerald <fitzgen@gmail.com> dateFri 29 Mar 2019, 21:02 (8 years ago) parentc5925c4
13 files changed+509−53
benches/benches.rs+4−2 js/change-list-interpreter.js+26 js/strace.js+9 src/cached.rs+37−7 src/cached_set.rs+58−26 src/change_list/emitter.rs+16 src/change_list/mod.rs+44−1 src/diff.rs+201−9 src/render_context.rs+43−2 src/vdom.rs+7−2 tests/web/cached.js+11 tests/web/cached.rs+49−2 tests/web/main.rs+4−2
benches/benches.rs+4−2
@@ -46,9 +46,10 @@ fn criterion_benchmark(c: &mut Criterion) {
4646 Benchmark::new("empty", {
4747 let mut bump = Bump::new();
4848 let cached_set = RefCell::new(CachedSet::default());
49+ let mut templates = Default::default();
4950 move |b| {
5051 bump.reset();
51− let mut cx = RenderContext::new(&bump, &cached_set);
52+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
5253 b.iter(|| {
5354 black_box(Empty.render(&mut cx));
5455 })
@@ -63,9 +64,10 @@ fn criterion_benchmark(c: &mut Criterion) {
6364 {
6465 let mut bump = Bump::new();
6566 let cached_set = RefCell::new(CachedSet::default());
67+ let mut templates = Default::default();
6668 move |b, n| {
6769 bump.reset();
68− let mut cx = RenderContext::new(&bump, &cached_set);
70+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
6971 b.iter(|| {
7072 black_box(SimpleList(*n).render(&mut cx));
7173 })
js/change-list-interpreter.js+26
@@ -259,6 +259,22 @@ const OP_TABLE = [
259259 const className = interpreter.getCachedString(classId);
260260 top(interpreter.stack).className = className;
261261 return i;
262+ },
263+
264+ // 24
265+ function saveTemplate(interpreter, mem8, mem32, i) {
266+ const id = mem32[i++];
267+ const template = top(interpreter.stack);
268+ interpreter.saveTemplate(id, template.cloneNode(true));
269+ return i;
270+ },
271+
272+ // 25
273+ function pushTemplate(interpreter, mem8, mem32, i) {
274+ const id = mem32[i++];
275+ const template = interpreter.getTemplate(id);
276+ interpreter.stack.push(template.cloneNode(true));
277+ return i;
262278 }
263279];
264280
@@ -270,6 +286,7 @@ export class ChangeListInterpreter {
270286 this.stack = [];
271287 this.strings = new Map();
272288 this.temporaries = [];
289+ this.templates = new Map();
273290 }
274291
275292 unmount() {
@@ -283,6 +300,7 @@ export class ChangeListInterpreter {
283300 this.stack = null;
284301 this.strings = null;
285302 this.temporaries = null;
303+ this.templates = null;
286304 }
287305
288306 addChangeListRange(start, len) {
@@ -330,6 +348,14 @@ export class ChangeListInterpreter {
330348 return this.strings.get(id);
331349 }
332350
351+ saveTemplate(id, template) {
352+ this.templates.set(id, template);
353+ }
354+
355+ getTemplate(id) {
356+ return this.templates.get(id);
357+ }
358+
333359 initEventsTrampoline(trampoline) {
334360 this.trampoline = trampoline;
335361 trampoline.mounted = true;
js/strace.js+9
@@ -6,9 +6,13 @@ export function initStrace() {
66}
77
88const counts = new Map;
9+counts.total = 0;
10+
911let timer = null;
1012
1113function log(ctorName, kind, key) {
14+ counts.total += 1;
15+
1216 const entry = `${kind} ${ctorName}#${key}`
1317 const count = counts.get(entry);
1418 if (count === undefined) {
@@ -27,8 +31,13 @@ function dumpAndReset() {
2731 const data = [...counts]
2832 .sort((a, b) => b[1] - a[1])
2933 .map(a => ({ "DOM Method": a[0], "Count": a[1] }));
34+
35+ data.push({ "DOM Method": "<total>", "Count": counts.total });
36+
3037 console.table(data, ["DOM Method", "Count"]);
38+
3139 counts.clear();
40+ counts.total = 0;
3241}
3342
3443function instrument(proto, key, desc) {
src/cached.rs+37−7
@@ -3,18 +3,30 @@ use crate::{
33 node::{CachedNode, NodeKey},
44 Node, Render, RenderContext,
55};
6+use std::any::TypeId;
67use std::cell::Cell;
78use std::ops::{Deref, DerefMut};
89
910/// A renderable that supports caching for when rendering is expensive but can
1011/// generate the same DOM tree.
11−#[derive(Clone, Debug)]
12−pub struct Cached<R> {
12+#[derive(Clone, Debug, Default)]
13+pub struct Cached<R>
14+where
15+ R: Default,
16+{
1317 inner: R,
1418 cached: Cell<Option<CachedNode>>,
1519}
1620
17−impl<R> Cached<R> {
21+pub_unstable_internal! {
22+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
23+ pub(crate) struct TemplateId(TypeId);
24+}
25+
26+impl<R> Cached<R>
27+where
28+ R: Default,
29+{
1830 /// Construct a new `Cached<R>` of an inner `R`.
1931 ///
2032 /// # Example
@@ -22,6 +34,7 @@ impl<R> Cached<R> {
2234 /// ```
2335 /// use dodrio::{Cached, Node, Render, RenderContext};
2436 ///
37+ /// #[derive(Default)]
2538 /// pub struct Counter {
2639 /// count: u32,
2740 /// }
@@ -61,6 +74,7 @@ impl<R> Cached<R> {
6174 /// use dodrio::{bumpalo, Cached, Node, Render, RenderContext};
6275 ///
6376 /// /// A component that renders to "<p>Hello, {who}!</p>"
77+ /// #[derive(Default)]
6478 /// pub struct Hello {
6579 /// who: String
6680 /// }
@@ -94,7 +108,19 @@ impl<R> Cached<R> {
94108 }
95109}
96110
97−impl<R> Deref for Cached<R> {
111+impl<R> Cached<R>
112+where
113+ R: 'static + Default,
114+{
115+ pub(crate) fn template_id() -> TemplateId {
116+ TemplateId(TypeId::of::<R>())
117+ }
118+}
119+
120+impl<R> Deref for Cached<R>
121+where
122+ R: Default,
123+{
98124 type Target = R;
99125
100126 fn deref(&self) -> &R {
@@ -102,7 +128,10 @@ impl<R> Deref for Cached<R> {
102128 }
103129}
104130
105−impl<R> DerefMut for Cached<R> {
131+impl<R> DerefMut for Cached<R>
132+where
133+ R: Default,
134+{
106135 fn deref_mut(&mut self) -> &mut R {
107136 &mut self.inner
108137 }
@@ -110,9 +139,10 @@ impl<R> DerefMut for Cached<R> {
110139
111140impl<R> Render for Cached<R>
112141where
113− R: Render,
142+ R: 'static + Default + Render,
114143{
115144 fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
145+ let template = cx.template::<R>();
116146 let cached = match self.cached.get() {
117147 // This does-the-cache-contain-this-id check is necessary because
118148 // the same `Cached<R>` instance can be rendered into vdom A, which
@@ -151,7 +181,7 @@ where
151181 }
152182 _ => {
153183 let mut key = NodeKey::NONE;
154− let id = CachedSet::insert(cx, |nested_cx| {
184+ let id = CachedSet::insert(cx, false, template, |nested_cx| {
155185 let node = self.inner.render(nested_cx);
156186 key = node.key();
157187 node
src/cached_set.rs+58−26
@@ -35,6 +35,28 @@ pub(crate) struct CacheEntry {
3535 // once, we don't have to repeat the tracing every time we want to garbage
3636 // collect cache entries.
3737 edges: FxHashSet<CacheId>,
38+
39+ // The template for this cached virtual subtree.
40+ //
41+ // When a cache entry has a template, that means that the `ChangeList` will
42+ // lazily build a physical DOM subtree based on the template, and when we
43+ // create new versions of this cached result, we clone the template's
44+ // physical DOM subtree and then modify it, rather than building the cached
45+ // result up from scratch. This is a nice win because we bounce between JS
46+ // and DOM methods less often, and get the C++ DOM implementation to do most
47+ // of the subtree construction for us.
48+ template: Option<CacheId>,
49+
50+ // Whether this entry should never be garbage collected. Typically only
51+ // templates are pinned.
52+ pinned: bool,
53+}
54+
55+impl From<CacheId> for u32 {
56+ #[inline]
57+ fn from(id: CacheId) -> u32 {
58+ id.0
59+ }
3860}
3961
4062impl CachedSet {
@@ -62,7 +84,7 @@ impl CachedSet {
6284 }
6385
6486 self.items.retain(|id, entry| {
65− let keep = marked.contains(id);
87+ let keep = entry.pinned || marked.contains(id);
6688 if !keep {
6789 let node: &Node = unsafe { &*entry.node };
6890 registry.remove_subtree(node);
@@ -108,21 +130,38 @@ impl CachedSet {
108130 CacheId(next.expect_throw("ID_COUNTER overflowed"))
109131 }
110132
111− pub(crate) fn insert<F>(cx: &mut RenderContext, f: F) -> CacheId
133+ pub(crate) fn insert<F>(
134+ cx: &mut RenderContext,
135+ pinned: bool,
136+ template: Option<CacheId>,
137+ f: F,
138+ ) -> CacheId
112139 where
113140 F: for<'a> FnOnce(&mut RenderContext<'a>) -> Node<'a>,
114141 {
115− let bump = Bump::new();
116142 let set = cx.cached_set;
117− let mut nested_cx = RenderContext::new(&bump, set);
118− let node = f(&mut nested_cx);
119− let node = bump.alloc(node);
120− let edges = {
121− let set = set.borrow();
122− set.trace(node)
143+ let bump = Bump::new();
144+ let (node, edges) = {
145+ let mut nested_cx = RenderContext::new(&bump, cx.cached_set, cx.templates);
146+ let node = f(&mut nested_cx);
147+ let node = bump.alloc(node);
148+ let edges = {
149+ let set = set.borrow();
150+ set.trace(node)
151+ };
152+ (
153+ node as *mut Node<'_> as usize as *const Node<'static>,
154+ edges,
155+ )
156+ };
157+
158+ let entry = CacheEntry {
159+ bump,
160+ node,
161+ edges,
162+ template,
163+ pinned,
123164 };
124− let node = node as *mut Node<'_> as usize as *const Node<'static>;
125− let entry = CacheEntry { bump, node, edges };
126165
127166 let mut set = set.borrow_mut();
128167 let id = set.next_id();
@@ -135,20 +174,13 @@ impl CachedSet {
135174 self.items.contains_key(&id)
136175 }
137176
138− /// Get the node for the given cache id.
139− pub fn get(&self, mut id: CacheId) -> &Node {
140− loop {
141− let entry = self
142− .items
143− .get(&id)
144− .expect_throw("CachedSet::get: should have id in set");
145− let node: &Node = unsafe { &*entry.node };
146− if let NodeKind::Cached(ref c) = node.kind {
147− id = c.id;
148− continue;
149− } else {
150− return node;
151− }
152− }
177+ /// Get the cached node and its template (if any) for the given cache id.
178+ pub fn get(&self, id: CacheId) -> (&Node, Option<CacheId>) {
179+ let entry = self
180+ .items
181+ .get(&id)
182+ .expect_throw("CachedSet::get: should have id in set");
183+ let node: &Node = unsafe { &*entry.node };
184+ (node, entry.template)
153185 }
154186}
src/change_list/emitter.rs+16
@@ -272,4 +272,20 @@ define_change_list_instructions! {
272272 /// node.className = class
273273 /// ```
274274 set_class(class) = 23,
275+
276+ /// Stack: `[... Node] -> [... Node]`
277+ ///
278+ /// ```text
279+ /// template = stack.top()
280+ /// saveTemplate(id, template)
281+ /// ```
282+ save_template(id) = 24,
283+
284+ /// Stack: `[...] -> [... Node]`
285+ ///
286+ /// ```text
287+ /// template = getTemplate(id)
288+ /// stack.push(template.cloneNode(true))
289+ /// ```
290+ push_template(id) = 25,
275291}
src/change_list/mod.rs+44−1
@@ -8,7 +8,8 @@ pub mod js;
88use self::emitter::InstructionEmitter;
99use self::strings::{StringKey, StringsCache};
1010use self::traversal::{MoveTo, Traversal};
11−use crate::Listener;
11+use crate::{cached_set::CacheId, Listener};
12+use fxhash::FxHashSet;
1213
1314#[derive(Debug)]
1415pub(crate) struct ChangeListPersistentState {
@@ -16,11 +17,13 @@ pub(crate) struct ChangeListPersistentState {
1617 emitter: InstructionEmitter,
1718 traversal: Traversal,
1819 interpreter: js::ChangeListInterpreter,
20+ templates: FxHashSet<CacheId>,
1921}
2022
2123pub(crate) struct ChangeListBuilder<'a> {
2224 state: &'a mut ChangeListPersistentState,
2325 next_temporary: u32,
26+ forcing_new_listeners: bool,
2427}
2528
2629impl Drop for ChangeListPersistentState {
@@ -35,11 +38,13 @@ impl ChangeListPersistentState {
3538 let emitter = InstructionEmitter::new();
3639 let traversal = Traversal::new();
3740 let interpreter = js::ChangeListInterpreter::new(container);
41+ let templates = Default::default();
3842 ChangeListPersistentState {
3943 strings,
4044 emitter,
4145 traversal,
4246 interpreter,
47+ templates,
4348 }
4449 }
4550
@@ -51,6 +56,7 @@ impl ChangeListPersistentState {
5156 ChangeListBuilder {
5257 state: self,
5358 next_temporary: 0,
59+ forcing_new_listeners: false,
5460 }
5561 }
5662}
@@ -281,6 +287,17 @@ impl ChangeListBuilder<'_> {
281287 .create_element_ns(tag_name_id.into(), ns_id.into());
282288 }
283289
290+ pub fn push_force_new_listeners(&mut self) -> bool {
291+ let old = self.forcing_new_listeners;
292+ self.forcing_new_listeners = true;
293+ old
294+ }
295+
296+ pub fn pop_force_new_listeners(&mut self, previous: bool) {
297+ debug_assert!(self.forcing_new_listeners);
298+ self.forcing_new_listeners = previous;
299+ }
300+
284301 pub fn new_event_listener(&mut self, listener: &Listener) {
285302 debug_assert!(self.traversal_is_committed());
286303 debug!("emit: new_event_listener({:?})", listener);
@@ -292,6 +309,12 @@ impl ChangeListBuilder<'_> {
292309
293310 pub fn update_event_listener(&mut self, listener: &Listener) {
294311 debug_assert!(self.traversal_is_committed());
312+
313+ if self.forcing_new_listeners {
314+ self.new_event_listener(listener);
315+ return;
316+ }
317+
295318 debug!("emit: update_event_listener({:?})", listener);
296319 let (a, b) = listener.get_callback_parts();
297320 debug_assert!(a != 0);
@@ -307,4 +330,24 @@ impl ChangeListBuilder<'_> {
307330 let event_id = self.ensure_string(event);
308331 self.state.emitter.remove_event_listener(event_id.into());
309332 }
333+
334+ #[inline]
335+ pub fn has_template(&mut self, id: CacheId) -> bool {
336+ self.state.templates.contains(&id)
337+ }
338+
339+ pub fn save_template(&mut self, id: CacheId) {
340+ debug_assert!(self.traversal_is_committed());
341+ debug_assert!(!self.has_template(id));
342+ debug!("emit: save_template({:?})", id);
343+ self.state.templates.insert(id);
344+ self.state.emitter.save_template(id.into());
345+ }
346+
347+ pub fn push_template(&mut self, id: CacheId) {
348+ debug_assert!(self.traversal_is_committed());
349+ debug_assert!(self.has_template(id));
350+ debug!("emit: push_template({:?})", id);
351+ self.state.emitter.push_template(id.into());
352+ }
310353}
src/diff.rs+201−9
@@ -98,9 +98,27 @@ pub(crate) fn diff(
9898 return;
9999 }
100100
101− let new = cached_set.get(new.id);
102− let old = cached_set.get(old.id);
103− diff(cached_set, change_list, registry, old, new, cached_roots);
101+ let (new, new_template) = cached_set.get(new.id);
102+ let (old, old_template) = cached_set.get(old.id);
103+ if new_template == old_template {
104+ // If they are both using the same template, then just diff the
105+ // subtrees.
106+ diff(cached_set, change_list, registry, old, new, cached_roots);
107+ } else {
108+ // Otherwise, they are probably different enough that
109+ // re-constructing the subtree from scratch should be faster.
110+ // This doubly holds true if we have a new template.
111+ change_list.commit_traversal();
112+ create_and_replace(
113+ cached_set,
114+ change_list,
115+ registry,
116+ new_template,
117+ old,
118+ new,
119+ cached_roots,
120+ );
121+ }
104122 }
105123
106124 // New cached node when the old node was not cached. In this scenario,
@@ -109,10 +127,16 @@ pub(crate) fn diff(
109127 (&NodeKind::Cached(ref c), _) => {
110128 change_list.commit_traversal();
111129 cached_roots.insert(c.id);
112− let new = cached_set.get(c.id);
113− create(cached_set, change_list, registry, new, cached_roots);
114− registry.remove_subtree(&old);
115− change_list.replace_with();
130+ let (new, new_template) = cached_set.get(c.id);
131+ create_and_replace(
132+ cached_set,
133+ change_list,
134+ registry,
135+ new_template,
136+ old,
137+ new,
138+ cached_roots,
139+ );
116140 }
117141
118142 // Old cached node and new non-cached node. Again, assume that they are
@@ -239,6 +263,35 @@ fn diff_children(
239263 return;
240264 }
241265
266+ if new.len() == 1 {
267+ match (old.first(), &new[0]) {
268+ (
269+ Some(&Node {
270+ kind: NodeKind::Text(TextNode { text: old_text }),
271+ }),
272+ &Node {
273+ kind: NodeKind::Text(TextNode { text: new_text }),
274+ },
275+ ) if old_text == new_text => {
276+ // Don't take this fast path...
277+ }
278+ (
279+ _,
280+ &Node {
281+ kind: NodeKind::Text(TextNode { text }),
282+ },
283+ ) => {
284+ change_list.commit_traversal();
285+ change_list.set_text(text);
286+ for o in old {
287+ registry.remove_subtree(o);
288+ }
289+ return;
290+ }
291+ (_, _) => {}
292+ }
293+ }
294+
242295 if old.is_empty() {
243296 if !new.is_empty() {
244297 change_list.commit_traversal();
@@ -926,8 +979,147 @@ fn create(
926979 }
927980 NodeKind::Cached(ref c) => {
928981 cached_roots.insert(c.id);
929− let node = cached_set.get(c.id);
930− create(cached_set, change_list, registry, node, cached_roots)
982+ let (node, template) = cached_set.get(c.id);
983+ if let Some(template) = template {
984+ create_with_template(
985+ cached_set,
986+ change_list,
987+ registry,
988+ template,
989+ node,
990+ cached_roots,
991+ );
992+ } else {
993+ create(cached_set, change_list, registry, node, cached_roots);
994+ }
995+ }
996+ }
997+}
998+
999+// Get or create the template.
1000+//
1001+// Upon entering this function the change list stack may be in any shape:
1002+//
1003+// [...]
1004+//
1005+// When this function returns, it leaves a freshly cloned copy of the template
1006+// on the top of the change list stack:
1007+//
1008+// [... template]
1009+#[inline]
1010+fn get_or_create_template<'a>(
1011+ cached_set: &'a CachedSet,
1012+ change_list: &mut ChangeListBuilder,
1013+ registry: &mut EventsRegistry,
1014+ cached_roots: &mut FxHashSet<CacheId>,
1015+ template_id: CacheId,
1016+) -> (&'a Node<'a>, bool) {
1017+ let (template, template_template) = cached_set.get(template_id);
1018+ debug_assert!(
1019+ template_template.is_none(),
1020+ "templates should not be templated themselves"
1021+ );
1022+
1023+ // If we haven't already created and saved the physical DOM subtree for this
1024+ // template, do that now.
1025+ if change_list.has_template(template_id) {
1026+ // Clone the template and push it onto the stack.
1027+ //
1028+ // [...]
1029+ change_list.push_template(template_id);
1030+ // [... template]
1031+
1032+ (template, true)
1033+ } else {
1034+ // [...]
1035+ create(cached_set, change_list, registry, template, cached_roots);
1036+ // [... template]
1037+ change_list.save_template(template_id);
1038+ // [... template]
1039+
1040+ (template, false)
1041+ }
1042+}
1043+
1044+fn create_and_replace(
1045+ cached_set: &CachedSet,
1046+ change_list: &mut ChangeListBuilder,
1047+ registry: &mut EventsRegistry,
1048+ new_template: Option<CacheId>,
1049+ old: &Node,
1050+ new: &Node,
1051+ cached_roots: &mut FxHashSet<CacheId>,
1052+) {
1053+ debug_assert!(change_list.traversal_is_committed());
1054+
1055+ if let Some(template_id) = new_template {
1056+ let (template, needs_listeners) =
1057+ get_or_create_template(cached_set, change_list, registry, cached_roots, template_id);
1058+ change_list.replace_with();
1059+
1060+ let mut old_forcing = None;
1061+ if needs_listeners {
1062+ old_forcing = Some(change_list.push_force_new_listeners());
9311063 }
1064+
1065+ diff(
1066+ cached_set,
1067+ change_list,
1068+ registry,
1069+ template,
1070+ new,
1071+ cached_roots,
1072+ );
1073+
1074+ if let Some(old) = old_forcing {
1075+ change_list.pop_force_new_listeners(old);
1076+ }
1077+
1078+ change_list.commit_traversal();
1079+ } else {
1080+ create(cached_set, change_list, registry, new, cached_roots);
1081+ change_list.replace_with();
9321082 }
1083+ registry.remove_subtree(old);
1084+}
1085+
1086+fn create_with_template(
1087+ cached_set: &CachedSet,
1088+ change_list: &mut ChangeListBuilder,
1089+ registry: &mut EventsRegistry,
1090+ template_id: CacheId,
1091+ node: &Node,
1092+ cached_roots: &mut FxHashSet<CacheId>,
1093+) {
1094+ debug_assert!(change_list.traversal_is_committed());
1095+
1096+ // [...]
1097+ let (template, needs_listeners) =
1098+ get_or_create_template(cached_set, change_list, registry, cached_roots, template_id);
1099+ // [... template]
1100+
1101+ // Now diff the node with its template.
1102+ //
1103+ // We must force adding new listeners instead of updating existing ones,
1104+ // since listeners don't get cloned in `cloneNode`.
1105+ let mut old_forcing = None;
1106+ if needs_listeners {
1107+ old_forcing = Some(change_list.push_force_new_listeners());
1108+ }
1109+
1110+ diff(
1111+ cached_set,
1112+ change_list,
1113+ registry,
1114+ template,
1115+ node,
1116+ cached_roots,
1117+ );
1118+
1119+ if let Some(old) = old_forcing {
1120+ change_list.pop_force_new_listeners(old);
1121+ }
1122+
1123+ // Make sure that we come back up to the level we were at originally.
1124+ change_list.commit_traversal();
9331125}
src/render_context.rs+43−2
@@ -1,5 +1,10 @@
1−use crate::cached_set::CachedSet;
1+use crate::{
2+ cached::{Cached, TemplateId},
3+ cached_set::{CacheId, CachedSet},
4+ Node, Render,
5+};
26use bumpalo::Bump;
7+use fxhash::FxHashMap;
38use std::fmt;
49
510/// Common context available to all `Render` implementations.
@@ -23,6 +28,8 @@ pub struct RenderContext<'a> {
2328
2429 pub(crate) cached_set: &'a crate::RefCell<CachedSet>,
2530
31+ pub(crate) templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>,
32+
2633 // Prevent exhaustive matching on the rendering context, so we can always
2734 // add more members in a semver-compatible way.
2835 _non_exhaustive: (),
@@ -38,14 +45,48 @@ impl fmt::Debug for RenderContext<'_> {
3845
3946impl<'a> RenderContext<'a> {
4047 pub_unstable_internal! {
41− pub(crate) fn new(bump: &'a Bump, cached_set: &'a crate::RefCell<CachedSet>) -> Self {
48+ pub(crate) fn new(
49+ bump: &'a Bump,
50+ cached_set: &'a crate::RefCell<CachedSet>,
51+ templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>
52+ ) -> Self {
4253 RenderContext {
4354 bump,
4455 cached_set,
56+ templates,
4557 _non_exhaustive: (),
4658 }
4759 }
4860 }
61+
62+ pub(crate) fn cache<F>(&mut self, pinned: bool, template: Option<CacheId>, f: F) -> CacheId
63+ where
64+ F: for<'b> FnOnce(&mut RenderContext<'b>) -> Node<'b>,
65+ {
66+ CachedSet::insert(self, pinned, template, f)
67+ }
68+
69+ /// Get or create the cached template for `Cached<R>`.
70+ pub(crate) fn template<R>(&mut self) -> Option<CacheId>
71+ where
72+ R: 'static + Default + Render,
73+ {
74+ let template_id = Cached::<R>::template_id();
75+ if let Some(cache_id) = self.templates.get(&template_id).cloned() {
76+ return cache_id;
77+ }
78+
79+ // Prevent re-entrancy from infinite looping. Any attempts to get `R`'s
80+ // template while constructing the template will simply fail to use the
81+ // templated fast path.
82+ self.templates.insert(template_id, None);
83+
84+ // Render the default `R` and save that as the template for all
85+ // `Cached<R>`s.
86+ let cache_id = self.cache(true, None, |nested_cx| R::default().render(nested_cx));
87+ self.templates.insert(template_id, Some(cache_id));
88+ Some(cache_id)
89+ }
4990}
5091
5192impl<'a, 'b> From<&'b RenderContext<'a>> for &'a Bump {
src/vdom.rs+7−2
@@ -1,11 +1,13 @@
11use super::change_list::ChangeListPersistentState;
22use super::RootRender;
3−use crate::cached_set::CachedSet;
3+use crate::cached::TemplateId;
4+use crate::cached_set::{CacheId, CachedSet};
45use crate::events::EventsRegistry;
56use crate::node::{Node, NodeKey};
67use crate::RenderContext;
78use bumpalo::Bump;
89use futures::future::Future;
10+use fxhash::FxHashMap;
911use std::cell::Cell;
1012use std::cell::RefCell;
1113use std::fmt;
@@ -61,6 +63,7 @@ pub(crate) struct VdomInnerExclusive {
6163 events_registry: Option<Rc<RefCell<EventsRegistry>>>,
6264 events_trampoline: Option<crate::EventsTrampoline>,
6365 cached_set: crate::RefCell<CachedSet>,
66+ templates: FxHashMap<TemplateId, Option<CacheId>>,
6467
6568 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
6669 // caching renders, into `self.component`'s bump.
@@ -186,6 +189,7 @@ impl Vdom {
186189 events_registry: None,
187190 events_trampoline: None,
188191 cached_set: crate::RefCell::new(Default::default()),
192+ templates: Default::default(),
189193 }),
190194 });
191195
@@ -265,7 +269,8 @@ impl VdomInnerExclusive {
265269 dom_buffers[1].reset();
266270
267271 // Render the new current contents into the inactive bump arena.
268− let mut cx = RenderContext::new(&dom_buffers[1], &self.cached_set);
272+ let mut cx =
273+ RenderContext::new(&dom_buffers[1], &self.cached_set, &mut self.templates);
269274 let new_contents = self.component.as_ref().unwrap_throw().render(&mut cx);
270275 let new_contents = extend_node_lifetime(new_contents);
271276
tests/web/cached.jsnew file+11
@@ -0,0 +1,11 @@
1+let cloneNodeCount = 0;
2+
3+const origCloneNode = Node.prototype.cloneNode;
4+Node.prototype.cloneNode = function (...args) {
5+ cloneNodeCount += 1;
6+ return origCloneNode.apply(this, args);
7+};
8+
9+export function getCloneNodeCount() {
10+ return cloneNodeCount;
11+}
tests/web/cached.rs+49−2
@@ -1,11 +1,12 @@
11use super::{assert_rendered, before_after, create_element, RenderFn};
2−use dodrio::{builder::*, bumpalo, Cached, Node, Render, RenderContext, Vdom};
2+use dodrio::{builder::*, bumpalo, Cached, Node, Render, RenderContext, RootRender, Vdom};
33use futures::prelude::*;
44use std::cell::Cell;
55use std::rc::Rc;
6−use wasm_bindgen::*;
6+use wasm_bindgen::prelude::*;
77use wasm_bindgen_test::*;
88
9+#[derive(Default)]
910pub struct CountRenders {
1011 render_count: Cell<usize>,
1112}
@@ -97,7 +98,53 @@ fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
9798 .map_err(|e| JsValue::from(e.to_string()))
9899}
99100
101+#[wasm_bindgen(module = "/tests/web/cached.js")]
102+extern "C" {
103+ #[wasm_bindgen(js_name = getCloneNodeCount)]
104+ fn get_clone_node_count() -> u32;
105+}
106+
107+#[wasm_bindgen_test(async)]
108+fn creating_cached_nodes_uses_clone_node() -> impl Future<Item = (), Error = JsValue> {
109+ use dodrio::builder::*;
110+
111+ let container = create_element("div");
112+
113+ // The initial vdom render should populate the cache and save a template.
114+ let vdom0 = Rc::new(Vdom::new(&container, Cached::new(CountRenders::new())));
115+ let vdom1 = vdom0.clone();
116+ let vdom2 = vdom0.clone();
117+
118+ let clone_node_count_before = get_clone_node_count();
119+
120+ vdom0
121+ .weak()
122+ // Render something entirely different...
123+ .set_component(Box::new(RenderFn(|_| text("hi"))) as Box<RootRender>)
124+ .and_then(move |_| {
125+ // Re-render our cached node. Since we are creating it from scratch,
126+ // and already have a template from the earlier render, we should
127+ // call `cloneNode` here.
128+ vdom1
129+ .weak()
130+ .set_component(Box::new(Cached::new(CountRenders::new())) as Box<RootRender>)
131+ })
132+ .map(move |_| {
133+ let clone_node_count_after = get_clone_node_count();
134+ assert_eq!(clone_node_count_after, clone_node_count_before + 1);
135+ drop(vdom2);
136+ })
137+ .map_err(|e| JsValue::from(e.to_string()))
138+}
139+
100140struct Id(&'static str);
141+
142+impl Default for Id {
143+ fn default() -> Id {
144+ Id("")
145+ }
146+}
147+
101148impl Render for Id {
102149 fn render<'a>(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
103150 text(self.0)
tests/web/main.rs+4−2
@@ -7,6 +7,7 @@ use dodrio::{
77 Attribute, CachedSet, ElementNode, Node, NodeKind, Render, RenderContext, TextNode, Vdom,
88};
99use futures::prelude::*;
10+use fxhash::FxHashMap;
1011use log::*;
1112use std::cell::RefCell;
1213use std::rc::Rc;
@@ -55,7 +56,8 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
5556
5657 let cached_set = &RefCell::new(CachedSet::default());
5758 let bump = &Bump::new();
58− let cx = &mut RenderContext::new(bump, cached_set);
59+ let templates = &mut FxHashMap::default();
60+ let cx = &mut RenderContext::new(bump, cached_set, templates);
5961 let node = r.render(cx);
6062 let child = container
6163 .first_child()
@@ -111,7 +113,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
111113 }
112114 }
113115 NodeKind::Cached(ref c) => {
114− let expected = cached_set.get(c.id);
116+ let (expected, _template) = cached_set.get(c.id);
115117 check_node(cached_set, actual, &expected);
116118 }
117119 }