<div>Hi Andy</div><br><div>I noticed in VFlow.onContentChange (line 1051) that the whole cellCache is cleared, instead of just the relevant cells that were changed being reset. On reviewing the FastCache implementation I can see the difficulty in why this is the case, so after giving it some thought I have two simple variants of FastCache, that would enable resetting of cells in the cache, for you to consider using instead.</div><br><div>First some background. The current FastCache is great for random data content, however if I'm not mistaken the RichTextArea data that it holds isn't random but rather consists of a range (window) of consecutive cells (paragraphs) from some start index to an end index. These ranges usually move linearly forwards or backwards a single cell at a time (for scrolling), or can jump to either the immediately preceeding or proceeding range (for paging), and sometimes jump to another range completely. It's with this type of data content and behavior that the following two variants are based on.</div><br><div>In the first variant the HashMap is simply dropped and the cache is based solely on the Entry<T> array using the modulo of the index and cache size. This then becomes a circular or ring cache:</div><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">public class FastCircularCache<T> {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    private static record Entry<T>(int index, T cell) { }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    private final Entry<T>[] data;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    private final int capacity;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public FastCircularCache(int capacity) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        data = new Entry[capacity];</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        this.capacity = capacity;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public T get(int row) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        Entry<T> entry = data[row % capacity];</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        if (entry != null && entry.index() == row) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">            return entry.cell();</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        return null;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    /**</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     * Adds a new cell to the cache. NOTE: this method does not check whether</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     * another cell for the given row is present, so this call must be preceded by a</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     * {@link #get(int)}.</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     */</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public void add(int index, T cell) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        data[index % capacity] = new Entry<>(index, cell);</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public void reset(int start, int end) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        for (int i = start; i < end; i++) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">            data[i % capacity] = null;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public void clear() {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        Arrays.fill(data, null);</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">}</pre></code><br><div>In the second variant the array is dropped instead and the cache is based solely on the HashMap together with min and max int variables, which are the minium and maximum indices present at a particular moment in the HashMap. These are used as eviction indices, where min is evicted from the HashMap if the new index is increasing otherwise max is evicted if the new index is decreasing. This is then a window or sliding cache:</div><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">public class FastSlidingCache<T> {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    private int min = Integer.MAX_VALUE, max = 0;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    private final HashMap<Integer, T> data;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    private final int capacity;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public FastSlidingCache(int capacity) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        data = new HashMap<>(capacity);</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        this.capacity = capacity;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public T get(int row) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        if (row < min || row > max) return null;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        return data.get(row);</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    /**</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     * Adds a new cell to the cache. When the cache is full, this method evicts a</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     * cell from the cache first. NOTE: this method does not check whether</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     * another cell for the given row is present, so this call must be preceded by a</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     * {@link #get(int)}.</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">     */</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public void add(int index, T cell) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        if (data.size() >= capacity) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">            if (index < min) while (!evict(max--));</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">            if (index > max) while (!evict(min++));</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        if (index < min) min = index;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        if (index > max) max = index;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        data.put(index, cell);</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    private boolean evict(int index) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        return data.remove(index) != null;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public void reset(int start, int end) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">            for (int i = start; i < end; i++) {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">         data.remove(i);</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em"> </pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    public void clear() {</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        min = Integer.MAX_VALUE;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        data.clear();</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">        max = 0;</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">    }</pre></code><code><pre style="background-color:rgba(0,0,0,0.05);padding:0.2em 1em">}</pre></code><div>Each variant has it's strengths and weaknesses. I hope you'll find one of them useful or at least inspiring in improving the current FastCache implementation.</div><br><div>Kind regards</div><div>Jurgen</div><br>