|
1 | 1 | (ns re-com.nested-grid-test |
2 | 2 | (:require |
3 | 3 | [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]])) |
5 | 6 |
|
6 | 7 | (def main-keys [:header-paths :keypaths :sizes :sum-size :positions]) |
7 | 8 |
|
|
94 | 95 | Once calculated, walk-size keeps this total-size in a cache, |
95 | 96 | keyed by the branch node's identity. |
96 | 97 |
|
| 98 | + This cache of total-sizes is similar to a range tree. |
| 99 | +
|
97 | 100 | When a branch-node is cached, the traversal does not descend into it. |
98 | 101 | One exception is when the branch-node's total-bounds intersect the window-bounds. |
99 | 102 | In that case, a descent is needed - not to calculate sizes, |
|
185 | 188 | [:a [:b [:d] :c]] [:root [:a [:b [:d]] [:c]]] |
186 | 189 | [:a [:b [:c [:d]]]] [:root [:a [:b [:c [:d]]]]] |
187 | 190 | [: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