on master help sign in
Optimize emitted DOM traversal instructions

This commit introduces a traversal path optimizer. The goal is that while we have to walk over large virtual DOM trees when diffing, if we don't end up actually emitting any change list instructions that mutate the physical DOM, then we don't emit the traversal instructions either. We do this by keeping track of where the last location we "committed" the traversal in order to mutate something, and if we go down into children and back up to the parent without commiting again, then we optimize that traversal into a no-op. We only emit the optimized traversal path once we "commit" the traversal.

commitfb227aaa4d5af8fe52d86c9053f657da0dc977d4 authorNick Fitzgerald <fitzgen@gmail.com> dateThu 18 Apr 2019, 14:02 (8 years ago) parent2be0c47
5 files changed+539−107
js/change-list-interpreter.js+18−7 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
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 },
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);