Skip to content

Commit 6d3c2b1

Browse files
committed
[nested-v-grid] Add cache-eviction test
1 parent 85a24b0 commit 6d3c2b1

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

src/re_com/nested_v_grid/util.cljc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
(vswap! sum-size + leaf-size)
141141
leaf-size))))]
142142
(walk [] header-tree {:hide? hide-root?})
143-
(vswap! size-cache assoc :depth @depth)
143+
(cache! :depth @depth)
144144
{:sum-size (or cached-sum-size @sum-size)
145145
:spans @spans
146146
:positions @sums

test/re_com/nested_grid_test.cljs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
(ns re-com.nested-grid-test
22
(:require
33
[cljs.test :refer-macros [is are deftest]]
4-
[re-com.nested-v-grid.util :as ngu]))
4+
[re-com.nested-v-grid.util :as ngu]
5+
[snitch.core :refer-macros [*let]]))
56

67
(def main-keys [:header-paths :keypaths :sizes :sum-size :positions])
78

@@ -94,6 +95,8 @@
9495
Once calculated, walk-size keeps this total-size in a cache,
9596
keyed by the branch node's identity.
9697
98+
This cache of total-sizes is similar to a range tree.
99+
97100
When a branch-node is cached, the traversal does not descend into it.
98101
One exception is when the branch-node's total-bounds intersect the window-bounds.
99102
In that case, a descent is needed - not to calculate sizes,
@@ -185,3 +188,68 @@
185188
[:a [:b [:d] :c]] [:root [:a [:b [:d]] [:c]]]
186189
[:a [:b [:c [:d]]]] [:root [:a [:b [:c [:d]]]]]
187190
[:a [:b [:c [:d]] [:e [:f]] :g]] [:root [:a [:b [:c [:d]] [:e [:f]]] [:g]]]))
191+
192+
(deftest cache-eviction
193+
(let [a {:id :a :size 10}
194+
b {:id :b :size 10}
195+
new-b {:id :b :size 11}
196+
c {:id :c :size 10}
197+
parent [a]
198+
old-subtree [b c]
199+
new-subtree [new-b c]
200+
old-tree (conj parent old-subtree)
201+
new-tree (conj parent new-subtree)
202+
size-cache (volatile! {})
203+
{:keys [keypaths header-paths]} (ngu/window {:header-tree old-tree
204+
:size-cache size-cache})]
205+
(is (= [{:id :a} {:id :b} {:id :c}] (nth header-paths 2)))
206+
(is (= [1 1] (nth keypaths 2)))
207+
(is (contains? @size-cache old-subtree))
208+
(ngu/window {:header-tree new-tree
209+
:size-cache size-cache})
210+
(is (and (contains? @size-cache old-subtree)
211+
(contains? @size-cache old-tree)
212+
(contains? @size-cache new-subtree)
213+
(contains? @size-cache new-tree))
214+
"From a CS perspective, the size-cache works like a segment tree.
215+
An internal node's interval is the union of the intervals of its
216+
children (wikipedia). Changing any node means changing all of its
217+
parent nodes.
218+
219+
More concretely, if any node of a header-tree changes identity,
220+
that node, and all its ancestors, will then miss the cache.
221+
That's because nodes contain their children, similar to hiccups.
222+
It is necessary to invalidate them in the cache,
223+
since all their sizes must be recalculated.
224+
225+
This scheme of implicit cache-invalidation is how `nested-grid`
226+
can handle reactive updates to the header-tree without doing expensive
227+
searches or complex state management. The user can simply pass in
228+
a new header-tree, and `nested-grid` implicitly \"knows\" which
229+
nodes have changed.
230+
231+
However, after an update, the old identities of that node and its ancestors
232+
remain in the cache, unless we remove them.
233+
Stale identities accumulate, especially when resizing,
234+
where these identities change on every render.
235+
This behaves like a memory leak.
236+
237+
`nested-grid` can't afford to check for stale values
238+
by searching or diffing the whole tree
239+
(if it could, then we wouldn't need virtualization).
240+
That means that whatever actor is responsible for updating the header-tree
241+
must also manage these stale cache values.")
242+
243+
(vswap! size-cache ngu/evict! old-tree [1 1])
244+
245+
(is (and (not (contains? @size-cache old-subtree))
246+
(not (contains? @size-cache old-tree))
247+
(contains? @size-cache new-subtree)
248+
(contains? @size-cache new-tree))
249+
"`re-com.nested-grid.util` provides an `evict!` function for this purpose.
250+
When passed the cache, a tree and a keypath to a header-spec,
251+
`evict!` removes that node, and all its ancestor nodes, from the cache.
252+
253+
Since `nested-grid` provides its own resize-buttons, it does this eviction
254+
just before calling the `on-resize` handler. That means `nested-grid`'s built-in
255+
resize behavior automatically prevents the \"memory leak\".")))

0 commit comments

Comments
 (0)