overflowdb.util.NodesList Maven / Gradle / Ivy
package overflowdb.util;
import gnu.trove.map.TLongIntMap;
import gnu.trove.map.TMap;
import gnu.trove.map.hash.THashMap;
import gnu.trove.map.hash.TLongIntHashMap;
import overflowdb.Node;
import overflowdb.storage.NodesWriter;
import java.util.*;
public class NodesList {
private Node[] nodes;
private int size = 0;
//index into `nodes` array by node id
private TLongIntMap nodeIndexByNodeId;
private TMap> nodesByLabel;
/** list of available slots in `nodes` array. slots become available after nodes have been removed */
private final BitSet emptySlots;
private static final int DEFAULT_CAPACITY = 10000;
public NodesList() {
this(DEFAULT_CAPACITY);
}
public NodesList(int initialCapacity) {
nodes = new Node[initialCapacity];
emptySlots = new BitSet(initialCapacity);
nodeIndexByNodeId = new TLongIntHashMap(initialCapacity);
nodesByLabel = new THashMap<>(10);
}
/** store Node in internal collections */
public synchronized void add(Node node) {
verifyUniqueId(node);
int index = tryClaimEmptySlot();
if (index == -1) {
// no empty spot available - append to nodes array instead
index = size;
ensureCapacity(size + 1);
}
nodes[index] = node;
nodeIndexByNodeId.put(node.id(), index);
if(nodesByLabel != null) {
nodesByLabel(node.label()).add(node);
}
size++;
}
private void verifyUniqueId(Node node) {
if (nodeIndexByNodeId.containsKey(node.id())) {
Node existingNode = nodeById(node.id());
throw new AssertionError("different Node with same id already exists in this NodesList: " + existingNode);
}
}
/** @return -1 if no available empty slots, otherwise the successfully claimed slot */
private int tryClaimEmptySlot() {
final int nextEmptySlot = emptySlots.nextSetBit(0);
if (nextEmptySlot != -1) {
emptySlots.clear(nextEmptySlot);
}
return nextEmptySlot;
}
public boolean contains(long id) {
return nodeIndexByNodeId.containsKey(id);
}
public Node nodeById(long id) {
if (nodeIndexByNodeId.containsKey(id)) {
return nodes[nodeIndexByNodeId.get(id)];
} else {
return null;
}
}
public synchronized void remove(Node node) {
int index = nodeIndexByNodeId.remove(node.id());
nodes[index] = null;
emptySlots.set(index);
/** We drop the entire `nodesByLabel` index and will rebuild it on the next index lookup...
* Context: we don't know the exact position of this node in the `nodesByLabel` index, and
* searching it is O(n).
* We considered using a separate TLongIntMap for the Id->Index mapping, but that would
* consume 12-20b per node */
this.nodesByLabel = null;
size--;
compactMaybe();
}
public int size() {
return size;
}
private TMap> getNodesByLabel() {
TMap> nodesByLabel = this.nodesByLabel;
if (nodesByLabel != null) {
return nodesByLabel;
} else {
initialiseNodesByLabel();
return getNodesByLabel();
}
}
private synchronized void initialiseNodesByLabel(){
if (nodesByLabel == null) {
TMap> tmp = new THashMap<>();
for (Node node : nodes) {
if (node != null) {
ArrayList nodelist = tmp.get(node.label());
if (nodelist == null) {
nodelist = new ArrayList<>();
tmp.put(node.label(), nodelist);
}
nodelist.add(node);
}
}
this.nodesByLabel = tmp;
}
}
public ArrayList nodesByLabel(String label) {
TMap> nodesByLabel = getNodesByLabel();
ArrayList nodelist = nodesByLabel.get(label);
if (nodelist == null){
nodelist = new ArrayList<>();
nodesByLabel.put(label, nodelist);
}
return nodelist;
}
public Set nodeLabels() {
TMap> nodesByLabel = getNodesByLabel();
Set ret = new HashSet<>(nodesByLabel.size());
nodesByLabel.entrySet().forEach(entry -> {
if (!entry.getValue().isEmpty()) {
ret.add(entry.getKey());
}
});
return ret;
}
public synchronized Iterator iterator() {
return new NodesIterator(Arrays.copyOf(nodes, nodes.length));
}
private void ensureCapacity(int minCapacity) {
if (nodes.length < minCapacity) grow(minCapacity);
}
/** compact if there are many empty slots, and they make up >= 30% of the node array */
private void compactMaybe() {
final int emptyCount = emptySlots.cardinality();
if (emptyCount > 10000 &&
emptyCount * 100 / nodes.length >= 30) {
compact();
}
}
/** Trims down internal collections to just about the necessary size, in order to allow the remainder to be
* garbage collected. Creates a new `nodes` array which contains all existing nodes. */
synchronized void compact() {
final ArrayList newNodes = new ArrayList<>(size);
Iterator iter = iterator();
while (iter.hasNext()) {
newNodes.add(iter.next());
}
nodes = newNodes.toArray(new Node[size]);
//reindex helper collections
emptySlots.clear();
nodeIndexByNodeId = new TLongIntHashMap(this.nodes.length);
int idx = 0;
while (idx < this.nodes.length) {
Node node = this.nodes[idx];
nodeIndexByNodeId.put(node.id(), idx);
idx++;
}
}
/** The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
* @see java.util.ArrayList (copied from there)
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/** Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* @see java.util.ArrayList (copied from there) */
private synchronized void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = nodes.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
nodes = Arrays.copyOf(nodes, newCapacity);
}
/** @see java.util.ArrayList (copied from there) */
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
/** just for unit test */
protected int _elementDataSize() {
return nodes.length;
}
/** cardinality of nodes for given label */
public int cardinality(String label) {
TMap> nodesByLabel = getNodesByLabel();
if (nodesByLabel.containsKey(label))
return nodesByLabel.get(label).size();
else
return 0;
}
public synchronized void persistAll(NodesWriter nodesWriter) {
nodesWriter.writeAndClearBatched(Arrays.spliterator(nodes), nodes.length);
}
public static class NodesIterator implements Iterator {
final Node[] nodes;
int idx = 0;
Node nextPeeked = null;
public NodesIterator(Node[] nodes) {
this.nodes = nodes;
}
@Override
public boolean hasNext() {
while (nextPeeked == null && idx < nodes.length) {
nextPeeked = nodes[idx++];
}
return nextPeeked != null;
}
@Override
public Node next() {
if (!hasNext()) throw new NoSuchElementException("next on empty iterator");
else {
Node ret = nextPeeked;
nextPeeked = null;
return ret;
}
}
}
}