/* * Copyright (C) 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook.collections.specialized; import com.facebook.collections.specialized.LongTupleHeap; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.NoSuchElementException; /** * List of long-tuples that are sorted by their first element in ascending order. * The first element may not be negative, but subsequent ones may be. * * The list will have an initial amount of space allocated according to initialListSize, but * will grow/shrink as items are added and removed * */ public abstract class AbstractLongTupleList implements LongTupleHeap { private static final int SORT_INDEX = 0; private static final int DEFAULT_INITIAL_LIST_SIZE = 1; // size in tuples private static final int DEFAULT_TUPLE_SIZE = 2; private static final int ALLOCATION_CHUNK_SIZE = 1; // size in tuples private static final int EMPTY = -1; // sentinel to indicate empty private long[] tuples; private volatile int size = 0; // in # of tuples /** * * @param initialListSize initial number of tuples for which to allocate spacee * @param tupleSize */ protected AbstractLongTupleList(int initialListSize, int tupleSize) { this.tuples = new long[initialListSize * tupleSize]; Arrays.fill(tuples, EMPTY); setHeadIndex(0); } protected AbstractLongTupleList(long[] tuples, int size) { this.tuples = tuples; this.size = size; } public AbstractLongTupleList(AbstractLongTupleList otherTupleList) { // we want a consistent view of otherTupleList, so synchronize on it synchronized (otherTupleList) { if (getTupleSize() != otherTupleList.getTupleSize()) { throw new IllegalArgumentException( String.format( "mismatched tuple sizes: [%d] and [%d]", getTupleSize(), otherTupleList.getTupleSize() ) ); } tuples = Arrays.copyOf(otherTupleList.tuples, otherTupleList.tuples.length); size = otherTupleList.size; } } /** * must not refer to 'this' at all; should return a constant, or value computed on other * well-formed objects * * @return */ protected abstract int getTupleSize(); /** * creates a new heap * @return */ protected abstract LongTupleHeap copyHeap(long[] tuples, int size); @Override public synchronized long[] peek() { return findSmallest(false); } @Override public synchronized long[] poll() { return findSmallest(true); } @Override public synchronized boolean add(long[] tuple) { if (tuple.length != getTupleSize()) { throw new IllegalArgumentException(String.format("tuples must be of size %d ", getTupleSize())); } if (tuple[SORT_INDEX] < 0) { throw new IllegalArgumentException( String.format( "tuple[%d] with value %d is not >= 0 ", SORT_INDEX, tuple[SORT_INDEX] ) ); } if (!spaceFor(1)) { resize(); } // returns the location we should insert at int insertLocation = findInsertLocation(tuple[SORT_INDEX]); if (insertLocation >= tuples.length) { // means we need to insert at the end, but it's not empty int numShifted = getTupleSize() * leftCompact(); insertLocation -= numShifted; } else if (!isEmpty(insertLocation)) { // we shift the tuples over by one at the insert location, and adjust our insert // location accordingly insertLocation = rightShift(insertLocation); } // else the location is empty already insertAt(tuple, insertLocation); updateHeadIndex(insertLocation); return true; } @Override public synchronized boolean addAll(Collection<? extends long[]> tuples) { throw new UnsupportedOperationException("not yet"); } @Override public int size() { return size; } /** * @return # of long elements saved */ @Override public synchronized int shrink() { if (size == 0 || translate(size) == tuples.length) { return 0; } leftCompact(); int minSize = Math.max(1, size); int rawSize = getTupleSize() * minSize; int saved = tuples.length - rawSize; long[] newTuples = new long[rawSize]; if (size > 0) { System.arraycopy(tuples, 0, newTuples, 0, rawSize); } else { Arrays.fill(newTuples, EMPTY); // indicate the 'head' index is 0 newTuples[0] = 0; } tuples = newTuples; return saved; } @Override public synchronized LongTupleHeap makeCopy() { return copyHeap(tuples, size); } @Override public Iterator<long[]> iterator() { return new Iter(); } /** * moves all values left so there are no empty slots at the start * * @return # of empty slots we found t the start */ private int leftCompact() { if (size == 0) { return 0; } // headIndex tells us the length of empty portion before the first non-empty location int headLength = getHeadIndex(); if (headLength == 0) { return 0; } System.arraycopy(tuples, headLength, tuples, 0, translate(size)); return headLength; } /** * moves all values left so there are no empty slots at the start. Works if there are empty gaps * between elements * * keeping this around for potential future tweaks if we find a sparse array makes sense * * @return # of empty slots we found at the start of the list */ private int leftCompactSparse() { int translatedWritePosition = 0; int translatedReadPosition = getTupleSize(); int numProcessed = 0; int emptySlots = 1; while (numProcessed < size) { if (isEmpty(translatedWritePosition)) { while (isEmpty(translatedReadPosition) && translatedReadPosition < tuples.length) { translatedReadPosition += getTupleSize(); emptySlots++; } if (translatedReadPosition >= tuples.length) { throw new IllegalStateException( "compact failed--couldn't find non-empty to copy to empty" ); } swap(translatedWritePosition, translatedReadPosition); } // invariant: after each iteration, the item at translatedWritePosition is considered // processed numProcessed++; translatedWritePosition += getTupleSize(); translatedReadPosition += getTupleSize(); } return emptySlots; } private void swap(int firstTranslatedPosition, int secondTranslatedPosition) { long[] tmpList = new long[getTupleSize()]; System.arraycopy(tuples, firstTranslatedPosition, tmpList, 0, getTupleSize()); System.arraycopy(tuples, secondTranslatedPosition, tuples, firstTranslatedPosition, getTupleSize()); System.arraycopy(tmpList, 0, tuples, secondTranslatedPosition, getTupleSize()); } /** * finds the first slot that has a tuple >= value * * @return index of the slot, or the index to the left if it's empty */ private int findInsertLocation(long value) { // negative value at 0th slot => start index of non-empty values int startIndex = getHeadIndex(); int endIndex = startIndex + translate(size); int i; for (i = startIndex; i < endIndex; i += getTupleSize()) { if (startIndex >= getTupleSize() && isEmpty(i - getTupleSize()) && tuples[i] >= value) { return i - getTupleSize(); } if (isEmpty(i) || tuples[i] >= value) { break; } } return i; } /** * invariant: there is an empty slot at the position returned. This method will call * leftCompact() if it needs to in order to make room for any right-shifts needed * * @param start */ private int rightShift(int start) { if (!isEmpty(tuples.length - getTupleSize())) { int numShifted = getTupleSize() * leftCompact(); start -= numShifted; } // last index of last tuple int endIndex = getHeadIndex() + translate(size) - 1; // right-shift by; [start, endIndex] (inclusive) System.arraycopy(tuples, start, tuples, start + getTupleSize(), endIndex - start + 1); return start; } private boolean isEmpty(int position) { return tuples[position] < 0; } private boolean spaceFor(int numItems) { return translate(size - 1) + getTupleSize() * numItems < tuples.length; } /** * @param tuple tuple to insert * @param translatedPosition translated translatedPosition */ private void insertAt(long[] tuple, int translatedPosition) { if (tuple.length != getTupleSize()) { throw new IllegalArgumentException(String.format("tuples must be of size %d ", getTupleSize())); } int i = 0; for (long item : tuple) { tuples[translatedPosition + i] = item; i++; } size++; } private void resize() { int newSize = getTupleSize() * (size + ALLOCATION_CHUNK_SIZE); long[] replacement = new long[newSize]; System.arraycopy(tuples, 0, replacement, 0, tuples.length); Arrays.fill(replacement, tuples.length, replacement.length, EMPTY); tuples = replacement; } private long[] findSmallest(boolean remove) { if (size == 0) { return null; } int headIndex = getHeadIndex(); long[] tupleAt = getTupleAt(headIndex); if (remove) { // mark this as empty tuples[headIndex] = EMPTY; // and set the head pointer setHeadIndex(headIndex + getTupleSize()); size--; } return tupleAt; } private int getHeadIndex() { // the first element is overloaded to be a pointer to the head of the non-empty segment // of the tuples when it is negative if (tuples[0] < 0) { return (int) (-1 * tuples[0]); } else { return 0; } } private void setHeadIndex(int index) { if (tuples[0] < 0) { tuples[0] = -index; } else { throw new IllegalStateException( String.format("trying to set head index when not empty. value: %d", tuples[0]) ); } } /** * reads the current head index and updates if the new one is smaller * * @param insertLocation */ private void updateHeadIndex(int insertLocation) { int headIndex = getHeadIndex(); if (headIndex > insertLocation) { setHeadIndex(insertLocation); } } /** * ex: with getTupleSize()=2, and position 3, this returns 6 * * @param position tuple position * @return position in flattened array */ private int translate(int position) { return getTupleSize() * position; } private int invertTranslation(int translatedPosition) { return translatedPosition / getTupleSize(); } private long[] getTupleAt(int translatedPosition) { long[] result = new long[getTupleSize()]; System.arraycopy(tuples, translatedPosition, result, 0, getTupleSize()); return result; } private class Iter implements Iterator<long[]> { private int position = getHeadIndex() / getTupleSize(); private long[] nextValue = null; @Override public boolean hasNext() { boolean hasNext = true; synchronized (AbstractLongTupleList.this) { if (nextValue == null) { int translatedPosition = translate(position); int translatedSize = translate(size); while (translatedPosition < translatedSize && isEmpty(translatedPosition)) { translatedPosition += getTupleSize(); } hasNext = translatedPosition < translatedSize; if (hasNext) { position = invertTranslation(translatedPosition); nextValue = getTupleAt(translatedPosition); } } } return hasNext; } @Override public long[] next() { synchronized (AbstractLongTupleList.this) { if (!hasNext()) { throw new NoSuchElementException( String.format( "position: %d, nextValue %s", position, nextValue ) ); } position++; long[] result = nextValue; // null out nextValue so hasNext() will fill it in nextValue = null; return result; } } @Override public void remove() { throw new UnsupportedOperationException("remove not supported; read-only"); } } }