diff --git a/benchmark/build.gradle b/benchmark/build.gradle index 3c15185d5..a82fd21db 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -9,7 +9,11 @@ android { compileSdk 34 defaultConfig { + minSdk 26 + targetSdk 34 + // Enable measuring on an emulator, or devices with low battery + testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner' testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR,LOW-BATTERY" } @@ -21,12 +25,7 @@ android { jvmTarget = '17' } - defaultConfig { - minSdk 26 - targetSdk 34 - - testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner' - } + sourceSets.androidTest.assets.srcDirs += ["../quartz/src/androidTest/assets"] testBuildType = "benchmark" buildTypes { diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt new file mode 100644 index 000000000..06b73d84a --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt @@ -0,0 +1,397 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.amethyst.commons.data.LargeCache +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.Event +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentSkipListMap +import java.util.function.BiConsumer +import java.util.function.Consumer +import java.util.zip.GZIPInputStream + +open class BaseCacheBenchmark { + fun getEventDB(): List { + // This file includes duplicates + val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") + + return Event.mapper.readValue>( + GZIPInputStream(fullDBInputStream), + ) + } + + fun getConcurrentSkipList(db: List): ConcurrentSkipListMap { + val cache = ConcurrentSkipListMap() + + db.forEach { + cache.put(it.id, it) + } + + return cache + } + + fun getConcurrentHashMap(db: List): ConcurrentHashMap { + val cache = ConcurrentHashMap() + + db.forEach { + cache.put(it.id, it) + } + + return cache + } + + fun getRegularHashMap(db: List): HashMap { + val cache = HashMap() + + db.forEach { + cache.put(it.id, it) + } + + return cache + } + + fun getLargeCache(db: List): LargeCache { + val cache = LargeCache() + + db.forEach { event -> + cache.getOrCreate(event.id) { + event + } + } + + return cache + } + + fun hasId(event: Event) { + assertTrue(event.id.isNotEmpty()) + } + + val consumer = + Consumer { + hasId(it) + } + + val biconsumer = + BiConsumer { hex, event -> + hasId(event) + } +} + +@RunWith(AndroidJUnit4::class) +class CacheLoadingBenchmark : BaseCacheBenchmark() { + @get:Rule val benchmarkRule = BenchmarkRule() + + @Test + fun loadConcurrentSkipList() { + val db = getEventDB() + benchmarkRule.measureRepeated { getConcurrentSkipList(db) } + } + + @Test + fun loadCountDuplicates() { + val db = getEventDB().distinctBy { it.id }.toList() + } + + @Test + fun loadConcurrentHashMap() { + val db = getEventDB() + benchmarkRule.measureRepeated { getConcurrentHashMap(db) } + } + + @Test + fun loadRegularHashMap() { + val db = getEventDB() + benchmarkRule.measureRepeated { getRegularHashMap(db) } + } + + @Test + fun loadLargeCache() { + val db = getEventDB() + benchmarkRule.measureRepeated { getLargeCache(db) } + } +} + +@RunWith(AndroidJUnit4::class) +class CommonForEachBenchmark : BaseCacheBenchmark() { + @get:Rule + val benchmarkRule = BenchmarkRule() + + @Test + fun benchForEachRegularList() { + val db = getEventDB().distinctBy { it.id }.toList() + benchmarkRule.measureRepeated { db.forEach { hasId(it) } } + } + + @Test + fun benchForEachConsumerList() { + val db = getEventDB().distinctBy { it.id }.toList() + benchmarkRule.measureRepeated { db.forEach(consumer) } + } + + @Test + fun forEachConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } } + } + + @Test + fun forEachConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } } + } + + @Test + fun forEachRegularHashMap() { + val cache = getRegularHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } } + } + + @Test + fun consumerForEachConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach(biconsumer) } + } + + @Test + fun consumerForEachConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach(biconsumer) } + } + + @Test + fun consumerForEachRegularHashMap() { + val cache = getRegularHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach(biconsumer) } + } + + @Test + fun consumerForEachLargeCache() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach(biconsumer) } + } +} + +@RunWith(AndroidJUnit4::class) +class CommonMapBenchmark : BaseCacheBenchmark() { + @get:Rule + val benchmarkRule = BenchmarkRule() + + @Test + fun benchMapRegularList() { + val db = getEventDB().distinctBy { it.id }.toList() + benchmarkRule.measureRepeated { db.map { it.id } } + } + + @Test + fun consumerMapConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { cache.map { it.value.id } } + } + + @Test + fun consumerMapConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.map { it.value.id } } + } + + @Test + fun consumerMapLargeCache() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.map { key, item -> item.id } } + } +} + +@RunWith(AndroidJUnit4::class) +class BiggerForEachBenchmark : BaseCacheBenchmark() { + @get:Rule val benchmarkRule = BenchmarkRule() + + @Test + fun benchForEachRegularList() { + val db = getEventDB().distinctBy { it.id }.toList() + benchmarkRule.measureRepeated { db.forEach { hasId(it) } } + } + + @Test + fun benchForEachConsumerList() { + val db = getEventDB().distinctBy { it.id }.toList() + benchmarkRule.measureRepeated { db.forEach(consumer) } + } + + @Test + fun forEachConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } } + } + + @Test + fun forEachConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } } + } + + @Test + fun forEachRegularHashMap() { + val cache = getRegularHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } } + } + + @Test + fun consumerForEachConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach(biconsumer) } + } + + @Test + fun consumerForEachConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach(biconsumer) } + } + + @Test + fun consumerForEachRegularHashMap() { + val cache = getRegularHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach(biconsumer) } + } + + @Test + fun consumerForEachLargeCache() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach(biconsumer) } + } + + @Test + fun valuesConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { cache.values.forEach { hasId(it) } } + } + + @Test + fun valuesConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.values.forEach { hasId(it) } } + } + + @Test + fun valuesRegularHashMap() { + val cache = getRegularHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.values.forEach { hasId(it) } } + } + + @Test + fun consumerValuesConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { cache.values.forEach(consumer) } + } + + @Test + fun consumerValuesConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.values.forEach(consumer) } + } + + @Test + fun consumerValuesRegularHashMap() { + val cache = getRegularHashMap(getEventDB()) + benchmarkRule.measureRepeated { cache.values.forEach(consumer) } + } + + @Test + fun iterableConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { + with(cache.iterator()) { + while (hasNext()) { + hasId(next().value) + } + } + } + } + + @Test + fun iterableConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { + with(cache.iterator()) { + while (hasNext()) { + hasId(next().value) + } + } + } + } + + @Test + fun iterableRegularHashMap() { + val cache = getRegularHashMap(getEventDB()) + benchmarkRule.measureRepeated { + with(cache.iterator()) { + while (hasNext()) { + hasId(next().value) + } + } + } + } + + @Test + fun iterableValuesConcurrentSkipList() { + val cache = getConcurrentSkipList(getEventDB()) + benchmarkRule.measureRepeated { + with(cache.values.iterator()) { + while (hasNext()) { + hasId(next()) + } + } + } + } + + @Test + fun iterableValuesConcurrentHashMap() { + val cache = getConcurrentHashMap(getEventDB()) + benchmarkRule.measureRepeated { + with(cache.values.iterator()) { + while (hasNext()) { + hasId(next()) + } + } + } + } + + @Test + fun iterableValuesRegularHashMap() { + val cache = getRegularHashMap(getEventDB()) + benchmarkRule.measureRepeated { + with(cache.values.iterator()) { + while (hasNext()) { + hasId(next()) + } + } + } + } +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt new file mode 100644 index 000000000..37c25c573 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.amethyst.commons.data.LargeCache +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.Event +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Arrays +import java.util.function.Consumer +import java.util.zip.GZIPInputStream + +open class BaseLargeCacheBenchmark { + fun getEventDB(): List { + // This file includes duplicates + val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") + + return Event.mapper.readValue>( + GZIPInputStream(fullDBInputStream), + ) + } + + fun getLargeCache(db: List): LargeCache { + val cache = LargeCache() + + db.forEach { + cache.getOrCreate(it.id) { key -> + it + } + } + + return cache + } + + fun hasId(event: Event) { + assertTrue(event.id.isNotEmpty()) + } + + val consumer = + Consumer { + hasId(it) + } +} + +@RunWith(AndroidJUnit4::class) +class LargeCacheForEachBenchmark : BaseLargeCacheBenchmark() { + @get:Rule val benchmarkRule = BenchmarkRule() + + // 191,353 ns 0 allocs Trace EMULATOR_LargeCacheForEachBenchmark.benchForEachConsumerList + @Test + fun benchForEachConsumerList() { + val db = getEventDB().distinctBy { it.id }.toList() + benchmarkRule.measureRepeated { db.forEach(consumer) } + } + + // 245,319 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.benchForEachRegularList + @Test + fun benchForEachRegularList() { + val db = getEventDB().distinctBy { it.id }.toList() + benchmarkRule.measureRepeated { db.forEach { hasId(it) } } + } + + // 435,097 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.forEach + @Test + fun forEach() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.forEach { key, it -> hasId(it) } } + } + + // 525,329 ns 18 allocs Trace EMULATOR_LargeCacheForEachBenchmark.filterKind1List + @Test + fun filterKind1List() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.filter { key, it -> it.kind == 1 } } + } + + // 690,323 ns 3581 allocs Trace EMULATOR_LargeCacheForEachBenchmark.filterKind1Set + @Test + fun filterKind1Set() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.filterIntoSet { key, it -> it.kind == 1 } } + } + + // 641,179 ns 23 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapToSigs + @Test + fun mapToSigs() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.map { key, it -> it.sig } } + } + + // 590,930 ns 23 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapNotNullTagList + @Test + fun mapNotNullTagList() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.mapNotNull { key, it -> it.tags.firstOrNull() } } + } + + // HashSet: 1,817,833 ns 30632 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapNotNullTagSet + // LinkedHashSet: 2,057,674 ns 30633 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapNotNullTagSet + @Test + fun mapNotNullTagSet() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.mapNotNullIntoSet { key, it -> it.tags.firstOrNull() } } + } + + // 2,619,604 ns 93505 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagList + @Test + fun mapFlattenTagList() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.mapFlatten { key, it -> it.tags.asList() } } + } + + // ---- + + // 4,802,623 ns 114928 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagSetAsList + @Test + fun mapFlattenTagSetAsList() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.mapFlattenIntoSet { key, it -> it.tags.asList() } } + } + + // 5,695,432 ns 146089 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagSetArraysAsList + @Test + fun mapFlattenTagSetArraysAsList() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.mapFlattenIntoSet { key, it -> Arrays.asList(*it.tags) } } + } + + // 7,008,496 ns 176161 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagSetListOf + @Test + fun mapFlattenTagSetListOf() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.mapFlattenIntoSet { key, it -> listOf(*it.tags) } } + } + + // 7,032,714 ns 193834 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagSetToList + @Test + fun mapFlattenTagSetToList() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.mapFlattenIntoSet { key, it -> it.tags.toList() } } + } + + // ---- + + // 467,227 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.sumOfKinds + @Test + fun sumOfKinds() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.sumOf { key, it -> it.kind } } + } + + // 458,998 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.sumOfKindLong + @Test + fun sumOfKindLong() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.sumOfLong { key, it -> it.createdAt } } + } + + // 1,021,368 ns 11683 allocs Trace EMULATOR_LargeCacheForEachBenchmark.groupByKind + @Test + fun groupByKind() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.groupBy { key, it -> it.kind } } + } + + // 1,133,156 ns 39899 allocs Trace EMULATOR_LargeCacheForEachBenchmark.countByGroupKind + @Test + fun countByGroupKind() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.countByGroup { key, it -> it.kind } } + } + + // 428,641 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.countNotEmptyTags + @Test + fun countNotEmptyTags() { + val cache = getLargeCache(getEventDB()) + benchmarkRule.measureRepeated { cache.count { key, it -> it.tags.isNotEmpty() } } + } +} diff --git a/quartz/src/androidTest/assets/nostr_vitor_startup_data.json b/quartz/src/androidTest/assets/nostr_vitor_startup_data.json new file mode 100644 index 000000000..4d1fc04e5 Binary files /dev/null and b/quartz/src/androidTest/assets/nostr_vitor_startup_data.json differ diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt index f558bd606..8bcbc4e8d 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import java.io.InputStreamReader +import java.util.zip.GZIPInputStream @RunWith(AndroidJUnit4::class) class LargeDBSignatureCheck { @@ -49,6 +50,28 @@ class LargeDBSignatureCheck { counter++ } + assertEquals(eventArray.size, counter) + } + + @Test + fun insertStartupDatabase() = + runBlocking { + // This file includes duplicates + val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") + + val eventArray = + Event.mapper.readValue>( + GZIPInputStream(fullDBInputStream), + ) as List + + var counter = 0 + eventArray.forEach { + if (it.sig != "") { + assertTrue(it.hasValidSignature()) + } + counter++ + } + assertEquals(eventArray.size, counter) } }