on master help sign in
Add simple string cache to the change list

The change list contains a lot of recurring strings, such as attribute names. In many cases, this also includes things like class names, which are repeated many times in e.g. list displays. This patch adds a simple cache for those strings. Whenever a string is encountered, it's checked against the cache. If it doesn't exist in the cache, it's added, and an opcode is emmitted sending that string to JS. After computing the full change list, the cache is iterated, and all strings not in use in the current change list are evicted, again emitting an opcode per entry instructing the JS side to drop that string, too. This seems to strike a reasonable balance between keeping the cache across multiple updates—where it helps in scenarios where e.g. multiple list entries are added one after the other—and wasting memory. In particular, right after a string was sent, it's almost certainly in use in the resulting DOM, so keeping it around in the string cache, too, shouldn't waste memory for the string, just for the cache entry itself. Note that this patch doesn't use the string cache for text nodes: those are deemed less likely to benefit from the cache.

commitd470c3d1c21a80683fc7148bb75e43703cfc482b authorTill Schneidereit <till@tillschneidereit.net> dateSun 10 Mar 2019, 21:45 (8 years ago) parent5b90826
3 files changed+155−58
js/change-list.js+45−21 src/change_list.rs+102−32 src/vdom.rs+8−5
js/change-list.js+45−21
@@ -43,12 +43,10 @@ const OP_TABLE = [
4343
4444 // 3
4545 function setAttribute(changeList, mem8, mem32, i) {
46− const pointer1 = mem32[i++];
47− const length1 = mem32[i++];
48− const name = string(mem8, pointer1, length1);
49− const pointer2 = mem32[i++];
50− const length2 = mem32[i++];
51− const value = string(mem8, pointer2, length2);
46+ const nameId = mem32[i++];
47+ const valueId = mem32[i++];
48+ const name = changeList.getString(nameId);
49+ const value = changeList.getString(valueId);
5250 const node = top(changeList.stack);
5351 node.setAttribute(name, value);
5452
@@ -68,9 +66,8 @@ const OP_TABLE = [
6866
6967 // 4
7068 function removeAttribute(changeList, mem8, mem32, i) {
71− const pointer = mem32[i++];
72− const length = mem32[i++];
73− const name = string(mem8, pointer, length);
69+ const nameId = mem32[i++];
70+ const name = changeList.getString(nameId);
7471 const node = top(changeList.stack);
7572 node.removeAttribute(name);
7673
@@ -125,18 +122,16 @@ const OP_TABLE = [
125122
126123 // 10
127124 function createElement(changeList, mem8, mem32, i) {
128− const pointer = mem32[i++];
129− const length = mem32[i++];
130− const tagName = string(mem8, pointer, length);
125+ const tagNameId = mem32[i++];
126+ const tagName = changeList.getString(tagNameId);
131127 changeList.stack.push(document.createElement(tagName));
132128 return i;
133129 },
134130
135131 // 11
136132 function newEventListener(changeList, mem8, mem32, i) {
137− const pointer = mem32[i++];
138− const length = mem32[i++];
139− const eventType = string(mem8, pointer, length);
133+ const eventId = mem32[i++];
134+ const eventType = changeList.getString(eventId);
140135 const a = mem32[i++];
141136 const b = mem32[i++];
142137 const el = top(changeList.stack);
@@ -149,9 +144,8 @@ const OP_TABLE = [
149144
150145 // 12
151146 function updateEventListener(changeList, mem8, mem32, i) {
152− const pointer = mem32[i++];
153− const length = mem32[i++];
154− const eventType = string(mem8, pointer, length);
147+ const eventId = mem32[i++];
148+ const eventType = changeList.getString(eventId);
155149 const el = top(changeList.stack);
156150 const listener = el[`dodrio-${eventType}`];
157151 listener.a = mem32[i++];
@@ -161,14 +155,30 @@ const OP_TABLE = [
161155
162156 // 13
163157 function removeEventListener(changeList, mem8, mem32, i) {
164− const pointer = mem32[i++];
165− const length = mem32[i++];
166− const eventType = string(mem8, pointer, length);
158+ const eventId = mem32[i++];
159+ const eventType = changeList.getString(eventId);
167160 const el = top(changeList.stack);
168161 const listener = el[`dodrio-${eventType}`];
169162 el.removeEventListener(eventType, listener.callback);
170163 changeList.listeners.delete(listener);
171164 return i;
165+ },
166+
167+ // 14
168+ function addString(changeList, mem8, mem32, i) {
169+ const pointer = mem32[i++];
170+ const length = mem32[i++];
171+ const id = mem32[i++];
172+ const str = string(mem8, pointer, length);
173+ changeList.addString(str, id);
174+ return i;
175+ },
176+
177+ // 15
178+ function dropString(changeList, mem8, mem32, i) {
179+ const id = mem32[i++];
180+ changeList.dropString(id);
181+ return i;
172182 }
173183];
174184
@@ -195,6 +205,7 @@ class ChangeList {
195205 this.container = container;
196206 this.ranges = [];
197207 this.stack = [];
208+ this.strings = new Map();
198209 }
199210
200211 unmount() {
@@ -214,6 +225,7 @@ class ChangeList {
214225 this.container = null;
215226 this.ranges = null;
216227 this.stack = null;
228+ this.strings = null;
217229 }
218230
219231 addChangeListRange(start, len) {
@@ -248,6 +260,18 @@ class ChangeList {
248260 }
249261 }
250262
263+ addString(str, id) {
264+ this.strings.set(id, str);
265+ }
266+
267+ dropString(id) {
268+ this.strings.delete(id);
269+ }
270+
271+ getString(id) {
272+ return this.strings.get(id);
273+ }
274+
251275 initEventsTrampoline(trampoline) {
252276 this.eventsTrampoline = (...args) => {
253277 trampoline(...args);
src/change_list.rs+102−32
@@ -1,5 +1,7 @@
11use crate::Listener;
22use bumpalo::Bump;
3+use std::cell::Cell;
4+use std::collections::HashMap;
35use std::fmt;
46use std::sync::Once;
57use wasm_bindgen::prelude::*;
@@ -32,8 +34,15 @@ pub mod js {
3234 }
3335}
3436
37+struct StringsCacheEntry {
38+ key: u32,
39+ used: bool,
40+}
41+
3542pub(crate) struct ChangeList {
3643 bump: Bump,
44+ strings_cache: HashMap<String, StringsCacheEntry>,
45+ next_string_key: Cell<u32>,
3746 js: js::ChangeList,
3847 events_trampoline: Option<Closure<Fn(web_sys::Event, u32, u32)>>,
3948}
@@ -69,9 +78,12 @@ impl ChangeList {
6978 });
7079
7180 let bump = Bump::new();
81+ let strings_cache = HashMap::new();
7282 let js = js::ChangeList::new(container);
7383 ChangeList {
7484 bump,
85+ strings_cache,
86+ next_string_key: Cell::new(0),
7587 js,
7688 events_trampoline: None,
7789 }
@@ -238,6 +250,24 @@ enum ChangeDiscriminant {
238250 /// stack.top().removeEventListener(String(pointer, length));
239251 /// ```
240252 RemoveEventListener = 13,
253+
254+ /// Immediates: `(pointer, length, id)`
255+ ///
256+ /// Stack: `[...] -> [...]`
257+ ///
258+ /// ```text
259+ /// addString(String(pointer, length), id);
260+ /// ```
261+ AddString = 14,
262+
263+ /// Immediates: `(id)`
264+ ///
265+ /// Stack: `[...] -> [...]`
266+ ///
267+ /// ```text
268+ /// dropString(id);
269+ /// ```
270+ DropString = 15,
241271}
242272
243273// Allocation utilities to ensure that we only allocation sequences of `u32`s
@@ -249,22 +279,62 @@ impl ChangeList {
249279 self.bump.alloc(discriminant as u32);
250280 }
251281
252− // Note: no 1-immediate opcodes at this time.
282+ // Allocate an opcode with one immediate.
283+ fn op1(&self, discriminant: ChangeDiscriminant, a: u32) {
284+ self.bump.alloc([discriminant as u32, a]);
285+ }
253286
254287 // Allocate an opcode with two immediates.
255288 fn op2(&self, discriminant: ChangeDiscriminant, a: u32, b: u32) {
256289 self.bump.alloc([discriminant as u32, a, b]);
257290 }
258291
259− // Note: no 3-immediate opcodes at this time.
260−
261− // Allocate an opcode with four immediates.
262− fn op4(&self, discriminant: ChangeDiscriminant, a: u32, b: u32, c: u32, d: u32) {
263− self.bump.alloc([discriminant as u32, a, b, c, d]);
292+ // Allocate an opcode with three immediates.
293+ fn op3(&self, discriminant: ChangeDiscriminant, a: u32, b: u32, c: u32) {
294+ self.bump.alloc([discriminant as u32, a, b, c]);
264295 }
296+
297+ // Note: no 4-immediate opcodes at this time.
265298}
266299
267300impl ChangeList {
301+ fn ensure_string(&mut self, string: &str) -> u32 {
302+ let entry = self.strings_cache.get_mut(string);
303+ if entry.is_some() {
304+ let entry = entry.unwrap();
305+ entry.used = true;
306+ entry.key
307+ } else {
308+ let key = self.next_string_key.get();
309+ self.next_string_key.set(key + 1);
310+ let entry = StringsCacheEntry { key, used: true };
311+ self.strings_cache.insert(string.to_string(), entry);
312+ self.op3(
313+ ChangeDiscriminant::AddString,
314+ string.as_ptr() as u32,
315+ string.len() as u32,
316+ key
317+ );
318+ key
319+ }
320+ }
321+
322+ pub(crate) fn drop_unused_strings(&mut self) {
323+ debug!("drop_unused_strings()");
324+ let mut new_cache = HashMap::new();
325+ for (key, val) in self.strings_cache.iter() {
326+ if val.used {
327+ new_cache.insert(key.clone(), StringsCacheEntry { key: val.key, used: false });
328+ } else {
329+ self.op1(
330+ ChangeDiscriminant::DropString,
331+ val.key,
332+ );
333+ }
334+ }
335+ self.strings_cache = new_cache;
336+ }
337+
268338 pub(crate) fn emit_set_text(&self, text: &str) {
269339 debug!("emit_set_text({:?})", text);
270340 self.op2(
@@ -284,23 +354,23 @@ impl ChangeList {
284354 self.op0(ChangeDiscriminant::ReplaceWith);
285355 }
286356
287− pub(crate) fn emit_set_attribute(&self, name: &str, value: &str) {
357+ pub(crate) fn emit_set_attribute(&mut self, name: &str, value: &str) {
288358 debug!("emit_set_attribute({:?}, {:?})", name, value);
289− self.op4(
359+ let name_id = self.ensure_string(name);
360+ let value_id = self.ensure_string(value);
361+ self.op2(
290362 ChangeDiscriminant::SetAttribute,
291− name.as_ptr() as u32,
292− name.len() as u32,
293− value.as_ptr() as u32,
294− value.len() as u32,
363+ name_id,
364+ value_id,
295365 );
296366 }
297367
298− pub(crate) fn emit_remove_attribute(&self, name: &str) {
368+ pub(crate) fn emit_remove_attribute(&mut self, name: &str) {
299369 debug!("emit_remove_attribute({:?})", name);
300− self.op2(
370+ let name_id = self.ensure_string(name);
371+ self.op1(
301372 ChangeDiscriminant::RemoveAttribute,
302− name.as_ptr() as u32,
303− name.len() as u32,
373+ name_id,
304374 );
305375 }
306376
@@ -333,47 +403,47 @@ impl ChangeList {
333403 );
334404 }
335405
336− pub(crate) fn emit_create_element(&self, tag_name: &str) {
406+ pub(crate) fn emit_create_element(&mut self, tag_name: &str) {
337407 debug!("emit_create_element({:?})", tag_name);
338− self.op2(
408+ let tag_name_id = self.ensure_string(tag_name);
409+ self.op1(
339410 ChangeDiscriminant::CreateElement,
340− tag_name.as_ptr() as u32,
341− tag_name.len() as u32,
411+ tag_name_id,
342412 );
343413 }
344414
345− pub(crate) fn emit_new_event_listener(&self, listener: &Listener) {
415+ pub(crate) fn emit_new_event_listener(&mut self, listener: &Listener) {
346416 debug!("emit_new_event_listener({:?})", listener);
347417 let (a, b) = listener.get_callback_parts();
348418 debug_assert!(a != 0);
349− self.op4(
419+ let event_id = self.ensure_string(listener.event);
420+ self.op3(
350421 ChangeDiscriminant::NewEventListener,
351− listener.event.as_ptr() as u32,
352− listener.event.len() as u32,
422+ event_id,
353423 a,
354424 b,
355425 );
356426 }
357427
358− pub(crate) fn emit_update_event_listener(&self, listener: &Listener) {
428+ pub(crate) fn emit_update_event_listener(&mut self, listener: &Listener) {
359429 debug!("emit_update_event_listener({:?})", listener);
360430 let (a, b) = listener.get_callback_parts();
361431 debug_assert!(a != 0);
362− self.op4(
432+ let event_id = self.ensure_string(listener.event);
433+ self.op3(
363434 ChangeDiscriminant::UpdateEventListener,
364− listener.event.as_ptr() as u32,
365− listener.event.len() as u32,
435+ event_id,
366436 a,
367437 b,
368438 );
369439 }
370440
371− pub(crate) fn emit_remove_event_listener(&self, event: &str) {
441+ pub(crate) fn emit_remove_event_listener(&mut self, event: &str) {
372442 debug!("emit_remove_event_listener({:?})", event);
373− self.op2(
443+ let event_id = self.ensure_string(event);
444+ self.op1(
374445 ChangeDiscriminant::RemoveEventListener,
375− event.as_ptr() as u32,
376− event.len() as u32,
446+ event_id,
377447 );
378448 }
379449}
src/vdom.rs+8−5
@@ -256,6 +256,9 @@ impl VdomInnerExclusive {
256256
257257 self.events_registry = Some(events_registry);
258258
259+ // Find and drop cached strings that aren't in use anymore.
260+ self.change_list.drop_unused_strings();
261+
259262 // Tell JS to apply our diff-generated changes to the physical DOM!
260263 self.change_list.apply_changes();
261264 }
@@ -271,7 +274,7 @@ impl VdomInnerExclusive {
271274 self.current_root = Some(current);
272275 }
273276
274− fn diff<'a>(&self, registry: &mut EventsRegistry, old: Node<'a>, new: Node<'a>) {
277+ fn diff<'a>(&mut self, registry: &mut EventsRegistry, old: Node<'a>, new: Node<'a>) {
275278 // debug!("---------------------------------------------------------");
276279 // debug!("dodrio::Vdom::diff");
277280 // debug!(" old = {:#?}", old);
@@ -323,7 +326,7 @@ impl VdomInnerExclusive {
323326 }
324327
325328 fn diff_listeners<'a>(
326− &self,
329+ &mut self,
327330 registry: &mut EventsRegistry,
328331 old: &'a [Listener<'a>],
329332 new: &'a [Listener<'a>],
@@ -357,7 +360,7 @@ impl VdomInnerExclusive {
357360 }
358361 }
359362
360− fn diff_attributes(&self, old: &[Attribute], new: &[Attribute]) {
363+ fn diff_attributes(&mut self, old: &[Attribute], new: &[Attribute]) {
361364 debug!(" updating attributes");
362365
363366 // Do O(n^2) passes to add/update and remove attributes, since
@@ -392,7 +395,7 @@ impl VdomInnerExclusive {
392395 }
393396
394397 fn diff_children<'a>(
395− &self,
398+ &mut self,
396399 registry: &mut EventsRegistry,
397400 old: &'a [Node<'a>],
398401 new: &'a [Node<'a>],
@@ -449,7 +452,7 @@ impl VdomInnerExclusive {
449452 }
450453 }
451454
452− fn create<'a>(&self, registry: &mut EventsRegistry, node: Node<'a>) {
455+ fn create<'a>(&mut self, registry: &mut EventsRegistry, node: Node<'a>) {
453456 match node {
454457 Node::Text(TextNode { text }) => {
455458 self.change_list.emit_create_text_node(text);