Skip to content

Commit

Permalink
Use Worker API to generate benchmark code
Browse files Browse the repository at this point in the history
- Update the Benchmark JS/Wasm/Native source generation tasks to use the Worker API, so that `kotlin-compiler-embeddable` can be a compileOnly dependency, thus resolving https://youtrack.jetbrains.com/issue/KT-66764
- Create `GenerateJsSourceWorker` and `GenerateWasmSourceWorker` for isolating the JS and Wasm generation code.
- Updated the Js/Wasm/Native generation tasks to invoke the source generators in a Worker isolated classpath.
- Created a new Configuration for declaring `kotlin-compiler-embeddable`, and pass it to the Js/Wasm/Native generation tasks.
- Create a BenchmarkDependencies utility for defining all Configurations used in the plugin. (This also lays the groundwork for unifying the JMH version #147 (comment))
- Updated BenchmarksPluginConstants to add `DEFAULT_KOTLIN_COMPILER_VERSION`, as sensible default for kotlin-compiler-embeddable.
- Add opt-in annotation RequiresKotlinCompilerEmbeddable to highlight code that requires `kotlin-compiler-embeddable`
- Some classes were updated to be `abstract`, to follow Gradle best practices for creating managed objects.

This commit also contains formatting changes, as the code style is not consistent.
  • Loading branch information
adam-enko committed Jul 4, 2024
1 parent 1aefc62 commit 279d309
Show file tree
Hide file tree
Showing 15 changed files with 509 additions and 145 deletions.
4 changes: 4 additions & 0 deletions plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,17 @@ def generatePluginConstants = tasks.register("generatePluginConstants") {
Provider<String> minSupportedGradleVersion = libs.versions.minSupportedGradle
inputs.property("minSupportedGradleVersion", minSupportedGradleVersion)

Provider<String> kotlinCompilerVersion = libs.versions.kotlin
inputs.property("kotlinCompilerVersion", kotlinCompilerVersion)

doLast {
constantsKtFile.write(
"""|package kotlinx.benchmark.gradle.internal
|
|internal object BenchmarksPluginConstants {
| const val BENCHMARK_PLUGIN_VERSION = "${benchmarkPluginVersion.get()}"
| const val MIN_SUPPORTED_GRADLE_VERSION = "${minSupportedGradleVersion.get()}"
| const val DEFAULT_KOTLIN_COMPILER_VERSION = "${kotlinCompilerVersion.get()}"
|}
|""".stripMargin()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kotlinx.benchmark.gradle

import kotlinx.benchmark.gradle.SuiteSourceGenerator.Companion.paramAnnotationFQN
import kotlinx.benchmark.gradle.internal.generator.RequiresKotlinCompilerEmbeddable
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.builtins.UnsignedTypes
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
Expand All @@ -10,6 +11,7 @@ import org.jetbrains.kotlin.js.descriptorUtils.getKotlinTypeFqName
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.annotations.argumentValue

@RequiresKotlinCompilerEmbeddable
internal fun validateBenchmarkFunctions(functions: List<FunctionDescriptor>) {
functions.forEach { function ->
if (function.visibility != DescriptorVisibilities.PUBLIC) {
Expand All @@ -30,6 +32,7 @@ internal fun validateBenchmarkFunctions(functions: List<FunctionDescriptor>) {
}
}

@RequiresKotlinCompilerEmbeddable
internal fun validateSetupFunctions(functions: List<FunctionDescriptor>) {
functions.forEach { function ->
if (function.visibility != DescriptorVisibilities.PUBLIC) {
Expand All @@ -44,6 +47,7 @@ internal fun validateSetupFunctions(functions: List<FunctionDescriptor>) {
}
}

@RequiresKotlinCompilerEmbeddable
internal fun validateTeardownFunctions(functions: List<FunctionDescriptor>) {
functions.forEach { function ->
if (function.visibility != DescriptorVisibilities.PUBLIC) {
Expand All @@ -58,6 +62,7 @@ internal fun validateTeardownFunctions(functions: List<FunctionDescriptor>) {
}
}

@RequiresKotlinCompilerEmbeddable
internal fun validateParameterProperties(properties: List<PropertyDescriptor>) {
properties.forEach { property ->
if (!property.isVar) {
Expand All @@ -81,4 +86,4 @@ internal fun validateParameterProperties(properties: List<PropertyDescriptor>) {
error("@Param annotation should have at least one argument. The annotation on property `${property.name}` has no arguments.")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import groovy.lang.Closure
import kotlinx.benchmark.gradle.internal.KotlinxBenchmarkPluginInternalApi
import org.gradle.api.*
import org.gradle.api.plugins.*
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
Expand All @@ -16,7 +17,7 @@ fun Project.benchmark(configure: Action<BenchmarksExtension>) {
extensions.configure(BenchmarksExtension::class.java, configure)
}

open class BenchmarksExtension
abstract class BenchmarksExtension
@KotlinxBenchmarkPluginInternalApi
constructor(
val project: Project
Expand All @@ -28,6 +29,8 @@ constructor(

val version = BenchmarksPlugin.PLUGIN_VERSION

abstract val kotlinCompilerVersion: Property<String>

fun configurations(configureClosure: Closure<NamedDomainObjectContainer<BenchmarkConfiguration>>): NamedDomainObjectContainer<BenchmarkConfiguration> {
return configurations.configure(configureClosure)
}
Expand All @@ -46,7 +49,8 @@ constructor(

val targets: NamedDomainObjectContainer<BenchmarkTarget> = run {
project.container(BenchmarkTarget::class.java) { name ->
val multiplatformClass = tryGetClass<KotlinMultiplatformExtension>("org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension")
val multiplatformClass =
tryGetClass<KotlinMultiplatformExtension>("org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension")
val multiplatform = multiplatformClass?.let { project.extensions.findByType(it) }
val javaExtension = project.extensions.findByType(JavaPluginExtension::class.java)

Expand Down
41 changes: 37 additions & 4 deletions plugin/main/src/kotlinx/benchmark/gradle/BenchmarksPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package kotlinx.benchmark.gradle

import kotlinx.benchmark.gradle.internal.BenchmarkDependencies
import kotlinx.benchmark.gradle.internal.BenchmarksPluginConstants
import kotlinx.benchmark.gradle.internal.BenchmarksPluginConstants.DEFAULT_KOTLIN_COMPILER_VERSION
import kotlinx.benchmark.gradle.internal.BenchmarksPluginConstants.MIN_SUPPORTED_GRADLE_VERSION
import kotlinx.benchmark.gradle.internal.KotlinxBenchmarkPluginInternalApi
import org.gradle.api.*
import org.gradle.api.provider.*
import org.gradle.util.GradleVersion
import javax.inject.Inject

@Suppress("unused")
abstract class BenchmarksPlugin
@KotlinxBenchmarkPluginInternalApi
constructor() : Plugin<Project> {
@Inject
constructor(
private val providers: ProviderFactory,
) : Plugin<Project> {

companion object {
const val PLUGIN_ID = "org.jetbrains.kotlinx.benchmark"
Expand Down Expand Up @@ -50,7 +57,7 @@ constructor() : Plugin<Project> {

override fun apply(project: Project) = project.run {
// DO NOT use properties of an extension immediately, it will not contain any user-specified data
val extension = extensions.create(BENCHMARK_EXTENSION_NAME, BenchmarksExtension::class.java, project)
val extension = createBenchmarksExtension(project)

if (GradleVersion.current() < GradleVersion.version(MIN_SUPPORTED_GRADLE_VERSION)) {
logger.error("JetBrains Gradle Benchmarks plugin requires Gradle version $MIN_SUPPORTED_GRADLE_VERSION or higher")
Expand All @@ -62,19 +69,24 @@ constructor() : Plugin<Project> {
if (!getKotlinVersion(kotlinPlugin.pluginVersion).isAtLeast(1, 9, 20)) {
logger.error("JetBrains Gradle Benchmarks plugin requires Kotlin version 1.9.20 or higher")
}
extension.kotlinCompilerVersion.set(kotlinPlugin.pluginVersion)
}

// Create empty task that will depend on all benchmark building tasks to build all benchmarks in a project
// Create a lifecycle task that will depend on all benchmark building tasks to build all benchmarks in a project
val assembleBenchmarks = task<DefaultTask>(ASSEMBLE_BENCHMARKS_TASKNAME) {
group = BENCHMARKS_TASK_GROUP
description = "Generate and build all benchmarks in a project"
}

val benchmarkDependencies = BenchmarkDependencies(project, extension)

configureBenchmarkTaskConventions(project, benchmarkDependencies)

// TODO: Design configuration avoidance
// I currently don't know how to do it correctly yet, so materialize all tasks after project evaluation.
afterEvaluate {
extension.configurations.forEach {
// Create empty task that will depend on all benchmark execution tasks to run all benchmarks in a project
// Create a lifecycle task that will depend on all benchmark execution tasks to run all benchmarks in a project
task<DefaultTask>(it.prefixName(RUN_BENCHMARKS_TASKNAME)) {
group = BENCHMARKS_TASK_GROUP
description = "Execute all benchmarks in a project"
Expand All @@ -92,6 +104,12 @@ constructor() : Plugin<Project> {
}
}

private fun createBenchmarksExtension(project: Project): BenchmarksExtension {
return project.extensions.create(BENCHMARK_EXTENSION_NAME, BenchmarksExtension::class.java, project).apply {
kotlinCompilerVersion.convention(DEFAULT_KOTLIN_COMPILER_VERSION)
}
}

private fun Project.processConfigurations(extension: BenchmarksExtension) {
// Calling `all` on NDOC causes all items to materialize and be configured
extension.targets.all { config ->
Expand All @@ -104,6 +122,21 @@ constructor() : Plugin<Project> {
}
}
}

private fun configureBenchmarkTaskConventions(
project: Project,
benchmarkDependencies: BenchmarkDependencies,
) {
project.tasks.withType(NativeSourceGeneratorTask::class.java).configureEach {
it.runtimeClasspath.from(benchmarkDependencies.benchmarkGeneratorResolver)
}
project.tasks.withType(WasmSourceGeneratorTask::class.java).configureEach {
it.runtimeClasspath.from(benchmarkDependencies.benchmarkGeneratorResolver)
}
project.tasks.withType(JsSourceGeneratorTask::class.java).configureEach {
it.runtimeClasspath.from(benchmarkDependencies.benchmarkGeneratorResolver)
}
}
}

private fun getKotlinVersion(kotlinVersion: String): KotlinVersion {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,43 @@
package kotlinx.benchmark.gradle

import kotlinx.benchmark.gradle.internal.KotlinxBenchmarkPluginInternalApi
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.*
import org.gradle.api.logging.*
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.generators.core.*
import org.openjdk.jmh.generators.reflection.*
import org.openjdk.jmh.util.*
import java.io.*
import java.net.*
import java.util.*
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.generators.core.BenchmarkGenerator
import org.openjdk.jmh.generators.core.FileSystemDestination
import org.openjdk.jmh.generators.reflection.RFGeneratorSource
import org.openjdk.jmh.util.FileUtils
import java.io.File
import java.net.URL
import java.net.URLClassLoader

@KotlinxBenchmarkPluginInternalApi
// TODO Make visibility of JmhBytecodeGeneratorWorker `internal` in version 1.0.
// Move to package kotlinx.benchmark.gradle.internal.generator.workers, alongside the other workers.
abstract class JmhBytecodeGeneratorWorker : WorkAction<JmhBytecodeGeneratorWorkParameters> {

@KotlinxBenchmarkPluginInternalApi
companion object {
// TODO in version 1.0 replace JmhBytecodeGeneratorWorkParameters with this interface:
//internal interface Parameters : WorkParameters {
// val inputClasses: ConfigurableFileCollection
// val inputClasspath: ConfigurableFileCollection
// val outputSourceDirectory: DirectoryProperty
// val outputResourceDirectory: DirectoryProperty
//}

internal companion object {
private const val classSuffix = ".class"
private val logger = Logging.getLogger(JmhBytecodeGeneratorWorker::class.java)
}

private val outputSourceDirectory: File get() = parameters.outputSourceDirectory.get().asFile
private val outputResourceDirectory: File get() = parameters.outputResourceDirectory.get().asFile

override fun execute() {
cleanup(outputSourceDirectory)
cleanup(outputResourceDirectory)
outputSourceDirectory.deleteRecursively()
outputResourceDirectory.deleteRecursively()

val urls = (parameters.inputClasses + parameters.inputClasspath).map { it.toURI().toURL() }.toTypedArray()

Expand All @@ -58,13 +69,13 @@ abstract class JmhBytecodeGeneratorWorker : WorkAction<JmhBytecodeGeneratorWorkP
// inside JMH bytecode gen. This hack seem to work, but we need to understand
val introspectionClassLoader = URLClassLoader(urls, benchmarkAnnotation.classLoader)

/*
println("Original_Parent_ParentCL: ${originalClassLoader.parent.parent}")
println("Original_ParentCL: ${originalClassLoader.parent}")
println("OriginalCL: $originalClassLoader")
println("IntrospectCL: $introspectionClassLoader")
println("BenchmarkCL: ${benchmarkAnnotation.classLoader}")
*/
/*
println("Original_Parent_ParentCL: ${originalClassLoader.parent.parent}")
println("Original_ParentCL: ${originalClassLoader.parent}")
println("OriginalCL: $originalClassLoader")
println("IntrospectCL: $introspectionClassLoader")
println("BenchmarkCL: ${benchmarkAnnotation.classLoader}")
*/

try {
currentThread.contextClassLoader = introspectionClassLoader
Expand Down Expand Up @@ -97,7 +108,7 @@ abstract class JmhBytecodeGeneratorWorker : WorkAction<JmhBytecodeGeneratorWorkP
}
}

println("Writing out Java source to $outputSourceDirectory and resources to $outputResourceDirectory")
logger.lifecycle("Writing out Java source to $outputSourceDirectory and resources to $outputResourceDirectory")
val gen = BenchmarkGenerator()
gen.generate(source, destination)
gen.complete(source, destination)
Expand All @@ -109,12 +120,15 @@ abstract class JmhBytecodeGeneratorWorker : WorkAction<JmhBytecodeGeneratorWorkP
errCount++
sb.append(" - ").append(e.toString()).append("\n")
}
throw RuntimeException("Generation of JMH bytecode failed with " + errCount + " errors:\n" + sb)
throw RuntimeException("Generation of JMH bytecode failed with $errCount errors:\n$sb")
}
}
}

@KotlinxBenchmarkPluginInternalApi
// TODO In version 1.0:
// - Make internal
// - Move to a nested interface inside of JmhBytecodeGeneratorWorker (like the other workers)
interface JmhBytecodeGeneratorWorkParameters : WorkParameters {
val inputClasses: ConfigurableFileCollection
val inputClasspath: ConfigurableFileCollection
Expand Down
51 changes: 22 additions & 29 deletions plugin/main/src/kotlinx/benchmark/gradle/JsSourceGeneratorTask.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package kotlinx.benchmark.gradle

import kotlinx.benchmark.gradle.internal.KotlinxBenchmarkPluginInternalApi
import org.gradle.api.DefaultTask
import org.gradle.api.file.FileCollection
import kotlinx.benchmark.gradle.internal.generator.RequiresKotlinCompilerEmbeddable
import kotlinx.benchmark.gradle.internal.generator.workers.GenerateJsSourceWorker
import org.gradle.api.*
import org.gradle.api.file.*
import org.gradle.api.tasks.*
import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.storage.LockBasedStorageManager
import org.jetbrains.kotlin.storage.StorageManager
import java.io.File
import javax.inject.Inject

@CacheableTask
open class JsSourceGeneratorTask
abstract class JsSourceGeneratorTask
@KotlinxBenchmarkPluginInternalApi
@Inject
constructor(
Expand All @@ -37,34 +36,28 @@ constructor(
@OutputDirectory
lateinit var outputSourcesDir: File

@get:Classpath
abstract val runtimeClasspath: ConfigurableFileCollection

@TaskAction
fun generate() {
cleanup(outputSourcesDir)
cleanup(outputResourcesDir)

inputClassesDirs.files.forEach { lib: File ->
generateSources(lib)
val workQueue = workerExecutor.classLoaderIsolation {
it.classpath.from(runtimeClasspath)
}
}

private fun generateSources(lib: File) {
val modules = loadIr(lib, LockBasedStorageManager("Inspect"))
modules.forEach { module ->
val generator = SuiteSourceGenerator(
title,
module,
outputSourcesDir,
if (useBenchmarkJs) Platform.JsBenchmarkJs else Platform.JsBuiltIn
)
generator.generate()
@OptIn(RequiresKotlinCompilerEmbeddable::class)
workQueue.submit(GenerateJsSourceWorker::class.java) {
it.title.set(title)
it.inputClasses.from(inputClassesDirs)
it.inputDependencies.from(inputDependencies)
it.outputSourcesDir.set(outputSourcesDir)
it.outputResourcesDir.set(outputResourcesDir)
it.useBenchmarkJs.set(useBenchmarkJs)
}
}

private fun loadIr(lib: File, storageManager: StorageManager): List<ModuleDescriptor> {
// skip processing of empty dirs (fails if not to do it)
if (lib.listFiles() == null) return emptyList()
val dependencies = inputDependencies.files.filterNot { it.extension == "js" }.toSet()
val module = KlibResolver.JS.createModuleDescriptor(lib, dependencies, storageManager)
return listOf(module)
workQueue.await() // I'm not sure if waiting is necessary,
// but I suspect that the task dependencies aren't configured correctly,
// so: better-safe-than-sorry.
// Try removing await() when Benchmarks follows Gradle best practices.
}
}
Loading

0 comments on commit 279d309

Please sign in to comment.