Skip to content

Commit

Permalink
chore: benchmark framework and baseline for persistence (WPB-8966) (#…
Browse files Browse the repository at this point in the history
…2727)

* chore: add benchmark baseline

* chore: add benchmark baseline

* chore: add report visualization

* chore: triggering workflow

* chore: triggering workflow

* chore: triggering workflow

* chore: triggering workflow

* chore: triggering workflow

* chore: output format

* chore: output format

* chore: detekt

* chore: detekt tesst workflow

* chore: detekt tesst workflow

* chore: detekt test workflow

* chore: align detekt test workflow

* chore: align detekt test workflow

* chore: use workflow

* chore: change output format
  • Loading branch information
yamilmedina authored May 3, 2024
1 parent 68b43d2 commit 4efbed4
Show file tree
Hide file tree
Showing 8 changed files with 1,745 additions and 2,595 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/benchmarks-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Benchmark Checks
on:
merge_group:
pull_request:
types: [ opened, synchronize ]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
detekt:
uses: ./.github/workflows/codestyle.yml
benchmarks-check:
needs: [ detekt ]
runs-on: ubuntu-latest
container: wirebot/cryptobox:1.4.0
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@v3
- name: Run Benchmarks
run: ./gradlew benchmark
- name: Generate HTML Report
run: ./gradlew jmhReport
- name: Archive Reports
if: always()
uses: actions/upload-artifact@v4
with:
name: benchmarks-reports
path: ./benchmarks/build/reports/benchmarks/**
- name: Create Benchmark Table
id: benchmark-table
uses: boswelja/kotlinx-benchmark-table-action@0.0.4
with:
benchmark-results: ./benchmarks/build/reports/benchmarks/main/**/jvm.json
- name: Post Results
run: |
echo '## Benchmarks results ⏱️' >> $GITHUB_STEP_SUMMARY
echo '${{steps.benchmark-table.outputs.benchmark-table}}️' >> $GITHUB_STEP_SUMMARY
- name: Cleanup Gradle Cache
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties
18 changes: 18 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Kalium benchmarks

The module consists of benchmarks aimed to track performance of different isolated layers
of `Kalium` like; `persistence`, `crypto`, `network`, etc.

Currently, the suite includes benchmarks on:

- [persistence module](../persistence)

The rest of the benchmarks, to other layers will be added later.

### Running benchmarks

To run the benchmarks you can use the following command:

```shell
./gradlew clean benchmark
```
52 changes: 52 additions & 0 deletions benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import kotlinx.benchmark.gradle.JvmBenchmarkTarget

plugins {
kotlin("multiplatform")
alias(libs.plugins.benchmark)
alias(libs.plugins.allOpen)
alias(libs.plugins.jhmReport)
}

group = "com.wire.kalium.benchmarks"
version = "0.0.1"

allOpen {
annotation("org.openjdk.jmh.annotations.State")
}

kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(JavaVersion.VERSION_17.majorVersion))
}

jvm()

sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":persistence"))
implementation(libs.coroutines.core)
implementation(libs.ktxDateTime)
implementation(libs.kotlinx.benchmark.runtime)
}
}

val jvmMain by getting
}
}

benchmark {
targets {
register("jvm") {
this as JvmBenchmarkTarget
jmhVersion = libs.versions.jmh.get()
}
}
}

