/*
* Copyright (C) 2013 Omry Yadan <omry@yadan.net>
* All rights reserved.
*
* See https://github.com/omry/banana/blob/master/BSD-LICENSE for licensing information
*/
package net.yadan.banana.map;
import net.yadan.banana.DebugLevel;
import net.yadan.banana.DefaultFormatter;
import net.yadan.banana.Formatter;
import net.yadan.banana.memory.Buffer;
import net.yadan.banana.memory.IBuffer;
import net.yadan.banana.memory.IMemAllocator;
import net.yadan.banana.memory.OutOfMemoryException;
import net.yadan.banana.memory.block.BlockAllocator;
import net.yadan.banana.memory.malloc.MultiSizeAllocator;
import net.yadan.banana.memory.malloc.TreeAllocator;
/**
* @author omry
* created May 7, 2013
*/
public class VarKeyHashMap implements IVarKeyHashMap {
private static final double DEFAULT_GROWTH_FACTOR = 2.0;
private static final int NEXT_OFFSET = 0;
private static final int KEY_OFFSET = 1;
private static final int USER_DATA_OFFSET = 2;
public static final int RESERVED_SIZE = USER_DATA_OFFSET;
private static final int KEY_SIZE_OFFSET = 0;
private static final int KEY_DATA_OFFSET = 1;
private final double m_loadFactor;
private double m_growthFactor;
/**
* Holds an array of pointers into m_memory
*/
private int m_table[];
private int m_size;
/**
* The table is rehashed when its size exceeds this threshold. (The value of
* this field is (int)(capacity * loadFactor).)
*/
private int m_threshold;
private DebugLevel m_debugLevel = DebugLevel.NONE;
private IMemAllocator m_valuesMemory;
private IMemAllocator m_keysMemory;
private Formatter m_formatter;
public VarKeyHashMap(int maxBlocks, int blockSize, double growthFactor, double loadFactor) {
this(new TreeAllocator(maxBlocks, blockSize + USER_DATA_OFFSET, growthFactor),
new MultiSizeAllocator(100, new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25, 50, 100 }, 2.0),
maxBlocks, loadFactor);
}
public VarKeyHashMap(IMemAllocator memory, IMemAllocator keysMemory, int initialCapacity,
double loadFactor) {
m_size = 0;
m_loadFactor = loadFactor;
m_growthFactor = DEFAULT_GROWTH_FACTOR;
m_valuesMemory = memory;
m_table = new int[initialCapacity];
m_threshold = (int) Math.min(getCapacity() * getLoadFactor(), Integer.MAX_VALUE);
m_keysMemory = keysMemory;
m_size = 0;
for (int i = 0; i < m_table.length; i++) {
m_table[i] = -1;
}
m_formatter = new DefaultFormatter();
}
@Override
public int size() {
return m_size;
}
@Override
public int getCapacity() {
return m_table.length;
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@Override
public boolean containsKey(IBuffer key) {
return findRecord(key) != -1;
}
@Override
public int createRecord(IBuffer key, int size) {
if (size() >= m_threshold && m_growthFactor > 0) {
increaseCapacity();
}
int listNum = hashCode(key, m_table.length);
int pointer = m_table[listNum];
// find if this key is already in the chain
int prev = -1;
while (pointer != -1) {
int keyPtr2 = m_valuesMemory.getInt(pointer, KEY_OFFSET);
int keySize2 = m_keysMemory.getInt(keyPtr2, KEY_SIZE_OFFSET);
// list already contain this key, reuse the space - resizing as needed
if (key.equals(m_keysMemory, keyPtr2, KEY_DATA_OFFSET, keySize2)) {
int pNext = m_valuesMemory.getInt(pointer, NEXT_OFFSET);
pointer = m_valuesMemory.realloc(pointer, size);
if (prev == -1) {
m_table[listNum] = pointer;
} else {
m_valuesMemory.setInt(prev, NEXT_OFFSET, pointer);
}
m_valuesMemory.initialize(pointer);
m_valuesMemory.setInt(pointer, KEY_OFFSET, keyPtr2);
m_valuesMemory.setInt(pointer, NEXT_OFFSET, pNext);
break;
}
prev = pointer;
pointer = m_valuesMemory.getInt(pointer, NEXT_OFFSET);
}
if (pointer == -1) {
int keySize = key.size();
int keyPtr = m_keysMemory.malloc(keySize + 1);
m_keysMemory.setInt(keyPtr, KEY_SIZE_OFFSET, keySize);
m_keysMemory.setInts(keyPtr, KEY_DATA_OFFSET, key.array(), 0, keySize);
try {
pointer = m_valuesMemory.malloc(size + RESERVED_SIZE);
} catch (OutOfMemoryException e) {
m_keysMemory.free(keyPtr);
throw e;
}
m_valuesMemory.setInt(pointer, KEY_OFFSET, keyPtr);
m_valuesMemory.setInt(pointer, NEXT_OFFSET, m_table[listNum]);
m_table[listNum] = pointer;
m_size++;
}
return pointer;
}
@Override
public int createRecord(IBuffer key, IBuffer value) {
int ret = createRecord(key, value.size());
setInts(ret, 0, value.array(), 0, value.size());
return ret;
}
@Override
public int findRecord(IBuffer key) {
int listNum = hashCode(key, m_table.length);
int n = m_table[listNum];
while (n != -1) {
int keyPtr = m_valuesMemory.getInt(n, KEY_OFFSET);
int keySize = m_keysMemory.getInt(keyPtr, KEY_SIZE_OFFSET);
if (key.equals(m_keysMemory, keyPtr, KEY_DATA_OFFSET, keySize)) {
break;
}
n = m_valuesMemory.getInt(n, NEXT_OFFSET);
}
return n;
}
@Override
public int reallocRecord(IBuffer key, int newSize) {
int listNum = hashCode(key, m_table.length);
int n = m_table[listNum];
int prev = -1;
while (n != -1) {
int keyPtr = m_valuesMemory.getInt(n, KEY_OFFSET);
int keySize = m_keysMemory.getInt(keyPtr, KEY_SIZE_OFFSET);
if (key.equals(m_keysMemory, keyPtr, KEY_DATA_OFFSET, keySize)) {
int ptr = m_valuesMemory.realloc(n, newSize + RESERVED_SIZE);
if (n == m_table[listNum]) {
m_table[listNum] = ptr;
} else {
m_valuesMemory.setInt(prev, NEXT_OFFSET, ptr);
}
return ptr;
}
prev = n;
n = m_valuesMemory.getInt(n, NEXT_OFFSET);
}
return -1;
}
@Override
public void setLong(int record_id, int offset_in_data, long data) {
m_valuesMemory.setLong(record_id, offset_in_data + USER_DATA_OFFSET, data);
}
@Override
public short getUpperShort(int link, int offset) {
return m_valuesMemory.getUpperShort(link, offset + USER_DATA_OFFSET);
}
@Override
public short getLowerShort(int link, int offset) {
return m_valuesMemory.getLowerShort(link, offset + USER_DATA_OFFSET);
}
@Override
public void setUpperShort(int link, int offset, int s) {
m_valuesMemory.setUpperShort(link, offset + USER_DATA_OFFSET, s);
}
@Override
public void setLowerShort(int link, int offset, int s) {
m_valuesMemory.setLowerShort(link, offset + USER_DATA_OFFSET, s);
}
@Override
public void setInt(int record_id, int offset_in_data, int data) {
m_valuesMemory.setInt(record_id, offset_in_data + USER_DATA_OFFSET, data);
}
@Override
public void setFloat(int record_id, int offset, float f) {
m_valuesMemory.setFloat(record_id, offset + USER_DATA_OFFSET, f);
}
@Override
public float getFloat(int record_id, int offset) {
return m_valuesMemory.getFloat(record_id, offset + USER_DATA_OFFSET);
}
@Override
public double getDouble(int record_id, int offset_in_data) {
return m_valuesMemory.getDouble(record_id, offset_in_data + USER_DATA_OFFSET);
}
@Override
public void setDouble(int record_id, int offset_in_data, double data) {
m_valuesMemory.setDouble(record_id, offset_in_data + USER_DATA_OFFSET, data);
}
@Override
public void setChars(int record_id, int dst_offset, char[] src_data, int src_pos, int num_chars) {
m_valuesMemory.setChars(record_id, dst_offset + USER_DATA_OFFSET, src_data, src_pos, num_chars);
}
@Override
public void getChars(int record_id, int src_offset, char[] dst_data, int dst_pos, int num_chars) {
m_valuesMemory.getChars(record_id, src_offset + USER_DATA_OFFSET, dst_data, dst_pos, num_chars);
}
@Override
public long getLong(int record_id, int offset_in_data) {
return m_valuesMemory.getLong(record_id, offset_in_data + USER_DATA_OFFSET);
}
@Override
public int getInt(int record_id, int offset_in_data) {
return m_valuesMemory.getInt(record_id, offset_in_data + USER_DATA_OFFSET);
}
@Override
public void setInts(int pointer, int dst_offset_in_record,
int[] src_data, int src_pos, int length) {
m_valuesMemory.setInts(pointer, dst_offset_in_record + USER_DATA_OFFSET, src_data, src_pos,
length);
}
@Override
public void getInts(int pointer, int src_offset_in_record,
int[] dst_data, int dst_pos, int length) {
m_valuesMemory.getInts(pointer, src_offset_in_record + USER_DATA_OFFSET, dst_data, dst_pos,
length);
}
@Override
public void getBuffer(int pointer, int src_offset_in_record, IBuffer dst, int length) {
m_valuesMemory.getBuffer(pointer, USER_DATA_OFFSET + src_offset_in_record, dst, length);
}
@Override
public boolean remove(IBuffer key) {
int listNum = hashCode(key, m_table.length);
int n = m_table[listNum];
int prev = -1;
boolean first = true;
while (n != -1) {
int keyPtr = m_valuesMemory.getInt(n, KEY_OFFSET);
int keySize = m_keysMemory.getInt(keyPtr, KEY_SIZE_OFFSET);
if (key.equals(m_keysMemory, keyPtr, KEY_DATA_OFFSET, keySize)) {
int next = m_valuesMemory.getInt(n, NEXT_OFFSET);
if (first) {
m_table[listNum] = next;
} else {
m_valuesMemory.setInt(prev, NEXT_OFFSET, next);
}
m_size--;
m_valuesMemory.free(n);
m_keysMemory.free(keyPtr);
return true;
}
prev = n;
first = false;
n = m_valuesMemory.getInt(n, NEXT_OFFSET);
}
return false;
}
@Override
public void clear() {
m_size = 0;
visitRecords(new VarKeyHashMapVisitorAdapter() {
@Override
public void visit(IVarKeyHashMap map, int keyPtr, int recordPtr, long num, long total) {
m_valuesMemory.free(recordPtr);
m_keysMemory.free(keyPtr);
}
});
for (int i = 0; i < m_table.length; i++) {
m_table[i] = -1;
}
}
@Override
public double getLoadFactor() {
return m_loadFactor;
}
@Override
public void setGrowthFactor(double d) {
if (!(d == 0 || d > 1))
throw new IllegalArgumentException("Growth factor " + d + " should be > 1 or 0 to disable");
m_growthFactor = d;
}
@Override
public void visitRecords(VarKeyHashMapVisitor visitor) {
visitor.begin(this);
int num = 0;
long total = size();
for (int i = 0; i < m_table.length; i++) {
int n = m_table[i];
while (n != -1) {
int keyPtr = m_valuesMemory.getInt(n, KEY_OFFSET);
int next = m_valuesMemory.getInt(n, NEXT_OFFSET);
visitor.visit(this, keyPtr, n, num, total);
n = next;
}
}
visitor.end(this);
}
private void increaseCapacity() {
// long t = Sysftem.currentTimeMillis();
IBuffer buffer = new Buffer(10, 2);
int capacity = getCapacity();
long newCapacity = Math.max(capacity + 1, (long) (capacity * m_growthFactor));
if (newCapacity > Integer.MAX_VALUE) {
throw new IllegalStateException("Attempted to resize table to " + newCapacity
+ " which is greated than Integer.MAX_VALUE");
}
int intCap = (int) newCapacity;
int newTable[] = new int[intCap];
for (int i = 0; i < newTable.length; i++) {
newTable[i] = -1;
}
for (int tableNum = 0; tableNum < m_table.length; tableNum++) {
int n = m_table[tableNum];
while (n != -1) {
int keyPtr = m_valuesMemory.getInt(n, KEY_OFFSET);
int keySize = m_keysMemory.getInt(keyPtr, KEY_SIZE_OFFSET);
buffer.ensureCapacity(keySize);
m_keysMemory.getInts(keyPtr, KEY_DATA_OFFSET, buffer.array(), 0, keySize);
buffer.setUsed(keySize);
int newTableNum = hashCode(buffer, intCap);
buffer.reset();
int next = m_valuesMemory.getInt(n, NEXT_OFFSET);
m_valuesMemory.setInt(n, NEXT_OFFSET, newTable[newTableNum]);
newTable[newTableNum] = n;
n = next;
}
}
m_table = newTable;
m_threshold = (int) Math.min(getCapacity() * getLoadFactor(), Integer.MAX_VALUE);
// System.out.println(String.format("Increased map capacity from %d to %d took %d ms",
// capacity,
// intCap, (System.currentTimeMillis() - t)));
}
private int hashCode(IBuffer key, int listSize) {
int h = key.hashCode();
int r = h % listSize;
return r < 0 ? r + listSize : r;
}
public static int getIntArraySize(int maxCapacity, int recordSize) {
return BlockAllocator.getIntArraySize(maxCapacity, recordSize + USER_DATA_OFFSET);
}
@Override
public long computeMemoryUsage() {
return 4 * m_table.length + m_keysMemory.computeMemoryUsage()
+ m_valuesMemory.computeMemoryUsage();
}
@Override
public void setDebug(DebugLevel level) {
m_debugLevel = level;
m_keysMemory.setDebug(level != DebugLevel.NONE);
m_valuesMemory.setDebug(level != DebugLevel.NONE);
}
@Override
public String toString() {
final StringBuilder s = new StringBuilder();
s.append(VarKeyHashMap.class.getName()).append(" ").append(size()).append(" / ")
.append(getCapacity());
if (m_debugLevel == DebugLevel.DEBUG_CONTENT) {
s.append("\n");
visitRecords(new VarKeyHashMapVisitorAdapter() {
@Override
public void visit(IVarKeyHashMap map, int keyPtr, int valuePtr, long num, long total) {
IMemAllocator keys = map.keysMemory();
int keySize = keys.getInt(keyPtr, KEY_SIZE_OFFSET);
for (int i = KEY_DATA_OFFSET; i < KEY_DATA_OFFSET + keySize; i++) {
int ii = keys.getInt(keyPtr, i);
char c1 = (char) (ii >> 16);
char c2 = (char) (0x00FF & ii);
if (c1 != 0) {
s.append(c1);
}
if (c2 != 0) {
s.append(c2);
}
}
s.append("=").append(m_formatter.format(map, valuePtr));
}
});
} else if (m_debugLevel == DebugLevel.DEBUG_STRUCTURE) {
s.append("\n");
for (int tableNum = 0; tableNum < m_table.length; tableNum++) {
int n = m_table[tableNum];
s.append(tableNum).append(" : ");
while (n != -1) {
IMemAllocator keys = keysMemory();
int keyPtr = m_valuesMemory.getInt(n, KEY_OFFSET);
int keySize = keys.getInt(keyPtr, 0);
int next = m_valuesMemory.getInt(n, NEXT_OFFSET);
s.append("(#").append(n).append(",K=");
for (int i = 0; i < keySize; i++) {
int ii = keys.getInt(keyPtr, i + 1);
char c1 = (char) (ii >> 16);
char c2 = (char) (0x00FF & ii);
if (c1 != 0) {
s.append(c1);
}
if (c2 != 0) {
s.append(c2);
}
}
s.append(",N=").append(m_valuesMemory.getInt(n, NEXT_OFFSET));
s.append(")=").append(m_formatter.format(this, n));
n = next;
s.append(" -> ");
}
s.append("END\n");
}
}
return s.toString();
}
@Override
public void getKeyData(int keyPtr, IBuffer key) {
int keySize = m_keysMemory.getInt(keyPtr, KEY_SIZE_OFFSET);
key.reset();
key.ensureCapacity(keySize);
m_keysMemory.getInts(keyPtr, KEY_DATA_OFFSET, key.array(), 0, keySize);
key.setUsed(keySize);
}
@Override
public IMemAllocator valueMemory() {
return m_valuesMemory;
}
@Override
public IMemAllocator keysMemory() {
return m_keysMemory;
}
@Override
public int maximumCapacityFor(int link) {
return m_valuesMemory.maximumCapacityFor(link) - RESERVED_SIZE;
}
@Override
public void setFormatter(Formatter formatter) {
m_formatter = formatter;
}
@Override
public DebugLevel getDebug() {
return m_debugLevel;
}
@Override
public Formatter getFormatter() {
return m_formatter;
}
}