dcapwell commented on code in PR #6:
URL: https://github.com/apache/cassandra-accord/pull/6#discussion_r946235636


##########
accord-core/src/main/java/accord/impl/SimpleProgressLog.java:
##########
@@ -375,6 +377,8 @@ public String toString()
         void recordBlocking(Command blocking, Keys someKeys)
         {
             this.blocking = blocking;
+            if (blocking.txn() != null && 
!blocking.txn().keys.containsAll(someKeys))
+                throw new IllegalStateException();

Review Comment:
   would be good to produce a meaningful error, so if this ever happens we have 
enough to debug



##########
accord-core/src/main/java/accord/primitives/Deps.java:
##########
@@ -0,0 +1,1394 @@
+package accord.primitives;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import java.util.stream.Collectors;
+
+import accord.api.Key;
+import accord.local.Command;
+import accord.utils.SortedArrays;
+import com.carrotsearch.hppc.ObjectIntHashMap;
+import com.google.common.base.Preconditions;
+
+import static accord.utils.ArrayBuffers.*;
+import static accord.utils.SortedArrays.*;
+
+/**
+ * A collection of dependencies for a transaction, organised by the key the 
dependency is adopted via.
+ * An inverse map from TxnId to Key may also be constructed and stored in this 
collection.
+ */
+public class Deps implements Iterable<Map.Entry<Key, TxnId>>
+{
+    private static final boolean DEBUG_CHECKS = true;
+
+    private static final TxnId[] NO_TXNIDS = new TxnId[0];
+    private static final int[] NO_INTS = new int[0];
+    public static final Deps NONE = new Deps(Keys.EMPTY, NO_TXNIDS, NO_INTS);
+
+    public static Deps none(Keys keys)
+    {
+        int[] keysToTxnId = new int[keys.size()];
+        Arrays.fill(keysToTxnId, keys.size());
+        return new Deps(keys, NO_TXNIDS, keysToTxnId);
+    }
+
+    /**
+     * A builder of Deps
+     *
+     * TODO: make reusable
+     */
+    public static class UnorderedBuilder
+    {
+        final Keys keys;
+        final ObjectIntHashMap<TxnId> txnIdLookup = new ObjectIntHashMap<>();
+        TxnId[] txnIds = new TxnId[4];
+        // Key -> [TxnId]
+        final int[][] keysToTxnId;
+        // Key -> Counter
+        final int[] keysToTxnIdCounts;
+
+        public UnorderedBuilder(Keys keys)
+        {
+            this.keys = keys;
+            this.keysToTxnId = new int[keys.size()][4];
+            this.keysToTxnIdCounts = new int[keys.size()];
+        }
+
+        public boolean isEmpty()
+        {
+            return txnIdLookup.isEmpty();
+        }
+
+        /**
+         * Add this command as a dependency for each intersecting key
+         */
+        public void add(Command command)
+        {
+            int idx = ensureTxnIdx(command.txnId());
+
+            long count = keys.foldlIntersect(command.txn().keys, (li, ri, k, 
p, v) -> {
+                if (keysToTxnId[li].length == keysToTxnIdCounts[li])
+                    keysToTxnId[li] = Arrays.copyOf(keysToTxnId[li], 
keysToTxnId[li].length * 2);
+                keysToTxnId[li][keysToTxnIdCounts[li]++] = idx;
+                return v + 1;
+            }, 0, 0, -1);
+
+            if (count == 0)
+                throw new IllegalArgumentException(command + " does not 
intersect " + keys);
+        }
+
+        /**
+         * Add a single Key &lt;-&gt; TxnId dependency
+         */
+        public void add(Key key, TxnId txnId)
+        {
+            int txnIdx = ensureTxnIdx(txnId);
+            int keyIdx = keys.indexOf(key);
+            if (keysToTxnIdCounts[keyIdx] == keysToTxnId[keyIdx].length)
+                keysToTxnId[keyIdx] = Arrays.copyOf(keysToTxnId[keyIdx], 
keysToTxnIdCounts[keyIdx] * 2);
+            keysToTxnId[keyIdx][keysToTxnIdCounts[keyIdx]++] = txnIdx;
+        }
+
+        public boolean contains(TxnId txnId)
+        {
+            return txnIdLookup.containsKey(txnId);
+        }
+
+        private int ensureTxnIdx(TxnId txnId)
+        {
+            int i = txnIdLookup.getOrDefault(txnId, -1);
+            if (i < 0)
+            {
+                txnIdLookup.put(txnId, i = txnIdLookup.size());
+                if (i == txnIds.length)
+                    txnIds = Arrays.copyOf(txnIds, txnIds.length * 2);
+            }
+            return i;
+        }
+
+        public Deps build()
+        {
+            TxnId[] txnIds = txnIdLookup.keys().toArray(TxnId.class);
+            Arrays.sort(txnIds, TxnId::compareTo);
+            // all data is indexed based off insertion order, but now that 
build is happening its desirable to have ids
+            // in tx order, in order to map from sorted -> insert order, need 
"txnIdMap" as the bridge
+            int[] txnIdMap = new int[txnIds.length];
+            for (int i = 0 ; i < txnIdMap.length ; i++)
+                txnIdMap[txnIdLookup.get(txnIds[i])] = i;
+
+            int keyCount = 0;
+            // Key -> distinct([*TxnID]])
+            // at the key position is the OFFSET into the array where the 
txnIds are for the key
+            // Example
+            // OFFSET(3), OFFSET(5), *tx1, *tx1, *tx2
+            int[] result; {
+                int count = 0;
+                for (int i = 0 ; i < keys.size() ; ++i)
+                {
+                    keyCount += keysToTxnIdCounts[i] > 0 ? 1 : 0;
+                    count += keysToTxnIdCounts[i];
+                }
+                result = new int[count + keyCount];
+            }
+
+            int keyIndex = 0;
+            int offset = keyCount;
+            for (int i = 0 ; i < keys.size() ; ++i)
+            {
+                if (keysToTxnIdCounts[i] > 0)
+                {
+                    int count = keysToTxnIdCounts[i];
+                    int[] src = keysToTxnId[i];
+                    // store all txnIds to results at offset
+                    for (int j = 0 ; j < count ; ++j)
+                        result[j + offset] = txnIdMap[src[j]];
+                    Arrays.sort(result, offset, count + offset);
+                    // the same txnId may be present multiple times; filter 
them with a linear scan
+                    int dups = 0;
+                    for (int j = offset + 1 ; j < offset + count ; ++j)
+                    {
+                        // if a dup is found, ignore it and we will either 
overwrite or truncate the length
+                        if (result[j] == result[j - 1]) ++dups;
+                        // otherwise if other dups have been found, the end of 
our list is further back and we must move this entry we are keeping
+                        else if (dups > 0) result[j - dups] = result[j];
+                    }
+                    result[keyIndex] = offset += count - dups;
+                    ++keyIndex;
+                }
+            }
+            if (offset < result.length)
+                result = Arrays.copyOf(result, offset);
+
+            Keys keys = this.keys;
+            if (keyCount < keys.size())
+            {
+                keyIndex = 0;
+                Key[] newKeys = new Key[keyCount];
+                for (int i = 0 ; i < keys.size() ; ++i)
+                {
+                    if (keysToTxnIdCounts[i] > 0)
+                        newKeys[keyIndex++] = keys.get(i);
+                }
+                keys = Keys.of(newKeys);
+            }
+
+            return new Deps(keys, txnIds, result);
+        }
+    }
+
+    /**
+     * Expects Command to be provided in TxnId order
+     */
+    public static OrderedBuilder orderedBuilder(boolean hasOrderedTxnId)
+    {
+        return new OrderedBuilder(hasOrderedTxnId);
+    }
+
+    // TODO: can use a variant of this for another approach to merges, that 
would be simpler, but probably worse constant factors
+    public static class OrderedBuilder implements AutoCloseable
+    {
+        final boolean hasOrderedTxnId;
+        Key[] keys;
+        int[] keyLimits;
+        // txnId -> Offset
+        TxnId[] keyToTxnId;
+        int keyCount;
+        int keyOffset;
+        int totalCount;
+
+        public OrderedBuilder(boolean hasOrderedTxnId)
+        {
+            this.keys = cachedKeys().get(16);
+            this.keyLimits = cachedInts().getInts(keys.length);
+            this.hasOrderedTxnId = hasOrderedTxnId;
+            this.keyToTxnId = cachedTxnIds().get(16);
+        }
+
+        public boolean isEmpty()
+        {
+            return totalCount() == 0;
+        }
+
+        private int totalCount()
+        {
+            return totalCount;
+        }
+
+        public void nextKey(Key key)
+        {
+            if (keyCount > 0 && keys[keyCount - 1].compareTo(key) >= 0)
+            {
+                throw new IllegalArgumentException("Key " + key + " has 
already been visited or was provided out of order ("
+                        + Arrays.toString(Arrays.copyOf(keys, keyCount)) + 
")");
+            }
+
+            finishKey();
+
+            if (keyCount == keys.length)
+            {
+                Key[] newKeys = cachedKeys().get(keyCount * 2);
+                System.arraycopy(keys, 0, newKeys, 0, keyCount);
+                cachedKeys().forceDiscard(keys, keyCount);
+                keys = newKeys;
+                int[] newKeyLimits = cachedInts().getInts(keyCount * 2);
+                System.arraycopy(keyLimits, 0, newKeyLimits, 0, keyCount);
+                cachedInts().forceDiscard(keyLimits);
+                keyLimits = newKeyLimits;
+            }
+            keys[keyCount++] = key;
+        }
+
+        private void finishKey()
+        {
+            if (totalCount == keyOffset && keyCount > 0)
+                --keyCount; // remove this key; no data
+
+            if (keyCount == 0)
+                return;
+
+            if (totalCount != keyOffset && !hasOrderedTxnId)
+            {
+                Arrays.sort(keyToTxnId, keyOffset, totalCount);
+                for (int i = keyOffset + 1 ; i < totalCount ; ++i)
+                {
+                    if (keyToTxnId[i - 1].equals(keyToTxnId[i]))
+                        throw new IllegalArgumentException("TxnId for " + 
keys[keyCount - 1] + " are not unique: " + 
Arrays.asList(keyToTxnId).subList(keyOffset, totalCount));
+                }
+            }
+
+            keyLimits[keyCount - 1] = totalCount;
+            keyOffset = totalCount;
+        }
+
+        public void add(Key key, TxnId txnId)
+        {
+            if (keyCount == 0 || !keys[keyCount - 1].equals(key))
+                nextKey(key);
+            add(txnId);
+        }
+
+        /**
+         * Add this command as a dependency for each intersecting key
+         */
+        public void add(TxnId txnId)
+        {
+            if (hasOrderedTxnId && totalCount > keyOffset && 
keyToTxnId[totalCount - 1].compareTo(txnId) >= 0)
+                throw new IllegalArgumentException("TxnId provided out of 
order");
+
+            if (totalCount >= keyToTxnId.length)
+            {
+                TxnId[] newTxnIds = cachedTxnIds().get(keyToTxnId.length * 2);
+                System.arraycopy(keyToTxnId, 0, newTxnIds, 0, totalCount);
+                cachedTxnIds().forceDiscard(keyToTxnId, totalCount);
+                keyToTxnId = newTxnIds;
+            }
+
+            keyToTxnId[totalCount++] = txnId;
+        }
+
+        // if make public, check these are sorted
+        private void addSortedUnique(TxnId[] txnIds, int count)
+        {
+            if (totalCount + count > keyToTxnId.length)
+            {
+                TxnId[] newTxnIds = 
cachedTxnIds().get(Math.max(keyToTxnId.length * 2, totalCount + count));
+                System.arraycopy(keyToTxnId, 0, newTxnIds, 0, totalCount);
+                cachedTxnIds().forceDiscard(keyToTxnId, totalCount);
+                keyToTxnId = newTxnIds;
+            }
+
+            System.arraycopy(txnIds, 0, keyToTxnId, totalCount, count);
+            totalCount += count;
+        }
+
+        public Deps build()
+        {
+            if (totalCount == 0)
+                return NONE;
+
+            finishKey();
+
+            TxnId[] uniqueTxnId = cachedTxnIds().get(totalCount);
+            System.arraycopy(keyToTxnId, 0, uniqueTxnId, 0, totalCount);
+            Arrays.sort(uniqueTxnId, 0, totalCount);
+            int txnIdCount = 1;
+            for (int i = 1 ; i < totalCount ; ++i)
+            {
+                if (!uniqueTxnId[txnIdCount - 1].equals(uniqueTxnId[i]))
+                    uniqueTxnId[txnIdCount++] = uniqueTxnId[i];
+            }
+
+            TxnId[] txnIds = cachedTxnIds().complete(uniqueTxnId, txnIdCount);
+            cachedTxnIds().discard(uniqueTxnId, totalCount);
+
+            int[] result = new int[keyCount + totalCount];
+            int offset = keyCount;
+            for (int k = 0 ; k < keyCount ; ++k)
+            {
+                result[k] = keyCount + keyLimits[k];
+                int from = k == 0 ? 0 : keyLimits[k - 1];
+                int to = keyLimits[k];
+                offset = (int)SortedArrays.foldlIntersection(txnIds, 0, 
txnIdCount, keyToTxnId, from, to, (li, ri, key, p, v) -> {
+                    result[(int)v] = li;
+                    return v + 1;
+                }, keyCount, offset, -1);
+            }
+
+            return new Deps(Keys.ofSortedUnchecked(cachedKeys().complete(keys, 
keyCount)), txnIds, result);
+        }
+
+        @Override
+        public void close()
+        {
+            cachedKeys().discard(keys, keyCount);
+            cachedInts().forceDiscard(keyLimits);
+            cachedTxnIds().forceDiscard(keyToTxnId, totalCount);
+        }
+    }
+
+    /**
+     * An object for managing a sequence of efficient linear merges Deps 
objects.
+     * Its primary purpose is to manage input and output buffers, so that we 
reuse output buffers
+     * as input to the next merge, and if any input is a superset of the other 
inputs that this input
+     * is returned unmodified.
+     *
+     * This is achieved by using PassThroughXBuffers so that the result 
buffers (and their sizes) are returned
+     * unmodified, and the buffers are cached as far as possible. In general, 
the buffers should be taken
+     * out of pre-existing caches, but if the buffers are too large then we 
cache any additional buffers we
+     * allocate for the duration of the merge.
+     */
+    private static class LinearMerger extends 
PassThroughObjectAndIntBuffers<TxnId> implements DepsConstructor<Key, TxnId, 
Object>
+    {
+        final PassThroughObjectBuffers<Key> keyBuffers;
+        Key[] bufKeys;
+        TxnId[] bufTxnIds;
+        int[] buf = null;
+        int bufKeysLength, bufTxnIdsLength = 0, bufLength = 0;
+        Deps from = null;
+
+        LinearMerger(boolean withCaching)
+        {
+            super(withCaching ? cachedTxnIds() : uncached(TxnId[]::new), 
withCaching ? cachedInts() : uncachedInts());
+            keyBuffers = new PassThroughObjectBuffers<>(withCaching ? 
cachedKeys() : uncached(Key[]::new));
+        }
+
+        @Override
+        public Object construct(Key[] keys, int keysLength, TxnId[] txnIds, 
int txnIdsLength, int[] out, int outLength)
+        {
+            if (from == null)
+            {
+                // if our input buffers were themselves buffers, we want to 
discard them unless they have been returned back to us
+                discard(keys, txnIds, out);
+            }
+            else if (buf != out)
+            {
+                // the output is not equal to a prior input
+                from = null;
+            }
+
+            if (from == null)
+            {
+                bufKeys = keys;
+                bufKeysLength = keysLength;
+                bufTxnIds = txnIds;
+                bufTxnIdsLength = txnIdsLength;
+                buf = out;
+                bufLength = outLength;
+            }
+            else
+            {
+                Preconditions.checkState(keys == bufKeys && keysLength == 
bufKeysLength);
+                Preconditions.checkState(txnIds == bufTxnIds && txnIdsLength 
== bufTxnIdsLength);
+                Preconditions.checkState(outLength == bufLength);
+            }
+            return null;
+        }
+
+        void update(Deps deps)
+        {
+            if (buf == null)
+            {
+                bufKeys = deps.keys.keys;
+                bufKeysLength = deps.keys.keys.length;
+                bufTxnIds = deps.txnIds;
+                bufTxnIdsLength = deps.txnIds.length;
+                buf = deps.keyToTxnId;
+                bufLength = deps.keyToTxnId.length;
+                from = deps;
+                return;
+            }
+
+            linearUnion(
+                    bufKeys, bufKeysLength, bufTxnIds, bufTxnIdsLength, buf, 
bufLength,
+                    deps.keys.keys, deps.keys.keys.length, deps.txnIds, 
deps.txnIds.length, deps.keyToTxnId, deps.keyToTxnId.length,
+                    keyBuffers, this, this, this
+            );
+            if (buf == deps.keyToTxnId)
+            {
+                Preconditions.checkState(deps.keys.keys == bufKeys && 
deps.keys.keys.length == bufKeysLength);
+                Preconditions.checkState(deps.txnIds == bufTxnIds && 
deps.txnIds.length == bufTxnIdsLength);
+                Preconditions.checkState(deps.keyToTxnId.length == bufLength);
+                from = deps;
+            }
+        }
+
+        Deps get()
+        {
+            if (buf == null)
+                return NONE;
+
+            if (from != null)
+                return from;
+
+            return new Deps(
+                    Keys.ofSortedUnchecked(keyBuffers.realComplete(bufKeys, 
bufKeysLength)),
+                    realComplete(bufTxnIds, bufTxnIdsLength),
+                    realComplete(buf, bufLength));
+        }
+
+        /**
+         * Free any buffers we no longer need
+         */
+        void discard()
+        {
+            if (from == null)
+                discard(null, null, null);
+        }
+
+        /**
+         * Free buffers unless they are equal to the corresponding parameter
+         */
+        void discard(Key[] freeKeysIfNot, TxnId[] freeTxnIdsIfNot, int[] 
freeBufIfNot)
+        {
+            if (from != null)
+                return;
+
+            if (bufKeys != freeKeysIfNot)
+            {
+                keyBuffers.realDiscard(bufKeys, bufKeysLength);
+                bufKeys = null;
+            }
+            if (bufTxnIds != freeTxnIdsIfNot)
+            {
+                realDiscard(bufTxnIds, bufTxnIdsLength);
+                bufTxnIds = null;
+            }
+            if (buf != freeBufIfNot)
+            {
+                realDiscard(buf, bufLength);
+                buf = null;
+            }
+        }
+    }
+
+    public static <T> Deps merge(List<T> merge, Function<T, Deps> getter)
+    {
+        return merge(true, merge, getter);
+    }
+
+    public static <T> Deps merge(boolean withCaching, List<T> merge, 
Function<T, Deps> getter)
+    {
+        LinearMerger linearMerger = new LinearMerger(withCaching);
+        try
+        {
+            int mergeIndex = 0, mergeSize = merge.size();
+            while (mergeIndex < mergeSize)
+            {
+                Deps deps = getter.apply(merge.get(mergeIndex++));
+                if (deps == null || deps.isEmpty())
+                    continue;
+
+                linearMerger.update(deps);
+            }
+
+            return linearMerger.get();
+        }
+        finally
+        {
+            linearMerger.discard();
+        }
+    }
+
+    // TODO: this is slower, so don't commit it - but keeping around for 
review purposes
+    public static <T> Deps mergeWithBuilder(Keys keys, List<T> merge, 
Function<T, Deps> getter)

Review Comment:
   cool to drop now.



##########
accord-core/src/main/java/accord/utils/ArrayBuffers.java:
##########
@@ -0,0 +1,548 @@
+package accord.utils;
+
+import accord.api.Key;
+import accord.primitives.TxnId;
+import com.google.common.base.Preconditions;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.function.IntFunction;
+
+/**
+ * A set of utility classes and interfaces for managing a collection of 
buffers for arrays of certain types.
+ *
+ * These buffers are designed to be used to combine simple one-shot methods 
that consume and produce one or more arrays
+ * with methods that may (or may not) call them repeatedly. Specifically, 
{@link accord.primitives.Deps#linearUnion},
+ * {@link SortedArrays#linearUnion} and {@link SortedArrays#linearIntersection}
+ *
+ * To support this efficiently and ergonomically for users of the one-shot 
methods, the cache management must
+ * support fetching buffers for re-use, but also returning either the buffer 
that was used (in the case where we
+ * intend to re-invoke this or another method with the buffer as input), or a 
properly sized final output array
+ * if the result of the method is to be consumed immediately.
+ *
+ * This functionality is implemented in {@link 
ObjectBuffers#complete(Object[], int)} and {@link IntBuffers#complete(int[], 
int)}
+ * which may either shrink the output array, or capture the size and return 
the buffer.
+ *
+ * Since these methods also may return either of their inputs, which may 
themselves be buffers, we support capturing
+ * the size of the input we have returned via {@link 
ObjectBuffers#completeWithExisting(Object[], int)}}
+ */
+public class ArrayBuffers
+{
+    private static final boolean FULLY_UNCACHED = true;
+
+    // TODO: we should periodically clear the thread locals to ensure we 
aren't slowly accumulating unnecessarily large objects on every thread
+    private static final ThreadLocal<IntBufferCache> INTS = 
ThreadLocal.withInitial(() -> new IntBufferCache(4, 1 << 14));
+    private static final ThreadLocal<ObjectBufferCache<Key>> KEYS = 
ThreadLocal.withInitial(() -> new ObjectBufferCache<>(3, 1 << 9, Key[]::new));
+    private static final ThreadLocal<ObjectBufferCache<TxnId>> TXN_IDS = 
ThreadLocal.withInitial(() -> new ObjectBufferCache<>(3, 1 << 12, 
TxnId[]::new));
+
+    public static IntBuffers cachedInts()
+    {
+        return INTS.get();
+    }
+
+    public static ObjectBuffers<Key> cachedKeys()
+    {
+        return KEYS.get();
+    }
+
+    public static ObjectBuffers<TxnId> cachedTxnIds()
+    {
+        return TXN_IDS.get();
+    }
+
+    public static <T> ObjectBuffers<T> uncached(IntFunction<T[]> allocator) { 
return new UncachedObjectBuffers<>(allocator); }
+
+    public static IntBuffers uncachedInts() { return 
UncachedIntBuffers.INSTANCE; }
+
+    public interface IntBufferAllocator
+    {
+        /**
+         * Return an {@code int[]} of size at least {@code minSize}, possibly 
from a pool
+         */
+        int[] getInts(int minSize);
+    }
+
+    public interface IntBuffers extends IntBufferAllocator
+    {
+        /**
+         * To be invoked on the result buffer with the number of elements 
contained;
+         * either the buffer will be returned and the size optionally 
captured, or else the result may be
+         * shrunk to the size of the contents, depending on implementation.
+         */
+        int[] complete(int[] buffer, int size);
+
+        /**
+         * The buffer is no longer needed by the caller, which is discarding 
the array;
+         * if {@link #complete(int[], int)} returned the buffer as its result 
this buffer should NOT be
+         * returned to any pool.
+         *
+         * Note that this method assumes {@link #complete(int[], int)} was 
invoked on this buffer previously.
+         * However, it is guaranteed that a failure to do so does not leak 
memory or pool space, only produces some
+         * additional garbage.
+         *
+         * @return true if the buffer is discarded (and discard-able), false 
if it was retained or is believed to be in use
+         */
+        boolean discard(int[] buffer, int size);
+
+        /**
+         * Indicate this buffer is definitely unused, and return it to a pool 
if possible
+         * @return true if the buffer is discarded (and discard-able), false 
if it was retained
+         */
+        boolean forceDiscard(int[] buffer);
+    }
+
+    public interface ObjectBuffers<T>
+    {
+        /**
+         * Return an {@code T[]} of size at least {@code minSize}, possibly 
from a pool
+         */
+        T[] get(int minSize);
+
+        /**
+         * To be invoked on the result buffer with the number of elements 
contained;
+         * either the buffer will be returned and the size optionally 
captured, or else the result may be
+         * shrunk to the size of the contents, depending on implementation.
+         */
+        T[] complete(T[] buffer, int size);
+
+        /**
+         * To be invoked on an input buffer that constitutes the result, with 
the number of elements it contained;
+         * either the buffer will be returned and the size optionally 
captured, or else the result may be
+         * shrunk to the size of the contents, depending on implementation.
+         */
+        T[] completeWithExisting(T[] buffer, int size);
+
+        /**
+         * The buffer is no longer needed by the caller, which is discarding 
the array;
+         * if {@link #complete(Object[], int)} returned the buffer as its 
result, this buffer should NOT be
+         * returned to any pool.
+         *
+         * Note that this method assumes {@link #complete(Object[], int)} was 
invoked on this buffer previously.
+         * However, it is guaranteed that a failure to do so does not leak 
memory or pool space, only produces some
+         * additional garbage.
+         *
+         * Note also that {@code size} should represent the size of the used 
space in the array, even if it was later
+         * truncated.
+         *
+         * @return true if the buffer is discarded (and discard-able), false 
if it was retained or is believed to be in use
+         */
+        boolean discard(T[] buffer, int size);
+
+        /**
+         * Indicate this buffer is definitely unused, and return it to a pool 
if possible
+         *
+         * Note that {@code size} should represent the size of the used space 
in the array, even if it was later truncated.
+         *
+         * @return true if the buffer is discarded (and discard-able), false 
if it was retained
+         */
+        boolean forceDiscard(T[] buffer, int size);
+
+        /**
+         * Returns the {@code size} parameter provided to the most recent 
{@link #complete(Object[], int)} or {@link #completeWithExisting(Object[], int)}
+         *
+         * Depending on implementation, this is either saved from the last 
such invocation, or else simply returns the size of the buffer parameter.
+         */
+        int lengthOfLast(T[] buffer);
+    }
+
+    private static final class UncachedIntBuffers implements IntBuffers
+    {
+        static final UncachedIntBuffers INSTANCE = new UncachedIntBuffers();
+        private UncachedIntBuffers()
+        {
+        }
+
+        @Override
+        public int[] getInts(int minSize)
+        {
+            return new int[minSize];
+        }
+
+        @Override
+        public int[] complete(int[] buffer, int size)
+        {
+            if (size == buffer.length)
+                return buffer;
+
+            return Arrays.copyOf(buffer, size);
+        }
+
+        @Override
+        public boolean discard(int[] buffer, int size)
+        {
+            return forceDiscard(buffer);
+        }
+
+        @Override
+        public boolean forceDiscard(int[] buffer)
+        {
+            // if FULLY_UNCACHED we want our caller to also not cache us, so 
we indicate the buffer has been retained
+            return !FULLY_UNCACHED;
+        }
+    }
+
+    private static final class UncachedObjectBuffers<T> implements 
ObjectBuffers<T>
+    {
+        final IntFunction<T[]> allocator;
+        private UncachedObjectBuffers(IntFunction<T[]> allocator)
+        {
+            this.allocator = allocator;
+        }
+
+        @Override
+        public T[] get(int minSize)
+        {
+            return allocator.apply(minSize);
+        }
+
+        @Override
+        public T[] complete(T[] buffer, int size)
+        {
+            if (size == buffer.length)
+                return buffer;
+
+            return Arrays.copyOf(buffer, size);
+        }
+
+        @Override
+        public T[] completeWithExisting(T[] buffer, int size)
+        {
+            Preconditions.checkArgument(buffer.length == size);
+            return buffer;
+        }
+
+        @Override
+        public int lengthOfLast(T[] buffer)
+        {
+            return buffer.length;
+        }
+
+        @Override
+        public boolean discard(T[] buffer, int size)
+        {
+            return forceDiscard(buffer, size);
+        }
+
+        @Override
+        public boolean forceDiscard(T[] buffer, int size)
+        {
+            // if FULLY_UNCACHED we want our caller to also not cache us, so 
we indicate the buffer has been retained
+            return !FULLY_UNCACHED;
+        }
+    }
+
+    /**
+     * A very simple cache that simply stores the largest {@code maxCount} 
arrays smaller than {@code maxSize}.
+     * Works on both primitive and Object arrays.
+     */
+    private static abstract class AbstractBufferCache<B>
+    {
+        interface Clear<B>
+        {
+            void clear(B array, int usedSize);
+        }
+
+        final IntFunction<B> allocator;
+        final Clear<B> clear;
+        final B empty;
+        final B[] cached;
+        final int maxSize;
+
+        AbstractBufferCache(IntFunction<B> allocator, Clear<B> clear, int 
maxCount, int maxSize)
+        {
+            this.allocator = allocator;
+            this.maxSize = maxSize;
+            this.cached = (B[])new Object[maxCount];
+            this.empty = allocator.apply(0);
+            this.clear = clear;
+        }
+
+        B getInternal(int minSize)
+        {
+            if (minSize == 0)
+                return empty;
+
+            if (minSize > maxSize)
+                return allocator.apply(minSize);
+
+            for (int i = 0 ; i < cached.length ; ++i)
+            {
+                if (cached[i] != null && Array.getLength(cached[i]) >= minSize)
+                {
+                    B result = cached[i];
+                    cached[i] = null;
+                    return result;
+                }
+            }
+
+            return allocator.apply(minSize);
+        }
+
+        boolean discardInternal(B buffer, int bufferSize, int usedSize, 
boolean force)
+        {
+            if (bufferSize > maxSize)
+                return true;
+
+            if (bufferSize == usedSize && !force)
+                return false;
+
+            for (int i = 0 ; i < cached.length ; ++i)
+            {
+                if (cached[i] == null || Array.getLength(cached[i]) < 
bufferSize)
+                {
+                    clear.clear(buffer, usedSize);
+                    cached[i] = buffer;
+                    return false;
+                }
+            }
+
+            return true;
+        }
+    }
+
+    public static class IntBufferCache extends AbstractBufferCache<int[]> 
implements IntBuffers
+    {
+        IntBufferCache(int maxCount, int maxSize)
+        {
+            super(int[]::new, (i1, i2) -> {}, maxCount, maxSize);
+        }
+
+        @Override
+        public int[] complete(int[] buffer, int size)
+        {
+            if (size == buffer.length)
+                return buffer;
+
+            return Arrays.copyOf(buffer, size);
+        }
+
+        @Override
+        public boolean discard(int[] buffer, int size)
+        {
+            return discardInternal(buffer, buffer.length, size, false);
+        }
+
+        @Override
+        public boolean forceDiscard(int[] buffer)
+        {
+            return discardInternal(buffer, buffer.length, -1, true);
+        }
+
+        @Override
+        public int[] getInts(int minSize)
+        {
+            return getInternal(minSize);
+        }
+    }
+
+    public static class ObjectBufferCache<T> extends AbstractBufferCache<T[]> 
implements ObjectBuffers<T>
+    {
+        final IntFunction<T[]> allocator;
+
+        ObjectBufferCache(int maxCount, int maxSize, IntFunction<T[]> 
allocator)
+        {
+            super(allocator, (array, usedSize) -> Arrays.fill(array, 0, 
usedSize, null), maxCount, maxSize);
+            this.allocator = allocator;
+        }
+
+        public T[] complete(T[] buffer, int size)
+        {
+            if (size == buffer.length)
+                return buffer;
+
+            return Arrays.copyOf(buffer, size);
+        }
+
+        @Override
+        public T[] completeWithExisting(T[] buffer, int size)
+        {
+            return buffer;
+        }
+
+        public int lengthOfLast(T[] buffer)
+        {
+            return buffer.length;
+        }
+
+        public boolean discard(T[] buffer, int size)
+        {
+            return discardInternal(buffer, buffer.length, size, false);
+        }
+
+        @Override
+        public boolean forceDiscard(T[] buffer, int size)
+        {
+            return discardInternal(buffer, buffer.length, size, true);
+        }
+
+        @Override
+        public T[] get(int minSize)
+        {
+            return getInternal(minSize);
+        }
+    }
+
+    /**
+     * Returns the buffer to the caller, saving the length if necessary
+     */
+    public static class PassThroughObjectBuffers<T> implements ObjectBuffers<T>
+    {
+        final ObjectBuffers<T> objs;
+        T[] savedObjs; // permit saving of precisely one unused buffer of any 
size to assist LinearMerge
+        int length;
+
+        public PassThroughObjectBuffers(ObjectBuffers<T> objs)
+        {
+            this.objs = objs;
+        }
+
+        @Override
+        public T[] get(int minSize)
+        {
+            length = -1;
+            if (savedObjs != null && savedObjs.length >= minSize)
+            {
+                T[] result = savedObjs;
+                savedObjs = null;
+                return result;
+            }
+            return objs.get(minSize);
+        }
+
+        @Override
+        public T[] complete(T[] buffer, int size)
+        {
+            length = size;
+            return buffer;
+        }
+
+        @Override
+        public T[] completeWithExisting(T[] buffer, int size)
+        {
+            length = size;
+            return buffer;
+        }
+
+        /**
+         * Invoke {@link #complete(Object[], int)} on the wrapped ObjectBuffers
+         */
+        public T[] realComplete(T[] buffer, int size)
+        {
+            return objs.complete(buffer, size);
+        }
+
+        @Override
+        public boolean discard(T[] buffer, int size)
+        {
+            return true;
+        }
+
+        @Override
+        public boolean forceDiscard(T[] buffer, int size)
+        {
+            if (!objs.forceDiscard(buffer, size))
+                return false;
+
+            return discardInternal(buffer);
+        }
+
+        /**
+         * Invoke {@link #discard(Object[], int)} on the wrapped ObjectBuffers
+         */
+        public void realDiscard(T[] buffer, int size)
+        {
+            if (!objs.discard(buffer, size))
+                return;
+
+            discardInternal(buffer);
+        }
+
+        private boolean discardInternal(T[] buffer)
+        {
+            if (savedObjs != null && savedObjs.length >= buffer.length)
+                return false;
+
+            savedObjs = buffer;
+            return true;
+        }
+
+        public int lengthOfLast(T[] buffer)
+        {
+            return length;

Review Comment:
   this can return `-1` which means we called without a call to `complete`, so 
rather than returning `-1` (which no caller handles) we should fail



##########
accord-core/src/main/java/accord/primitives/KeyRanges.java:
##########
@@ -0,0 +1,456 @@
+package accord.primitives;
+
+import accord.api.Key;
+import accord.utils.SortedArrays;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterators;
+
+import java.util.*;
+
+public class KeyRanges implements Iterable<KeyRange>
+{
+    public static final KeyRanges EMPTY = ofSortedAndDeoverlappedUnchecked(new 
KeyRange[0]);
+
+    final KeyRange[] ranges;
+
+    private KeyRanges(KeyRange[] ranges)
+    {
+        Preconditions.checkNotNull(ranges);
+        this.ranges = ranges;
+    }
+
+    public KeyRanges(List<KeyRange> ranges)
+    {
+        this(ranges.toArray(KeyRange[]::new));
+    }
+
+    @Override
+    public String toString()
+    {
+        return Arrays.toString(ranges);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        KeyRanges ranges1 = (KeyRanges) o;
+        return Arrays.equals(ranges, ranges1.ranges);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Arrays.hashCode(ranges);
+    }
+
+    @Override
+    public Iterator<KeyRange> iterator()
+    {
+        return Iterators.forArray(ranges);
+    }
+
+    public int rangeIndexForKey(int lowerBound, int upperBound, Key key)
+    {
+        return Arrays.binarySearch(ranges, lowerBound, upperBound, key,

Review Comment:
   now that generics are removed, can replace with `return 
SortedArrays.binarySearch(ranges, lowerBound, upperBound, key, (k, r) -> 
r.compareKey(k), SortedArrays.Search.FAST);`



##########
accord-core/src/main/java/accord/primitives/KeyRanges.java:
##########
@@ -0,0 +1,456 @@
+package accord.primitives;
+
+import accord.api.Key;
+import accord.utils.SortedArrays;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterators;
+
+import java.util.*;
+
+public class KeyRanges implements Iterable<KeyRange>
+{
+    public static final KeyRanges EMPTY = ofSortedAndDeoverlappedUnchecked(new 
KeyRange[0]);
+
+    final KeyRange[] ranges;
+
+    private KeyRanges(KeyRange[] ranges)
+    {
+        Preconditions.checkNotNull(ranges);
+        this.ranges = ranges;
+    }
+
+    public KeyRanges(List<KeyRange> ranges)
+    {
+        this(ranges.toArray(KeyRange[]::new));
+    }
+
+    @Override
+    public String toString()
+    {
+        return Arrays.toString(ranges);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        KeyRanges ranges1 = (KeyRanges) o;
+        return Arrays.equals(ranges, ranges1.ranges);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Arrays.hashCode(ranges);
+    }
+
+    @Override
+    public Iterator<KeyRange> iterator()
+    {
+        return Iterators.forArray(ranges);
+    }
+
+    public int rangeIndexForKey(int lowerBound, int upperBound, Key key)
+    {
+        return Arrays.binarySearch(ranges, lowerBound, upperBound, key,
+                                   (r, k) -> -((KeyRange) r).compareKey((Key) 
k));
+    }
+
+    public int rangeIndexForKey(Key key)
+    {
+        return rangeIndexForKey(0, ranges.length, key);
+    }
+
+    public boolean contains(Key key)
+    {
+        return rangeIndexForKey(key) >= 0;
+    }
+
+    public boolean containsAll(Keys keys)
+    {
+        return keys.rangeFoldl(this, (from, to, p, v) -> v + (to - from), 0, 
0, 0) == keys.size();
+    }
+
+    public int size()
+    {
+        return ranges.length;
+    }
+
+    public KeyRange get(int i)
+    {
+        return ranges[i];
+    }
+
+    public boolean isEmpty()
+    {
+        return size() == 0;
+    }
+
+    public KeyRanges select(int[] indexes)
+    {
+        KeyRange[] selection = new KeyRange[indexes.length];
+        for (int i=0; i<indexes.length; i++)
+            selection[i] = ranges[indexes[i]];
+        return new KeyRanges(selection);

Review Comment:
   nothing validates the inputs are sorted, so should use 
`ofSortedAndDeoverlapped`



##########
accord-core/src/main/java/accord/primitives/KeyRanges.java:
##########
@@ -0,0 +1,456 @@
+package accord.primitives;
+
+import accord.api.Key;
+import accord.utils.SortedArrays;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterators;
+
+import java.util.*;
+
+public class KeyRanges implements Iterable<KeyRange>
+{
+    public static final KeyRanges EMPTY = ofSortedAndDeoverlappedUnchecked(new 
KeyRange[0]);
+
+    final KeyRange[] ranges;
+
+    private KeyRanges(KeyRange[] ranges)
+    {
+        Preconditions.checkNotNull(ranges);
+        this.ranges = ranges;
+    }
+
+    public KeyRanges(List<KeyRange> ranges)
+    {
+        this(ranges.toArray(KeyRange[]::new));
+    }
+
+    @Override
+    public String toString()
+    {
+        return Arrays.toString(ranges);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        KeyRanges ranges1 = (KeyRanges) o;
+        return Arrays.equals(ranges, ranges1.ranges);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Arrays.hashCode(ranges);
+    }
+
+    @Override
+    public Iterator<KeyRange> iterator()
+    {
+        return Iterators.forArray(ranges);
+    }
+
+    public int rangeIndexForKey(int lowerBound, int upperBound, Key key)
+    {
+        return Arrays.binarySearch(ranges, lowerBound, upperBound, key,
+                                   (r, k) -> -((KeyRange) r).compareKey((Key) 
k));
+    }
+
+    public int rangeIndexForKey(Key key)
+    {
+        return rangeIndexForKey(0, ranges.length, key);
+    }
+
+    public boolean contains(Key key)
+    {
+        return rangeIndexForKey(key) >= 0;
+    }
+
+    public boolean containsAll(Keys keys)
+    {
+        return keys.rangeFoldl(this, (from, to, p, v) -> v + (to - from), 0, 
0, 0) == keys.size();
+    }
+
+    public int size()
+    {
+        return ranges.length;
+    }
+
+    public KeyRange get(int i)
+    {
+        return ranges[i];
+    }
+
+    public boolean isEmpty()
+    {
+        return size() == 0;
+    }
+
+    public KeyRanges select(int[] indexes)
+    {
+        KeyRange[] selection = new KeyRange[indexes.length];
+        for (int i=0; i<indexes.length; i++)
+            selection[i] = ranges[indexes[i]];
+        return new KeyRanges(selection);
+    }
+
+    public boolean intersects(Keys keys)
+    {
+        return findNextIntersection(0, keys, 0) >= 0;
+    }
+
+    public boolean intersects(KeyRanges that)
+    {
+        return SortedArrays.findNextIntersection(this.ranges, 0, that.ranges, 
0, KeyRange::compareIntersecting) >= 0;
+    }
+
+    public int findFirstKey(Keys keys)
+    {
+        return findNextKey(0, keys, 0);
+    }
+
+    public int findNextKey(int ri, Keys keys, int ki)
+    {
+        return (int) (findNextIntersection(ri, keys, ki) >> 32);
+    }
+
+    // returns ki in top 32 bits, ri in bottom, or -1 if no match found
+    public long findNextIntersection(int ri, Keys keys, int ki)
+    {
+        return SortedArrays.findNextIntersectionWithMultipleMatches(keys.keys, 
ki, ranges, ri);
+    }
+
+    public int findFirstKey(Key[] keys)
+    {
+        return findNextKey(0, keys, 0);
+    }
+
+    public int findNextKey(int ri, Key[] keys, int ki)
+    {
+        return (int) (findNextIntersection(ri, keys, ki) >> 32);
+    }
+
+    // returns ki in top 32 bits, ri in bottom, or -1 if no match found
+    public long findNextIntersection(int ri, Key[] keys, int ki)
+    {
+        return SortedArrays.findNextIntersectionWithMultipleMatches(keys, ki, 
ranges, ri);
+    }
+
+    /**
+     * Subtracts the given set of key ranges from this
+     * @param that
+     * @return
+     */
+    public KeyRanges difference(KeyRanges that)
+    {
+        if (that == this)
+            return KeyRanges.EMPTY;
+
+        List<KeyRange> result = new ArrayList<>(this.size() + that.size());
+        int thatIdx = 0;
+
+        for (int thisIdx=0; thisIdx<this.size(); thisIdx++)
+        {
+            KeyRange thisRange = this.ranges[thisIdx];
+            while (thatIdx < that.size())
+            {
+                KeyRange thatRange = that.ranges[thatIdx];
+
+                int cmp = thisRange.compareIntersecting(thatRange);
+                if (cmp > 0)
+                {
+                    thatIdx++;
+                    continue;
+                }
+                if (cmp < 0) break;
+
+                int scmp = thisRange.start().compareTo(thatRange.start());
+                int ecmp = thisRange.end().compareTo(thatRange.end());
+
+                if (scmp < 0)
+                    result.add(thisRange.subRange(thisRange.start(), 
thatRange.start()));
+
+                if (ecmp <= 0)
+                {
+                    thisRange = null;
+                    break;
+                }
+                else
+                {
+                    thisRange = thisRange.subRange(thatRange.end(), 
thisRange.end());
+                    thatIdx++;
+                }
+            }
+            if (thisRange != null)
+                result.add(thisRange);
+        }
+        return new KeyRanges(result.toArray(KeyRange[]::new));
+    }
+
+    /**
+     * attempts a linear merge where {@code as} is expected to be a superset 
of {@code bs},
+     * terminating at the first indexes where this ceases to be true
+     * @return index of {@code as} in upper 32bits, {@code bs} in lower 32bits
+     *
+     * TODO: better support for merging runs of overlapping or adjacent ranges
+     */
+    private static long supersetLinearMerge(KeyRange[] as, KeyRange[] bs)
+    {
+        int ai = 0, bi = 0;
+        out: while (ai < as.length && bi < bs.length)
+        {
+            KeyRange a = as[ai];
+            KeyRange b = bs[bi];
+
+            int c = a.compareIntersecting(b);
+            if (c < 0)
+            {
+                ai++;
+            }
+            else if (c > 0)
+            {
+                break;
+            }
+            else if (b.start().compareTo(a.start()) < 0)
+            {
+                break;
+            }
+            else if ((c = b.end().compareTo(a.end())) <= 0)
+            {
+                bi++;
+                if (c == 0) ai++;
+            }
+            else
+            {
+                // use a temporary counter, so that if we don't find a run of 
ranges that enforce the superset
+                // condition we exit at the start of the mismatch run (and 
permit it to be merged)
+                // TODO: use exponentialSearch
+                int tmpai = ai;
+                do
+                {
+                    if (++tmpai == as.length || 
!a.end().equals(as[tmpai].start()))
+                        break out;
+                    a = as[tmpai];
+                }
+                while (a.end().compareTo(b.end()) < 0);
+                bi++;
+                ai = tmpai;
+            }
+        }
+
+        return ((long)ai << 32) | bi;
+    }
+
+    /**
+     * @return true iff {@code that} is a subset of {@code this}
+     */
+    public boolean contains(KeyRanges that)
+    {
+        if (this.isEmpty()) return that.isEmpty();
+        if (that.isEmpty()) return true;
+
+        return ((int) supersetLinearMerge(this.ranges, that.ranges)) == 
that.size();
+    }
+
+    /**
+     * @return the union of {@code this} and {@code that}, returning one of 
the two inputs if possible
+     */
+    public KeyRanges union(KeyRanges that)
+    {
+        if (this == that) return this;
+        if (this.isEmpty()) return that;
+        if (that.isEmpty()) return this;
+
+        KeyRange[] as = this.ranges, bs = that.ranges;
+        {
+            // make sure as/ai represent the ranges that might fully contain 
the other
+            int c = as[0].start().compareTo(bs[0].start());
+            if (c > 0 || c == 0 && as[as.length - 
1].end().compareTo(bs[bs.length - 1].end()) < 0)
+            {
+                KeyRange[] tmp = as; as = bs; bs = tmp;
+            }
+        }
+
+        int ai, bi; {
+            long tmp = supersetLinearMerge(as, bs);
+            ai = (int)(tmp >>> 32);
+            bi = (int)tmp;
+        }
+
+        if (bi == bs.length)
+            return as == this.ranges ? this : that;
+
+        KeyRange[] result = new KeyRange[as.length + (bs.length - bi)];
+        int resultCount = copyAndMergeTouching(as, 0, result, 0, ai);
+
+        while (ai < as.length && bi < bs.length)
+        {
+            KeyRange a = as[ai];
+            KeyRange b = bs[bi];
+
+            int c = a.compareIntersecting(b);
+            if (c < 0)
+            {
+                result[resultCount++] = a;
+                ai++;
+            }
+            else if (c > 0)
+            {
+                result[resultCount++] = b;
+                bi++;
+            }
+            else
+            {
+                Key start = a.start().compareTo(b.start()) <= 0 ? a.start() : 
b.start();
+                Key end = a.end().compareTo(b.end()) >= 0 ? a.end() : b.end();
+                ai++;
+                bi++;
+                while (ai < as.length || bi < bs.length)
+                {
+                    KeyRange min;
+                    if (ai == as.length) min = bs[bi];
+                    else if (bi == bs.length) min = a = as[ai];
+                    else min = as[ai].start().compareTo(bs[bi].start()) < 0 ? 
a = as[ai] : bs[bi];
+                    if (min.start().compareTo(end) > 0)
+                        break;
+                    if (min.end().compareTo(end) > 0)
+                        end = min.end();
+                    if (a == min) ai++;
+                    else bi++;
+                }
+                result[resultCount++] = a.subRange(start, end);
+            }
+        }
+
+        while (ai < as.length)
+            result[resultCount++] = as[ai++];
+
+        while (bi < bs.length)
+            result[resultCount++] = bs[bi++];
+
+        if (resultCount < result.length)
+            result = Arrays.copyOf(result, resultCount);
+
+        return new KeyRanges(result);
+    }
+
+    public KeyRanges mergeTouching()
+    {
+        if (ranges.length == 0)
+            return this;
+
+        KeyRange[] result = new KeyRange[ranges.length];
+        int count = copyAndMergeTouching(ranges, 0, result, 0, ranges.length);
+        if (count == result.length)
+            return this;
+        result = Arrays.copyOf(result, count);
+        return new KeyRanges(result);
+    }
+
+    private static int copyAndMergeTouching(KeyRange[] src, int srcPosition, 
KeyRange[] trg, int trgPosition, int srcCount)
+    {
+        if (srcCount == 0)
+            return 0;
+
+        int count = 0;
+        KeyRange prev = src[srcPosition];
+        Key end = prev.end();
+        for (int i = 1 ; i < srcCount ; ++i)
+        {
+            KeyRange next = src[srcPosition + i];
+            if (end.equals(next.start()))
+            {
+                end = next.end();
+            }
+            else
+            {
+                trg[trgPosition + count++] = maybeUpdateEnd(prev, end);
+                prev = next;
+                end = next.end();
+            }

Review Comment:
   could replace with
   
   ```
   if (!end.equals(next.start()))
               {
                   trg[trgPosition + count++] = maybeUpdateEnd(prev, end);
                   prev = next;
               }
               end = next.end();
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to