mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
fix(level-hash/persistent): implement expansion logic for persistent level hash
This commit is contained in:
@@ -126,17 +126,11 @@ interface LevelHash<K : Any, V : Any?> : AutoCloseable {
|
||||
fun findSlot(key: K): LevelSlot<K, V>?
|
||||
|
||||
/**
|
||||
* Expand the level hash by the given ADDITIONAL level size.
|
||||
*
|
||||
* @param addtionalLevelSize The additional level size of the level hash. If this is
|
||||
* less than the current level size, an exception is thrown. If this is equal
|
||||
* to the current level size, this function has no effect.
|
||||
* Expand the level hash, doubling its capacity.
|
||||
*
|
||||
* @return Whether the level hash was resized.
|
||||
* @throws IllegalArgumentException If the level size if less than the current
|
||||
* level size.
|
||||
*/
|
||||
fun expand(addtionalLevelSize: Int): Boolean
|
||||
fun expand(): Boolean
|
||||
|
||||
/**
|
||||
* Get the load factor of the level hash.
|
||||
@@ -201,7 +195,7 @@ interface LevelHash<K : Any, V : Any?> : AutoCloseable {
|
||||
* An [InsertionError] which is thrown when the level hash is full and unable
|
||||
* to insert new values.
|
||||
*/
|
||||
class OverflowException(capacity: Int, occupied: Int) : InsertionError("Level hash has reached is capacity. cap=$capacity occ=$occupied")
|
||||
class OverflowException(capacity: Int, occupied: Int) : InsertionError("Level hash needs to be expanded. cap=$capacity occ=$occupied")
|
||||
|
||||
/**
|
||||
* Thrown when the level hash cannot be resized.
|
||||
|
||||
+9
-10
@@ -123,16 +123,16 @@ internal abstract class AbstractLevelHash<K : Any, V : Any?> internal constructo
|
||||
|
||||
override fun insert(key: K, value: V): Boolean {
|
||||
|
||||
if (this.levelItemCounts[0] == this.topLevelBucketCount * this.bucketSize) {
|
||||
if (autoExpand) {
|
||||
check(expand(1)) {
|
||||
"Failed to expand the level hash"
|
||||
}
|
||||
} else {
|
||||
throw OverflowException(this.totalSlotCount, this.levelItemCounts.sum())
|
||||
if (loadFactor() >= 0.92 && autoExpand) {
|
||||
check(expand()) {
|
||||
"Failed to expand the level hash"
|
||||
}
|
||||
}
|
||||
|
||||
if (loadFactor() == 1f) {
|
||||
throw OverflowException(totalSlotCount, levelItemCounts.sum())
|
||||
}
|
||||
|
||||
val insertFn =
|
||||
fun(levelIdx: Int, bucketIdx: Int, slotIdx: Int): Boolean {
|
||||
val slot = getSlot(levelIdx, bucketIdx, slotIdx)
|
||||
@@ -245,15 +245,14 @@ internal abstract class AbstractLevelHash<K : Any, V : Any?> internal constructo
|
||||
return null
|
||||
}
|
||||
|
||||
override fun expand(addtionalLevelSize: Int): Boolean {
|
||||
override fun expand(): Boolean {
|
||||
check(this.resizeState == RESIZE_STATE_NOT_RESIZING) {
|
||||
"Level hash is already being resized. Maybe on a different thread?"
|
||||
}
|
||||
require(addtionalLevelSize > 0) { "levelSize must be > 0" }
|
||||
|
||||
try {
|
||||
this.resizeState = RESIZE_STATE_EXPANDING
|
||||
return doExpand(this.levelSize + addtionalLevelSize)
|
||||
return doExpand(this.levelSize + 1)
|
||||
} finally {
|
||||
this.resizeState = RESIZE_STATE_NOT_RESIZING
|
||||
}
|
||||
|
||||
+16
-4
@@ -17,6 +17,7 @@
|
||||
|
||||
package com.itsaky.androidide.levelhash.internal
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.itsaky.androidide.levelhash.DataExternalizer
|
||||
import com.itsaky.androidide.levelhash.HashT
|
||||
import com.itsaky.androidide.levelhash.LevelHashFn
|
||||
@@ -42,13 +43,14 @@ internal class PersistentLevelHash<K : Any, V : Any?>(
|
||||
uniqueKeys = uniqueKeys, autoExpand = autoExpand, levelHashFn = levelHashFn,
|
||||
seeds = seeds) {
|
||||
|
||||
private val io = PersistentLevelHashIO(indexFile, levelSize, bucketSize, keyExternalizer,
|
||||
@VisibleForTesting
|
||||
internal val io = PersistentLevelHashIO(indexFile, levelSize, bucketSize, keyExternalizer,
|
||||
valueExternalizer)
|
||||
|
||||
override var levelSize: Int
|
||||
get() = io.metaIo.levelSize
|
||||
get() = io.levelSize
|
||||
set(value) {
|
||||
io.metaIo.levelSize = value
|
||||
io.levelSize = value
|
||||
}
|
||||
|
||||
override fun getSlot(levelIdx: Int, bucketIdx: Int, slotIdx: Int
|
||||
@@ -64,11 +66,21 @@ internal class PersistentLevelHash<K : Any, V : Any?>(
|
||||
io.close()
|
||||
}
|
||||
|
||||
override fun prepareExpansion(bucketCount: Int) {
|
||||
super.prepareExpansion(bucketCount)
|
||||
io.prepareInterimLevel(bucketCount)
|
||||
}
|
||||
|
||||
override fun moveForExpansion(slot: LevelSlot<K, V>, bucketIdx: Int,
|
||||
slotIdx: Int
|
||||
): Boolean {
|
||||
slot as PersistentLevelSlot<K, V>
|
||||
return false
|
||||
return io.moveToInterim(slot.levelIdx, slot.bucketIdx, slot.slotIdx, bucketIdx, slotIdx)
|
||||
}
|
||||
|
||||
override fun onExpand(newLevelSize: Int, interimItemCount: Int) {
|
||||
super.onExpand(newLevelSize, interimItemCount)
|
||||
io.finalizeExpansion()
|
||||
}
|
||||
|
||||
private class PersistentLevelSlot<K : Any, V : Any?>(
|
||||
|
||||
+110
-18
@@ -17,6 +17,7 @@
|
||||
|
||||
package com.itsaky.androidide.levelhash.internal
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.itsaky.androidide.levelhash.DataExternalizer
|
||||
import com.itsaky.androidide.levelhash.LevelHash.ResizeFailure
|
||||
import com.itsaky.androidide.levelhash.seekInt
|
||||
@@ -32,6 +33,16 @@ import kotlin.math.pow
|
||||
|
||||
private val logger = LoggerFactory.getLogger(PersistentLevelHashIO::class.java)
|
||||
|
||||
// TODO: Update valuesFirstEntry and valuesNxtEntry whenever an entry is added or deleted
|
||||
// TODO: Entries in the values file are always appended at the end. When an entry
|
||||
// is to be deleted, we simply use fallocate with FALLOC_FL_PUNCH_HOLE to deallocate
|
||||
// the disk space occupied by that entry. However, this may result in the region
|
||||
// at the start of the values file become empty. In that case, we might consider
|
||||
// moving the entries at the start of the file. This may (or may not) result in
|
||||
// improved performance when seeking through the file.
|
||||
// TODO: The above is also applicable for the keymap file when the level hash is
|
||||
// expanded and new regions for the levels are allocated.
|
||||
|
||||
/**
|
||||
* This class is responsible for performing the IO operations for the level hash
|
||||
* backed by the disk.
|
||||
@@ -162,9 +173,11 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
// as the values file is randomly accessed, and by multiple functions, or
|
||||
// even multiple threads, the current position cannot be determined
|
||||
|
||||
@VisibleForTesting
|
||||
internal val metaIo =
|
||||
PersistentMetaIO(File(indexFile.parentFile, "${indexFile.name}._meta"),
|
||||
levelSize, bucketSize)
|
||||
|
||||
private val keymapFile = File(indexFile.parentFile, "${indexFile.name}._i")
|
||||
private val raKeymapFile by lazy { RandomAccessFile(keymapFile, "rw") }
|
||||
private val raValuesFile by lazy { RandomAccessFile(indexFile, "rw") }
|
||||
@@ -172,17 +185,18 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
private val keymapIo = MappedRandomAccessIO()
|
||||
private var valIo = MappedRandomAccessIO()
|
||||
|
||||
private var interimLvlAddr: Long? = null
|
||||
|
||||
private val topLevelBucketCount: Long
|
||||
get() = 2.0.pow(metaIo.levelSize).toLong()
|
||||
get() = 2.0.pow(metaIo.levelSize)
|
||||
.toLong()
|
||||
|
||||
private val topLevelSizeBytes: Long
|
||||
get() = topLevelBucketCount * metaIo.bucketSize * KEYMAP_ENTRY_SIZE_BYTES
|
||||
|
||||
private val totalBucketCount: Long
|
||||
get() = topLevelBucketCount + (topLevelBucketCount shr 1)
|
||||
|
||||
private val totalSlotCount: Long
|
||||
get() = totalBucketCount * metaIo.bucketSize
|
||||
internal var levelSize: Int
|
||||
get() = metaIo.levelSize
|
||||
set(value) { metaIo.levelSize = value }
|
||||
|
||||
init {
|
||||
BinaryFileUtils.initSparseFile(file = keymapFile,
|
||||
@@ -196,8 +210,8 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
}
|
||||
|
||||
private fun realValueOffset(offset: Long) = VALUES_HEADER_SIZE_BYTES + offset
|
||||
|
||||
private fun realKeymapOffset(offset: Long) = KEYMAP_HEADER_SIZE_BYTES + offset
|
||||
private fun keymapBufOffset(offset: Long) = offset - KEYMAP_HEADER_SIZE_BYTES
|
||||
|
||||
private fun valuesDeallocate(offset: Long, len: Long) =
|
||||
FileUtils.deallocate(raValuesFile, realValueOffset(offset), len)
|
||||
@@ -212,13 +226,13 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
*
|
||||
* @param newSize The new size of the values file, in bytes.
|
||||
*/
|
||||
private fun valuesResize(newSize: Long) {
|
||||
private fun valuesResize(newSize: Long) {
|
||||
metaIo.valuesFileSize = newSize
|
||||
raValuesFile.setLength(newSize)
|
||||
valuesRemap(newSize)
|
||||
}
|
||||
|
||||
private fun keymapResize(newSize: Long) {
|
||||
private fun keymapResize(newSize: Long) {
|
||||
if (raKeymapFile.length() != newSize) {
|
||||
raKeymapFile.setLength(newSize)
|
||||
}
|
||||
@@ -236,7 +250,7 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
) {
|
||||
val buffer = file.channel.map(FileChannel.MapMode.READ_WRITE, offset, size)
|
||||
io.close()
|
||||
io.reset(buffer, 0L, size)
|
||||
io.reset(buffer)
|
||||
}
|
||||
|
||||
private fun slotAddress(
|
||||
@@ -244,13 +258,21 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
bucketIdx: Int,
|
||||
slotIdx: Int,
|
||||
): Long {
|
||||
val levelPos = when (levelIdx) {
|
||||
val levelAddr = when (levelIdx) {
|
||||
0 -> metaIo.l0Addr
|
||||
1 -> metaIo.l1Addr
|
||||
else -> throw IllegalArgumentException("Invalid level index: $levelIdx")
|
||||
}
|
||||
|
||||
return levelPos + // start position of level
|
||||
return slotAddrForLvlAddr(levelAddr, bucketIdx, slotIdx)
|
||||
}
|
||||
|
||||
private fun slotAddrForLvlAddr(
|
||||
lvlAddr: Long,
|
||||
bucketIdx: Int,
|
||||
slotIdx: Int,
|
||||
): Long {
|
||||
return lvlAddr + // start position of level
|
||||
(KEYMAP_ENTRY_SIZE_BYTES * metaIo.bucketSize * bucketIdx) + // bucket position
|
||||
(KEYMAP_ENTRY_SIZE_BYTES * slotIdx) // slot position in bucket
|
||||
}
|
||||
@@ -391,8 +413,6 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun writeEntry(
|
||||
levelNum: Int,
|
||||
bucketIdx: Int,
|
||||
@@ -518,11 +538,17 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
var keymapSize = topLevelSizeBytes
|
||||
keymapSize += topLevelSizeBytes shr 1
|
||||
metaIo.valuesFirstEntry = 0L
|
||||
metaIo.valuesNextEntry = 0L
|
||||
metaIo.l0Addr = 0
|
||||
metaIo.l1Addr = 2.0.pow(metaIo.levelSize).toLong() * metaIo.bucketSize * KEYMAP_ENTRY_SIZE_BYTES
|
||||
val kmSize = metaIo.l1Addr + (metaIo.l1Addr shr 1)
|
||||
|
||||
keymapDeallocate(KEYMAP_HEADER_SIZE_BYTES, keymapSize)
|
||||
valuesDeallocate(VALUES_HEADER_SIZE_BYTES, metaIo.valuesFileSize)
|
||||
keymapResize(realKeymapOffset(kmSize))
|
||||
keymapDeallocate(0, kmSize)
|
||||
|
||||
valuesResize(realValueOffset(VALUES_SEGMENT_SIZE_BYTES))
|
||||
valuesDeallocate(0, VALUES_SEGMENT_SIZE_BYTES)
|
||||
}
|
||||
|
||||
private fun <T> writeSlotKeyOrVal(obj: T?, externalizer: DataExternalizer<T>
|
||||
@@ -550,6 +576,72 @@ internal class PersistentLevelHashIO<K : Any, V : Any?>(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun prepareInterimLevel(bucketCount: Int) {
|
||||
check(interimLvlAddr == null) {
|
||||
"Interim level address must be null. Is the level hash being resized on multiple threads?"
|
||||
}
|
||||
|
||||
val interimSize = bucketCount * metaIo.bucketSize * KEYMAP_ENTRY_SIZE_BYTES
|
||||
val start = metaIo.kmStartAddr
|
||||
if (realKeymapOffset(interimSize) < start) {
|
||||
// we can reuse the empty space at the beginning of the file
|
||||
interimLvlAddr = 0L
|
||||
} else {
|
||||
// ensure the keymap can accommodate the interim level
|
||||
val length = raKeymapFile.length()
|
||||
keymapResize(length + interimSize)
|
||||
interimLvlAddr = keymapBufOffset(length)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun moveToInterim(
|
||||
levelNum: Int,
|
||||
bucketIdx: Int,
|
||||
slotIdx: Int,
|
||||
interimBucketIdx: Int,
|
||||
interimSlotIdx: Int,
|
||||
): Boolean {
|
||||
// current (source) slot
|
||||
val sSlotAddr = slotAddress(levelNum, bucketIdx, slotIdx)
|
||||
|
||||
// destination slot
|
||||
val dSlotAddr =
|
||||
slotAddrForLvlAddr(interimLvlAddr!!, interimBucketIdx, interimSlotIdx)
|
||||
|
||||
keymapIo.position(dSlotAddr)
|
||||
val dValAddr = keymapIo.readLong()
|
||||
if (dValAddr > 0) {
|
||||
// interim (destination) slot is occupied
|
||||
return false
|
||||
}
|
||||
|
||||
// interim slot is not occupied
|
||||
// as the values in the keymap are just pointers to the actual value entries
|
||||
// in the values file, we just need to update destination slot to point
|
||||
// where the source slot points
|
||||
|
||||
// 1. read the address where the source slot points
|
||||
keymapIo.position(sSlotAddr)
|
||||
val eValAddr = keymapIo.readLong()
|
||||
|
||||
// 2. move the destination slot and write the address of the source slot's value
|
||||
keymapIo.position(dSlotAddr)
|
||||
keymapIo.writeLong(eValAddr)
|
||||
|
||||
// 3. deallocate the space occupied by the source slot
|
||||
keymapDeallocate(sSlotAddr, KEYMAP_HEADER_SIZE_BYTES)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun finalizeExpansion() {
|
||||
// current top level becomes the bottom level
|
||||
// and interim level becomes the new top level
|
||||
metaIo.l1Addr = metaIo.l0Addr
|
||||
metaIo.l0Addr = interimLvlAddr!!
|
||||
interimLvlAddr = null
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
closeAndLogErrs(metaIo, keymapIo, valIo, raKeymapFile, raValuesFile)
|
||||
}
|
||||
|
||||
+11
-10
@@ -17,6 +17,7 @@
|
||||
|
||||
package com.itsaky.androidide.levelhash.internal
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.collection.MutableLongLongMap
|
||||
import com.itsaky.androidide.levelhash.internal.PersistentLevelHashIO.Companion.KEYMAP_ENTRY_SIZE_BYTES
|
||||
import com.itsaky.androidide.levelhash.internal.PersistentLevelHashIO.Companion.LEVEL_KEYMAP_VERSION
|
||||
@@ -78,14 +79,15 @@ internal class PersistentMetaIO(private val metaFile: File,
|
||||
get() = readLong(KM__L1_ADDR__OFFSET)
|
||||
set(value) { writeLong(KM__L1_ADDR__OFFSET, value) }
|
||||
|
||||
val kmStartAddr: Long
|
||||
get() = min(l0Addr, l1Addr)
|
||||
|
||||
val keymapSize: Long
|
||||
get() {
|
||||
// for this to work, l0 and l1 must be in a contiguosly allocated space
|
||||
// in the keymap file
|
||||
val l0Addr = this.l0Addr
|
||||
val l1Addr = this.l1Addr
|
||||
val l0SizeBytes = 2.0.pow(levelSize).toLong() * bucketSize * KEYMAP_ENTRY_SIZE_BYTES
|
||||
var size = min(l0Addr, l1Addr)
|
||||
var size = kmStartAddr
|
||||
size += l0SizeBytes
|
||||
size += l0SizeBytes shr 1
|
||||
return size
|
||||
@@ -105,12 +107,6 @@ internal class PersistentMetaIO(private val metaFile: File,
|
||||
if (valuesFileSize == 0L) {
|
||||
valuesFileSize = VALUES_SEGMENT_SIZE_BYTES
|
||||
}
|
||||
if (valuesFirstEntry == 0L) {
|
||||
valuesFirstEntry = 0
|
||||
}
|
||||
if (valuesNextEntry == 0L) {
|
||||
valuesNextEntry = 0
|
||||
}
|
||||
if (this.levelSize != levelSize) {
|
||||
this.levelSize = levelSize
|
||||
}
|
||||
@@ -168,13 +164,18 @@ internal class PersistentMetaIO(private val metaFile: File,
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
cache.clear()
|
||||
clearCache()
|
||||
raFile.close()
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to close meta file", e)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun clearCache() {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
private inline fun <reified T : Number> readOrDefault(default: T,
|
||||
read: () -> T
|
||||
): T {
|
||||
|
||||
+17
-9
@@ -31,6 +31,7 @@ import java.io.DataInputStream
|
||||
import java.io.DataOutput
|
||||
import java.io.EOFException
|
||||
import java.nio.MappedByteBuffer
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
@@ -43,7 +44,6 @@ internal class MappedRandomAccessIO : DataInput, DataOutput, RandomAccessIO, Aut
|
||||
|
||||
private var positionOffset = 0L
|
||||
private var position = 0L
|
||||
private var size = 0L
|
||||
|
||||
private lateinit var buffer: MappedByteBuffer
|
||||
|
||||
@@ -57,10 +57,9 @@ internal class MappedRandomAccessIO : DataInput, DataOutput, RandomAccessIO, Aut
|
||||
return this.buffer.apply { position(position.toInt()) }
|
||||
}
|
||||
|
||||
fun reset(buffer: MappedByteBuffer, position: Long, length: Long, positionOffset: Long = 0L) {
|
||||
fun reset(buffer: MappedByteBuffer, position: Long = 0L, positionOffset: Long = 0L) {
|
||||
this.buffer = buffer
|
||||
this.position = position
|
||||
this.size = length
|
||||
this.positionOffset = positionOffset
|
||||
}
|
||||
|
||||
@@ -76,16 +75,25 @@ internal class MappedRandomAccessIO : DataInput, DataOutput, RandomAccessIO, Aut
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekRelative(count: Long) {
|
||||
if (count == 0L) {
|
||||
return
|
||||
}
|
||||
var pos = position() + count
|
||||
pos = max(0L, min(size().toLong(), pos))
|
||||
position(pos)
|
||||
}
|
||||
|
||||
override fun position(position: Long) {
|
||||
require(position >= 0L) { "position must be >= 0, position=$position" }
|
||||
require(position < buffer.limit()) {
|
||||
"position must be < buffer.limit(), position=$position buffer.limit=${buffer.limit()}"
|
||||
require(position <= size()) {
|
||||
"position must be < buffer.limit(), position=$position buffer.limit=${size()}"
|
||||
}
|
||||
tryPosition(position)
|
||||
check(tryPosition(position))
|
||||
}
|
||||
|
||||
override fun tryPosition(position: Long): Boolean {
|
||||
if (position < 0L || position >= buffer.limit()) {
|
||||
if (position < 0L || position > size()) {
|
||||
return false
|
||||
}
|
||||
val currentPos = position()
|
||||
@@ -100,7 +108,7 @@ internal class MappedRandomAccessIO : DataInput, DataOutput, RandomAccessIO, Aut
|
||||
return this.position
|
||||
}
|
||||
|
||||
fun size() = this.size
|
||||
fun size() = buffer.limit()
|
||||
|
||||
override fun readFully(bytes: ByteArray) {
|
||||
if (bytes.isEmpty()) return
|
||||
@@ -130,7 +138,7 @@ internal class MappedRandomAccessIO : DataInput, DataOutput, RandomAccessIO, Aut
|
||||
}
|
||||
|
||||
override fun readByte(): Byte {
|
||||
return buf(position).get().also { seekByte() }
|
||||
return buf().get().also { seekByte() }
|
||||
}
|
||||
|
||||
override fun readUnsignedByte(): Int {
|
||||
|
||||
+83
-8
@@ -19,6 +19,8 @@ package com.itsaky.androidide.levelhash
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.androidide.levelhash.LevelHash.OverflowException
|
||||
import com.itsaky.androidide.levelhash.internal.PersistentLevelHash
|
||||
import com.itsaky.androidide.levelhash.internal.PersistentLevelHashIO.Companion.KEYMAP_ENTRY_SIZE_BYTES
|
||||
import com.itsaky.androidide.levelhash.util.DataExternalizers
|
||||
import com.itsaky.androidide.levelhash.util.nullable
|
||||
import com.itsaky.androidide.testing.common.LinuxOnlyTestRule
|
||||
@@ -75,7 +77,7 @@ class PersistentLevelHashTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertion() {
|
||||
fun `test insertion`() {
|
||||
persistentStringHash("insert").use { hash ->
|
||||
val key = "key"
|
||||
val value = "value"
|
||||
@@ -88,7 +90,7 @@ class PersistentLevelHashTest {
|
||||
}
|
||||
|
||||
@Test(expected = OverflowException::class)
|
||||
fun testOverflow() {
|
||||
fun `test overflow`() {
|
||||
persistentStringHash("overflow") { autoExpand(false) }.use { hash ->
|
||||
for (i in 0..<hash.totalSlotCount) {
|
||||
val key = "key${i}"
|
||||
@@ -102,7 +104,11 @@ class PersistentLevelHashTest {
|
||||
|
||||
@Test
|
||||
fun `test values file expands and remaps in case of buffer overflow`() {
|
||||
persistentStringHash("huge") { autoExpand(false).levelSize(15).bucketSize(10) }.use { hash ->
|
||||
persistentStringHash("huge") {
|
||||
autoExpand(true)
|
||||
levelSize(15)
|
||||
bucketSize(10)
|
||||
}.use { hash ->
|
||||
for (i in 0..<hash.totalSlotCount) {
|
||||
val key = "key${i}"
|
||||
val value = "value${i}"
|
||||
@@ -115,7 +121,7 @@ class PersistentLevelHashTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoval() {
|
||||
fun `test removal`() {
|
||||
persistentStringHash("remove").use { hash ->
|
||||
assertThat(hash.insert("key", "value")).isTrue()
|
||||
assertThat(hash.findSlot("key")).isNotNull()
|
||||
@@ -203,22 +209,91 @@ class PersistentLevelHashTest {
|
||||
persistentStringHash(
|
||||
fileName = "expand",
|
||||
valueExternalizer = DataExternalizers.STRING.nullable()
|
||||
).use { hash ->
|
||||
for (i in 0..<hash.totalSlotCount) {
|
||||
) {
|
||||
levelSize(5)
|
||||
bucketSize(10)
|
||||
autoExpand(false)
|
||||
}.use { hash ->
|
||||
val slots = hash.totalSlotCount - hash.bucketSize
|
||||
for (i in 0..<slots) {
|
||||
val key = "key${i}"
|
||||
val value = "value${i}"
|
||||
assertThat(hash.insert(key, value)).isTrue()
|
||||
}
|
||||
|
||||
// expand the level hash
|
||||
hash.expand(1)
|
||||
hash.expand()
|
||||
|
||||
// then check that all existing entries are still present
|
||||
for (i in 0..<hash.totalSlotCount) {
|
||||
for (i in 0..<slots) {
|
||||
val key = "key${i}"
|
||||
val value = "value${i}"
|
||||
assertThat(hash[key]).isEqualTo(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test expansion with huge slot count`() {
|
||||
persistentStringHash(
|
||||
fileName = "huge-expand",
|
||||
valueExternalizer = DataExternalizers.STRING.nullable()
|
||||
){
|
||||
levelSize(15)
|
||||
bucketSize(10)
|
||||
autoExpand(false)
|
||||
}.use { hash ->
|
||||
var i = 0
|
||||
while(hash.insert("key${i}", "value${i}")) {
|
||||
i++
|
||||
}
|
||||
|
||||
hash.expand()
|
||||
|
||||
for (j in 0..<i) {
|
||||
val key = "key${j}"
|
||||
val value = "value${j}"
|
||||
assertThat(hash[key]).isEqualTo(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test meta after expansion`() {
|
||||
persistentStringHash(
|
||||
fileName = "expand",
|
||||
valueExternalizer = DataExternalizers.STRING.nullable()
|
||||
) {
|
||||
levelSize(5)
|
||||
bucketSize(10)
|
||||
autoExpand(false)
|
||||
}.use { hash ->
|
||||
hash as PersistentLevelHash<String, String?>
|
||||
|
||||
val l0Size =
|
||||
hash.topLevelBucketCount * hash.io.metaIo.bucketSize * KEYMAP_ENTRY_SIZE_BYTES
|
||||
|
||||
assertThat(hash.io.metaIo.levelSize).isEqualTo(5)
|
||||
assertThat(hash.io.metaIo.bucketSize).isEqualTo(10)
|
||||
assertThat(hash.io.metaIo.l0Addr).isEqualTo(0)
|
||||
assertThat(hash.io.metaIo.l1Addr).isEqualTo(l0Size)
|
||||
|
||||
hash.expand()
|
||||
|
||||
// clear cache so we read values directly from the file
|
||||
hash.io.metaIo.clearCache()
|
||||
|
||||
assertThat(hash.io.metaIo.levelSize).isEqualTo(6)
|
||||
assertThat(hash.io.metaIo.bucketSize).isEqualTo(10)
|
||||
|
||||
// top level (l0) is moved after the bottom level (l1)
|
||||
// the bottom level used to span from `l0Size` to `l0Size + l0Size / 2`
|
||||
// and the interim level was added AFTER the previous bottom level
|
||||
assertThat(hash.io.metaIo.l0Addr).isEqualTo(l0Size + (l0Size shr 1))
|
||||
|
||||
// previous top level, which used to be located at 0,
|
||||
// becomes the new bottom level
|
||||
assertThat(hash.io.metaIo.l1Addr).isEqualTo(0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user