on master help sign in
Automatically create "templates" for `Cached<R>`
f9901e6Nick Fitzgerald
benches/benches.rs+4−2
@@ -46,9 +46,10 @@ fn criterion_benchmark(c: &mut Criterion) {
46 Benchmark::new("empty", {
46 Benchmark::new("empty", {
47 let mut bump = Bump::new();
47 let mut bump = Bump::new();
48 let cached_set = RefCell::new(CachedSet::default());
48 let cached_set = RefCell::new(CachedSet::default());
49+ let mut templates = Default::default();
49 move |b| {
50 move |b| {
50 bump.reset();
51 bump.reset();
51− let mut cx = RenderContext::new(&bump, &cached_set);
52+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
52 b.iter(|| {
53 b.iter(|| {
53 black_box(Empty.render(&mut cx));
54 black_box(Empty.render(&mut cx));
54 })
55 })
@@ -63,9 +64,10 @@ fn criterion_benchmark(c: &mut Criterion) {
63 {
64 {
64 let mut bump = Bump::new();
65 let mut bump = Bump::new();
65 let cached_set = RefCell::new(CachedSet::default());
66 let cached_set = RefCell::new(CachedSet::default());
67+ let mut templates = Default::default();
66 move |b, n| {
68 move |b, n| {
67 bump.reset();
69 bump.reset();
68− let mut cx = RenderContext::new(&bump, &cached_set);
70+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
69 b.iter(|| {
71 b.iter(|| {
70 black_box(SimpleList(*n).render(&mut cx));
72 black_box(SimpleList(*n).render(&mut cx));
71 })
73 })
js/change-list-interpreter.js+26
@@ -259,6 +259,22 @@ const OP_TABLE = [
259 const className = interpreter.getCachedString(classId);
259 const className = interpreter.getCachedString(classId);
260 top(interpreter.stack).className = className;
260 top(interpreter.stack).className = className;
261 return i;
261 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;
262 }
278 }
263];
279];
264
280
@@ -270,6 +286,7 @@ export class ChangeListInterpreter {
270 this.stack = [];
286 this.stack = [];
271 this.strings = new Map();
287 this.strings = new Map();
272 this.temporaries = [];
288 this.temporaries = [];
289+ this.templates = new Map();
273 }
290 }
274
291
275 unmount() {
292 unmount() {
@@ -283,6 +300,7 @@ export class ChangeListInterpreter {
283 this.stack = null;
300 this.stack = null;
284 this.strings = null;
301 this.strings = null;
285 this.temporaries = null;
302 this.temporaries = null;
303+ this.templates = null;
286 }
304 }
287
305
288 addChangeListRange(start, len) {
306 addChangeListRange(start, len) {
@@ -330,6 +348,14 @@ export class ChangeListInterpreter {
330 return this.strings.get(id);
348 return this.strings.get(id);
331 }
349 }
332
350
351+ saveTemplate(id, template) {
352+ this.templates.set(id, template);
353+ }
354+
355+ getTemplate(id) {
356+ return this.templates.get(id);
357+ }
358+
333 initEventsTrampoline(trampoline) {
359 initEventsTrampoline(trampoline) {
334 this.trampoline = trampoline;
360 this.trampoline = trampoline;
335 trampoline.mounted = true;
361 trampoline.mounted = true;
js/strace.js+9
@@ -6,9 +6,13 @@ export function initStrace() {
6}
6}
7
7
8const counts = new Map;
8const counts = new Map;
9+counts.total = 0;
10+
9let timer = null;
11let timer = null;
10
12
11function log(ctorName, kind, key) {
13function log(ctorName, kind, key) {
14+ counts.total += 1;
15+
12 const entry = `${kind} ${ctorName}#${key}`
16 const entry = `${kind} ${ctorName}#${key}`
13 const count = counts.get(entry);
17 const count = counts.get(entry);
14 if (count === undefined) {
18 if (count === undefined) {
@@ -27,8 +31,13 @@ function dumpAndReset() {
27 const data = [...counts]
31 const data = [...counts]
28 .sort((a, b) => b[1] - a[1])
32 .sort((a, b) => b[1] - a[1])
29 .map(a => ({ "DOM Method": a[0], "Count": a[1] }));
33 .map(a => ({ "DOM Method": a[0], "Count": a[1] }));
34+
35+ data.push({ "DOM Method": "<total>", "Count": counts.total });
36+
30 console.table(data, ["DOM Method", "Count"]);
37 console.table(data, ["DOM Method", "Count"]);
38+
31 counts.clear();
39 counts.clear();
40+ counts.total = 0;
32}
41}
33
42
34function instrument(proto, key, desc) {
43function instrument(proto, key, desc) {
src/cached.rs+37−7
@@ -3,18 +3,30 @@ use crate::{
3 node::{CachedNode, NodeKey},
3 node::{CachedNode, NodeKey},
4 Node, Render, RenderContext,
4 Node, Render, RenderContext,
5};
5};
6+use std::any::TypeId;
6use std::cell::Cell;
7use std::cell::Cell;
7use std::ops::{Deref, DerefMut};
8use std::ops::{Deref, DerefMut};
8
9
9/// A renderable that supports caching for when rendering is expensive but can
10/// A renderable that supports caching for when rendering is expensive but can
10/// generate the same DOM tree.
11/// generate the same DOM tree.
11−#[derive(Clone, Debug)]
12+#[derive(Clone, Debug, Default)]
12−pub struct Cached<R> {
13+pub struct Cached<R>
14+where
15+ R: Default,
16+{
13 inner: R,
17 inner: R,
14 cached: Cell<Option<CachedNode>>,
18 cached: Cell<Option<CachedNode>>,
15}
19}
16
20
21+pub_unstable_internal! {
22+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
23+ pub(crate) struct TemplateId(TypeId);
24+}
25+
17−impl<R> Cached<R> {
26+impl<R> Cached<R>
27+where
28+ R: Default,
29+{
18 /// Construct a new `Cached<R>` of an inner `R`.
30 /// Construct a new `Cached<R>` of an inner `R`.
19 ///
31 ///
20 /// # Example
32 /// # Example
@@ -22,6 +34,7 @@ impl<R> Cached<R> {
22 /// ```
34 /// ```
23 /// use dodrio::{Cached, Node, Render, RenderContext};
35 /// use dodrio::{Cached, Node, Render, RenderContext};
24 ///
36 ///
37+ /// #[derive(Default)]
25 /// pub struct Counter {
38 /// pub struct Counter {
26 /// count: u32,
39 /// count: u32,
27 /// }
40 /// }
@@ -61,6 +74,7 @@ impl<R> Cached<R> {
61 /// use dodrio::{bumpalo, Cached, Node, Render, RenderContext};
74 /// use dodrio::{bumpalo, Cached, Node, Render, RenderContext};
62 ///
75 ///
63 /// /// A component that renders to "<p>Hello, {who}!</p>"
76 /// /// A component that renders to "<p>Hello, {who}!</p>"
77+ /// #[derive(Default)]
64 /// pub struct Hello {
78 /// pub struct Hello {
65 /// who: String
79 /// who: String
66 /// }
80 /// }
@@ -94,7 +108,19 @@ impl<R> Cached<R> {
94 }
108 }
95}
109}
96
110
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+
97−impl<R> Deref for Cached<R> {
120+impl<R> Deref for Cached<R>
121+where
122+ R: Default,
123+{
98 type Target = R;
124 type Target = R;
99
125
100 fn deref(&self) -> &R {
126 fn deref(&self) -> &R {
@@ -102,7 +128,10 @@ impl<R> Deref for Cached<R> {
102 }
128 }
103}
129}
104
130
105−impl<R> DerefMut for Cached<R> {
131+impl<R> DerefMut for Cached<R>
132+where
133+ R: Default,
134+{
106 fn deref_mut(&mut self) -> &mut R {
135 fn deref_mut(&mut self) -> &mut R {
107 &mut self.inner
136 &mut self.inner
108 }
137 }
@@ -110,9 +139,10 @@ impl<R> DerefMut for Cached<R> {
110
139
111impl<R> Render for Cached<R>
140impl<R> Render for Cached<R>
112where
141where
113− R: Render,
142+ R: 'static + Default + Render,
114{
143{
115 fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
144 fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
145+ let template = cx.template::<R>();
116 let cached = match self.cached.get() {
146 let cached = match self.cached.get() {
117 // This does-the-cache-contain-this-id check is necessary because
147 // This does-the-cache-contain-this-id check is necessary because
118 // the same `Cached<R>` instance can be rendered into vdom A, which
148 // the same `Cached<R>` instance can be rendered into vdom A, which
@@ -151,7 +181,7 @@ where
151 }
181 }
152 _ => {
182 _ => {
153 let mut key = NodeKey::NONE;
183 let mut key = NodeKey::NONE;
154− let id = CachedSet::insert(cx, |nested_cx| {
184+ let id = CachedSet::insert(cx, false, template, |nested_cx| {
155 let node = self.inner.render(nested_cx);
185 let node = self.inner.render(nested_cx);
156 key = node.key();
186 key = node.key();
157 node
187 node
src/cached_set.rs+58−26
@@ -35,6 +35,28 @@ pub(crate) struct CacheEntry {
35 // once, we don't have to repeat the tracing every time we want to garbage
35 // once, we don't have to repeat the tracing every time we want to garbage
36 // collect cache entries.
36 // collect cache entries.
37 edges: FxHashSet<CacheId>,
37 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+ }
38}
60}
39
61
40impl CachedSet {
62impl CachedSet {
@@ -62,7 +84,7 @@ impl CachedSet {
62 }
84 }
63
85
64 self.items.retain(|id, entry| {
86 self.items.retain(|id, entry| {
65− let keep = marked.contains(id);
87+ let keep = entry.pinned || marked.contains(id);
66 if !keep {
88 if !keep {
67 let node: &Node = unsafe { &*entry.node };
89 let node: &Node = unsafe { &*entry.node };
68 registry.remove_subtree(node);
90 registry.remove_subtree(node);
@@ -108,21 +130,38 @@ impl CachedSet {
108 CacheId(next.expect_throw("ID_COUNTER overflowed"))
130 CacheId(next.expect_throw("ID_COUNTER overflowed"))
109 }
131 }
110
132
133+ pub(crate) fn insert<F>(
111− pub(crate) fn insert<F>(cx: &mut RenderContext, f: F) -> CacheId
134+ cx: &mut RenderContext,
135+ pinned: bool,
136+ template: Option<CacheId>,
137+ f: F,
138+ ) -> CacheId
112 where
139 where
113 F: for<'a> FnOnce(&mut RenderContext<'a>) -> Node<'a>,
140 F: for<'a> FnOnce(&mut RenderContext<'a>) -> Node<'a>,
114 {
141 {
115− let bump = Bump::new();
116 let set = cx.cached_set;
142 let set = cx.cached_set;
143+ let bump = Bump::new();
144+ let (node, edges) = {
117− let mut nested_cx = RenderContext::new(&bump, set);
145+ let mut nested_cx = RenderContext::new(&bump, cx.cached_set, cx.templates);
118− let node = f(&mut nested_cx);
146+ let node = f(&mut nested_cx);
119− let node = bump.alloc(node);
147+ let node = bump.alloc(node);
120− let edges = {
148+ let edges = {
121− let set = set.borrow();
149+ let set = set.borrow();
122− set.trace(node)
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,
123 };
164 };
124− let node = node as *mut Node<'_> as usize as *const Node<'static>;
125− let entry = CacheEntry { bump, node, edges };
126
165
127 let mut set = set.borrow_mut();
166 let mut set = set.borrow_mut();
128 let id = set.next_id();
167 let id = set.next_id();
@@ -135,20 +174,13 @@ impl CachedSet {
135 self.items.contains_key(&id)
174 self.items.contains_key(&id)
136 }
175 }
137
176
138− /// Get the node for the given cache id.
177+ /// Get the cached node and its template (if any) for the given cache id.
139− pub fn get(&self, mut id: CacheId) -> &Node {
178+ pub fn get(&self, id: CacheId) -> (&Node, Option<CacheId>) {
140− loop {
141− let entry = self
179+ let entry = self
142− .items
180+ .items
143− .get(&id)
181+ .get(&id)
144− .expect_throw("CachedSet::get: should have id in set");
182+ .expect_throw("CachedSet::get: should have id in set");
145− let node: &Node = unsafe { &*entry.node };
183+ let node: &Node = unsafe { &*entry.node };
146− if let NodeKind::Cached(ref c) = node.kind {
184+ (node, entry.template)
147− id = c.id;
148− continue;
149− } else {
150− return node;
151− }
152− }
153 }
185 }
154}
186}
src/change_list/emitter.rs+16
@@ -272,4 +272,20 @@ define_change_list_instructions! {
272 /// node.className = class
272 /// node.className = class
273 /// ```
273 /// ```
274 set_class(class) = 23,
274 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,
275}
291}
src/change_list/mod.rs+44−1
@@ -8,7 +8,8 @@ pub mod js;
8use self::emitter::InstructionEmitter;
8use self::emitter::InstructionEmitter;
9use self::strings::{StringKey, StringsCache};
9use self::strings::{StringKey, StringsCache};
10use self::traversal::{MoveTo, Traversal};
10use self::traversal::{MoveTo, Traversal};
11−use crate::Listener;
11+use crate::{cached_set::CacheId, Listener};
12+use fxhash::FxHashSet;
12
13
13#[derive(Debug)]
14#[derive(Debug)]
14pub(crate) struct ChangeListPersistentState {
15pub(crate) struct ChangeListPersistentState {
@@ -16,11 +17,13 @@ pub(crate) struct ChangeListPersistentState {
16 emitter: InstructionEmitter,
17 emitter: InstructionEmitter,
17 traversal: Traversal,
18 traversal: Traversal,
18 interpreter: js::ChangeListInterpreter,
19 interpreter: js::ChangeListInterpreter,
20+ templates: FxHashSet<CacheId>,
19}
21}
20
22
21pub(crate) struct ChangeListBuilder<'a> {
23pub(crate) struct ChangeListBuilder<'a> {
22 state: &'a mut ChangeListPersistentState,
24 state: &'a mut ChangeListPersistentState,
23 next_temporary: u32,
25 next_temporary: u32,
26+ forcing_new_listeners: bool,
24}
27}
25
28
26impl Drop for ChangeListPersistentState {
29impl Drop for ChangeListPersistentState {
@@ -35,11 +38,13 @@ impl ChangeListPersistentState {
35 let emitter = InstructionEmitter::new();
38 let emitter = InstructionEmitter::new();
36 let traversal = Traversal::new();
39 let traversal = Traversal::new();
37 let interpreter = js::ChangeListInterpreter::new(container);
40 let interpreter = js::ChangeListInterpreter::new(container);
41+ let templates = Default::default();
38 ChangeListPersistentState {
42 ChangeListPersistentState {
39 strings,
43 strings,
40 emitter,
44 emitter,
41 traversal,
45 traversal,
42 interpreter,
46 interpreter,
47+ templates,
43 }
48 }
44 }
49 }
45
50
@@ -51,6 +56,7 @@ impl ChangeListPersistentState {
51 ChangeListBuilder {
56 ChangeListBuilder {
52 state: self,
57 state: self,
53 next_temporary: 0,
58 next_temporary: 0,
59+ forcing_new_listeners: false,
54 }
60 }
55 }
61 }
56}
62}
@@ -281,6 +287,17 @@ impl ChangeListBuilder<'_> {
281 .create_element_ns(tag_name_id.into(), ns_id.into());
287 .create_element_ns(tag_name_id.into(), ns_id.into());
282 }
288 }
283
289
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+
284 pub fn new_event_listener(&mut self, listener: &Listener) {
301 pub fn new_event_listener(&mut self, listener: &Listener) {
285 debug_assert!(self.traversal_is_committed());
302 debug_assert!(self.traversal_is_committed());
286 debug!("emit: new_event_listener({:?})", listener);
303 debug!("emit: new_event_listener({:?})", listener);
@@ -292,6 +309,12 @@ impl ChangeListBuilder<'_> {
292
309
293 pub fn update_event_listener(&mut self, listener: &Listener) {
310 pub fn update_event_listener(&mut self, listener: &Listener) {
294 debug_assert!(self.traversal_is_committed());
311 debug_assert!(self.traversal_is_committed());
312+
313+ if self.forcing_new_listeners {
314+ self.new_event_listener(listener);
315+ return;
316+ }
317+
295 debug!("emit: update_event_listener({:?})", listener);
318 debug!("emit: update_event_listener({:?})", listener);
296 let (a, b) = listener.get_callback_parts();
319 let (a, b) = listener.get_callback_parts();
297 debug_assert!(a != 0);
320 debug_assert!(a != 0);
@@ -307,4 +330,24 @@ impl ChangeListBuilder<'_> {
307 let event_id = self.ensure_string(event);
330 let event_id = self.ensure_string(event);
308 self.state.emitter.remove_event_listener(event_id.into());
331 self.state.emitter.remove_event_listener(event_id.into());
309 }
332 }
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+ }
310}
353}
src/diff.rs+201−9
@@ -98,9 +98,27 @@ pub(crate) fn diff(
98 return;
98 return;
99 }
99 }
100
100
101− let new = cached_set.get(new.id);
101+ let (new, new_template) = cached_set.get(new.id);
102− let old = cached_set.get(old.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.
103− diff(cached_set, change_list, registry, old, new, cached_roots);
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+ }
104 }
122 }
105
123
106 // New cached node when the old node was not cached. In this scenario,
124 // New cached node when the old node was not cached. In this scenario,
@@ -109,10 +127,16 @@ pub(crate) fn diff(
109 (&NodeKind::Cached(ref c), _) => {
127 (&NodeKind::Cached(ref c), _) => {
110 change_list.commit_traversal();
128 change_list.commit_traversal();
111 cached_roots.insert(c.id);
129 cached_roots.insert(c.id);
112− let new = cached_set.get(c.id);
130+ let (new, new_template) = cached_set.get(c.id);
113− create(cached_set, change_list, registry, new, cached_roots);
131+ create_and_replace(
114− registry.remove_subtree(&old);
132+ cached_set,
115− change_list.replace_with();
133+ change_list,
134+ registry,
135+ new_template,
136+ old,
137+ new,
138+ cached_roots,
139+ );
116 }
140 }
117
141
118 // Old cached node and new non-cached node. Again, assume that they are
142 // Old cached node and new non-cached node. Again, assume that they are
@@ -239,6 +263,35 @@ fn diff_children(
239 return;
263 return;
240 }
264 }
241
265
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+
242 if old.is_empty() {
295 if old.is_empty() {
243 if !new.is_empty() {
296 if !new.is_empty() {
244 change_list.commit_traversal();
297 change_list.commit_traversal();
@@ -926,8 +979,147 @@ fn create(
926 }
979 }
927 NodeKind::Cached(ref c) => {
980 NodeKind::Cached(ref c) => {
928 cached_roots.insert(c.id);
981 cached_roots.insert(c.id);
929− let node = cached_set.get(c.id);
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 {
930− create(cached_set, change_list, registry, node, cached_roots)
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());
931 }
1063 }
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();
932 }
1082 }
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();
933}
1125}
src/render_context.rs+43−2
@@ -1,5 +1,10 @@
1+use crate::{
2+ cached::{Cached, TemplateId},
1−use crate::cached_set::CachedSet;
3+ cached_set::{CacheId, CachedSet},
4+ Node, Render,
5+};
2use bumpalo::Bump;
6use bumpalo::Bump;
7+use fxhash::FxHashMap;
3use std::fmt;
8use std::fmt;
4
9
5/// Common context available to all `Render` implementations.
10/// Common context available to all `Render` implementations.
@@ -23,6 +28,8 @@ pub struct RenderContext<'a> {
23
28
24 pub(crate) cached_set: &'a crate::RefCell<CachedSet>,
29 pub(crate) cached_set: &'a crate::RefCell<CachedSet>,
25
30
31+ pub(crate) templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>,
32+
26 // Prevent exhaustive matching on the rendering context, so we can always
33 // Prevent exhaustive matching on the rendering context, so we can always
27 // add more members in a semver-compatible way.
34 // add more members in a semver-compatible way.
28 _non_exhaustive: (),
35 _non_exhaustive: (),
@@ -38,14 +45,48 @@ impl fmt::Debug for RenderContext<'_> {
38
45
39impl<'a> RenderContext<'a> {
46impl<'a> RenderContext<'a> {
40 pub_unstable_internal! {
47 pub_unstable_internal! {
48+ pub(crate) fn new(
49+ bump: &'a Bump,
41− pub(crate) fn new(bump: &'a Bump, cached_set: &'a crate::RefCell<CachedSet>) -> Self {
50+ cached_set: &'a crate::RefCell<CachedSet>,
51+ templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>
52+ ) -> Self {
42 RenderContext {
53 RenderContext {
43 bump,
54 bump,
44 cached_set,
55 cached_set,
56+ templates,
45 _non_exhaustive: (),
57 _non_exhaustive: (),
46 }
58 }
47 }
59 }
48 }
60 }
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+ }
49}
90}
50
91
51impl<'a, 'b> From<&'b RenderContext<'a>> for &'a Bump {
92impl<'a, 'b> From<&'b RenderContext<'a>> for &'a Bump {
src/vdom.rs+7−2
@@ -1,11 +1,13 @@
1use super::change_list::ChangeListPersistentState;
1use super::change_list::ChangeListPersistentState;
2use super::RootRender;
2use super::RootRender;
3+use crate::cached::TemplateId;
3−use crate::cached_set::CachedSet;
4+use crate::cached_set::{CacheId, CachedSet};
4use crate::events::EventsRegistry;
5use crate::events::EventsRegistry;
5use crate::node::{Node, NodeKey};
6use crate::node::{Node, NodeKey};
6use crate::RenderContext;
7use crate::RenderContext;
7use bumpalo::Bump;
8use bumpalo::Bump;
8use futures::future::Future;
9use futures::future::Future;
10+use fxhash::FxHashMap;
9use std::cell::Cell;
11use std::cell::Cell;
10use std::cell::RefCell;
12use std::cell::RefCell;
11use std::fmt;
13use std::fmt;
@@ -61,6 +63,7 @@ pub(crate) struct VdomInnerExclusive {
61 events_registry: Option<Rc<RefCell<EventsRegistry>>>,
63 events_registry: Option<Rc<RefCell<EventsRegistry>>>,
62 events_trampoline: Option<crate::EventsTrampoline>,
64 events_trampoline: Option<crate::EventsTrampoline>,
63 cached_set: crate::RefCell<CachedSet>,
65 cached_set: crate::RefCell<CachedSet>,
66+ templates: FxHashMap<TemplateId, Option<CacheId>>,
64
67
65 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
68 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
66 // caching renders, into `self.component`'s bump.
69 // caching renders, into `self.component`'s bump.
@@ -186,6 +189,7 @@ impl Vdom {
186 events_registry: None,
189 events_registry: None,
187 events_trampoline: None,
190 events_trampoline: None,
188 cached_set: crate::RefCell::new(Default::default()),
191 cached_set: crate::RefCell::new(Default::default()),
192+ templates: Default::default(),
189 }),
193 }),
190 });
194 });
191
195
@@ -265,7 +269,8 @@ impl VdomInnerExclusive {
265 dom_buffers[1].reset();
269 dom_buffers[1].reset();
266
270
267 // Render the new current contents into the inactive bump arena.
271 // Render the new current contents into the inactive bump arena.
272+ let mut cx =
268− let mut cx = RenderContext::new(&dom_buffers[1], &self.cached_set);
273+ RenderContext::new(&dom_buffers[1], &self.cached_set, &mut self.templates);
269 let new_contents = self.component.as_ref().unwrap_throw().render(&mut cx);
274 let new_contents = self.component.as_ref().unwrap_throw().render(&mut cx);
270 let new_contents = extend_node_lifetime(new_contents);
275 let new_contents = extend_node_lifetime(new_contents);
271
276
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 @@
1use super::{assert_rendered, before_after, create_element, RenderFn};
1use 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};
3use futures::prelude::*;
3use futures::prelude::*;
4use std::cell::Cell;
4use std::cell::Cell;
5use std::rc::Rc;
5use std::rc::Rc;
6−use wasm_bindgen::*;
6+use wasm_bindgen::prelude::*;
7use wasm_bindgen_test::*;
7use wasm_bindgen_test::*;
8
8
9+#[derive(Default)]
9pub struct CountRenders {
10pub struct CountRenders {
10 render_count: Cell<usize>,
11 render_count: Cell<usize>,
11}
12}
@@ -97,7 +98,53 @@ fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
97 .map_err(|e| JsValue::from(e.to_string()))
98 .map_err(|e| JsValue::from(e.to_string()))
98}
99}
99
100
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+
100struct Id(&'static str);
140struct Id(&'static str);
141+
142+impl Default for Id {
143+ fn default() -> Id {
144+ Id("")
145+ }
146+}
147+
101impl Render for Id {
148impl Render for Id {
102 fn render<'a>(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
149 fn render<'a>(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
103 text(self.0)
150 text(self.0)
tests/web/main.rs+4−2
@@ -7,6 +7,7 @@ use dodrio::{
7 Attribute, CachedSet, ElementNode, Node, NodeKind, Render, RenderContext, TextNode, Vdom,
7 Attribute, CachedSet, ElementNode, Node, NodeKind, Render, RenderContext, TextNode, Vdom,
8};
8};
9use futures::prelude::*;
9use futures::prelude::*;
10+use fxhash::FxHashMap;
10use log::*;
11use log::*;
11use std::cell::RefCell;
12use std::cell::RefCell;
12use std::rc::Rc;
13use std::rc::Rc;
@@ -55,7 +56,8 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
55
56
56 let cached_set = &RefCell::new(CachedSet::default());
57 let cached_set = &RefCell::new(CachedSet::default());
57 let bump = &Bump::new();
58 let bump = &Bump::new();
59+ let templates = &mut FxHashMap::default();
58− let cx = &mut RenderContext::new(bump, cached_set);
60+ let cx = &mut RenderContext::new(bump, cached_set, templates);
59 let node = r.render(cx);
61 let node = r.render(cx);
60 let child = container
62 let child = container
61 .first_child()
63 .first_child()
@@ -111,7 +113,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
111 }
113 }
112 }
114 }
113 NodeKind::Cached(ref c) => {
115 NodeKind::Cached(ref c) => {
114− let expected = cached_set.get(c.id);
116+ let (expected, _template) = cached_set.get(c.id);
115 check_node(cached_set, actual, &expected);
117 check_node(cached_set, actual, &expected);
116 }
118 }
117 }
119 }