diff --git a/examples/map/README.md b/examples/map/README.md index 02b7b5d..ef96237 100644 --- a/examples/map/README.md +++ b/examples/map/README.md @@ -23,12 +23,26 @@ java -jar build/libs/*shadowjar*.jar * Same as onheap but uses NativeMemoryMap with off-heap storage. * Uses the ConcurrentHashMap backend for NativeMemoryMap, which does not support eviction. * Total of 10GB of data in off-heap memory. +* At end of test after full GC uses ~130MB of Java heap space. ## offheap-eviction example * Same as offheap but uses the Caffeine backend with maximumSize of 10,000 entries. * Expect 10,000 map entries at end of run with 10,000 total evictions. +* Total of 5GB of data in off-heap memory. +* At end of test after full GC uses ~90MB of Java heap space. ## offheap-eviction-operationcounters example -* Same as offheap-eviction but enables operation counters and logs them at the end of the demo. \ No newline at end of file +* Same as offheap-eviction but enables operation counters and logs them at the end of the demo. +* Total of 5GB of data in off-heap memory. +* At end of test after full GC uses ~90MB of Java heap space. + +## offheap-flatbuffers example + +* Example using [Google FlatBuffers](https://google.github.io/flatbuffers/) for serialization. +* Put 20,000 DemoCacheObject instances into a NativeMemoryMap. Each DemoCacheObject contains a list of 2,000 + DemoCacheObjectListEntry objects. +* Total of ~11GB of data in off-heap memory. +* DemoCacheObjectSerializer does conversions between DemoCacheObject and FlatBuffer objects. +* At end of test after full GC uses ~130MB of Java heap space. \ No newline at end of file diff --git a/examples/map/offheap-flatbuffers/build.gradle.kts b/examples/map/offheap-flatbuffers/build.gradle.kts new file mode 100644 index 0000000..18d70f1 --- /dev/null +++ b/examples/map/offheap-flatbuffers/build.gradle.kts @@ -0,0 +1,30 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + kotlin("jvm") + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":examples:map:utils")) + implementation("com.google.flatbuffers:flatbuffers-java:2.0.3") +} + +tasks { + named("shadowJar") { + archiveBaseName.set("offheap-flatbuffers-shadowjar") + manifest { + attributes(mapOf("Main-Class" to "com.target.nativememoryallocator.examples.map.offheap.flatbuffers.OffHeapFlatBuffersKt")) + } + } +} + +tasks { + build { + dependsOn(shadowJar) + } +} \ No newline at end of file diff --git a/examples/map/offheap-flatbuffers/generate_fbs_code.sh b/examples/map/offheap-flatbuffers/generate_fbs_code.sh new file mode 100755 index 0000000..35c379b --- /dev/null +++ b/examples/map/offheap-flatbuffers/generate_fbs_code.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Script to invoke the FlatBuffers compiler flatc to generate Kotlin FlatBuffer classes. +# You must have flatc installed locally and in your PATH before running this script. + +BASE_DIRECTORY=$(dirname $0) + +rm -fr $BASE_DIRECTORY/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/generated + +flatc --kotlin -o $BASE_DIRECTORY/src/main/kotlin $BASE_DIRECTORY/src/main/flatbuffers/DemoSchema.fbs \ No newline at end of file diff --git a/examples/map/offheap-flatbuffers/src/main/flatbuffers/DemoSchema.fbs b/examples/map/offheap-flatbuffers/src/main/flatbuffers/DemoSchema.fbs new file mode 100644 index 0000000..e52301c --- /dev/null +++ b/examples/map/offheap-flatbuffers/src/main/flatbuffers/DemoSchema.fbs @@ -0,0 +1,12 @@ +namespace com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated; + +table FlatBufferDemoCacheObjectListEntry { + id: int; + boolean_field: bool; + string_field: string; +} + +table FlatBufferDemoCacheObject { + id: int; + entry_list: [FlatBufferDemoCacheObjectListEntry]; +} \ No newline at end of file diff --git a/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/OffHeapFlatBuffers.kt b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/OffHeapFlatBuffers.kt new file mode 100644 index 0000000..5962bd1 --- /dev/null +++ b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/OffHeapFlatBuffers.kt @@ -0,0 +1,110 @@ +package com.target.nativememoryallocator.examples.map.offheap.flatbuffers + +import com.target.nativememoryallocator.allocator.NativeMemoryAllocatorBuilder +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.model.DemoCacheObject +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.model.DemoCacheObjectListEntry +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.serializer.DemoCacheObjectSerializer +import com.target.nativememoryallocator.examples.map.utils.buildRandomString +import com.target.nativememoryallocator.map.NativeMemoryMapBackend +import com.target.nativememoryallocator.map.NativeMemoryMapBuilder +import kotlinx.coroutines.* +import mu.KotlinLogging +import java.util.concurrent.ThreadLocalRandom +import kotlin.random.Random + +private val logger = KotlinLogging.logger {} + + +/** + * Demo application that puts 20,000 [DemoCacheObject] instances into a [NativeMemoryMap]. + * + * This demo uses the ConcurrentHashMap backend for [NativeMemoryMap]. + * + * Each [DemoCacheObject] instance contains a list of 2,000 [DemoCacheObjectListEntry]s. + * + * This is a total of ~11 GB of data in off-heap memory. + */ +private class OffHeapFlatBuffers { + + private val numMapEntries = 20_000 + + private val numEntriesPerObject = 2_000 + + private val randomIndex = Random.nextInt(0, numMapEntries) + + private var randomPutValue: DemoCacheObject? = null + + private val nativeMemoryAllocator = NativeMemoryAllocatorBuilder( + pageSizeBytes = 4_096, // 4 KB + nativeMemorySizeBytes = (20L * 1_024L * 1_024L * 1_024L), // 20 GB + ).build() + + private val nativeMemoryMap = NativeMemoryMapBuilder( + valueSerializer = DemoCacheObjectSerializer(), + nativeMemoryAllocator = nativeMemoryAllocator, + backend = NativeMemoryMapBackend.CONCURRENT_HASH_MAP, + ).build() + + private fun buildDemoCacheObject(i: Int) = + DemoCacheObject( + id = i, + entryList = (0 until numEntriesPerObject).map { j -> + DemoCacheObjectListEntry( + id = j, + booleanField = ThreadLocalRandom.current().nextBoolean(), + stringField = buildRandomString(length = 250), + ) + } + ) + + private fun putValueIntoMap(i: Int) { + if ((i % 100) == 0) { + logger.info { "put i = $i" } + } + val demoCacheObject = buildDemoCacheObject(i = i) + if (i == randomIndex) { + logger.info { "put randomIndex = $randomIndex demoCacheObject.entryList.size = ${demoCacheObject.entryList.size}" } + logger.info { + "substring = ${demoCacheObject.entryList[0].stringField.substring(0, 20)}" + } + randomPutValue = demoCacheObject + } + nativeMemoryMap.put( + key = i, + value = demoCacheObject, + ) + } + + suspend fun run() { + logger.info { "begin run randomIndex = $randomIndex" } + + coroutineScope { + (0 until numMapEntries).forEach { i -> + launch { + putValueIntoMap(i = i) + } + } + } + + logger.info { "nativeMemoryMap.size = ${nativeMemoryMap.size}" } + logger.info { "nativeMemoryAllocator.nativeMemoryAllocatorMetadata = ${nativeMemoryAllocator.nativeMemoryAllocatorMetadata}" } + + val randomIndexValue = nativeMemoryMap.get(key = randomIndex) + randomIndexValue?.let { + logger.info { "get randomIndex = $randomIndex" } + logger.info { "randomIndexValue.entryList.size = ${it.entryList.size}" } + logger.info { "substring = ${it.entryList[0].stringField.substring(0, 20)}" } + logger.info { "randomIndexValue == randomPutValue = ${it == randomPutValue}" } + } + + while (true) { + delay(1_000) + } + } +} + +suspend fun main() { + withContext(Dispatchers.Default) { + OffHeapFlatBuffers().run() + } +} \ No newline at end of file diff --git a/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/generated/FlatBufferDemoCacheObject.kt b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/generated/FlatBufferDemoCacheObject.kt new file mode 100644 index 0000000..cf3904a --- /dev/null +++ b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/generated/FlatBufferDemoCacheObject.kt @@ -0,0 +1,66 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +package com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated + +import java.nio.* +import kotlin.math.sign +import com.google.flatbuffers.* + +@Suppress("unused") +class FlatBufferDemoCacheObject : Table() { + + fun __init(_i: Int, _bb: ByteBuffer) { + __reset(_i, _bb) + } + fun __assign(_i: Int, _bb: ByteBuffer) : FlatBufferDemoCacheObject { + __init(_i, _bb) + return this + } + val id : Int + get() { + val o = __offset(4) + return if(o != 0) bb.getInt(o + bb_pos) else 0 + } + fun entryList(j: Int) : com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated.FlatBufferDemoCacheObjectListEntry? = entryList(com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated.FlatBufferDemoCacheObjectListEntry(), j) + fun entryList(obj: com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated.FlatBufferDemoCacheObjectListEntry, j: Int) : com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated.FlatBufferDemoCacheObjectListEntry? { + val o = __offset(6) + return if (o != 0) { + obj.__assign(__indirect(__vector(o) + j * 4), bb) + } else { + null + } + } + val entryListLength : Int + get() { + val o = __offset(6); return if (o != 0) __vector_len(o) else 0 + } + companion object { + fun validateVersion() = Constants.FLATBUFFERS_2_0_0() + fun getRootAsFlatBufferDemoCacheObject(_bb: ByteBuffer): FlatBufferDemoCacheObject = getRootAsFlatBufferDemoCacheObject(_bb, FlatBufferDemoCacheObject()) + fun getRootAsFlatBufferDemoCacheObject(_bb: ByteBuffer, obj: FlatBufferDemoCacheObject): FlatBufferDemoCacheObject { + _bb.order(ByteOrder.LITTLE_ENDIAN) + return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)) + } + fun createFlatBufferDemoCacheObject(builder: FlatBufferBuilder, id: Int, entryListOffset: Int) : Int { + builder.startTable(2) + addEntryList(builder, entryListOffset) + addId(builder, id) + return endFlatBufferDemoCacheObject(builder) + } + fun startFlatBufferDemoCacheObject(builder: FlatBufferBuilder) = builder.startTable(2) + fun addId(builder: FlatBufferBuilder, id: Int) = builder.addInt(0, id, 0) + fun addEntryList(builder: FlatBufferBuilder, entryList: Int) = builder.addOffset(1, entryList, 0) + fun createEntryListVector(builder: FlatBufferBuilder, data: IntArray) : Int { + builder.startVector(4, data.size, 4) + for (i in data.size - 1 downTo 0) { + builder.addOffset(data[i]) + } + return builder.endVector() + } + fun startEntryListVector(builder: FlatBufferBuilder, numElems: Int) = builder.startVector(4, numElems, 4) + fun endFlatBufferDemoCacheObject(builder: FlatBufferBuilder) : Int { + val o = builder.endTable() + return o + } + } +} diff --git a/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/generated/FlatBufferDemoCacheObjectListEntry.kt b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/generated/FlatBufferDemoCacheObjectListEntry.kt new file mode 100644 index 0000000..143e019 --- /dev/null +++ b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/generated/FlatBufferDemoCacheObjectListEntry.kt @@ -0,0 +1,59 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +package com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated + +import java.nio.* +import kotlin.math.sign +import com.google.flatbuffers.* + +@Suppress("unused") +class FlatBufferDemoCacheObjectListEntry : Table() { + + fun __init(_i: Int, _bb: ByteBuffer) { + __reset(_i, _bb) + } + fun __assign(_i: Int, _bb: ByteBuffer) : FlatBufferDemoCacheObjectListEntry { + __init(_i, _bb) + return this + } + val id : Int + get() { + val o = __offset(4) + return if(o != 0) bb.getInt(o + bb_pos) else 0 + } + val booleanField : Boolean + get() { + val o = __offset(6) + return if(o != 0) 0.toByte() != bb.get(o + bb_pos) else false + } + val stringField : String? + get() { + val o = __offset(8) + return if (o != 0) __string(o + bb_pos) else null + } + val stringFieldAsByteBuffer : ByteBuffer get() = __vector_as_bytebuffer(8, 1) + fun stringFieldInByteBuffer(_bb: ByteBuffer) : ByteBuffer = __vector_in_bytebuffer(_bb, 8, 1) + companion object { + fun validateVersion() = Constants.FLATBUFFERS_2_0_0() + fun getRootAsFlatBufferDemoCacheObjectListEntry(_bb: ByteBuffer): FlatBufferDemoCacheObjectListEntry = getRootAsFlatBufferDemoCacheObjectListEntry(_bb, FlatBufferDemoCacheObjectListEntry()) + fun getRootAsFlatBufferDemoCacheObjectListEntry(_bb: ByteBuffer, obj: FlatBufferDemoCacheObjectListEntry): FlatBufferDemoCacheObjectListEntry { + _bb.order(ByteOrder.LITTLE_ENDIAN) + return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)) + } + fun createFlatBufferDemoCacheObjectListEntry(builder: FlatBufferBuilder, id: Int, booleanField: Boolean, stringFieldOffset: Int) : Int { + builder.startTable(3) + addStringField(builder, stringFieldOffset) + addId(builder, id) + addBooleanField(builder, booleanField) + return endFlatBufferDemoCacheObjectListEntry(builder) + } + fun startFlatBufferDemoCacheObjectListEntry(builder: FlatBufferBuilder) = builder.startTable(3) + fun addId(builder: FlatBufferBuilder, id: Int) = builder.addInt(0, id, 0) + fun addBooleanField(builder: FlatBufferBuilder, booleanField: Boolean) = builder.addBoolean(1, booleanField, false) + fun addStringField(builder: FlatBufferBuilder, stringField: Int) = builder.addOffset(2, stringField, 0) + fun endFlatBufferDemoCacheObjectListEntry(builder: FlatBufferBuilder) : Int { + val o = builder.endTable() + return o + } + } +} diff --git a/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/mapper/FlatBuffersMapper.kt b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/mapper/FlatBuffersMapper.kt new file mode 100644 index 0000000..1c6ecfd --- /dev/null +++ b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/mapper/FlatBuffersMapper.kt @@ -0,0 +1,70 @@ +package com.target.nativememoryallocator.examples.map.offheap.flatbuffers.mapper + +import com.google.flatbuffers.FlatBufferBuilder +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated.FlatBufferDemoCacheObject +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated.FlatBufferDemoCacheObjectListEntry +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.model.DemoCacheObject +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.model.DemoCacheObjectListEntry +import java.nio.ByteBuffer + +/** + * Mapper between [DemoCacheObject] and [FlatBufferDemoCacheObject]. + */ +object FlatBuffersMapper { + + fun demoCacheObjectToFlatBuffer( + demoCacheObject: DemoCacheObject, + ): FlatBufferDemoCacheObject { + val fbb = FlatBufferBuilder() + fbb.forceDefaults(false) + + val entryListOffset = FlatBufferDemoCacheObject.createEntryListVector( + builder = fbb, + data = demoCacheObject.entryList.map { entry -> + FlatBufferDemoCacheObjectListEntry.createFlatBufferDemoCacheObjectListEntry( + builder = fbb, + id = entry.id, + booleanField = entry.booleanField, + stringFieldOffset = fbb.createString(entry.stringField), + ) + }.toIntArray(), + ) + + val flatBufferDemoCacheObject = FlatBufferDemoCacheObject.createFlatBufferDemoCacheObject( + builder = fbb, + id = demoCacheObject.id, + entryListOffset = entryListOffset, + ) + + fbb.finish(flatBufferDemoCacheObject) + + return FlatBufferDemoCacheObject.getRootAsFlatBufferDemoCacheObject( + ByteBuffer.wrap( + fbb.sizedByteArray(), + ) + ) + } + + fun flatBufferToDemoCacheObject( + flatBufferDemoCacheObject: FlatBufferDemoCacheObject, + ): DemoCacheObject { + + val entryList = (0 until flatBufferDemoCacheObject.entryListLength).mapNotNull { i -> + val flatBufferEntry = flatBufferDemoCacheObject.entryList(i) + if (flatBufferEntry == null) { + null + } else { + DemoCacheObjectListEntry( + id = flatBufferEntry.id, + booleanField = flatBufferEntry.booleanField, + stringField = flatBufferEntry.stringField.orEmpty(), + ) + } + } + + return DemoCacheObject( + id = flatBufferDemoCacheObject.id, + entryList = entryList, + ) + } +} \ No newline at end of file diff --git a/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/model/DemoCacheObject.kt b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/model/DemoCacheObject.kt new file mode 100644 index 0000000..7c6db77 --- /dev/null +++ b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/model/DemoCacheObject.kt @@ -0,0 +1,18 @@ +package com.target.nativememoryallocator.examples.map.offheap.flatbuffers.model + +/** + * A demo cache object containing an [id] field and a list of entries. + */ +data class DemoCacheObject( + val id: Int, + val entryList: List, +) + +/** + * A demo cache object list entry. + */ +data class DemoCacheObjectListEntry( + val id: Int, + val booleanField: Boolean, + val stringField: String, +) \ No newline at end of file diff --git a/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/serializer/DemoCacheObjectSerializer.kt b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/serializer/DemoCacheObjectSerializer.kt new file mode 100644 index 0000000..729f905 --- /dev/null +++ b/examples/map/offheap-flatbuffers/src/main/kotlin/com/target/nativememoryallocator/examples/map/offheap/flatbuffers/serializer/DemoCacheObjectSerializer.kt @@ -0,0 +1,36 @@ +package com.target.nativememoryallocator.examples.map.offheap.flatbuffers.serializer + +import com.target.nativememoryallocator.buffer.OnHeapMemoryBuffer +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.generated.FlatBufferDemoCacheObject +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.mapper.FlatBuffersMapper +import com.target.nativememoryallocator.examples.map.offheap.flatbuffers.model.DemoCacheObject +import com.target.nativememoryallocator.map.NativeMemoryMapSerializer + +/** + * A [NativeMemoryMapSerializer] for [DemoCacheObject]. + * + * This serializer converts to [FlatBufferDemoCacheObject] using [FlatBuffersMapper]. + * + * The FlatBuffers byte array is stored in off-heap storage in the [NativeMemoryMap]. + */ +class DemoCacheObjectSerializer : NativeMemoryMapSerializer { + + override fun serializeToByteArray(value: DemoCacheObject): ByteArray { + val flatBufferDemoCacheObject = FlatBuffersMapper.demoCacheObjectToFlatBuffer( + demoCacheObject = value, + ) + + return flatBufferDemoCacheObject.byteBuffer.array() + } + + override fun deserializeFromOnHeapMemoryBuffer(onHeapMemoryBuffer: OnHeapMemoryBuffer): DemoCacheObject { + val flatBufferDemoCacheObject = FlatBufferDemoCacheObject.getRootAsFlatBufferDemoCacheObject( + onHeapMemoryBuffer.asByteBuffer(), + ) + + return FlatBuffersMapper.flatBufferToDemoCacheObject( + flatBufferDemoCacheObject = flatBufferDemoCacheObject, + ) + } + +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 484a133..d83bfc7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,4 +8,5 @@ include( "examples:map:offheap", "examples:map:offheap-eviction", "examples:map:offheap-eviction-operationcounters", + "examples:map:offheap-flatbuffers", )