Skip to content

Commit

Permalink
Benchmarks (#13)
Browse files Browse the repository at this point in the history
* Setup `:benchmark` module

* Add Koin for benchmark purposes

* Add my first benchmark

* Configure benchmarks

* Add startup benchmark

* Add benchmark for small Android dep graph

* Configure benchmarks

* Increase stat confidence

* Fix the Android Common graph

* Improve benchmark configs

* Improve configuration

* Fix config

* Improve config

* Improve config

* Add benchmark workflow

* Refactor

* Fix the benchmark

* Increase the complexity of the DI graph

* Complicate the DI graph
  • Loading branch information
ILIYANGERMANOV authored Dec 14, 2024
1 parent 5cc6dd4 commit 63d5b26
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 3 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Benchmark
on:
workflow_dispatch:

jobs:
benchmark:
name: Benchmark
runs-on: ubuntu-latest
steps:
- name: Checkout GIT
uses: actions/checkout@v4

- name: Gradle cache
uses: gradle/actions/setup-gradle@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Run benchmarks
run: ./gradlew :benchmark:benchmark

- name: Upload benchmark results
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: benchmark/build/reports/benchmark/
38 changes: 38 additions & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlin.allOpen)
alias(libs.plugins.kotlin.benchmark)
}

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

benchmark {
targets {
register("main")
}
}

benchmark {
targets {
register("jvm")
}
configurations {
named("main") {
warmups = 5
iterations = 10
iterationTime = 5
iterationTimeUnit = "s"

reportFormat = "csv"
outputTimeUnit = "ms"
}
}
}

dependencies {
implementation(libs.kotlin.benchmark)
implementation(project(":di"))
implementation(libs.koin.core)
}
62 changes: 62 additions & 0 deletions benchmark/src/main/kotlin/ivy/di/benchmark/DiBenchmark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package ivy.di.benchmark

