From 63d5b26db08ba4522cc51733a21ba241ff9d46e1 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sun, 15 Dec 2024 01:41:33 +0200 Subject: [PATCH] Benchmarks (#13) * 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 --- .github/workflows/benchmark.yml | 29 ++++++ benchmark/build.gradle.kts | 38 ++++++++ .../kotlin/ivy/di/benchmark/DiBenchmark.kt | 62 ++++++++++++ .../fixtures/android/AndroidCommon.kt | 96 +++++++++++++++++++ .../fixtures/android/AndroidCommonIvyDi.kt | 54 +++++++++++ .../fixtures/android/AndroidCommonKoin.kt | 47 +++++++++ build.gradle.kts | 3 + di/build.gradle.kts | 2 - gradle/libs.versions.toml | 9 +- scripts/runBenchmarks.sh | 14 +++ settings.gradle.kts | 1 + 11 files changed, 352 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/benchmark.yml create mode 100644 benchmark/build.gradle.kts create mode 100644 benchmark/src/main/kotlin/ivy/di/benchmark/DiBenchmark.kt create mode 100644 benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommon.kt create mode 100644 benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommonIvyDi.kt create mode 100644 benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommonKoin.kt create mode 100755 scripts/runBenchmarks.sh diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..c54733a --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -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/ \ No newline at end of file diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts new file mode 100644 index 0000000..a0b8be3 --- /dev/null +++ b/benchmark/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/ivy/di/benchmark/DiBenchmark.kt b/benchmark/src/main/kotlin/ivy/di/benchmark/DiBenchmark.kt new file mode 100644 index 0000000..09ba068 --- /dev/null +++ b/benchmark/src/main/kotlin/ivy/di/benchmark/DiBenchmark.kt @@ -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() + Di.get() + Di.get() + Di.get() + Di.get() + } + } + + @Benchmark + fun androidCommonKoin() { + startKoin { + modules(AndroidCommonModuleKoin) + } + repeat(diGetIterations) { + getKoin().get() + getKoin().get() + getKoin().get() + getKoin().get() + getKoin().get() + } + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommon.kt b/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommon.kt new file mode 100644 index 0000000..9fb8d3b --- /dev/null +++ b/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommon.kt @@ -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, + 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, +) \ No newline at end of file diff --git a/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommonIvyDi.kt b/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommonIvyDi.kt new file mode 100644 index 0000000..c9a7dac --- /dev/null +++ b/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommonIvyDi.kt @@ -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() + + autoWire(::AndroidLogger) + bind() + + singleton { HttpClient() } + autoWire(::LocalStorage) + + singleton { Backstack("/") } + autoWireSingleton(::Navigation) + + autoWireSingleton(::SessionManager) + + register { + RemoteArticlesDataSource( + httpClient = Di.getLazy(), + sessionManger = Di.get() + ) + } + + autoWire(::ArticlesRepositoryImpl) + bind() + + autoWire(::ArticlesUseCase) + + autoWire(::AuthorDataSource) + autoWire(::AuthorRepository) + + autoWire(::ArticlesViewModel) + autoWire(::AuthorViewModel) + + autoWire(::ContentScreen) + autoWire(::ArticlesScreen) + autoWire(::AuthorScreen) + + autoWire(::App) + autoWireSingleton(::AppHolder) + autoWire(::AppAppHolder) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommonKoin.kt b/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommonKoin.kt new file mode 100644 index 0000000..44f46eb --- /dev/null +++ b/benchmark/src/main/kotlin/ivy/di/benchmark/fixtures/android/AndroidCommonKoin.kt @@ -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() } + + factoryOf(::AndroidLogger) { bind() } + + single { HttpClient() } + factoryOf(::LocalStorage) + + single { Backstack("/") } + singleOf(::Navigation) + + singleOf(::SessionManager) + + factory { + RemoteArticlesDataSource( + httpClient = lazy { get() }, + sessionManger = get() + ) + } + + singleOf(::ArticlesRepositoryImpl) { bind() } + + singleOf(::ArticlesUseCase) + + factoryOf(::AuthorDataSource) + factoryOf(::AuthorRepository) + + factoryOf(::ArticlesViewModel) + factoryOf(::AuthorViewModel) + + singleOf(::ContentScreen) + singleOf(::ArticlesScreen) + singleOf(::AuthorScreen) + + factoryOf(::App) + singleOf(::AppHolder) + factoryOf(::AppAppHolder) +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 29dafc1..e1bb8d6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 } diff --git a/di/build.gradle.kts b/di/build.gradle.kts index 467dc2b..80629ed 100644 --- a/di/build.gradle.kts +++ b/di/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -11,7 +10,6 @@ kotlin { jvm() androidTarget { publishLibraryVariants("release") - @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { jvmTarget.set(JvmTarget.JVM_1_8) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0cf2ac..b60218c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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 = [ @@ -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" } \ No newline at end of file +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" } \ No newline at end of file diff --git a/scripts/runBenchmarks.sh b/scripts/runBenchmarks.sh new file mode 100755 index 0000000..6950cd3 --- /dev/null +++ b/scripts/runBenchmarks.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# ------------------- +# 0. ROOT DIRECTORY CHECK +# ------------------- + +# Check if the script is being run from the root directory of the repo +if [ ! -f "settings.gradle.kts" ]; then + echo "ERROR:" + echo "Please run this script from the root directory of the repo." + exit 1 +fi + +./gradlew :benchmark:benchmark \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f79e4a4..6f101d5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,4 @@ dependencyResolutionManagement { rootProject.name = "ivy-apps-di" include(":di") include(":samples") +include(":benchmark")