Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#130: Support benchmarks having @State annotations in parent classes #193

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions examples/kotlin/benchmarks/src/TestInheritedBenchmark.kt
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add some integration tests instead?

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package test

import org.openjdk.jmh.annotations.*
import java.util.concurrent.TimeUnit
import kotlin.math.cos
import kotlin.math.sqrt

@State(Scope.Benchmark)
@Fork(1)
@Warmup(iterations = 0)
@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
abstract class BaseBenchmark {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the base class declare benchmark methods as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. As to why? -> It doesn't make sense to run the same benchmark again if two or more classes are inheriting from same non-final class.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can think of the following use case:

import kotlinx.benchmark.*

@State(Scope.Benchmark)
@Measurement(iterations = 7, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS)
@Warmup(iterations = 7, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS)
@OutputTimeUnit(BenchmarkTimeUnit.MILLISECONDS)
abstract class IterationBenchmarkBase {
    abstract val list: List<String>

    @Param("10", "100")
    var size: Int = 0

    @Benchmark
    fun iterationBenchmark(bh: Blackhole) {
        for (element in list) {
            bh.consume(element)
        }
    }
}

class ArrayListIterationBenchmark : IterationBenchmarkBase() {
    override val list: List<String>
        get() = ArrayList<String>(size).apply { fill(size) }
}

class ArrayDequeIterationBenchmark : IterationBenchmarkBase() {
    override val list: List<String>
        get() = ArrayDeque<String>(size).apply { fill(size) }
}

private fun MutableList<String>.fill(size: Int) {
    addAll((1..size).map { it.toString() })
}


protected lateinit var data: TestData

@Setup
fun setUp() {
data = TestData(50.0)
}
}

class TestInheritedBenchmark: BaseBenchmark() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a TestInheritedBenchmark subclass declare benchmark methods as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand a bit on your question?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am referring to cases of multi-hierarchy benchmark classes, within which multiple classes define benchmark methods.

@Benchmark
fun sqrtBenchmark(): Double {
return sqrt(data.value)
}

@Benchmark
fun cosBenchmark(): Double {
return cos(data.value)
}
}


