Skip to content

Commit

Permalink
Storage optimization (#274)
Browse files Browse the repository at this point in the history
[jacodb-core] Add de-duplication of attribute values in AttributesImmutable

As of this commit, values array contains only different attribute values.

[jacodb-core] Close ers iff it was initialized

[jacodb-core] Processed only jar files referenced in the Class-Path attribute of manifest

[jacodb-storage] Fix minor memory usage issue in AttributesImmutable

Avoid using class constructor parameters in lazy fields.

[jacodb-storage] Reduce memory usage by InstanceIdCollections

In this commit, two implementation of InstanceIdCollection added,
SortedIntArrayInstanceIdCollection & UnsortedIntArrayInstanceIdCollection.
If instances ids of particular attributes cannot be packed in a LongRange,
but if they all can be represented as an Int, then corresponding
implementations wrap IntArray, not LongArray.

[jacodb-storage] Minor: toAttributesImmutable() is an extension for list of attribute pairs

[jacodb-storage] Optimize AttributesImmutable.navigate() in case of sorted by value attributes

If attributes are sorted by value, in binary search algorithm, getting instance id
by index of an attribute value can be skipped. In that case, copying of the attribute
value in an extra byte array is redundant, and comparing can be done by a special
comparison function.

[jacodb-storage] Re-order built-in bindings

[jacodb-storage] Optimize dealing with instance ids in AttributesImmutable

Instead of LongArray, instance ids are represented by implementors of InstanceIdCollection.
LongRangeInstanceIdCollection saves memory by using LongRange instead of LongArray.
The getByIndex() method introduced and used where possible in order to skip
conversion of instance id to index in InstanceIdCollection to load values from
values byte array.

[jacodb-benchmarks] Make RAMEntityRelationshipStorageBenchmarks more correct

Fewer operations on each invocation, this reduces Java GC load.

[jacodb-storage] Optimize PropertiesMutable.getEntitiesLtValue() & PropertiesMutable.getEntitiesEqOrLtValue()

Minor optimization: exit iteration over value index earlier.
This is okay since all further values are greater than specified one.

[jacodb-benchmarks] Add benchmarks of RAM implementation of ERS API

[jacodb-storage] Get rid of using one-nio library

ByteArrayBuilder added instead of one-nio's one.
SparseBitSet moved to the 'org.jacodb.util.collections' package.
  • Loading branch information
Saloed authored Oct 29, 2024
1 parent e86abeb commit 335977f
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 44 deletions.
5 changes: 5 additions & 0 deletions jacodb-benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ plugins {

dependencies {
implementation(project(":jacodb-core"))
implementation(project(":jacodb-storage"))
implementation(testFixtures(project(":jacodb-core")))

implementation(Libs.kotlin_logging)
Expand Down Expand Up @@ -79,6 +80,10 @@ benchmark {
include("GuavaCacheBenchmarks")
include("XodusCacheBenchmarks")
}
register("ersRam") {
include("RAMEntityRelationshipStorageMutableBenchmarks")
include("RAMEntityRelationshipStorageImmutableBenchmarks")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.jacodb.testing.performance.ers

import org.jacodb.api.storage.ers.EmptyErsSettings
import org.jacodb.api.storage.ers.Entity
import org.jacodb.api.storage.ers.EntityId
import org.jacodb.api.storage.ers.EntityRelationshipStorage
import org.jacodb.api.storage.ers.EntityRelationshipStorageSPI
import org.jacodb.api.storage.ers.Transaction
import org.jacodb.impl.storage.ers.ram.RAM_ERS_SPI
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.BenchmarkMode
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Level
import org.openjdk.jmh.annotations.Measurement
import org.openjdk.jmh.annotations.Mode
import org.openjdk.jmh.annotations.OutputTimeUnit
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.Setup
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.TearDown
import org.openjdk.jmh.annotations.Warmup
import org.openjdk.jmh.infra.Blackhole
import java.util.concurrent.TimeUnit

abstract class RAMEntityRelationshipStorageBenchmarks {

private val ersSpi by lazy(LazyThreadSafetyMode.NONE) {
EntityRelationshipStorageSPI.getProvider(RAM_ERS_SPI)
}
protected lateinit var storage: EntityRelationshipStorage
private lateinit var txn: Transaction
private lateinit var entity: Entity
private lateinit var loginSearchValue: String
private lateinit var passwordSearchValue: String

@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
fun getLogin(hole: Blackhole) {
hole.consume(entity.getRawProperty("login"))
}

@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
fun getPassword(hole: Blackhole) {
hole.consume(entity.getRawProperty("password"))
}

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
fun findByLogin(hole: Blackhole) {
hole.consume(txn.find("User", "login", loginSearchValue).first())
}

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
fun findByPassword(hole: Blackhole) {
hole.consume(txn.find("User", "password", passwordSearchValue).first())
}

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
fun findLt(hole: Blackhole) {
hole.consume(txn.findLt("User", "age", 50).first())
}

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
fun findEqOrLt(hole: Blackhole) {
hole.consume(txn.findEqOrLt("User", "age", 50).first())
}

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
fun findGt(hole: Blackhole) {
hole.consume(txn.findGt("User", "age", 50).first())
}

@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
fun findEqOrGt(hole: Blackhole) {
hole.consume(txn.findEqOrGt("User", "age", 50).first())
}

@Setup(Level.Iteration)
fun setupIteration() {
createMutable()
populate()
setImmutable()
txn = storage.beginTransaction(readonly = true)
entity = txn.getEntityUnsafe(EntityId(0, (Math.random() * 1_000_000).toLong()))
loginSearchValue = "login${entity.id.instanceId}"
passwordSearchValue = "a very secure password ${entity.id.instanceId}"
}

@TearDown(Level.Iteration)
fun tearDownIteration() {
txn.abort()
storage.close()
}

@Setup(Level.Invocation)
fun setupInvocation() {
loginSearchValue = "login${entity.id.instanceId}"
passwordSearchValue = "a very secure password ${entity.id.instanceId}"
}

private fun createMutable() {
storage = ersSpi.newStorage(null, EmptyErsSettings)
}

private fun populate() {
storage.transactional { txn ->
repeat(1_000_000) { i ->
val user = txn.newEntity("User")
user["login"] = "login$i"
user["password"] = "a very secure password $i"
user["age"] = 20 + i % 80
}
}
}

abstract fun setImmutable()
}

@State(Scope.Benchmark)
@Fork(1, jvmArgs = ["-Xmx8g", "-Xms8g"])
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@BenchmarkMode(Mode.AverageTime)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
class RAMEntityRelationshipStorageMutableBenchmarks : RAMEntityRelationshipStorageBenchmarks() {

override fun setImmutable() {
// do nothing im mutable benchmark
}
}

@State(Scope.Benchmark)
@Fork(1, jvmArgs = ["-Xmx8g", "-Xms8g"])
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@BenchmarkMode(Mode.AverageTime)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
class RAMEntityRelationshipStorageImmutableBenchmarks : RAMEntityRelationshipStorageBenchmarks() {

override fun setImmutable() {
storage = storage.asReadonly
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fun File.asByteCodeLocation(runtimeVersion: JavaVersion, isRuntime: Boolean = fa
if (!exists()) {
throw IllegalArgumentException("file $absolutePath doesn't exist")
}
if (isFile && name.endsWith(".jar") || name.endsWith(".jmod")) {
if (isJar()) {
return mutableSetOf<File>().also { classPath(it) }.map { JarLocation(it, isRuntime, runtimeVersion) }
} else if (isDirectory) {
return listOf(BuildFolderLocation(this))
Expand All @@ -52,7 +52,7 @@ fun Collection<File>.filterExisting(): List<File> = filter { file ->
}

private fun File.classPath(classpath: MutableCollection<File>) {
if (exists() && classpath.add(this)) {
if (isJar() && exists() && classpath.add(this)) {
JarFile(this).use { jarFile ->
jarFile.manifest?.mainAttributes?.getValue("Class-Path")?.split(' ')?.forEach { ref ->
Paths.get(
Expand All @@ -61,4 +61,6 @@ private fun File.classPath(classpath: MutableCollection<File>) {
}
}
}
}
}

private fun File.isJar() = isFile && name.endsWith(".jar") || name.endsWith(".jmod")
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ class SQLitePersistenceImpl(
internal val jooq = DSL.using(connection, SQLDialect.SQLITE, Settings().withExecuteLogging(false))
private val lock = ReentrantLock()
private val persistenceService = SQLitePersistenceService(this)
private var ersInitialized = false

override val ers: EntityRelationshipStorage by lazy {
SqlEntityRelationshipStorage(
dataSource,
BuiltInBindingProvider
)
).also {
ersInitialized = true
}
}

companion object {
Expand All @@ -81,7 +85,9 @@ class SQLitePersistenceImpl(

override fun close() {
try {
ers.close()
if (ersInitialized) {
ers.close()
}
connection.close()
super.close()
} catch (e: Exception) {
Expand Down
1 change: 1 addition & 0 deletions jacodb-storage/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dependencies {
compileOnly(Libs.lmdb_java)
compileOnly(Libs.rocks_db)


testImplementation(Libs.xodusEnvironment)
testImplementation(Libs.lmdb_java)
testImplementation(Libs.rocks_db)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ fun <T : Any> Class<T>.getBinding(): Binding<T> = (when {
fun <T : Any> getBinding(obj: T): Binding<T> = obj.javaClass.getBinding()

private val builtInBindings: Array<BuiltInBinding<*>> = arrayOf(
ByteArrayBinding, // trivial binding with no conversion to make it possible to deal with ByteArray properties
StringBinding, // UTF-8 strings
IntegerBinding, // 4-byte signed integers
LongBinding, // 8-byte signed integers (longs)
IntegerBinding, // 4-byte signed integers
BooleanBinding, // boolean values
DoubleBinging, // 8-byte floating point numbers (doubles)
BooleanBinding // boolean values
ByteArrayBinding // trivial binding with no conversion to make it possible to deal with ByteArray properties
)

private abstract class BuiltInBinding<T : Any> : Binding<T> {
Expand Down
Loading

0 comments on commit 335977f

Please sign in to comment.