on master help sign in
Merge pull request #61 from fitzgen/traversal-optimization-take-2

Traversal optimization + some little things

commitd27867800a2b2a95c1a799b9b9da7efad76db8e4 authorNick Fitzgerald <fitzgen@gmail.com> dateMon 22 Apr 2019, 14:15 (8 years ago) parent19718f82c6e730
12 files changed+693−136
Cargo.toml+8 js/change-list-interpreter.js+18−7 js/strace.js+71 src/builder.rs+12−3 src/change_list/emitter.rs+14−8 src/change_list/mod.rs+96−25 src/change_list/traversal.rs+344 src/diff.rs+67−67 src/lib.rs+2−1 src/node.rs+35−24 src/strace.rs+22 src/vdom.rs+4−1
Cargo.toml+8
@@ -29,6 +29,14 @@ harness = false
2929required-features = ["xxx-unstable-internal-use-only"]
3030
3131[features]
32+# Don't rely on this feature! Its behavior will change and it could go away at
33+# any time. This enables monkey-patching DOM methods so that we wrap them and
34+# trace what DOM methods are used and how many times they are invoked when
35+# applying our emitted change lists to the physical DOM. This is useful for
36+# evaluating the differences between how Dodrio applies changes from virtual
37+# nodes to the physical DOM versus how other virtual DOM libraries do the same.
38+xxx-unstable-strace = []
39+
3240# Don't use this feature! It only exists for exposing otherwise-internal APIs
3341# for testing and benchmarks.
3442xxx-unstable-internal-use-only = []
js/change-list-interpreter.js+18−7
@@ -86,15 +86,23 @@ const OP_TABLE = [
8686 },
8787
8888 // 5
89− function pushFirstChild(interpreter, mem8, mem32, i) {
90− interpreter.stack.push(top(interpreter.stack).firstChild);
89+ function pushReverseChild(interpreter, mem8, mem32, i) {
90+ const n = mem32[i++];
91+ const parent = top(interpreter.stack);
92+ const children = parent.childNodes;
93+ const child = children[children.length - n - 1];
94+ interpreter.stack.push(child);
9195 return i;
9296 },
9397
9498 // 6
95− function popPushNextSibling(interpreter, mem8, mem32, i) {
96− const node = interpreter.stack.pop();
97− interpreter.stack.push(node.nextSibling);
99+ function popPushChild(interpreter, mem8, mem32, i) {
100+ const n = mem32[i++];
101+ interpreter.stack.pop();
102+ const parent = top(interpreter.stack);
103+ const children = parent.childNodes;
104+ const child = children[n];
105+ interpreter.stack.push(child);
98106 return i;
99107 },
100108
@@ -226,9 +234,12 @@ const OP_TABLE = [
226234 },
227235
228236 // 21
229− function pushLastChild(interpreter, mem8, mem32, i) {
237+ function popPushReverseChild(interpreter, mem8, mem32, i) {
238+ const n = mem32[i++];
239+ interpreter.stack.pop();
230240 const parent = top(interpreter.stack);
231− const child = parent.lastChild;
241+ const children = parent.childNodes;
242+ const child = children[children.length - n - 1];
232243 interpreter.stack.push(child);
233244 return i;
234245 },
js/strace.jsnew file+71
@@ -0,0 +1,71 @@
1+export function initStrace() {
2+ instrumentAll(Element.prototype);
3+ instrumentAll(Document.prototype);
4+ instrumentAll(Node.prototype);
5+ instrumentAll(EventTarget.prototype);
6+}
7+
8+const counts = new Map;
9+let timer = null;
10+
11+function log(ctorName, kind, key) {
12+ const entry = `${kind} ${ctorName}#${key}`
13+ const count = counts.get(entry);
14+ if (count === undefined) {
15+ counts.set(entry, 1);
16+ } else {
17+ counts.set(entry, count + 1);
18+ }
19+
20+ if (timer !== null) {
21+ clearTimeout(timer);
22+ }
23+ timer = setTimeout(dumpAndReset, 100);
24+}
25+
26+function dumpAndReset() {
27+ const data = [...counts]
28+ .sort((a, b) => b[1] - a[1])
29+ .map(a => ({ "DOM Method": a[0], "Count": a[1] }));
30+ console.table(data, ["DOM Method", "Count"]);
31+ counts.clear();
32+}
33+
34+function instrument(proto, key, desc) {
35+ const ctorName = proto.constructor.name;
36+
37+ if (typeof desc.value == "function") {
38+ const f = desc.value;
39+ desc.value = function (...args) {
40+ log(ctorName, "call", key);
41+ return f.apply(this, args);
42+ };
43+ }
44+
45+ if (typeof desc.get == "function") {
46+ const get = desc.get;
47+ desc.get = function (...args) {
48+ log(ctorName, "get", key);
49+ return get.apply(this, args);
50+ };
51+ }
52+
53+ if (typeof desc.set == "function") {
54+ const set = desc.set;
55+ desc.set = function (...args) {
56+ log(ctorName, "set", key);
57+ return set.apply(this, args);
58+ };
59+ }
60+
61+ Object.defineProperty(proto, key, desc);
62+}
63+
64+function instrumentAll(proto) {
65+ for (const key of Object.keys(proto)) {
66+ const desc = Object.getOwnPropertyDescriptor(proto, key);
67+ if (desc.configurable) {
68+ instrument(proto, key, desc);
69+ }
70+ }
71+}
src/builder.rs+12−3
@@ -282,13 +282,22 @@ where
282282 /// ```
283283 #[inline]
284284 pub fn finish(self) -> Node<'a> {
285+ let children: &'a Children = self.bump.alloc(self.children);
286+ let children: &'a [Node<'a>] = children.as_ref();
287+
288+ let listeners: &'a Listeners = self.bump.alloc(self.listeners);
289+ let listeners: &'a [Listener<'a>] = listeners.as_ref();
290+
291+ let attributes: &'a Attributes = self.bump.alloc(self.attributes);
292+ let attributes: &'a [Attribute<'a>] = attributes.as_ref();
293+
285294 Node::element(
286295 self.bump,
287296 self.key,
288297 self.tag_name,
289− self.listeners,
290− self.attributes,
291− self.children,
298+ listeners,
299+ attributes,
300+ children,
292301 self.namespace,
293302 )
294303 }
src/change_list/emitter.rs+14−8
@@ -114,16 +114,21 @@ define_change_list_instructions! {
114114 /// Stack: `[... Node] -> [... Node Node]`
115115 ///
116116 /// ```text
117− /// stack.push(stack.top().firstChild)
117+ /// parent = stack.top()
118+ /// child = parent.childNodes[parent.childNodes.length - n - 1]
119+ /// stack.push(child)
118120 /// ```
119− push_first_child() = 5,
121+ push_reverse_child(n) = 5,
120122
121− /// Stack: `[... Node] -> [... Node]`
123+ /// Stack: `[... Node Node] -> [... Node Node]`
122124 ///
123125 /// ```text
124− /// stack.push(stack.pop().nextSibling)
126+ /// stack.pop();
127+ /// parent = stack.top();
128+ /// child = parent.childNodes[n]
129+ /// stack.push(child)
125130 /// ```
126− pop_push_next_sibling() = 6,
131+ pop_push_child(n) = 6,
127132
128133 /// Stack: `[... T] -> [...]`
129134 ///
@@ -240,14 +245,15 @@ define_change_list_instructions! {
240245 /// ```
241246 insert_before() = 20,
242247
243− /// Stack: `[... Node] -> [... Node Node]`
248+ /// Stack: `[... Node Node] -> [... Node Node]`
244249 ///
245250 /// ```text
251+ /// stack.pop()
246252 /// parent = stack.top()
247− /// child = parent.lastChild
253+ /// child = parent.childNodes[parent.childNodes.length - n - 1]
248254 /// stack.push(child)
249255 /// ```
250− push_last_child() = 21,
256+ pop_push_reverse_child(n) = 21,
251257
252258 /// Stack: `[... Node] -> [... Node]`
253259 ///
src/change_list/mod.rs+96−25
@@ -1,17 +1,20 @@
11pub(crate) mod emitter;
22pub(crate) mod strings;
3+pub(crate) mod traversal;
34
45// Note: has to be `pub` because of `wasm-bindgen` visibility restrictions.
56pub mod js;
67
78use self::emitter::InstructionEmitter;
89use self::strings::{StringKey, StringsCache};
10+use self::traversal::{MoveTo, Traversal};
911use crate::Listener;
1012
1113#[derive(Debug)]
1214pub(crate) struct ChangeListPersistentState {
1315 strings: StringsCache,
1416 emitter: InstructionEmitter,
17+ traversal: Traversal,
1518 interpreter: js::ChangeListInterpreter,
1619}
1720
@@ -30,10 +33,12 @@ impl ChangeListPersistentState {
3033 pub(crate) fn new(container: &crate::Element) -> ChangeListPersistentState {
3134 let strings = StringsCache::new();
3235 let emitter = InstructionEmitter::new();
36+ let traversal = Traversal::new();
3337 let interpreter = js::ChangeListInterpreter::new(container);
3438 ChangeListPersistentState {
3539 strings,
3640 emitter,
41+ traversal,
3742 interpreter,
3843 }
3944 }
@@ -59,6 +64,7 @@ cfg_if::cfg_if! {
5964 // Nothing to actually apply the changes to.
6065
6166 self.state.emitter.reset();
67+ self.state.traversal.reset();
6268 }
6369 }
6470 } else {
@@ -74,11 +80,85 @@ cfg_if::cfg_if! {
7480 interpreter.apply_changes(wasm_bindgen::memory());
7581
7682 self.state.emitter.reset();
83+ self.state.traversal.reset();
7784 }
7885 }
7986 }
8087}
8188
89+/// Traversal methods.
90+impl ChangeListBuilder<'_> {
91+ pub fn go_down(&mut self) {
92+ self.state.traversal.down();
93+ }
94+
95+ pub fn go_down_to_child(&mut self, index: usize) {
96+ self.state.traversal.down();
97+ self.state.traversal.sibling(index);
98+ }
99+
100+ pub fn go_down_to_reverse_child(&mut self, index: usize) {
101+ self.state.traversal.down();
102+ self.state.traversal.reverse_sibling(index);
103+ }
104+
105+ pub fn go_up(&mut self) {
106+ self.state.traversal.up();
107+ }
108+
109+ pub fn go_to_sibling(&mut self, index: usize) {
110+ self.state.traversal.sibling(index);
111+ }
112+
113+ pub fn go_to_temp_sibling(&mut self, temp: u32) {
114+ self.state.traversal.up();
115+ self.state.traversal.down_to_temp(temp);
116+ }
117+
118+ pub fn go_down_to_temp_child(&mut self, temp: u32) {
119+ self.state.traversal.down_to_temp(temp);
120+ }
121+
122+ pub fn commit_traversal(&mut self) {
123+ if self.state.traversal.is_committed() {
124+ return;
125+ }
126+
127+ for mv in self.state.traversal.commit() {
128+ match mv {
129+ MoveTo::Parent => {
130+ debug!("emit: pop");
131+ self.state.emitter.pop();
132+ }
133+ MoveTo::Child(n) => {
134+ debug!("emit: push_child({})", n);
135+ self.state.emitter.push_child(n);
136+ }
137+ MoveTo::ReverseChild(n) => {
138+ debug!("emit: push_reverse_child({})", n);
139+ self.state.emitter.push_reverse_child(n);
140+ }
141+ MoveTo::Sibling(n) => {
142+ debug!("emit: pop_push_child({})", n);
143+ self.state.emitter.pop_push_child(n);
144+ }
145+ MoveTo::ReverseSibling(n) => {
146+ debug!("emit: pop_push_reverse_child({})", n);
147+ self.state.emitter.pop_push_reverse_child(n);
148+ }
149+ MoveTo::TempChild(temp) => {
150+ debug!("emit: push_temporary({})", temp);
151+ self.state.emitter.push_temporary(temp);
152+ }
153+ }
154+ }
155+ }
156+
157+ pub fn traversal_is_committed(&self) -> bool {
158+ self.state.traversal.is_committed()
159+ }
160+}
161+
82162impl ChangeListBuilder<'_> {
83163 pub fn next_temporary(&self) -> u32 {
84164 self.next_temporary
@@ -89,6 +169,7 @@ impl ChangeListBuilder<'_> {
89169 }
90170
91171 pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 {
172+ debug_assert!(self.traversal_is_committed());
92173 debug_assert!(start < end);
93174 let temp_base = self.next_temporary;
94175 debug!(
@@ -103,26 +184,19 @@ impl ChangeListBuilder<'_> {
103184 }
104185
105186 pub fn push_temporary(&self, temp: u32) {
187+ debug_assert!(self.traversal_is_committed());
106188 debug!("emit: push_temporary({})", temp);
107189 self.state.emitter.push_temporary(temp);
108190 }
109191
110− pub fn push_child(&self, child: usize) {
111− debug!("emit: push_child({})", child);
112− self.state.emitter.push_child(child as u32);
113− }
114−
115192 pub fn remove_child(&self, child: usize) {
193+ debug_assert!(self.traversal_is_committed());
116194 debug!("emit: remove_child({})", child);
117195 self.state.emitter.remove_child(child as u32);
118196 }
119197
120− pub fn push_last_child(&self) {
121− debug!("emit: push_last_child()");
122− self.state.emitter.push_last_child();
123− }
124−
125198 pub fn insert_before(&self) {
199+ debug_assert!(self.traversal_is_committed());
126200 debug!("emit: insert_before()");
127201 self.state.emitter.insert_before();
128202 }
@@ -134,6 +208,7 @@ impl ChangeListBuilder<'_> {
134208 }
135209
136210 pub fn set_text(&self, text: &str) {
211+ debug_assert!(self.traversal_is_committed());
137212 debug!("emit: set_text({:?})", text);
138213 self.state
139214 .emitter
@@ -141,16 +216,19 @@ impl ChangeListBuilder<'_> {
141216 }
142217
143218 pub fn remove_self_and_next_siblings(&self) {
219+ debug_assert!(self.traversal_is_committed());
144220 debug!("emit: remove_self_and_next_siblings()");
145221 self.state.emitter.remove_self_and_next_siblings();
146222 }
147223
148224 pub fn replace_with(&self) {
225+ debug_assert!(self.traversal_is_committed());
149226 debug!("emit: replace_with()");
150227 self.state.emitter.replace_with();
151228 }
152229
153230 pub fn set_attribute(&mut self, name: &str, value: &str) {
231+ debug_assert!(self.traversal_is_committed());
154232 debug!("emit: set_attribute({:?}, {:?})", name, value);
155233 let name_id = self.ensure_string(name);
156234 let value_id = self.ensure_string(value);
@@ -160,32 +238,20 @@ impl ChangeListBuilder<'_> {
160238 }
161239
162240 pub fn remove_attribute(&mut self, name: &str) {
241+ debug_assert!(self.traversal_is_committed());
163242 debug!("emit: remove_attribute({:?})", name);
164243 let name_id = self.ensure_string(name);
165244 self.state.emitter.remove_attribute(name_id.into());
166245 }
167246
168− pub fn push_first_child(&self) {
169− debug!("emit: push_first_child");
170− self.state.emitter.push_first_child();
171− }
172−
173− pub fn pop_push_next_sibling(&self) {
174− debug!("emit: pop_push_next_sibling");
175− self.state.emitter.pop_push_next_sibling();
176− }
177−
178− pub fn pop(&self) {
179− debug!("emit: pop");
180− self.state.emitter.pop();
181− }
182−
183247 pub fn append_child(&self) {
248+ debug_assert!(self.traversal_is_committed());
184249 debug!("emit: append_child()");
185250 self.state.emitter.append_child();
186251 }
187252
188253 pub fn create_text_node(&self, text: &str) {
254+ debug_assert!(self.traversal_is_committed());
189255 debug!("emit: create_text_node({:?})", text);
190256 self.state
191257 .emitter
@@ -193,12 +259,14 @@ impl ChangeListBuilder<'_> {
193259 }
194260
195261 pub fn create_element(&mut self, tag_name: &str) {
262+ debug_assert!(self.traversal_is_committed());
196263 debug!("emit: create_element({:?})", tag_name);
197264 let tag_name_id = self.ensure_string(tag_name);
198265 self.state.emitter.create_element(tag_name_id.into());
199266 }
200267
201268 pub fn create_element_ns(&mut self, tag_name: &str, ns: &str) {
269+ debug_assert!(self.traversal_is_committed());
202270 debug!("emit: create_element_ns({:?}, {:?})", tag_name, ns);
203271 let tag_name_id = self.ensure_string(tag_name);
204272 let ns_id = self.ensure_string(ns);
@@ -208,6 +276,7 @@ impl ChangeListBuilder<'_> {
208276 }
209277
210278 pub fn new_event_listener(&mut self, listener: &Listener) {
279+ debug_assert!(self.traversal_is_committed());
211280 debug!("emit: new_event_listener({:?})", listener);
212281 let (a, b) = listener.get_callback_parts();
213282 debug_assert!(a != 0);
@@ -216,6 +285,7 @@ impl ChangeListBuilder<'_> {
216285 }
217286
218287 pub fn update_event_listener(&mut self, listener: &Listener) {
288+ debug_assert!(self.traversal_is_committed());
219289 debug!("emit: update_event_listener({:?})", listener);
220290 let (a, b) = listener.get_callback_parts();
221291 debug_assert!(a != 0);
@@ -226,6 +296,7 @@ impl ChangeListBuilder<'_> {
226296 }
227297
228298 pub fn remove_event_listener(&mut self, event: &str) {
299+ debug_assert!(self.traversal_is_committed());
229300 debug!("emit: remove_event_listener({:?})", event);
230301 let event_id = self.ensure_string(event);
231302 self.state.emitter.remove_event_listener(event_id.into());
src/change_list/traversal.rsnew file+344
@@ -0,0 +1,344 @@
1+//! Keeps track of where we are moving in a DOM tree, and shortens traversal
2+//! paths between mutations to their minimal number of operations.
3+
4+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5+pub enum MoveTo {
6+ /// Move from the current node up to its parent.
7+ Parent,
8+
9+ /// Move to the current node's n^th child.
10+ Child(u32),
11+
12+ /// Move to the current node's n^th from last child.
13+ ReverseChild(u32),
14+
15+ /// Move to the n^th sibling. Not relative from the current
16+ /// location. Absolute indexed within all of the current siblings.
17+ Sibling(u32),
18+
19+ /// Move to the n^th from last sibling. Not relative from the current
20+ /// location. Absolute indexed within all of the current siblings.
21+ ReverseSibling(u32),
22+
23+ /// Move down to the given saved temporary child.
24+ TempChild(u32),
25+}
26+
27+#[derive(Debug)]
28+pub struct Traversal {
29+ uncommitted: Vec<MoveTo>,
30+}
31+
32+impl Traversal {
33+ /// Construct a new `Traversal` with its internal storage backed by the
34+ /// given bump arena.
35+ pub fn new() -> Traversal {
36+ Traversal {
37+ uncommitted: Vec::with_capacity(32),
38+ }
39+ }
40+
41+ /// Move the traversal up in the tree.
42+ pub fn up(&mut self) {
43+ match self.uncommitted.last() {
44+ Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
45+ self.uncommitted.pop();
46+ self.uncommitted.push(MoveTo::Parent);
47+ }
48+ Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) | Some(MoveTo::ReverseChild(_)) => {
49+ self.uncommitted.pop();
50+ // And we're back at the parent.
51+ }
52+ _ => {
53+ self.uncommitted.push(MoveTo::Parent);
54+ }
55+ }
56+ }
57+
58+ /// Move the traversal down in the tree to the first child of the current
59+ /// node.
60+ pub fn down(&mut self) {
61+ if let Some(&MoveTo::Parent) = self.uncommitted.last() {
62+ self.uncommitted.pop();
63+ self.sibling(0);
64+ } else {
65+ self.uncommitted.push(MoveTo::Child(0));
66+ }
67+ }
68+
69+ /// Move the traversal to the n^th sibling.
70+ pub fn sibling(&mut self, index: usize) {
71+ let index = index as u32;
72+ match self.uncommitted.last_mut() {
73+ Some(MoveTo::Sibling(ref mut n)) | Some(MoveTo::Child(ref mut n)) => {
74+ *n = index;
75+ }
76+ Some(MoveTo::ReverseSibling(_)) => {
77+ self.uncommitted.pop();
78+ self.uncommitted.push(MoveTo::Sibling(index));
79+ }
80+ Some(MoveTo::TempChild(_)) | Some(MoveTo::ReverseChild(_)) => {
81+ self.uncommitted.pop();
82+ self.uncommitted.push(MoveTo::Child(index))
83+ }
84+ _ => {
85+ self.uncommitted.push(MoveTo::Sibling(index));
86+ }
87+ }
88+ }
89+
90+ /// Move the the n^th from last sibling.
91+ pub fn reverse_sibling(&mut self, index: usize) {
92+ let index = index as u32;
93+ match self.uncommitted.last_mut() {
94+ Some(MoveTo::ReverseSibling(ref mut n)) | Some(MoveTo::ReverseChild(ref mut n)) => {
95+ *n = index;
96+ }
97+ Some(MoveTo::Sibling(_)) => {
98+ self.uncommitted.pop();
99+ self.uncommitted.push(MoveTo::ReverseSibling(index));
100+ }
101+ Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) => {
102+ self.uncommitted.pop();
103+ self.uncommitted.push(MoveTo::ReverseChild(index))
104+ }
105+ _ => {
106+ self.uncommitted.push(MoveTo::ReverseSibling(index));
107+ }
108+ }
109+ }
110+
111+ /// Go to the given saved temporary.
112+ pub fn down_to_temp(&mut self, temp: u32) {
113+ match self.uncommitted.last() {
114+ Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
115+ self.uncommitted.pop();
116+ }
117+ Some(MoveTo::Parent)
118+ | Some(MoveTo::TempChild(_))
119+ | Some(MoveTo::Child(_))
120+ | Some(MoveTo::ReverseChild(_))
121+ | None => {
122+ // Can't remove moves to parents since we rely on their stack
123+ // pops.
124+ }
125+ }
126+ self.uncommitted.push(MoveTo::TempChild(temp));
127+ }
128+
129+ /// Are all the traversal's moves committed? That is, are there no moves
130+ /// that have *not* been committed yet?
131+ #[inline]
132+ pub fn is_committed(&self) -> bool {
133+ self.uncommitted.is_empty()
134+ }
135+
136+ /// Commit this traversals moves and return the optimized path from the last
137+ /// commit.
138+ #[inline]
139+ pub fn commit(&mut self) -> Moves {
140+ Moves {
141+ inner: self.uncommitted.drain(..),
142+ }
143+ }
144+
145+ #[inline]
146+ pub fn reset(&mut self) {
147+ self.uncommitted.clear();
148+ }
149+}
150+
151+pub struct Moves<'a> {
152+ inner: std::vec::Drain<'a, MoveTo>,
153+}
154+
155+impl Iterator for Moves<'_> {
156+ type Item = MoveTo;
157+
158+ #[inline]
159+ fn next(&mut self) -> Option<MoveTo> {
160+ self.inner.next()
161+ }
162+}
163+
164+#[cfg(test)]
165+mod tests {
166+ use super::*;
167+
168+ #[test]
169+ fn test_traversal() {
170+ fn t<F>(f: F) -> Box<FnMut(&mut Traversal)>
171+ where
172+ F: 'static + FnMut(&mut Traversal),
173+ {
174+ Box::new(f) as _
175+ }
176+
177+ for (mut traverse, expected_moves) in vec![
178+ (
179+ t(|t| {
180+ t.down();
181+ }),
182+ vec![MoveTo::Child(0)],
183+ ),
184+ (
185+ t(|t| {
186+ t.up();
187+ }),
188+ vec![MoveTo::Parent],
189+ ),
190+ (
191+ t(|t| {
192+ t.sibling(42);
193+ }),
194+ vec![MoveTo::Sibling(42)],
195+ ),
196+ (
197+ t(|t| {
198+ t.down();
199+ t.up();
200+ }),
201+ vec![],
202+ ),
203+ (
204+ t(|t| {
205+ t.down();
206+ t.sibling(2);
207+ t.up();
208+ }),
209+ vec![],
210+ ),
211+ (
212+ t(|t| {
213+ t.down();
214+ t.sibling(3);
215+ }),
216+ vec![MoveTo::Child(3)],
217+ ),
218+ (
219+ t(|t| {
220+ t.down();
221+ t.sibling(4);
222+ t.sibling(8);
223+ }),
224+ vec![MoveTo::Child(8)],
225+ ),
226+ (
227+ t(|t| {
228+ t.sibling(1);
229+ t.sibling(1);
230+ }),
231+ vec![MoveTo::Sibling(1)],
232+ ),
233+ (
234+ t(|t| {
235+ t.reverse_sibling(3);
236+ }),
237+ vec![MoveTo::ReverseSibling(3)],
238+ ),
239+ (
240+ t(|t| {
241+ t.down();
242+ t.reverse_sibling(3);
243+ }),
244+ vec![MoveTo::ReverseChild(3)],
245+ ),
246+ (
247+ t(|t| {
248+ t.down();
249+ t.reverse_sibling(3);
250+ t.up();
251+ }),
252+ vec![],
253+ ),
254+ (
255+ t(|t| {
256+ t.down();
257+ t.reverse_sibling(3);
258+ t.reverse_sibling(6);
259+ }),
260+ vec![MoveTo::ReverseChild(6)],
261+ ),
262+ (
263+ t(|t| {
264+ t.up();
265+ t.reverse_sibling(3);
266+ t.reverse_sibling(6);
267+ }),
268+ vec![MoveTo::Parent, MoveTo::ReverseSibling(6)],
269+ ),
270+ (
271+ t(|t| {
272+ t.up();
273+ t.sibling(3);
274+ t.sibling(6);
275+ }),
276+ vec![MoveTo::Parent, MoveTo::Sibling(6)],
277+ ),
278+ (
279+ t(|t| {
280+ t.sibling(3);
281+ t.sibling(6);
282+ t.up();
283+ }),
284+ vec![MoveTo::Parent],
285+ ),
286+ (
287+ t(|t| {
288+ t.reverse_sibling(3);
289+ t.reverse_sibling(6);
290+ t.up();
291+ }),
292+ vec![MoveTo::Parent],
293+ ),
294+ (
295+ t(|t| {
296+ t.down();
297+ t.down_to_temp(3);
298+ }),
299+ vec![MoveTo::Child(0), MoveTo::TempChild(3)],
300+ ),
301+ (
302+ t(|t| {
303+ t.down_to_temp(3);
304+ t.sibling(5);
305+ }),
306+ vec![MoveTo::Child(5)],
307+ ),
308+ (
309+ t(|t| {
310+ t.down_to_temp(3);
311+ t.reverse_sibling(5);
312+ }),
313+ vec![MoveTo::ReverseChild(5)],
314+ ),
315+ (
316+ t(|t| {
317+ t.down_to_temp(3);
318+ t.up();
319+ }),
320+ vec![],
321+ ),
322+ (
323+ t(|t| {
324+ t.sibling(2);
325+ t.up();
326+ t.down_to_temp(3);
327+ }),
328+ vec![MoveTo::Parent, MoveTo::TempChild(3)],
329+ ),
330+ (
331+ t(|t| {
332+ t.up();
333+ t.down_to_temp(3);
334+ }),
335+ vec![MoveTo::Parent, MoveTo::TempChild(3)],
336+ ),
337+ ] {
338+ let mut traversal = Traversal::new();
339+ traverse(&mut traversal);
340+ let actual_moves: Vec<_> = traversal.commit().collect();
341+ assert_eq!(actual_moves, expected_moves);
342+ }
343+ }
344+}
src/diff.rs+67−67
@@ -32,17 +32,20 @@ pub(crate) fn diff(
3232 &NodeKind::Text(TextNode { text: old_text }),
3333 ) => {
3434 if new_text != old_text {
35+ change_list.commit_traversal();
3536 change_list.set_text(new_text);
3637 }
3738 }
3839
3940 (&NodeKind::Text(_), &NodeKind::Element(_)) => {
41+ change_list.commit_traversal();
4042 create(cached_set, change_list, registry, new, cached_roots);
4143 registry.remove_subtree(&old);
4244 change_list.replace_with();
4345 }
4446
4547 (&NodeKind::Element(_), &NodeKind::Text(_)) => {
48+ change_list.commit_traversal();
4649 create(cached_set, change_list, registry, new, cached_roots);
4750 // Note: text nodes cannot have event listeners, so we don't need to
4851 // remove the old node's listeners from our registry her.
@@ -68,6 +71,7 @@ pub(crate) fn diff(
6871 }),
6972 ) => {
7073 if new_tag_name != old_tag_name || new_namespace != old_namespace {
74+ change_list.commit_traversal();
7175 create(cached_set, change_list, registry, new, cached_roots);
7276 registry.remove_subtree(&old);
7377 change_list.replace_with();
@@ -103,6 +107,7 @@ pub(crate) fn diff(
103107 // we assume that they are pretty different, and it isn't worth diffing
104108 // the subtrees, so we just create the new cached node afresh.
105109 (&NodeKind::Cached(ref c), _) => {
110+ change_list.commit_traversal();
106111 cached_roots.insert(c.id);
107112 let new = cached_set.get(c.id);
108113 create(cached_set, change_list, registry, new, cached_roots);
@@ -113,6 +118,7 @@ pub(crate) fn diff(
113118 // Old cached node and new non-cached node. Again, assume that they are
114119 // probably pretty different and create the new non-cached node afresh.
115120 (_, &NodeKind::Cached(_)) => {
121+ change_list.commit_traversal();
116122 create(cached_set, change_list, registry, new, cached_roots);
117123 registry.remove_subtree(&old);
118124 change_list.replace_with();
@@ -133,6 +139,10 @@ fn diff_listeners(
133139 old: &[Listener],
134140 new: &[Listener],
135141) {
142+ if !old.is_empty() || !new.is_empty() {
143+ change_list.commit_traversal();
144+ }
145+
136146 'outer1: for new_l in new {
137147 unsafe {
138148 // Safety relies on removing `new_l` from the registry manually at
@@ -175,16 +185,20 @@ fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new:
175185 // there are almost always very few attributes.
176186 'outer: for new_attr in new {
177187 if new_attr.is_volatile() {
188+ change_list.commit_traversal();
178189 change_list.set_attribute(new_attr.name, new_attr.value);
179190 } else {
180191 for old_attr in old {
181192 if old_attr.name == new_attr.name {
182193 if old_attr.value != new_attr.value {
194+ change_list.commit_traversal();
183195 change_list.set_attribute(new_attr.name, new_attr.value);
184196 }
185197 continue 'outer;
186198 }
187199 }
200+
201+ change_list.commit_traversal();
188202 change_list.set_attribute(new_attr.name, new_attr.value);
189203 }
190204 }
@@ -195,6 +209,8 @@ fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new:
195209 continue 'outer2;
196210 }
197211 }
212+
213+ change_list.commit_traversal();
198214 change_list.remove_attribute(old_attr.name);
199215 }
200216}
@@ -217,13 +233,17 @@ fn diff_children(
217233) {
218234 if new.is_empty() {
219235 if !old.is_empty() {
236+ change_list.commit_traversal();
220237 remove_all_children(change_list, registry, old);
221238 }
222239 return;
223240 }
224241
225242 if old.is_empty() {
226− create_and_append_children(cached_set, change_list, registry, new, cached_roots);
243+ if !new.is_empty() {
244+ change_list.commit_traversal();
245+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
246+ }
227247 return;
228248 }
229249
@@ -384,42 +404,24 @@ fn diff_keyed_prefix(
384404 new: &[Node],
385405 cached_roots: &mut FxHashSet<CacheId>,
386406) -> KeyedPrefixResult {
387− let mut pushed = false;
407+ change_list.go_down();
388408 let mut shared_prefix_count = 0;
389409
390− for (old, new) in old.iter().zip(new.iter()) {
410+ for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() {
391411 if old.key() != new.key() {
392412 break;
393413 }
394414
395− if pushed {
396− debug_assert!(shared_prefix_count > 0);
397− change_list.pop_push_next_sibling();
398− } else {
399− debug_assert_eq!(shared_prefix_count, 0);
400− change_list.push_first_child();
401− pushed = true;
402− }
403−
415+ change_list.go_to_sibling(i);
404416 diff(cached_set, change_list, registry, old, new, cached_roots);
405417 shared_prefix_count += 1;
406−
407− // At the end of the loop, the change list stack looks like
408− //
409− // [... parent child_we_just_diffed]
410− debug_assert!(pushed);
411418 }
412419
413420 // If that was all of the old children, then create and append the remaining
414421 // new children and we're finished.
415422 if shared_prefix_count == old.len() {
416− debug_assert!(
417− pushed,
418− "we handle the case of empty children before calling this method, so
419− `shared_prefix_count` must be greater than zero, and therefore we
420− must have pushed in the above loop"
421− );
422− change_list.pop();
423+ change_list.go_up();
424+ change_list.commit_traversal();
423425 create_and_append_children(
424426 cached_set,
425427 change_list,
@@ -433,17 +435,13 @@ fn diff_keyed_prefix(
433435 // And if that was all of the new children, then remove all of the remaining
434436 // old children and we're finished.
435437 if shared_prefix_count == new.len() {
436− // Same as above.
437− debug_assert!(pushed);
438− change_list.pop_push_next_sibling();
439− change_list.remove_self_and_next_siblings();
438+ change_list.go_to_sibling(shared_prefix_count);
439+ change_list.commit_traversal();
440+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
440441 return KeyedPrefixResult::Finished;
441442 }
442443
443− if pushed {
444− debug_assert!(shared_prefix_count > 0);
445− change_list.pop();
446− }
444+ change_list.go_up();
447445 KeyedPrefixResult::MoreWorkToDo(shared_prefix_count)
448446}
449447
@@ -507,9 +505,11 @@ fn diff_keyed_middle(
507505 // afresh.
508506 if shared_suffix_count == 0 && shared_keys.is_empty() {
509507 if shared_prefix_count == 0 {
508+ change_list.commit_traversal();
510509 remove_all_children(change_list, registry, old);
511510 } else {
512− change_list.pop_push_next_sibling();
511+ change_list.go_down_to_child(shared_prefix_count);
512+ change_list.commit_traversal();
513513 remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
514514 }
515515 create_and_append_children(cached_set, change_list, registry, new, cached_roots);
@@ -529,6 +529,7 @@ fn diff_keyed_middle(
529529 .unwrap_or(old.len());
530530
531531 if end - start > 0 {
532+ change_list.commit_traversal();
532533 let mut t = change_list.save_children_to_temporaries(
533534 shared_prefix_count + start,
534535 shared_prefix_count + end,
@@ -553,6 +554,7 @@ fn diff_keyed_middle(
553554 for (i, old_child) in old.iter().enumerate().rev() {
554555 if !shared_keys.contains(&old_child.key()) {
555556 registry.remove_subtree(old_child);
557+ change_list.commit_traversal();
556558 change_list.remove_child(i + shared_prefix_count);
557559 removed_count += 1;
558560 }
@@ -596,7 +598,7 @@ fn diff_keyed_middle(
596598 // shared suffix to the change list stack.
597599 //
598600 // [... parent]
599− change_list.push_child(old_shared_suffix_start - removed_count);
601+ change_list.go_down_to_child(old_shared_suffix_start - removed_count);
600602 // [... parent first_child_of_shared_suffix]
601603 } else {
602604 // There is no shared suffix coming after these middle children.
@@ -609,7 +611,7 @@ fn diff_keyed_middle(
609611 let old_index = new_index_to_old_index[last_index];
610612 let temp = old_index_to_temp[old_index];
611613 // [... parent]
612− change_list.push_temporary(temp);
614+ change_list.go_down_to_temp_child(temp);
613615 // [... parent last]
614616 diff(
615617 cached_set,
@@ -622,18 +624,21 @@ fn diff_keyed_middle(
622624 if new_index_is_in_lis.contains(&last_index) {
623625 // Don't move it, since it is already where it needs to be.
624626 } else {
627+ change_list.commit_traversal();
628+ // [... parent last]
625629 change_list.append_child();
626630 // [... parent]
627− change_list.push_temporary(temp);
631+ change_list.go_down_to_temp_child(temp);
628632 // [... parent last]
629633 }
630634 } else {
635+ change_list.commit_traversal();
631636 // [... parent]
632637 create(cached_set, change_list, registry, last, cached_roots);
633638 // [... parent last]
634639 change_list.append_child();
635640 // [... parent]
636− change_list.push_last_child();
641+ change_list.go_down_to_reverse_child(0);
637642 // [... parent last]
638643 }
639644 }
@@ -642,6 +647,7 @@ fn diff_keyed_middle(
642647 let old_index = new_index_to_old_index[new_index];
643648 if old_index == u32::MAX as usize {
644649 debug_assert!(!shared_keys.contains(&new_child.key()));
650+ change_list.commit_traversal();
645651 // [... parent successor]
646652 create(cached_set, change_list, registry, new_child, cached_roots);
647653 // [... parent successor new_child]
@@ -654,11 +660,10 @@ fn diff_keyed_middle(
654660
655661 if new_index_is_in_lis.contains(&new_index) {
656662 // [... parent successor]
657− change_list.pop();
658− // [... parent]
659− change_list.push_temporary(temp);
663+ change_list.go_to_temp_sibling(temp);
660664 // [... parent new_child]
661665 } else {
666+ change_list.commit_traversal();
662667 // [... parent successor]
663668 change_list.push_temporary(temp);
664669 // [... parent successor new_child]
@@ -678,7 +683,7 @@ fn diff_keyed_middle(
678683 }
679684
680685 // [... parent child]
681− change_list.pop();
686+ change_list.go_up();
682687 // [... parent]
683688}
684689
@@ -702,10 +707,11 @@ fn diff_keyed_suffix(
702707 debug_assert!(!old.is_empty());
703708
704709 // [... parent]
705− change_list.push_child(new_shared_suffix_start);
710+ change_list.go_down();
706711 // [... parent new_child]
707712
708− for (old_child, new_child) in old.iter().zip(new.iter()) {
713+ for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() {
714+ change_list.go_to_sibling(new_shared_suffix_start + i);
709715 diff(
710716 cached_set,
711717 change_list,
@@ -714,14 +720,10 @@ fn diff_keyed_suffix(
714720 new_child,
715721 cached_roots,
716722 );
717−
718− // [... parent this_new_child]
719− change_list.pop_push_next_sibling();
720− // [... parent next_new_child]
721723 }
722724
723725 // [... parent]
724− change_list.pop();
726+ change_list.go_up();
725727}
726728
727729// Diff children that are not keyed.
@@ -744,17 +746,14 @@ fn diff_non_keyed_children(
744746 debug_assert!(!new.is_empty());
745747 debug_assert!(!old.is_empty());
746748
747− for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() {
748− if i == 0 {
749− // [... parent]
750− change_list.push_first_child();
751− // [... parent first_child]
752− } else {
753− // [... parent prev_sibling]
754− change_list.pop_push_next_sibling();
755− // [... parent next_sibling]
756− }
749+ // [... parent]
750+ change_list.go_down();
751+ // [... parent child]
757752
753+ for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() {
754+ // [... parent prev_child]
755+ change_list.go_to_sibling(i);
756+ // [... parent this_child]
758757 diff(
759758 cached_set,
760759 change_list,
@@ -765,23 +764,20 @@ fn diff_non_keyed_children(
765764 );
766765 }
767766
768− // Note that because `new` and `old` are not empty, the previous loop always
769− // executes at least once, so the change list stack is now:
770− //
771− // [... parent child]
772−
773767 match old.len().cmp(&new.len()) {
774768 Ordering::Greater => {
775− // [... parent last_shared_child]
776− change_list.pop_push_next_sibling();
769+ // [... parent prev_child]
770+ change_list.go_to_sibling(new.len());
777771 // [... parent first_child_to_remove]
772+ change_list.commit_traversal();
778773 remove_self_and_next_siblings(change_list, registry, &old[new.len()..]);
779774 // [... parent]
780775 }
781776 Ordering::Less => {
782777 // [... parent last_child]
783− change_list.pop();
778+ change_list.go_up();
784779 // [... parent]
780+ change_list.commit_traversal();
785781 create_and_append_children(
786782 cached_set,
787783 change_list,
@@ -792,7 +788,7 @@ fn diff_non_keyed_children(
792788 }
793789 Ordering::Equal => {
794790 // [... parent child]
795− change_list.pop();
791+ change_list.go_up();
796792 // [... parent]
797793 }
798794 }
@@ -812,6 +808,7 @@ fn create_and_append_children(
812808 new: &[Node],
813809 cached_roots: &mut FxHashSet<CacheId>,
814810) {
811+ debug_assert!(change_list.traversal_is_committed());
815812 for child in new {
816813 create(cached_set, change_list, registry, child, cached_roots);
817814 change_list.append_child();
@@ -830,6 +827,7 @@ fn remove_all_children(
830827 registry: &mut EventsRegistry,
831828 old: &[Node],
832829) {
830+ debug_assert!(change_list.traversal_is_committed());
833831 for child in old {
834832 registry.remove_subtree(child);
835833 }
@@ -852,6 +850,7 @@ fn remove_self_and_next_siblings(
852850 registry: &mut EventsRegistry,
853851 old: &[Node],
854852) {
853+ debug_assert!(change_list.traversal_is_committed());
855854 for child in old {
856855 registry.remove_subtree(child);
857856 }
@@ -874,6 +873,7 @@ fn create(
874873 node: &Node,
875874 cached_roots: &mut FxHashSet<CacheId>,
876875) {
876+ debug_assert!(change_list.traversal_is_committed());
877877 match node.kind {
878878 NodeKind::Text(TextNode { text }) => {
879879 change_list.create_text_node(text);
src/lib.rs+2−1
@@ -92,13 +92,14 @@ mod events;
9292mod node;
9393mod render;
9494mod render_context;
95+mod strace;
9596mod vdom;
9697
9798pub mod builder;
9899
99100// Re-export items at the top level.
100101pub use self::cached::Cached;
101−pub use self::node::{Attribute, Listener, Node};
102+pub use self::node::{Attribute, Listener, Node, NodeKey};
102103pub use self::render::{Render, RootRender};
103104pub use self::render_context::RenderContext;
104105pub use self::vdom::{Vdom, VdomWeak};
src/node.rs+35−24
@@ -68,24 +68,44 @@ pub_unstable_internal! {
6868 }
6969}
7070
71−pub_unstable_internal! {
72− /// The key for keyed children.
73− #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74− pub(crate) struct NodeKey(pub(crate) u32);
71+/// The key for keyed children.
72+///
73+/// Keys must be unique among siblings.
74+///
75+/// If any sibling is keyed, then they all must be keyed.
76+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77+pub struct NodeKey(pub(crate) u32);
78+
79+impl Default for NodeKey {
80+ fn default() -> NodeKey {
81+ NodeKey::NONE
82+ }
7583}
7684
7785impl NodeKey {
86+ /// The default, lack of a key.
7887 pub const NONE: NodeKey = NodeKey(u32::MAX);
7988
89+ /// Is this key `NodeKey::NONE`?
8090 #[inline]
8191 pub fn is_none(&self) -> bool {
8292 *self == Self::NONE
8393 }
8494
95+ /// Is this key not `NodeKey::NONE`?
8596 #[inline]
8697 pub fn is_some(&self) -> bool {
8798 !self.is_none()
8899 }
100+
101+ /// Create a new `NodeKey`.
102+ ///
103+ /// `key` must not be `u32::MAX`.
104+ #[inline]
105+ pub fn new(key: u32) -> Self {
106+ debug_assert_ne!(key, u32::MAX);
107+ NodeKey(key)
108+ }
89109}
90110
91111/// An event listener callback function.
@@ -162,31 +182,22 @@ impl<'a> Attribute<'a> {
162182}
163183
164184impl<'a> Node<'a> {
165− /// Construct a new Node of type element with given tag name and children
185+ /// Low-level constructor for making a new `Node` of type element with given
186+ /// parts.
187+ ///
188+ /// This is primarily intended for JSX and templating proc-macros to compile
189+ /// down into. If you are building nodes by-hand, prefer using the
190+ /// `dodrio::builder::*` APIs.
166191 #[inline]
167− pub(crate) fn element<Listeners, Attributes, Children>(
192+ pub fn element(
168193 bump: &'a Bump,
169194 key: NodeKey,
170195 tag_name: &'a str,
171− listeners: Listeners,
172− attributes: Attributes,
173− children: Children,
196+ listeners: &'a [Listener<'a>],
197+ attributes: &'a [Attribute<'a>],
198+ children: &'a [Node<'a>],
174199 namespace: Option<&'a str>,
175− ) -> Node<'a>
176− where
177− Listeners: 'a + AsRef<[Listener<'a>]>,
178− Attributes: 'a + AsRef<[Attribute<'a>]>,
179− Children: 'a + AsRef<[Node<'a>]>,
180− {
181− let children: &'a Children = bump.alloc(children);
182− let children: &'a [Node<'a>] = children.as_ref();
183−
184− let listeners: &'a Listeners = bump.alloc(listeners);
185− let listeners: &'a [Listener<'a>] = listeners.as_ref();
186−
187− let attributes: &'a Attributes = bump.alloc(attributes);
188− let attributes: &'a [Attribute<'a>] = attributes.as_ref();
189−
200+ ) -> Node<'a> {
190201 let element = bump.alloc_with(|| ElementNode {
191202 key,
192203 tag_name,
src/strace.rsnew file+22
@@ -0,0 +1,22 @@
1+cfg_if::cfg_if! {
2+ if #[cfg(feature = "xxx-unstable-strace")] {
3+ use wasm_bindgen::prelude::*;
4+
5+ #[wasm_bindgen(module = "/js/strace.js")]
6+ extern "C" {
7+ #[wasm_bindgen(js_name = initStrace)]
8+ fn really_init_strace();
9+ }
10+
11+ pub fn init_strace() {
12+ use std::sync::Once;
13+ static STRACE: Once = Once::new();
14+ STRACE.call_once(|| {
15+ really_init_strace();
16+ });
17+ }
18+ } else {
19+ #[inline]
20+ pub fn init_strace() {}
21+ }
22+}
src/vdom.rs+4−1
@@ -161,12 +161,15 @@ impl Vdom {
161161 /// Construct a `Vdom` with the already-boxed-as-a-trait-object root
162162 /// rendering component.
163163 pub fn with_boxed_root_render(container: &crate::Element, component: Box<RootRender>) -> Vdom {
164+ crate::strace::init_strace();
165+
164166 let dom_buffers = [Bump::new(), Bump::new()];
165167 let change_list = ManuallyDrop::new(ChangeListPersistentState::new(container));
166168
167169 // Create a dummy `<div/>` in our container.
168170 initialize_container(container);
169− let current_root = Node::element(&dom_buffers[0], NodeKey::NONE, "div", [], [], [], None);
171+ let current_root =
172+ Node::element(&dom_buffers[0], NodeKey::NONE, "div", &[], &[], &[], None);
170173 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
171174
172175 let container = container.clone();