diff --git a/build.gradle.kts b/build.gradle.kts index dcffd9dd..a14b2ca8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,7 @@ buildscript { repositories { maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlinx/maven") gradlePluginPortal() + google() addDevRepositoryIfEnabled(this, project) } @@ -25,6 +26,7 @@ buildscript { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } + classpath("com.android.tools.build:gradle:8.5.1") } } diff --git a/examples/kotlin-multiplatform/build.gradle b/examples/kotlin-multiplatform/build.gradle index 0e3e30c5..7097b46f 100644 --- a/examples/kotlin-multiplatform/build.gradle +++ b/examples/kotlin-multiplatform/build.gradle @@ -1,9 +1,11 @@ import kotlinx.benchmark.gradle.JsBenchmarksExecutor +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id 'org.jetbrains.kotlin.multiplatform' id 'org.jetbrains.kotlin.plugin.allopen' version "2.0.20" id 'org.jetbrains.kotlinx.benchmark' + id 'com.android.library' } // how to apply plugin to a specific source set? @@ -11,7 +13,43 @@ allOpen { annotation("org.openjdk.jmh.annotations.State") } +android { + compileSdk 34 + namespace = "org.jetbrains.kotlinx.examples" + + defaultConfig { + minSdk = 29 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { +// isMinifyEnabled = false +// proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +// Could not determine the dependencies of task ':examples:kotlin-multiplatform:extractReleaseAnnotations'. +// > Could not resolve all task dependencies for configuration ':examples:kotlin-multiplatform:detachedConfiguration1'. +// > Could not find com.android.tools.lint:lint-gradle:31.2.2. +repositories { + google() +} + kotlin { + androidTarget { + // Android target does not have any compilations + println("android compilations: ${compilations.size()}") + } jvm { compilations.create('benchmark') { associateWith(compilations.main) } } @@ -47,6 +85,20 @@ kotlin { } nativeMain {} + + androidMain { + kotlin.setSrcDirs(["src/androidMain/kotlin"]) + } + + forEach { + // Android target has 3 source sets: androidMain, androidUnitTest, and androidInstrumentedTest + println("SourceSet: $it") + println(it.name) + println(it.kotlin) + println(it.kotlin.srcDirs) + println(it.kotlin.sourceDirectories) + } + } } @@ -121,5 +173,17 @@ benchmark { register("macosArm64") register("linuxX64") register("mingwX64") + register("android") } } + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } +} \ No newline at end of file diff --git a/examples/kotlin-multiplatform/src/androidMain/kotlin/AndroidTestBenchmark.kt b/examples/kotlin-multiplatform/src/androidMain/kotlin/AndroidTestBenchmark.kt new file mode 100644 index 00000000..6563c129 --- /dev/null +++ b/examples/kotlin-multiplatform/src/androidMain/kotlin/AndroidTestBenchmark.kt @@ -0,0 +1,31 @@ +package test + +import kotlinx.benchmark.* +import kotlin.math.* + +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS) +@Measurement(iterations = 3, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS) +class AndroidTestBenchmark { + private var data = 0.0 + + @Setup + fun setUp() { + data = 3.0 + } + + @TearDown + fun teardown() { + // println("Teardown!") + } + + @Benchmark + fun sqrtBenchmark(): Double { + return sqrt(data) + } + + @Benchmark + fun cosBenchmark(): Double { + return cos(data) + } +} \ No newline at end of file diff --git a/examples/kotlin-multiplatform/src/commonMain/kotlin/CommonBenchmark.kt b/examples/kotlin-multiplatform/src/commonMain/kotlin/CommonBenchmark.kt index d02dff56..a0a238c6 100644 --- a/examples/kotlin-multiplatform/src/commonMain/kotlin/CommonBenchmark.kt +++ b/examples/kotlin-multiplatform/src/commonMain/kotlin/CommonBenchmark.kt @@ -4,6 +4,7 @@ import kotlinx.benchmark.* import kotlin.math.* @State(Scope.Benchmark) +@Warmup(iterations = 3, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS) @Measurement(iterations = 3, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS) @OutputTimeUnit(BenchmarkTimeUnit.MILLISECONDS) @BenchmarkMode(Mode.AverageTime) diff --git a/integration/src/main/kotlin/kotlinx/benchmark/integration/GradleTestVersion.kt b/integration/src/main/kotlin/kotlinx/benchmark/integration/GradleTestVersion.kt index f9ed7b07..056d44b7 100644 --- a/integration/src/main/kotlin/kotlinx/benchmark/integration/GradleTestVersion.kt +++ b/integration/src/main/kotlin/kotlinx/benchmark/integration/GradleTestVersion.kt @@ -1,6 +1,7 @@ package kotlinx.benchmark.integration enum class GradleTestVersion(val versionString: String) { + v8_7("8.7"), v8_0("8.0.2"), MinSupportedGradleVersion("7.4"), UnsupportedGradleVersion("7.3"), diff --git a/integration/src/main/kotlin/kotlinx/benchmark/integration/ProjectBuilder.kt b/integration/src/main/kotlin/kotlinx/benchmark/integration/ProjectBuilder.kt index e1abd21a..45a6d6c2 100644 --- a/integration/src/main/kotlin/kotlinx/benchmark/integration/ProjectBuilder.kt +++ b/integration/src/main/kotlin/kotlinx/benchmark/integration/ProjectBuilder.kt @@ -51,20 +51,24 @@ private fun generateBuildScript(kotlinVersion: String, jvmToolchain: Int) = repositories { $kotlin_repo $plugin_repo_url + google() mavenCentral() } dependencies { classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion' classpath 'org.jetbrains.kotlinx:kotlinx-benchmark-plugin:0.5.0-SNAPSHOT' + classpath 'com.android.tools.build:gradle:8.5.1' } } apply plugin: 'kotlin-multiplatform' apply plugin: 'org.jetbrains.kotlinx.benchmark' + apply plugin: 'com.android.library' repositories { $kotlin_repo $runtime_repo_url + google() mavenCentral() } diff --git a/integration/src/main/kotlin/kotlinx/benchmark/integration/Runner.kt b/integration/src/main/kotlin/kotlinx/benchmark/integration/Runner.kt index f7d7f3ff..c6c245ff 100644 --- a/integration/src/main/kotlin/kotlinx/benchmark/integration/Runner.kt +++ b/integration/src/main/kotlin/kotlinx/benchmark/integration/Runner.kt @@ -93,4 +93,10 @@ class Runner( projectDir.resolve("build/benchmarks/${targetName}/sources/kotlinx/benchmark/generated").resolve(filePath) ) } + + fun generatedAndroidDir(targetName: String, targetCompilation: String, filePath: String, fileTestAction: (File) -> Unit) { + fileTestAction( + projectDir.resolve("build/benchmarks/$targetName/$targetCompilation/GeneratedAndroidProject").resolve(filePath) + ) + } } diff --git a/integration/src/test/kotlin/kotlinx/benchmark/integration/AndroidProjectGeneratorTest.kt b/integration/src/test/kotlin/kotlinx/benchmark/integration/AndroidProjectGeneratorTest.kt new file mode 100644 index 00000000..0b8d5b21 --- /dev/null +++ b/integration/src/test/kotlin/kotlinx/benchmark/integration/AndroidProjectGeneratorTest.kt @@ -0,0 +1,28 @@ +package kotlinx.benchmark.integration + +import org.junit.Test +import kotlin.test.assertTrue + +class AndroidProjectGeneratorTest: GradleTest() { + private fun testAndroidProjectGeneration(setupBlock: Runner.() -> Unit, checkBlock: Runner.() -> Unit) { + project("source-generation", print = true, gradleVersion = GradleTestVersion.v8_7).apply { + setupBlock() + runAndSucceed("androidReleaseBenchmarkGenerate") + checkBlock() + } + } + + @Test + fun generateAndroidFromResources() { + testAndroidProjectGeneration( + setupBlock = { + runAndSucceed("setupReleaseAndroidProject") + }, + checkBlock = { + generatedAndroidDir("android", "release", "") { generatedAndroidDir -> + assertTrue(generatedAndroidDir.exists(), "Generated Android project does not exist") + } + } + ) + } +} \ No newline at end of file diff --git a/integration/src/test/resources/templates/source-generation/build.gradle b/integration/src/test/resources/templates/source-generation/build.gradle index 41ba62d1..4032309a 100644 --- a/integration/src/test/resources/templates/source-generation/build.gradle +++ b/integration/src/test/resources/templates/source-generation/build.gradle @@ -1,10 +1,32 @@ import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.konan.target.HostManager +android { + compileSdk 34 + namespace = "org.jetbrains.kotlinx.tests" + + defaultConfig { + minSdk = 29 + targetSdk = 34 + } + + + buildTypes { + release {} + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + kotlin { + jvmToolchain(8) + jvm { } js { nodejs() } wasmJs { d8() } + androidTarget {} if (HostManager.hostIsLinux) linuxX64('native') if (HostManager.hostIsMingw) mingwX64('native') @@ -18,5 +40,6 @@ benchmark { register("js") register("wasmJs") register("native") + register("android") } } diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 13d641d0..5d06d335 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -180,4 +180,4 @@ if (project.findProperty("publication_repository") == "space") { apiValidation { nonPublicMarkers += listOf("kotlinx.benchmark.gradle.internal.KotlinxBenchmarkPluginInternalApi") -} +} \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/build.gradle.kts b/plugin/main/resources/GeneratedAndroidProject/app/build.gradle.kts new file mode 100644 index 00000000..bec49d6a --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) +} + +android { + namespace = "kotlinx.benchmark.generatedandroidproject" + compileSdk = 34 + + defaultConfig { + applicationId = "kotlinx.benchmark.generatedandroidproject" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) +} \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/proguard-rules.pro b/plugin/main/resources/GeneratedAndroidProject/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/AndroidManifest.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..effed8c7 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/drawable/ic_launcher_background.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/drawable/ic_launcher_foreground.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b2dfe3d1 Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..62b611da Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1b9a6956 Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9287f508 Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9126ae37 Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values-night/themes.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..43c08171 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/colors.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/strings.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..8396ea94 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + GeneratedAndroidProject + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/themes.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..a632a2a3 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/xml/backup_rules.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 00000000..fa0f996d --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/xml/data_extraction_rules.xml b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 00000000..9ee9997b --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/build.gradle.kts b/plugin/main/resources/GeneratedAndroidProject/build.gradle.kts new file mode 100644 index 00000000..14f4bb01 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/build.gradle.kts @@ -0,0 +1,6 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false +} \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/gradle.properties b/plugin/main/resources/GeneratedAndroidProject/gradle.properties new file mode 100644 index 00000000..20e2a015 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/gradle/libs.versions.toml b/plugin/main/resources/GeneratedAndroidProject/gradle/libs.versions.toml new file mode 100644 index 00000000..939401c0 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/gradle/libs.versions.toml @@ -0,0 +1,23 @@ +[versions] +agp = "8.5.1" +kotlin = "1.9.0" +coreKtx = "1.13.1" +appcompat = "1.7.0" +material = "1.12.0" +junit = "4.13.2" +androidxTestExt = "1.2.1" +androidxBenchmark = "1.2.4" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-test-ext = { group = "androidx.test.ext", name = "junit", version.ref = "androidxTestExt" } +androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "androidxBenchmark" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "agp" } + diff --git a/plugin/main/resources/GeneratedAndroidProject/gradle/wrapper/gradle-wrapper.jar b/plugin/main/resources/GeneratedAndroidProject/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/plugin/main/resources/GeneratedAndroidProject/gradle/wrapper/gradle-wrapper.jar differ diff --git a/plugin/main/resources/GeneratedAndroidProject/gradle/wrapper/gradle-wrapper.properties b/plugin/main/resources/GeneratedAndroidProject/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..2c9839e8 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Aug 05 13:46:08 TJT 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/plugin/main/resources/GeneratedAndroidProject/gradlew b/plugin/main/resources/GeneratedAndroidProject/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/plugin/main/resources/GeneratedAndroidProject/gradlew.bat b/plugin/main/resources/GeneratedAndroidProject/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugin/main/resources/GeneratedAndroidProject/microbenchmark/build.gradle.kts b/plugin/main/resources/GeneratedAndroidProject/microbenchmark/build.gradle.kts new file mode 100644 index 00000000..3401858a --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/microbenchmark/build.gradle.kts @@ -0,0 +1,43 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.jetbrains.kotlin.android) +} + +android { + namespace = "kotlinx.benchmark.android.microbenchmark" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + targetSdk = 34 + + testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner" + testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR, DEBUGGABLE" + } + + buildTypes { + release { + isMinifyEnabled = false +// proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + debug { + isJniDebuggable = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + androidTestImplementation(files("<>")) + androidTestImplementation(libs.androidx.benchmark.junit4) + + androidTestImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext) +} \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/microbenchmark/src/main/AndroidManifest.xml b/plugin/main/resources/GeneratedAndroidProject/microbenchmark/src/main/AndroidManifest.xml new file mode 100644 index 00000000..10728cc7 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/microbenchmark/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugin/main/resources/GeneratedAndroidProject/settings.gradle.kts b/plugin/main/resources/GeneratedAndroidProject/settings.gradle.kts new file mode 100644 index 00000000..436015f7 --- /dev/null +++ b/plugin/main/resources/GeneratedAndroidProject/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "GeneratedAndroidProject" +include(":app") +include(":microbenchmark") diff --git a/plugin/main/src/kotlinx/benchmark/gradle/AndroidMultiplatformTasks.kt b/plugin/main/src/kotlinx/benchmark/gradle/AndroidMultiplatformTasks.kt new file mode 100644 index 00000000..02ec1387 --- /dev/null +++ b/plugin/main/src/kotlinx/benchmark/gradle/AndroidMultiplatformTasks.kt @@ -0,0 +1,237 @@ +package kotlinx.benchmark.gradle + +import org.gradle.api.* +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation +import java.io.InputStream +import java.net.URLDecoder +import java.util.* +import java.util.concurrent.TimeUnit + +private const val GENERATED_ANDROID_PROJECT_NAME = "GeneratedAndroidProject" + +internal fun Project.processAndroidCompilation(target: AndroidBenchmarkTarget, compilation: KotlinJvmAndroidCompilation) { + project.logger.info("Configuring benchmarks for '${compilation.name}' using $target") + + createUnpackAarTask(target, compilation) + createSetupAndroidProjectTask(target, compilation) + createAndroidBenchmarkGenerateSourceTask(target, compilation) + createAndroidBenchmarkExecTask(target, compilation) +} + +private fun Project.androidBenchmarkBuildDir(target: AndroidBenchmarkTarget, compilation: KotlinJvmAndroidCompilation) = + benchmarkBuildDir(target).resolve(compilation.name) + +private fun Project.generatedAndroidProjectDir(target: AndroidBenchmarkTarget, compilation: KotlinJvmAndroidCompilation) = + androidBenchmarkBuildDir(target, compilation).resolve(GENERATED_ANDROID_PROJECT_NAME) + +private fun Project.createSetupAndroidProjectTask(target: AndroidBenchmarkTarget, compilation: KotlinJvmAndroidCompilation) { + task("setup${compilation.name.capitalize(Locale.ROOT)}AndroidProject") { + group = "benchmark" + description = "Sets up an empty android project to generate benchmarks into" + + doFirst { + sync { + it.apply { + val pluginJarPath = BenchmarksPlugin::class.java.protectionDomain.codeSource.location.path + from(project.zipTree(URLDecoder.decode(pluginJarPath, "UTF-8"))) + into(androidBenchmarkBuildDir(target, compilation)) + include("$GENERATED_ANDROID_PROJECT_NAME/**") + } + } + } + doLast { + val generatedAndroidProjectDir = generatedAndroidProjectDir(target, compilation) + logger.info("Setting up an empty Android project at $generatedAndroidProjectDir") + + generatedAndroidProjectDir.resolve("microbenchmark/build.gradle.kts").let { + val unpackedDir = getUnpackAarDir(compilation) + val newText = it.readText().replace( + "<>", + unpackedDir.resolve("classes.jar").absolutePath.replace("\\", "/") + ) + it.writeText(newText) + } + + generatedAndroidProjectDir.resolve("local.properties").let { + val sdkPath = target.sdkDir.orNull + if (sdkPath.isNullOrBlank()) { + throw GradleException("Android SDK path is not set. Please set ANDROID_HOME environment variable or specify sdkPath in the build script.") + } else { + it.writeText("sdk.dir=${sdkPath.replace("\\", "/")}\n") + logger.info("SDK path written to local.properties: ${it.readText()}") + } + } + } + } +} + +private fun Project.createUnpackAarTask(target: AndroidBenchmarkTarget, compilation: KotlinJvmAndroidCompilation) { + task("unpack${compilation.name.capitalize(Locale.ROOT)}Aar") { + group = "benchmark" + description = "Unpacks the AAR file produced by ${target.name} compilation '${compilation.name}'" + dependsOn("bundle${compilation.name.capitalize(Locale.ROOT)}Aar") + doLast { + logger.info("Unpacking AAR file produced by ${target.name} compilation '${compilation.name}'") + + val aarFile = getAarFile(compilation) + + if (!aarFile.exists()) { + throw IllegalStateException("AAR file not found: ${aarFile.absolutePath}") + } + + // TODO: Register the unpacked dir as an output of this task + // TODO: Delete the directory if exists before unpacking + unpackAarFile(aarFile, compilation) + } + } +} + +private fun generateSourcesTaskName(target: AndroidBenchmarkTarget, compilation: KotlinJvmAndroidCompilation): String { + return "${target.name}${compilation.name.capitalize(Locale.ROOT)}${BenchmarksPlugin.BENCHMARK_GENERATE_SUFFIX}" +} + +private fun Project.createAndroidBenchmarkGenerateSourceTask(target: AndroidBenchmarkTarget, compilation: KotlinJvmAndroidCompilation) { + task(generateSourcesTaskName(target, compilation)) { + group = "benchmark" + description = "Generates Android source files for ${target.name} compilation '${compilation.name}'" + dependsOn("unpack${compilation.name.capitalize(Locale.ROOT)}Aar") + dependsOn("setup${compilation.name.capitalize(Locale.ROOT)}AndroidProject") + + doLast { + + val unpackedDir = getUnpackAarDir(compilation) + processClassesJar(unpackedDir, compilation) { classDescriptors -> + val targetDir = generatedAndroidProjectDir(target, compilation) + .resolve("microbenchmark/src/androidTest/kotlin") + + if (targetDir.exists()) { + targetDir.deleteRecursively() + } + targetDir.mkdirs() + + generateBenchmarkSourceFiles(targetDir, classDescriptors) + } + } + } +} + +private fun detectAndroidDevice() { + println("Detect running Android devices...") + val devices = ProcessBuilder("adb", "devices") + .start() + .inputStream + .bufferedReader() + .useLines { lines -> + lines.filter { it.endsWith("device") } + .map { it.substringBefore("\t") } + .toList() + } + devices.takeIf { it.isNotEmpty() } + ?.let { + println("Connected Android devices/emulators:\n\t${it.joinToString("\n\t")}") + } ?: throw RuntimeException("No Android devices/emulators found, please start an emulator or connect a device.") +} + + +// Use shell command to execute separate project gradle task +private fun Project.createAndroidBenchmarkExecTask(target: AndroidBenchmarkTarget, compilation: KotlinJvmAndroidCompilation) { + task("android${compilation.name.capitalize(Locale.ROOT)}Benchmark") { + group = "benchmark" + description = "Executes benchmarks for ${target.name} compilation '${compilation.name}'" + dependsOn(generateSourcesTaskName(target, compilation)) + doLast { + detectAndroidDevice() + + // TODO: Project path needs to execute benchmark task + val executeBenchmarkPath = generatedAndroidProjectDir(target, compilation).path + // Using ./gradlew on Windows shows error: + // CreateProcess error=193, %1 is not a valid Win32 application + val osName = System.getProperty("os.name").toLowerCase(Locale.ROOT) + val gradlewPath = "$executeBenchmarkPath/gradlew" + if (osName.contains("win")) ".bat" else "" + val args = listOf("-p", executeBenchmarkPath, "connectedAndroidTest") + + try { + println("Running command: $gradlewPath ${args.joinToString(" ")}") + + val process = ProcessBuilder(gradlewPath, *args.toTypedArray()) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() + + val outputGobbler = StreamGobbler(process.inputStream) { } + val errorGobbler = StreamGobbler(process.errorStream) { } + + outputGobbler.start() + errorGobbler.start() + + clearLogcat() + val exitCode = process.waitFor(10, TimeUnit.MINUTES) + captureLogcatOutput() + if (!exitCode || process.exitValue() != 0) { + println("Android benchmark task failed with exit code ${process.exitValue()}") + } else { + println("Benchmark for Android target finished.") + } + } catch (e: Exception) { + e.printStackTrace() + throw GradleException("Failed to execute benchmark task", e) + } + } + } +} + +private fun captureLogcatOutput() { + try { + val logcatProcess = ProcessBuilder("adb", "logcat", "TestRunner:D", "KotlinBenchmark:D", "*:S") + .redirectErrorStream(true) + .start() + + val logcatGobbler = StreamGobbler(logcatProcess.inputStream) { line -> + when { + line.contains("started") -> + println( + "Android: " + + line.substringAfter("started: ") + .substringBefore("(") + .replace(Regex("\\[\\d+: "), "[") + ) + + line.contains("Warmup") -> println(line.substring(line.indexOf("Warmup"))) + line.contains("Iteration") -> println(line.substring(line.indexOf("Iteration"))) + line.contains("run finished") -> println(line.substring(line.indexOf("run finished"))) + line.contains("finished") -> println() + } + } + + logcatGobbler.start() + + if (!logcatProcess.waitFor(10, TimeUnit.SECONDS)) { + logcatProcess.destroy() + } + + logcatGobbler.join() + } catch (e: Exception) { + e.printStackTrace() + throw GradleException("Failed to capture logcat output", e) + } +} + +private fun clearLogcat() { + try { + ProcessBuilder("adb", "logcat", "-c") + .redirectErrorStream(true) + .start() + .waitFor(5, TimeUnit.SECONDS) + } catch (e: Exception) { + e.printStackTrace() + throw GradleException("Failed to clear logcat", e) + } +} + +private class StreamGobbler(private val inputStream: InputStream, private val consumer: (String) -> Unit) : Thread() { + override fun run() { + inputStream.bufferedReader().useLines { lines -> + lines.forEach { consumer(it) } + } + } +} \ No newline at end of file diff --git a/plugin/main/src/kotlinx/benchmark/gradle/AndroidSourceGenerator.kt b/plugin/main/src/kotlinx/benchmark/gradle/AndroidSourceGenerator.kt new file mode 100644 index 00000000..cf07fb27 --- /dev/null +++ b/plugin/main/src/kotlinx/benchmark/gradle/AndroidSourceGenerator.kt @@ -0,0 +1,315 @@ +@file:OptIn(RequiresKotlinCompilerEmbeddable::class) + +package kotlinx.benchmark.gradle + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import kotlinx.benchmark.gradle.SuiteSourceGenerator.Companion.measureAnnotationFQN +import kotlinx.benchmark.gradle.SuiteSourceGenerator.Companion.paramAnnotationFQN +import kotlinx.benchmark.gradle.SuiteSourceGenerator.Companion.setupAnnotationFQN +import kotlinx.benchmark.gradle.SuiteSourceGenerator.Companion.teardownAnnotationFQN +import kotlinx.benchmark.gradle.SuiteSourceGenerator.Companion.warmupAnnotationFQN +import kotlinx.benchmark.gradle.internal.generator.RequiresKotlinCompilerEmbeddable +import java.io.File +import java.util.* + +internal fun generateBenchmarkSourceFiles( + targetDir: File, + classDescriptors: List, +) { + classDescriptors.forEach { descriptor -> + if (descriptor.visibility == Visibility.PUBLIC && !descriptor.isAbstract) { + if (descriptor.getSpecificField(paramAnnotationFQN).isNotEmpty()) { + generateParameterizedDescriptorFile(descriptor, targetDir) + } else { + generateDescriptorFile(descriptor, targetDir) + } + } + } +} + +private fun generateDescriptorFile(descriptor: ClassAnnotationsDescriptor, androidTestDir: File) { + val descriptorName = "${descriptor.name}_Descriptor" + val packageName = descriptor.packageName + val fileSpecBuilder = FileSpec.builder(packageName, descriptorName) + .addImport("androidx.test.ext.junit.runners", "AndroidJUnit4") + .addImport("org.junit", "Test") + .addImport("org.junit", "Before") + .addImport("org.junit", "After") + .addImport("org.junit.runner", "RunWith") + .addImport("androidx.benchmark", "BenchmarkState") + .addImport("androidx.benchmark", "ExperimentalBenchmarkStateApi") + .addImport("android.util", "Log") + + if (descriptor.hasSetupOrTeardownMethods()) { + fileSpecBuilder + .addImport("org.junit", "Before") + .addImport("org.junit", "After") + } + + val typeSpecBuilder = TypeSpec.classBuilder(descriptorName) + .addAnnotation( + AnnotationSpec.builder(ClassName("org.junit.runner", "RunWith")) + .addMember("%T::class", ClassName("androidx.test.ext.junit.runners", "AndroidJUnit4")) + .build() + ) + + addBenchmarkMethods(typeSpecBuilder, descriptor) + + fileSpecBuilder.addType(typeSpecBuilder.build()) + fileSpecBuilder.build().writeTo(androidTestDir) +} + +private fun generateParameterizedDescriptorFile(descriptor: ClassAnnotationsDescriptor, androidTestDir: File) { + val descriptorName = "${descriptor.name}_Descriptor" + val packageName = descriptor.packageName + val fileSpecBuilder = FileSpec.builder(packageName, descriptorName) + .addImport("org.junit.runner", "RunWith") + .addImport("org.junit.runners", "Parameterized") + .addImport("androidx.benchmark", "BenchmarkState") + .addImport("androidx.benchmark", "ExperimentalBenchmarkStateApi") + .addImport("org.junit", "Test") + .addImport("android.util", "Log") + + if (descriptor.hasSetupOrTeardownMethods()) { + fileSpecBuilder + .addImport("org.junit", "Before") + .addImport("org.junit", "After") + } + + // Generate constructor + val constructorSpec = FunSpec.constructorBuilder() + val paramFields = descriptor.getSpecificField(paramAnnotationFQN) + paramFields.forEach { param -> + constructorSpec.addParameter(param.name, getTypeName(param.type)) + } + + val typeSpecBuilder = TypeSpec.classBuilder(descriptorName) + .addAnnotation( + AnnotationSpec.builder(ClassName("org.junit.runner", "RunWith")) + .addMember("%T::class", ClassName("org.junit.runners", "Parameterized")) + .build() + ) + .primaryConstructor(constructorSpec.build()) + .addProperties(paramFields.map { param -> + PropertySpec.builder(param.name, getTypeName(param.type)) + .initializer(param.name) + .addModifiers(KModifier.PRIVATE) + .build() + }) + + addBenchmarkMethods(typeSpecBuilder, descriptor, true) + + // Generate companion object with parameters + val companionSpec = TypeSpec.companionObjectBuilder() + .addFunction(generateParametersFunction(paramFields)) + .build() + + typeSpecBuilder.addType(companionSpec) + + fileSpecBuilder.addType(typeSpecBuilder.build()) + fileSpecBuilder.build().writeTo(androidTestDir) +} + +private fun generateParametersFunction(paramFields: List): FunSpec { + val dataFunctionBuilder = FunSpec.builder("data") + .addAnnotation(JvmStatic::class) + .returns( + ClassName("kotlin.collections", "Collection") + .parameterizedBy( + ClassName("kotlin", "Array") + .parameterizedBy(ANY) + ) + ) + + val paramNameAndIndex = paramFields.mapIndexed { index, param -> + "${param.name}={${index}}" + }.joinToString(", ") + + val paramAnnotationValue = "{index}: $paramNameAndIndex" + + dataFunctionBuilder.addAnnotation( + AnnotationSpec.builder(ClassName("org.junit.runners", "Parameterized.Parameters")) + .addMember("name = \"%L\"", paramAnnotationValue) + .build() + ) + + val paramValueLists = paramFields.map { param -> + val values = param.annotations + .find { it.name == paramAnnotationFQN } + ?.parameters?.get("value") as List<*> + + values.map { value -> + if (param.type == "java.lang.String") { + "\"\"\"$value\"\"\"" + } else { + value.toString() + } + } + } + + val cartesianProduct = cartesianProduct(paramValueLists as List>) + + val returnStatement = StringBuilder("return listOf(\n") + cartesianProduct.forEachIndexed { index, combination -> + val arrayContent = combination.joinToString(", ") + returnStatement.append(" arrayOf($arrayContent)") + if (index != cartesianProduct.size - 1) { + returnStatement.append(",\n") + } + } + returnStatement.append("\n)") + dataFunctionBuilder.addStatement(returnStatement.toString()) + + return dataFunctionBuilder.build() +} + +private fun cartesianProduct(lists: List>): List> { + if (lists.isEmpty()) return emptyList() + return lists.fold(listOf(listOf())) { acc, list -> + acc.flatMap { prefix -> list.map { value -> prefix + value } } + } +} + +private fun addBenchmarkMethods( + typeSpecBuilder: TypeSpec.Builder, + descriptor: ClassAnnotationsDescriptor, + isParameterized: Boolean = false +) { + val className = "${descriptor.packageName}.${descriptor.name}" + val propertyName = descriptor.name.decapitalize(Locale.getDefault()) + + typeSpecBuilder.addProperty( + PropertySpec.builder(propertyName, ClassName.bestGuess(className)) + .initializer("%T()", ClassName.bestGuess(className)) + .addModifiers(KModifier.PRIVATE) + .build() + ) + + // TODO: Handle methods with parameters (Blackhole) + descriptor.methods + .filter { it.visibility == Visibility.PUBLIC && it.parameters.isEmpty() } + .filterNot { method -> + method.annotations.any { annotation -> annotation.name == paramAnnotationFQN } + } + .forEach { method -> + when { + method.annotations.any { it.name == setupAnnotationFQN || it.name == teardownAnnotationFQN } -> { + generateNonMeasurableMethod(descriptor, method, propertyName, typeSpecBuilder) + } + + isParameterized && descriptor.getSpecificField(paramAnnotationFQN).isNotEmpty() -> { + generateParameterizedMeasurableMethod(descriptor, method, propertyName, typeSpecBuilder) + } + + else -> { + generateMeasurableMethod(descriptor, method, propertyName, typeSpecBuilder) + } + } + } +} + +private fun generateCommonMeasurableMethod( + descriptor: ClassAnnotationsDescriptor, + method: MethodAnnotationsDescriptor, + propertyName: String, + typeSpecBuilder: TypeSpec.Builder, + isParameterized: Boolean +) { + val measurementIterations = descriptor.annotations + .find { it.name == measureAnnotationFQN } + ?.parameters?.get("iterations") as? Int ?: 5 + val warmupIterations = descriptor.annotations + .find { it.name == warmupAnnotationFQN } + ?.parameters?.get("iterations") as? Int ?: 5 + + val methodSpecBuilder = FunSpec.builder("benchmark_${descriptor.name}_${method.name}") + .addAnnotation(ClassName("org.junit", "Test")) + .addAnnotation( + AnnotationSpec.builder(ClassName("kotlin", "OptIn")) + .addMember("%T::class", ClassName("androidx.benchmark", "ExperimentalBenchmarkStateApi")) + .build() + ) + + if (isParameterized) { + descriptor.getSpecificField(paramAnnotationFQN).forEach { field -> + methodSpecBuilder.addStatement("$propertyName.${field.name} = ${field.name}") + } + } + + methodSpecBuilder + .addStatement( + "val state = %T(repeatCount = ${warmupIterations + measurementIterations})", + ClassName("androidx.benchmark", "BenchmarkState") + ) + .beginControlFlow("while (state.keepRunning())") + .addStatement("$propertyName.${method.name}()") + .endControlFlow() + .addStatement("val measurementResult = state.getMeasurementTimeNs()") + .beginControlFlow("measurementResult.forEachIndexed { index, time ->") + .beginControlFlow("if (index < $warmupIterations)") + .addStatement("Log.d(\"KotlinBenchmark\", \"Warmup \${index + 1}: \$time ns\")") + .nextControlFlow("else") + .addStatement("Log.d(\"KotlinBenchmark\", \"Iteration \${index - $warmupIterations + 1}: \$time ns\")") + .endControlFlow() + .endControlFlow() + + typeSpecBuilder.addFunction(methodSpecBuilder.build()) +} + +private fun generateParameterizedMeasurableMethod( + descriptor: ClassAnnotationsDescriptor, + method: MethodAnnotationsDescriptor, + propertyName: String, + typeSpecBuilder: TypeSpec.Builder +) { + generateCommonMeasurableMethod(descriptor, method, propertyName, typeSpecBuilder, isParameterized = true) +} + +private fun generateMeasurableMethod( + descriptor: ClassAnnotationsDescriptor, + method: MethodAnnotationsDescriptor, + propertyName: String, + typeSpecBuilder: TypeSpec.Builder +) { + generateCommonMeasurableMethod(descriptor, method, propertyName, typeSpecBuilder, isParameterized = false) +} + + +private fun generateNonMeasurableMethod( + descriptor: ClassAnnotationsDescriptor, + method: MethodAnnotationsDescriptor, + propertyName: String, + typeSpecBuilder: TypeSpec.Builder +) { + when (method.annotations.first().name) { + setupAnnotationFQN -> { + val methodSpecBuilder = FunSpec.builder("benchmark_${descriptor.name}_setUp") + .addAnnotation(ClassName("org.junit", "Before")) + .addStatement("$propertyName.${method.name}()") + typeSpecBuilder.addFunction(methodSpecBuilder.build()) + } + + teardownAnnotationFQN -> { + val methodSpecBuilder = FunSpec.builder("benchmark_${descriptor.name}_tearDown") + .addAnnotation(ClassName("org.junit", "After")) + .addStatement("$propertyName.${method.name}()") + typeSpecBuilder.addFunction(methodSpecBuilder.build()) + } + } +} + +private fun getTypeName(type: String): TypeName { + return when (type) { + "int" -> Int::class.asTypeName() + "long" -> Long::class.asTypeName() + "boolean" -> Boolean::class.asTypeName() + "float" -> Float::class.asTypeName() + "double" -> Double::class.asTypeName() + "char" -> Char::class.asTypeName() + "byte" -> Byte::class.asTypeName() + "short" -> Short::class.asTypeName() + "java.lang.String" -> String::class.asTypeName() + else -> ClassName.bestGuess(type) + } +} \ No newline at end of file diff --git a/plugin/main/src/kotlinx/benchmark/gradle/AndroidTasks.kt b/plugin/main/src/kotlinx/benchmark/gradle/AndroidTasks.kt new file mode 100644 index 00000000..20f0eacb --- /dev/null +++ b/plugin/main/src/kotlinx/benchmark/gradle/AndroidTasks.kt @@ -0,0 +1,49 @@ +package kotlinx.benchmark.gradle + +import org.gradle.api.* +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation +import java.io.File +import java.util.jar.JarFile + +internal fun Project.getAarFile(compilation: KotlinJvmAndroidCompilation): File { + return File("${project.projectDir}/build/outputs/aar/${project.name}-${compilation.name}.aar") +} + +internal fun Project.getUnpackAarDir(compilation: KotlinJvmAndroidCompilation): File { + return File("${project.projectDir}/build/outputs/unpacked-aar/${compilation.name}") +} + +internal fun Project.unpackAarFile(aarFile: File, compilation: KotlinJvmAndroidCompilation): File { + val unpackedDir = getUnpackAarDir(compilation) + project.sync { + it.from(project.zipTree(aarFile)) + it.into(unpackedDir) + } + // unpack classes.jar + val classesJar = File(unpackedDir, "classes.jar") + if (classesJar.exists()) { + val unpackedClassesDir = File(unpackedDir, "classes") + project.copy { + it.from(project.zipTree(classesJar)) + it.into(unpackedClassesDir) + } + } + return unpackedDir +} + +internal fun processClassesJar( + unpackedDir: File, + compilation: KotlinJvmAndroidCompilation, + onProcessed: (List) -> Unit) { + val classesJar = File(unpackedDir, "classes.jar") + if (classesJar.exists()) { + println("Processing classes.jar for ${compilation.name}") + val jar = JarFile(classesJar) + val annotationProcessor = AnnotationProcessor() + annotationProcessor.processJarFile(jar) + jar.close() + onProcessed(annotationProcessor.getClassDescriptors()) + } else { + println("classes.jar not found in AAR file") + } +} \ No newline at end of file diff --git a/plugin/main/src/kotlinx/benchmark/gradle/AnnotationProcessor.kt b/plugin/main/src/kotlinx/benchmark/gradle/AnnotationProcessor.kt new file mode 100644 index 00000000..82f7ccfe --- /dev/null +++ b/plugin/main/src/kotlinx/benchmark/gradle/AnnotationProcessor.kt @@ -0,0 +1,270 @@ +package kotlinx.benchmark.gradle + +import kotlinx.benchmark.gradle.SuiteSourceGenerator.Companion.setupAnnotationFQN +import kotlinx.benchmark.gradle.SuiteSourceGenerator.Companion.teardownAnnotationFQN +import kotlinx.benchmark.gradle.internal.generator.RequiresKotlinCompilerEmbeddable +import org.jetbrains.org.objectweb.asm.* +import org.jetbrains.org.objectweb.asm.tree.* +import java.util.* +import java.util.jar.* + +data class AnnotationData( + val name: String, + val parameters: Map +) + +data class ClassAnnotationsDescriptor( + val packageName: String, + val name: String, + val visibility: Visibility, + val isAbstract: Boolean, + val annotations: List, + val methods: List, + val fields: List +) + +data class MethodAnnotationsDescriptor( + val name: String, + val visibility: Visibility, + val annotations: List, + val parameters: List +) + +data class FieldAnnotationsDescriptor( + val name: String, + val visibility: Visibility, + val annotations: List, + val type: String +) + +enum class Visibility { + PUBLIC, PROTECTED, PRIVATE, PACKAGE_PRIVATE +} + +class AnnotationProcessor { + + private val classAnnotationsDescriptors = mutableListOf() + + fun processJarFile(jarFile: JarFile) { + val entries = jarFile.entries() + + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (entry.name.endsWith(".class")) { + val inputStream = jarFile.getInputStream(entry) + val classBytes = inputStream.readBytes() + processClassBytes(classBytes) + } + } + } + + private fun processClassBytes(classBytes: ByteArray) { + val classReader = ClassReader(classBytes) + val classNode = ClassNode() + classReader.accept(classNode, 0) + + val classAnnotations = classNode.visibleAnnotations + ?.filter { it.desc != "Lkotlin/Metadata;" } + ?.map { parseAnnotation(it) } + ?: emptyList() + + val methodAnnotationsMap: Map> = classNode.methods + .filterNot { it.name == "" } + .filter { methodNode -> + methodNode.name.matches(Regex("^get[A-Z].*\\\$annotations\$")) && + methodNode.visibleAnnotations?.isNotEmpty() == true + } + .associate { methodNode -> + val fieldName = methodNode.name + .removePrefix("get") + .removeSuffix("\$annotations") + .decapitalize(Locale.ROOT) + + val methodAnnotations = methodNode.visibleAnnotations + ?.filter { it.desc != "Lkotlin/Metadata;" } + ?.map { parseAnnotation(it) } + ?: emptyList() + + fieldName to methodAnnotations + } + + val methodDescriptors = classNode.methods + .filter { methodNode -> + methodNode.visibleAnnotations?.any { it.desc != "Lkotlin/Metadata;" } == true + } + .filterNot { it.name == "" } + .filterNot { it.name.matches(Regex("^get[A-Z].*\\\$annotations\$")) } + .map { methodNode -> + val methodAnnotations = methodNode.visibleAnnotations + ?.filter { it.desc != "Lkotlin/Metadata;" } + ?.map { parseAnnotation(it) } + ?: emptyList() + val parameters = Type.getArgumentTypes(methodNode.desc).map { it.className } + MethodAnnotationsDescriptor( + methodNode.name, + getVisibility(methodNode.access), + methodAnnotations, + parameters + ) + } + + val fieldDescriptors = classNode.fields.map { fieldNode -> + val fieldAnnotations = methodAnnotationsMap.getOrDefault(fieldNode.name, emptyList()) + val fieldType = Type.getType(fieldNode.desc).className + + FieldAnnotationsDescriptor( + fieldNode.name, + getFieldVisibility(classNode, fieldNode), + fieldAnnotations, + fieldType + ) + } + + val packageName = classNode.name.substringBeforeLast('/', "").replace('/', '.') + val className = classNode.name.substringAfterLast('/') + val classDescriptor = ClassAnnotationsDescriptor( + packageName, + className, + getVisibility(classNode.access), + isAbstract(classNode.access), + classAnnotations, + methodDescriptors, + fieldDescriptors + ) + + classAnnotationsDescriptors.add(classDescriptor) + println("Class: ${classDescriptor.name}, Annotations: ${classDescriptor.annotations}") + classDescriptor.methods.forEach { method -> + println("Method: ${method.name}, Annotations: ${method.annotations},Method Parameters: ${method.parameters}") + } + classDescriptor.fields.forEach { field -> + println("Field: ${field.name}, Visibility: ${field.visibility}, Annotations: ${field.annotations}, Type: ${field.type}") + } + } + + private fun parseAnnotation(annotationNode: AnnotationNode): AnnotationData { + val parameters = mutableMapOf() + annotationNode.values?.let { values -> + for (i in values.indices step 2) { + val name = values[i] as String + val value = values[i + 1] + parameters[name] = formatAnnotationValue(value) + } + } + return AnnotationData(formatDescriptor(annotationNode.desc), parameters) + } + + private fun formatAnnotationValue(value: Any?): Any? { + return when (value) { + is List<*> -> value.flatMap { + val formattedValue = formatAnnotationValue(it) + if (formattedValue is List<*>) { + formattedValue + } else { + listOf(formattedValue) + } + } + is Array<*> -> value.flatMap { + val formattedValue = formatAnnotationValue(it) + if (formattedValue is List<*>) { + formattedValue + } else { + listOf(formattedValue) + } + } + is TypePath -> value.toString() + is AnnotationNode -> formatAnnotationNode(value) + is Type -> value.className.replace('/', '.') + is String -> { + if (value.startsWith("L") && value.endsWith(";")) { + formatDescriptor(value) + } else { + value + } + } + is ByteArray -> value.toList() + is CharArray -> value.map { it.toString() } + is ShortArray -> value.toList() + is IntArray -> value.toList() + is LongArray -> value.toList() + is FloatArray -> value.toList() + is DoubleArray -> value.toList() + is BooleanArray -> value.toList() + else -> value + } + } + + private fun formatAnnotationNode(annotationNode: AnnotationNode): String { + val sb = StringBuilder("@${formatDescriptor(annotationNode.desc)}(") + annotationNode.values?.let { values -> + for (i in values.indices step 2) { + val name = values[i] + val value = values[i + 1] + sb.append("$name = ${formatAnnotationValue(value)}, ") + } + } + if (sb.endsWith(", ")) { + sb.setLength(sb.length - 2) + } + sb.append(")") + return sb.toString() + } + + private fun formatDescriptor(descriptor: String): String { + return descriptor.removePrefix("L").removeSuffix(";").replace('/', '.') + } + + private fun getVisibility(access: Int): Visibility { + return when { + (access and Opcodes.ACC_PUBLIC) != 0 -> Visibility.PUBLIC + (access and Opcodes.ACC_PROTECTED) != 0 -> Visibility.PROTECTED + (access and Opcodes.ACC_PRIVATE) != 0 -> Visibility.PRIVATE + else -> Visibility.PACKAGE_PRIVATE + } + } + + private fun getFieldVisibility(classNode: ClassNode, fieldNode: FieldNode): Visibility { + val getterName = "get${fieldNode.name.capitalize(Locale.ROOT)}" + val setterName = "set${fieldNode.name.capitalize(Locale.ROOT)}" + + val getterMethod = classNode.methods.find { it.name == getterName } + val setterMethod = classNode.methods.find { it.name == setterName } + + val getterVisibility = getterMethod?.let { getVisibility(it.access) } + val setterVisibility = setterMethod?.let { getVisibility(it.access) } + + return when { + getterVisibility != null && setterVisibility != null -> { + if (getterVisibility == setterVisibility) { + getterVisibility + } else { + getVisibility(fieldNode.access) + } + } + getterVisibility != null -> getterVisibility + setterVisibility != null -> setterVisibility + else -> getVisibility(fieldNode.access) + } + } + + private fun isAbstract(access: Int): Boolean { + return (access and Opcodes.ACC_ABSTRACT) != 0 + } + + fun getClassDescriptors(): List { + return classAnnotationsDescriptors + } +} + +internal fun ClassAnnotationsDescriptor.getSpecificField(annotationName: String): List { + return fields.filter { field -> + field.annotations.any { it.name == annotationName } + } +} + +@OptIn(RequiresKotlinCompilerEmbeddable::class) +internal fun ClassAnnotationsDescriptor.hasSetupOrTeardownMethods(): Boolean { + return methods.any { method -> + method.annotations.any { it.name == setupAnnotationFQN || it.name == teardownAnnotationFQN } + } +} \ No newline at end of file diff --git a/plugin/main/src/kotlinx/benchmark/gradle/BenchmarkConfiguration.kt b/plugin/main/src/kotlinx/benchmark/gradle/BenchmarkConfiguration.kt index 8eee9e32..a0315cff 100644 --- a/plugin/main/src/kotlinx/benchmark/gradle/BenchmarkConfiguration.kt +++ b/plugin/main/src/kotlinx/benchmark/gradle/BenchmarkConfiguration.kt @@ -1,11 +1,14 @@ package kotlinx.benchmark.gradle import kotlinx.benchmark.gradle.internal.KotlinxBenchmarkPluginInternalApi +import org.gradle.api.provider.Property import org.gradle.api.tasks.* +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrCompilation +import java.util.* open class BenchmarkConfiguration @KotlinxBenchmarkPluginInternalApi @@ -45,13 +48,13 @@ constructor( } @KotlinxBenchmarkPluginInternalApi - fun capitalizedName() = if (name == "main") "" else name.capitalize() + fun capitalizedName() = if (name == "main") "" else name.capitalize(Locale.ROOT) @KotlinxBenchmarkPluginInternalApi - fun prefixName(suffix: String) = if (name == "main") suffix else name + suffix.capitalize() + fun prefixName(suffix: String) = if (name == "main") suffix else name + suffix.capitalize(Locale.ROOT) @KotlinxBenchmarkPluginInternalApi - fun reportFileExt(): String = reportFormat?.toLowerCase() ?: "json" + fun reportFileExt(): String = reportFormat?.toLowerCase(Locale.ROOT) ?: "json" } open class BenchmarkTarget @@ -126,3 +129,15 @@ constructor( ) : BenchmarkTarget(extension, name) { var buildType: NativeBuildType = NativeBuildType.RELEASE } + +class AndroidBenchmarkTarget +@KotlinxBenchmarkPluginInternalApi +constructor( + extension: BenchmarksExtension, + name: String, + val target: KotlinAndroidTarget, +) : BenchmarkTarget(extension, name) { + val sdkDir: Property = extension.project.objects.property(String::class.java).convention( + System.getenv("ANDROID_HOME") ?: "" + ) +} \ No newline at end of file diff --git a/plugin/main/src/kotlinx/benchmark/gradle/BenchmarksExtension.kt b/plugin/main/src/kotlinx/benchmark/gradle/BenchmarksExtension.kt index 5c95e77d..6429c116 100644 --- a/plugin/main/src/kotlinx/benchmark/gradle/BenchmarksExtension.kt +++ b/plugin/main/src/kotlinx/benchmark/gradle/BenchmarksExtension.kt @@ -8,9 +8,7 @@ import org.gradle.api.provider.* import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.* import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrCompilation fun Project.benchmark(configure: Action) { @@ -63,10 +61,17 @@ constructor( when { multiplatform != null -> { val target = multiplatform.targets.findByName(name) + println("target: $target") + if (target is KotlinAndroidTarget) { + return@container AndroidBenchmarkTarget(this, name, target) + } + target?.compilations?.findByName(KotlinCompilation.MAIN_COMPILATION_NAME) + .let { println("compilation: $it") } // We allow the name to be either a target or a source set when (val compilation = target?.compilations?.findByName(KotlinCompilation.MAIN_COMPILATION_NAME) ?: multiplatform.targets.flatMap { it.compilations } - .find { it.defaultSourceSet.name == name }) { + .find { it.defaultSourceSet.name == name } + ) { null -> { project.logger.warn("Warning: Cannot find a benchmark compilation '$name', ignoring.") BenchmarkTarget(this, name) // ignore diff --git a/plugin/main/src/kotlinx/benchmark/gradle/BenchmarksPlugin.kt b/plugin/main/src/kotlinx/benchmark/gradle/BenchmarksPlugin.kt index 7d921cc1..594c5aef 100644 --- a/plugin/main/src/kotlinx/benchmark/gradle/BenchmarksPlugin.kt +++ b/plugin/main/src/kotlinx/benchmark/gradle/BenchmarksPlugin.kt @@ -118,6 +118,16 @@ constructor( is JsBenchmarkTarget -> processJsCompilation(config) is WasmBenchmarkTarget -> processWasmCompilation(config) is NativeBenchmarkTarget -> processNativeCompilation(config) + // is AndroidBenchmarkTarget -> processAndroidCompilation(config) + is AndroidBenchmarkTarget -> { + println("processConfigurations: AndroidBenchmarkTarget") + config.target.compilations.all { compilation -> + // This block is called for each compilation when they are materialized + if (compilation.compilationName == "release") { + processAndroidCompilation(config, compilation) + } + } + } } } } diff --git a/plugin/main/src/kotlinx/benchmark/gradle/JvmJavaTasks.kt b/plugin/main/src/kotlinx/benchmark/gradle/JvmJavaTasks.kt index 8124f52d..e24e4b6d 100644 --- a/plugin/main/src/kotlinx/benchmark/gradle/JvmJavaTasks.kt +++ b/plugin/main/src/kotlinx/benchmark/gradle/JvmJavaTasks.kt @@ -2,6 +2,7 @@ package kotlinx.benchmark.gradle import kotlinx.benchmark.gradle.internal.KotlinxBenchmarkPluginInternalApi import org.gradle.api.* +import java.util.* @KotlinxBenchmarkPluginInternalApi fun Project.processJavaSourceSet(target: JavaBenchmarkTarget) { @@ -43,7 +44,7 @@ private fun Project.configureJmhDependency(target: JavaBenchmarkTarget) { val dependencyConfiguration = if (target.name == "main") configurationRoot else - "${target.name}${configurationRoot.capitalize()}" + "${target.name}${configurationRoot.capitalize(Locale.ROOT)}" dependencies.add(dependencyConfiguration, jmhCore) } diff --git a/runtime/androidMain/src/kotlinx.benchmark/AndroidBenchmarkAnnotations.kt b/runtime/androidMain/src/kotlinx.benchmark/AndroidBenchmarkAnnotations.kt new file mode 100644 index 00000000..2c17e109 --- /dev/null +++ b/runtime/androidMain/src/kotlinx.benchmark/AndroidBenchmarkAnnotations.kt @@ -0,0 +1,50 @@ +package kotlinx.benchmark + +actual enum class Scope { + Benchmark +} + +@Target(AnnotationTarget.CLASS) +actual annotation class State(actual val value: Scope) + +@Target(AnnotationTarget.FUNCTION) +actual annotation class Setup + +@Target(AnnotationTarget.FUNCTION) +actual annotation class TearDown + +@Target(AnnotationTarget.FUNCTION) +actual annotation class Benchmark + + +@Target(AnnotationTarget.CLASS) +actual annotation class BenchmarkMode(actual vararg val value: Mode) + +actual enum class Mode { + Throughput, AverageTime +} + +@Target(AnnotationTarget.CLASS) +actual annotation class OutputTimeUnit(actual val value: BenchmarkTimeUnit) + +actual enum class BenchmarkTimeUnit { + NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES +} + +@Target(AnnotationTarget.CLASS) +actual annotation class Warmup( + actual val iterations: Int, + actual val time: Int, + actual val timeUnit: BenchmarkTimeUnit, + actual val batchSize: Int +) + +@Target(AnnotationTarget.CLASS) +actual annotation class Measurement( + actual val iterations: Int, + actual val time: Int, + actual val timeUnit: BenchmarkTimeUnit, + actual val batchSize: Int +) + +actual annotation class Param(actual vararg val value: String) \ No newline at end of file diff --git a/runtime/androidMain/src/kotlinx.benchmark/Utils.kt b/runtime/androidMain/src/kotlinx.benchmark/Utils.kt new file mode 100644 index 00000000..7a3f3e68 --- /dev/null +++ b/runtime/androidMain/src/kotlinx.benchmark/Utils.kt @@ -0,0 +1,14 @@ +package kotlinx.benchmark + + +internal actual fun Double.format(precision: Int, useGrouping: Boolean): String = + error("Not implemented") + +internal actual fun String.writeFile(text: String): Unit = + error("Not implemented") + +internal actual fun String.readFile(): String = + error("Not implemented") + +internal actual inline fun measureNanoseconds(block: () -> Unit): Long = + error("Not implemented") \ No newline at end of file diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index f26567a5..8bc11165 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -1,14 +1,38 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile import java.util.* plugins { alias(libs.plugins.kotlin.multiplatform) + id("com.android.library") } repositories { mavenCentral() + google() +} + +android { + compileSdk = 34 + namespace = "org.jetbrains.kotlinx.examples" + + defaultConfig { + minSdk = 29 + targetSdk = 34 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } } kotlin { @@ -47,12 +71,15 @@ kotlin { @OptIn(ExperimentalWasmDsl::class) wasmJs { d8() } + androidTarget {} + @OptIn(ExperimentalKotlinGradlePluginApi::class) applyDefaultHierarchyTemplate { common { group("jsWasmJsShared") { withJs() withWasmJs() + withAndroidTarget() } } } @@ -125,3 +152,14 @@ tasks.withType(KotlinNativeCompile::class).configureEach { "-opt-in=kotlinx.cinterop.ExperimentalForeignApi", ) } + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } +} \ No newline at end of file diff --git a/runtime/jsWasmJsSharedMain/src/kotlinx/benchmark/CommonBlackhole.kt b/runtime/jsWasmJsSharedMain/src/kotlinx/benchmark/Blackhole.kt similarity index 100% rename from runtime/jsWasmJsSharedMain/src/kotlinx/benchmark/CommonBlackhole.kt rename to runtime/jsWasmJsSharedMain/src/kotlinx/benchmark/Blackhole.kt