102 changes: 90 additions & 12 deletions plugin/main/src/kotlinx/benchmark/gradle/SuiteSourceGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kotlinx.benchmark.gradle
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.name.*
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.annotations.*
Expand Down Expand Up @@ -99,21 +100,99 @@ class SuiteSourceGenerator(val title: String, val module: ModuleDescriptor, val

private fun processPackage(module: ModuleDescriptor, packageView: PackageViewDescriptor) {
for (packageFragment in packageView.fragments.filter { it.module == module }) {
DescriptorUtils.getAllDescriptors(packageFragment.getMemberScope())
val classDescriptors = DescriptorUtils.getAllDescriptors(packageFragment.getMemberScope())
.filterIsInstance<ClassDescriptor>()

val inheritableClassDescriptors = classDescriptors.filter { it.modality != Modality.FINAL }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about using "non-final" instead of "inheritable"?
The term "non-final" just sounds more familiar to me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.


// Abstract classes which are annotated directly.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that directlyAnnotatedInheritableClassDescriptors includes more than just abstract classes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops yes old comment. Will fix!

val directlyAnnotatedInheritableClassDescriptors = inheritableClassDescriptors
.filter { it.annotations.any { it.fqName.toString() == stateAnnotationFQN } }
.filter { it.modality != Modality.ABSTRACT }
.forEach {
generateBenchmark(it)

// Contains both directly and indirectly annotated ClassDescriptors.
val annotatedInheritableClassDescriptors = inheritableClassDescriptors
.filter { inheritableClass ->
directlyAnnotatedInheritableClassDescriptors.forEach { annotatedInheritableClass ->
if (inheritableClass.isSubclassOf(annotatedInheritableClass)) {
// If any benchmark class is not annotated but extended with an abstract class
// annotated with @State, it should be included when generating benchmarks.
return@filter true
}
}
return@filter false
}

val annotatedClassDescriptors = mutableListOf<AnnotatedClassDescriptor>()
.apply {
classDescriptors
.filter { it.modality != Modality.ABSTRACT }
.forEach {
if (it.annotations.any {
annotationDescriptor -> annotationDescriptor.fqName.toString() == stateAnnotationFQN
}) {
add(AnnotatedClassDescriptor(it))
} else {
val parent = it.getParentAnnotated(annotatedInheritableClassDescriptors)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could directlyAnnotatedInheritableClassDescriptors be used here?
I am struggling to understand the need for annotatedInheritableClassDescriptors. Cloud you please clarify?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh after another thought, I think directlyAnnotatedInheritableClassDescriptors should be used here. Will do!

if (parent != null) {
add(AnnotatedClassDescriptor(it, parent))
}
}
}
}

annotatedClassDescriptors.forEach {
generateBenchmark(it)
}
}

for (subpackageName in module.getSubPackagesOf(packageView.fqName, MemberScope.ALL_NAME_FILTER)) {
processPackage(module, module.getPackage(subpackageName))
}
}

private fun generateBenchmark(original: ClassDescriptor) {
/** @param annotatedInheritableClassDescriptors descriptors of classes which are inheritable and directly annotated.
* with [stateAnnotationFQN].
* @return Top most parent class descriptor which was annotated with [stateAnnotationFQN] and inherited by `this` class
* descriptor or null if none of its parent classes are not annotated.*/
private fun ClassDescriptor.getParentAnnotated(
annotatedInheritableClassDescriptors: List<ClassDescriptor>
): ClassDescriptor? {
annotatedInheritableClassDescriptors.forEach { annotatedAbstractClass ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can find be used to simplify the code?

if (this.isSubclassOf(annotatedAbstractClass)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be that there are multiple superclasses for this class descriptor?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was expecting this! So as for phase-1 I was just looking out for bare bones implementation which means this here just a base setup. Currently this function picks the first superclass that is directly annotated. Now there are some cases:

  1. Simple straight forward: Only one annotated superclass.
  2. Multiple superclasses but only one is annotated.
  3. Multiple superclasses and intermediaries (super class of this but sub class of the top most annotated parent class) are annotated as well.
  4. Multiple superclasses and intermediaries with annotations E.x., @Warmup, @Measurement etc without @State.

What should be the route to follow here?
For 4th point, my suggestion is that independent annotations should be ignored. For the 3rd point, I would suggest nearest annotated parent. 1st and 2nd are easily dealt with.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should be the route to follow here?

It depends on how JMH handles this aspect. Being consistent with that library is important, since it runs the benchmarks on the JVM. Achieving consistent behavior across other platforms would be ideal. It is worth mentioning that we do not aim to replicate all of JMH's sophistication, but when something is not supported in non-JVM targets, we would like to issue a warning (or throw an exception) about it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would greatly appreciate it if you could check how JMH handles this case. It would greatly help us decide which route we should take.

return annotatedAbstractClass
}
}
return null
}

/** @param parentAnnotatedClassDescriptor if null, [original] is directly annotated with [stateAnnotationFQN].*/
private data class AnnotatedClassDescriptor(
val original: ClassDescriptor,
val parentAnnotatedClassDescriptor: ClassDescriptor? = null
)

private fun ClassDescriptor.getParameterProperties(): List<PropertyDescriptor> =
DescriptorUtils.getAllDescriptors(this.unsubstitutedMemberScope)
.filterIsInstance<PropertyDescriptor>()
.filter { it.annotations.any { it.fqName.toString() == paramAnnotationFQN } }

private fun ClassDescriptor.getMeasureAnnotation(): AnnotationDescriptor? =
this.annotations.singleOrNull { it.fqName.toString() == measureAnnotationFQN }

private fun ClassDescriptor.getWarmupAnnotation(): AnnotationDescriptor? =
this.annotations.singleOrNull { it.fqName.toString() == warmupAnnotationFQN }

private fun ClassDescriptor.getOutputTimeAnnotation(): AnnotationDescriptor? =
this.annotations.singleOrNull { it.fqName.toString() == outputTimeAnnotationFQN }

private fun ClassDescriptor.getModeAnnotation(): AnnotationDescriptor? =
this.annotations.singleOrNull { it.fqName.toString() == modeAnnotationFQN }


private fun generateBenchmark(classDescriptor: AnnotatedClassDescriptor) {
val original = classDescriptor.original
val parent = classDescriptor.parentAnnotatedClassDescriptor

val originalPackage = original.fqNameSafe.parent()
val originalName = original.fqNameSafe.shortName()
val originalClass = ClassName(originalPackage.toString(), originalName.toString())
Expand All @@ -125,14 +204,13 @@ class SuiteSourceGenerator(val title: String, val module: ModuleDescriptor, val
val functions = DescriptorUtils.getAllDescriptors(original.unsubstitutedMemberScope)
.filterIsInstance<FunctionDescriptor>()

val parameterProperties = DescriptorUtils.getAllDescriptors(original.unsubstitutedMemberScope)
.filterIsInstance<PropertyDescriptor>()
.filter { it.annotations.any { it.fqName.toString() == paramAnnotationFQN } }
val parameterProperties = original.getParameterProperties()

val measureAnnotation = original.annotations.singleOrNull { it.fqName.toString() == measureAnnotationFQN }
val warmupAnnotation = original.annotations.singleOrNull { it.fqName.toString() == warmupAnnotationFQN }
val outputTimeAnnotation = original.annotations.singleOrNull { it.fqName.toString() == outputTimeAnnotationFQN }
val modeAnnotation = original.annotations.singleOrNull { it.fqName.toString() == modeAnnotationFQN }
// original's annotations are given higher priority than parent's annotation.
val measureAnnotation = original.getMeasureAnnotation() ?: parent?.getMeasureAnnotation()
val warmupAnnotation = original.getWarmupAnnotation() ?: parent?.getWarmupAnnotation()
val outputTimeAnnotation = original.getOutputTimeAnnotation() ?: parent?.getOutputTimeAnnotation()
val modeAnnotation = original.getModeAnnotation() ?: parent?.getModeAnnotation()

val outputTimeUnitValue = outputTimeAnnotation?.argumentValue("value") as EnumValue?
val outputTimeUnit = outputTimeUnitValue?.enumEntryName?.toString()
Expand Down