import ivy.di.Di
import ivy.di.benchmark.fixtures.android.*
import kotlinx.benchmark.*
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.java.KoinJavaComponent.getKoin
import org.openjdk.jmh.annotations.Level
import java.util.concurrent.TimeUnit

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
class DiComparisonBenchmark {

private val diGetIterations = 100

@TearDown(Level.Invocation)
fun cleanup() {
stopKoin() // Clean up Koin
Di.reset() // Clean up Ivy DI
}

@Benchmark
fun startIvyDi() {
Di.appScope {}
}

@Benchmark
fun startKoin() {
startKoin {
modules(emptyList())
}
}

@Benchmark
fun androidCommonIvyDi() {
Di.init(AndroidCommonModuleIvyDi)
repeat(diGetIterations) {
Di.get<ArticlesViewModel>()
Di.get<AuthorViewModel>()
Di.get<App>()
Di.get<AppHolder>()
Di.get<AppAppHolder>()
}
}

@Benchmark
fun androidCommonKoin() {
startKoin {
modules(AndroidCommonModuleKoin)
}
repeat(diGetIterations) {
getKoin().get<ArticlesViewModel>()
getKoin().get<AuthorViewModel>()
getKoin().get<App>()
getKoin().get<AppHolder>()
getKoin().get<AppAppHolder>()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
@file:Suppress("unused")

package ivy.di.benchmark.fixtures.android

interface DispatchersProvider
class AndroidDispatchersProvider : DispatchersProvider

class Context
interface Logger
class AndroidLogger : Logger

class HttpClient
class LocalStorage

class SessionManager(val localStorage: LocalStorage, val logger: Logger)

class Backstack(val initialRoute: String)
class Navigation(
val backstack: Backstack,
val logger: Logger,
val context: Context,
)

interface ArticlesDataSource
class RemoteArticlesDataSource(
val httpClient: Lazy<HttpClient>,
val sessionManger: SessionManager
) : ArticlesDataSource

interface ArticlesRepository
class ArticlesRepositoryImpl(
val dataSource: ArticlesDataSource,
val logger: Logger,
) : ArticlesRepository

class ArticlesUseCase(
val dispatchers: DispatchersProvider,
val articlesRepo: ArticlesRepository,
val logger: Logger,
)

class AuthorDataSource(val httpClient: HttpClient)
class AuthorRepository(val dataSource: AuthorDataSource)

class ArticlesViewModel(
val navigation: Navigation,
val dispatchers: DispatchersProvider,
val articlesUseCase: ArticlesUseCase,
val authorRepository: AuthorRepository,
val logger: Logger,
)
class AuthorViewModel(
val navigation: Navigation,
val dispatchers: DispatchersProvider,
val articlesUseCase: ArticlesUseCase,
val authorRepository: AuthorRepository,
val sessionManger: SessionManager,
val context: Context,
)

class ContentScreen(
val authorViewModel: AuthorViewModel,
val articlesViewModel: ArticlesViewModel,
val context: Context,
)

class AuthorScreen(
val authorViewModel: AuthorViewModel,
val context: Context,
)

class ArticlesScreen(
val articlesViewModel: ArticlesViewModel,
val context: Context,
)

class App(
val context: Context,
val navigation: Navigation,
val contentScreen: ContentScreen,
val authorScreen: AuthorScreen,
val articlesScreen: ArticlesScreen,
val logger: Logger,
)

class AppHolder(
val app: App,
val context: Context,
val logger: Logger,
)

// Complicate the DI graph
class AppAppHolder(
val app: App,
val appHolder: AppHolder,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ivy.di.benchmark.fixtures.android

import ivy.di.Di
import ivy.di.Di.bind
import ivy.di.Di.register
import ivy.di.Di.singleton
import ivy.di.autowire.autoWire
import ivy.di.autowire.autoWireSingleton

object AndroidCommonModuleIvyDi : Di.Module {
override fun init() = Di.appScope {
autoWireSingleton(::Context)

autoWire(::AndroidDispatchersProvider)
bind<DispatchersProvider, AndroidDispatchersProvider>()

autoWire(::AndroidLogger)
bind<Logger, AndroidLogger>()

singleton { HttpClient() }
autoWire(::LocalStorage)

singleton { Backstack("/") }
autoWireSingleton(::Navigation)

autoWireSingleton(::SessionManager)

register<ArticlesDataSource> {
RemoteArticlesDataSource(
httpClient = Di.getLazy(),
sessionManger = Di.get()
)
}

autoWire(::ArticlesRepositoryImpl)
bind<ArticlesRepository, ArticlesRepositoryImpl>()

autoWire(::ArticlesUseCase)

autoWire(::AuthorDataSource)
autoWire(::AuthorRepository)

autoWire(::ArticlesViewModel)
autoWire(::AuthorViewModel)

autoWire(::ContentScreen)
autoWire(::ArticlesScreen)
autoWire(::AuthorScreen)

autoWire(::App)
autoWireSingleton(::AppHolder)
autoWire(::AppAppHolder)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ivy.di.benchmark.fixtures.android

import org.koin.core.module.dsl.bind
import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module

val AndroidCommonModuleKoin = module {
singleOf(::Context)

factoryOf(::AndroidDispatchersProvider) { bind<DispatchersProvider>() }

factoryOf(::AndroidLogger) { bind<Logger>() }

single { HttpClient() }
factoryOf(::LocalStorage)

single { Backstack("/") }
singleOf(::Navigation)

singleOf(::SessionManager)

factory<ArticlesDataSource> {
RemoteArticlesDataSource(
httpClient = lazy { get() },
sessionManger = get()
)
}

singleOf(::ArticlesRepositoryImpl) { bind<ArticlesRepository>() }

singleOf(::ArticlesUseCase)

factoryOf(::AuthorDataSource)
factoryOf(::AuthorRepository)

factoryOf(::ArticlesViewModel)
factoryOf(::AuthorViewModel)

singleOf(::ContentScreen)
singleOf(::ArticlesScreen)
singleOf(::AuthorScreen)

factoryOf(::App)
singleOf(::AppHolder)
factoryOf(::AppAppHolder)
}
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
plugins {
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.publish) apply false
alias(libs.plugins.kotlin.allOpen) apply false
alias(libs.plugins.kotlin.benchmark) apply false
}
2 changes: 0 additions & 2 deletions di/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
Expand All @@ -11,7 +10,6 @@ kotlin {
jvm()
androidTarget {
publishLibraryVariants("release")
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
Expand Down
9 changes: 8 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
[versions]
agp = "8.7.3"
koin = "4.1.0-Beta1"
kotlin = "2.1.0"
android-minSdk = "24"
android-compileSdk = "34"
kotest = "5.9.1"
kotlin-benchmark = "0.4.13"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotlin-benchmark = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlin-benchmark" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }

[bundles]
test = [
Expand All @@ -17,5 +21,8 @@ test = [

[plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
kotlin-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlin-benchmark" }
kotlin-allOpen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" }
Loading

0 comments on commit 63d5b26

Please sign in to comment.