jmhReport {
val baseFolder = project.file("build/reports/benchmarks/main").absolutePath
val lastFolder = project.file(baseFolder).list()?.sortedArray()?.lastOrNull() ?: ""
jmhResultPath = "$baseFolder/$lastFolder/jvm.json"
jmhReportOutput = "$baseFolder/$lastFolder"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.benchmarks.persistence

import com.wire.kalium.persistence.dao.ConnectionEntity
import com.wire.kalium.persistence.dao.QualifiedIDEntity
import com.wire.kalium.persistence.dao.SupportedProtocolEntity
import com.wire.kalium.persistence.dao.UserAvailabilityStatusEntity
import com.wire.kalium.persistence.dao.UserEntity
import com.wire.kalium.persistence.dao.UserTypeEntity
import com.wire.kalium.persistence.dao.conversation.ConversationEntity
import kotlinx.datetime.toInstant

object DBTestSetup {
val conversationId = QualifiedIDEntity("conversationId", "wire.com")
val conversationEntity = newConversationEntity(conversationId.value)
val userEntity1 = newUserEntity(QualifiedIDEntity("userEntity1", "wire.com"))
val userEntity2 = newUserEntity(QualifiedIDEntity("userEntity2", "wire.com"))

private fun newUserEntity(qualifiedID: QualifiedIDEntity, id: String = "test") =
UserEntity(
id = qualifiedID,
name = "user$id",
handle = "handle$id",
email = "email$id",
phone = "phone$id",
accentId = 1,
team = "team",
ConnectionEntity.State.ACCEPTED,
null,
null,
UserAvailabilityStatusEntity.NONE,
UserTypeEntity.STANDARD,
botService = null,
deleted = false,
hasIncompleteMetadata = false,
expiresAt = null,
defederated = false,
supportedProtocols = setOf(SupportedProtocolEntity.PROTEUS),
activeOneOnOneConversationId = null
)

private fun newConversationEntity(id: String = "test") = ConversationEntity(
id = QualifiedIDEntity(id, "wire.com"),
name = "conversation1",
type = ConversationEntity.Type.ONE_ON_ONE,
teamId = "teamID",
protocolInfo = ConversationEntity.ProtocolInfo.Proteus,
creatorId = "someValue",
lastNotificationDate = null,
lastModifiedDate = "2022-03-30T15:36:00.000Z".toInstant(),
lastReadDate = "2000-01-01T12:00:00.000Z".toInstant(),
access = listOf(ConversationEntity.Access.LINK, ConversationEntity.Access.INVITE),
accessRole = listOf(ConversationEntity.AccessRole.NON_TEAM_MEMBER, ConversationEntity.AccessRole.TEAM_MEMBER),
receiptMode = ConversationEntity.ReceiptMode.DISABLED,
messageTimer = null,
userMessageTimer = null,
archived = false,
archivedInstant = null,
mlsVerificationStatus = ConversationEntity.VerificationStatus.NOT_VERIFIED,
proteusVerificationStatus = ConversationEntity.VerificationStatus.NOT_VERIFIED,
legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
@file:Suppress("MagicNumber")

package com.wire.kalium.benchmarks.persistence

import com.wire.kalium.benchmarks.persistence.DBTestSetup.conversationEntity
import com.wire.kalium.benchmarks.persistence.DBTestSetup.conversationId
import com.wire.kalium.benchmarks.persistence.DBTestSetup.userEntity1
import com.wire.kalium.benchmarks.persistence.DBTestSetup.userEntity2
import com.wire.kalium.persistence.dao.UserIDEntity
import com.wire.kalium.persistence.dao.message.MessageEntity
import com.wire.kalium.persistence.dao.message.MessageEntityContent
import com.wire.kalium.persistence.db.PlatformDatabaseData
import com.wire.kalium.persistence.db.StorageData
import com.wire.kalium.persistence.db.UserDatabaseBuilder
import com.wire.kalium.persistence.db.userDatabaseBuilder
import kotlinx.benchmark.Benchmark
import kotlinx.benchmark.BenchmarkMode
import kotlinx.benchmark.Blackhole
import kotlinx.benchmark.Measurement
import kotlinx.benchmark.Mode
import kotlinx.benchmark.OutputTimeUnit
import kotlinx.benchmark.Scope
import kotlinx.benchmark.Setup
import kotlinx.benchmark.State
import kotlinx.benchmark.TearDown
import kotlinx.benchmark.Warmup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Instant
import org.openjdk.jmh.annotations.Level
import java.util.concurrent.TimeUnit
import kotlin.random.Random
import kotlin.random.nextInt

@State(Scope.Benchmark)
@Warmup(iterations = 3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(iterations = 10)
class MessagesNoPragmaTuneBenchmark {

@Benchmark
fun messageInsertionBenchmark(dbState: DBState, blackHole: Blackhole) = runBlocking {
val messagesToInsert = generateRandomMessages(5000)
blackHole.consume(dbState.db.messageDAO.insertOrIgnoreMessages(messagesToInsert))
}

@Benchmark
fun queryMessagesBenchmark(dbState: DBState, blackHole: Blackhole) = runBlocking {
val messages = dbState.db.messageDAO.getMessagesByConversationAndVisibility(
conversationId,
MESSAGES_COUNT,
0,
listOf(MessageEntity.Visibility.VISIBLE)
)

val messagesCount = messages.first().size
blackHole.consume(messagesCount)
}

companion object {

const val MESSAGES_COUNT = 1000

private fun generateRandomMessages(count: Int = MESSAGES_COUNT): List<MessageEntity> {
val users = listOf(userEntity1, userEntity2)
return buildList {
repeat(count) {
add(
MessageEntity.Regular(
id = it.toString(),
conversationId = conversationId,
date = Instant.fromEpochMilliseconds(it.toLong()),
senderUserId = users.random().id,
status = MessageEntity.Status.entries.toTypedArray().random(),
visibility = MessageEntity.Visibility.VISIBLE,
content = generateRandomMessageContent(),
senderClientId = Random.nextLong(2_000).toString(),
editStatus = MessageEntity.EditStatus.NotEdited,
senderName = "senderName",
readCount = 0
)
)
}
}
}

private fun generateRandomMessageContent() = when (Random.nextInt(0..3)) {
0 -> MessageEntityContent.Unknown(typeName = null, Random.nextBytes(1000))
1 -> MessageEntityContent.Text(Random.nextBytes(100).toString())
2 -> MessageEntityContent.Asset(
1000,
assetName = "test name",
assetMimeType = "MP4",
assetOtrKey = byteArrayOf(1),
assetSha256Key = byteArrayOf(1),
assetId = "assetId",
assetToken = "",
assetDomain = "domain",
assetEncryptionAlgorithm = "",
assetWidth = 111,
assetHeight = 111,
assetDurationMs = 10,
assetNormalizedLoudness = byteArrayOf(1),
)

else -> MessageEntityContent.Knock(Random.nextBoolean())
}

@State(value = Scope.Benchmark)
class DBState {
private val selfUserId = UserIDEntity("selfValue", "selfDomain")
lateinit var db: UserDatabaseBuilder
private val UserIDEntity.databaseFile
get() = java.nio.file.Files.createTempDirectory("test-storage").toFile().resolve("test-$domain-$value.db")

@Setup(Level.Trial)
fun setUp() {
db = userDatabaseBuilder(
platformDatabaseData = PlatformDatabaseData(StorageData.FileBacked(selfUserId.databaseFile)),
userId = selfUserId,
passphrase = null,
dispatcher = Dispatchers.IO,
enableWAL = true
)

setupData()
}

private fun setupData() = runBlocking {
db.conversationDAO.insertConversations(listOf(conversationEntity))
db.userDAO.upsertUser(userEntity1)
db.userDAO.upsertUser(userEntity2)

val messagesToInsert = generateRandomMessages(MESSAGES_COUNT)
db.messageDAO.insertOrIgnoreMessages(messagesToInsert)
}

@TearDown(Level.Trial)
fun tearDown() {
selfUserId.databaseFile.delete()
}
}
}
}
Empty file.
Loading

0 comments on commit 4efbed4

Please sign in to comment.