on master help sign in
Merge pull request #71 from fitzgen/use-templates-and-clone-node

Automatically create "templates" for `Cached<R>`

commit22faf1eb60f9bcb16094e1b2f4575edfb203338e authorNick Fitzgerald <fitzgen@gmail.com> dateFri 3 May 2019, 09:16 (7 years ago) parent6679c9bf9901e6
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
@@ -273,4 +273,20 @@ define_change_list_instructions! {
273273 /// node.className = class
274274 /// ```
275275 set_class(class) = 23,
276+
277+ /// Stack: `[... Node] -> [... Node]`
278+ ///
279+ /// ```text
280+ /// template = stack.top()
281+ /// saveTemplate(id, template)
282+ /// ```
283+ save_template(id) = 24,
284+
285+ /// Stack: `[...] -> [... Node]`
286+ ///
287+ /// ```text
288+ /// template = getTemplate(id)
289+ /// stack.push(template.cloneNode(true))
290+ /// ```
291+ push_template(id) = 25,
276292}
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 }