Skip to content

Commit fd0bfa0

Browse files
Abduqodiri Qurbonzodaqurbonzoda
authored andcommitted
Improve error messages for invalid use of annotations
1 parent 074ce4f commit fd0bfa0

File tree

9 files changed

+713
-30
lines changed

9 files changed

+713
-30
lines changed

docs/writing-benchmarks.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ The key point to remember is that the `@Setup` method's execution time is not in
8787
results - the timer starts only when the `@Benchmark` method begins. This makes `@Setup` an ideal place
8888
for initialization tasks that should not impact the timing results of your benchmark.
8989

90+
The method annotated with `@Setup` should be `public` and have no arguments.
91+
In Kotlin/JVM, these restrictions are slightly less strict.
9092
Refer to [JMH documentation of @Setup](https://javadoc.io/doc/org.openjdk.jmh/jmh-core/latest/org/openjdk/jmh/annotations/Setup.html)
9193
for details about the effect and restrictions of the annotation in Kotlin/JVM.
9294

@@ -106,6 +108,8 @@ The `@TearDown` annotation is crucial for avoiding performance bias, ensuring th
106108
and preparing a clean environment for the next run. Similar to the `@Setup` method, the execution time of the
107109
`@TearDown` method is not included in the final benchmark results.
108110

111+
The method annotated with `@TearDown` should be `public` and have no arguments.
112+
In Kotlin/JVM, these restrictions are slightly less strict.
109113
Refer to [JMH documentation of @TearDown](https://javadoc.io/doc/org.openjdk.jmh/jmh-core/latest/org/openjdk/jmh/annotations/TearDown.html)
110114
for more information on the effect and restrictions of the annotation in Kotlin/JVM.
111115

@@ -118,7 +122,7 @@ It's the actual test you're running. The code you want to benchmark goes inside
118122
All other annotations are employed to configure the benchmark's environment and execution.
119123

120124
Benchmark methods may include only a single [Blackhole](#blackhole) type as an argument, or have no arguments at all.
121-
It's important to note that in Kotlin/JVM benchmark methods must always be `public`.
125+
It's important to note that benchmark methods should be `public`. In Kotlin/JVM, these restrictions are slightly less strict.
122126
Refer to [JMH documentation of @Benchmark](https://javadoc.io/doc/org.openjdk.jmh/jmh-core/latest/org/openjdk/jmh/annotations/Benchmark.html)
123127
for details about restrictions for benchmark methods in Kotlin/JVM.
124128

@@ -194,11 +198,15 @@ with each iteration lasting one second, for the final performance measurement.
194198

195199
The `@Param` annotation is used to pass different parameters to your benchmark method.
196200
It allows you to run the same benchmark method with different input values, so you can see how these variations affect
197-
performance. The values you provide for the `@Param` annotation are the different inputs you want to use in your
198-
benchmark test. The benchmark will run once for each provided value.
201+
performance.
199202

200-
The property marked with this annotation must be mutable (`var`) and not `private.`
203+
The values provided by the `@Param` annotation represent the different inputs you want to use in your benchmark.
204+
Since the benchmark runs once for each provided value, the annotation should have at least one argument.
205+
The annotation values are given in `String` and will be coerced as needed to match the property type.
206+
207+
The property marked with this annotation should be mutable (`var`) and `public`.
201208
Additionally, only properties of primitive types or the `String` type can be annotated with `@Param`.
209+
In Kotlin/JVM, these restrictions are slightly less strict.
202210
Refer to [JMH documentation of @Param](https://javadoc.io/doc/org.openjdk.jmh/jmh-core/latest/org/openjdk/jmh/annotations/Param.html)
203211
for details about the effect and restrictions of the annotation in Kotlin/JVM.
204212

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,111 @@
11
package kotlinx.benchmark.integration
22

3+
@OptIn(ExperimentalStdlibApi::class)
34
class AnnotationsSpecifier {
4-
private var isMeasurementSpecified: Boolean = false
5-
private var iterations: Int? = null
6-
private var time: Int? = null
7-
private var timeUnit: String? = null
5+
private val classAnnotations = mutableListOf<Annotation>()
6+
private val propertyAnnotations = mutableListOf<AnnotatedMember>()
7+
private val functionAnnotations = mutableListOf<AnnotatedMember>()
88

99
fun measurement(iterations: Int, time: Int, timeUnit: String) {
10-
isMeasurementSpecified = true
11-
this.iterations = iterations
12-
this.time = time
13-
this.timeUnit = timeUnit
10+
classAnnotations.add(
11+
Annotation("@Measurement", listOf(iterations, time, timeUnit))
12+
)
1413
}
1514

16-
fun replacementForLine(line: String): String {
15+
fun outputTimeUnit(timeUnit: String) {
16+
classAnnotations.add(
17+
Annotation("@OutputTimeUnit", listOf(timeUnit))
18+
)
19+
}
20+
21+
fun benchmarkMode(mode: String) {
22+
classAnnotations.add(
23+
Annotation("@BenchmarkMode", listOf(mode))
24+
)
25+
}
26+
27+
fun benchmark(functionName: String) {
28+
functionAnnotations.add(
29+
AnnotatedMember(functionName, Annotation("@Benchmark"))
30+
)
31+
}
32+
33+
fun setup(functionName: String) {
34+
functionAnnotations.add(
35+
AnnotatedMember(functionName, Annotation("@Setup"))
36+
)
37+
}
38+
39+
fun teardown(functionName: String) {
40+
functionAnnotations.add(
41+
AnnotatedMember(functionName, Annotation("@TearDown"))
42+
)
43+
}
44+
45+
fun param(propertyName: String, vararg values: String) {
46+
require(values.all { '\"' !in it }) { "TODO: Support param values that contain '\"'." }
47+
48+
propertyAnnotations.add(
49+
AnnotatedMember(propertyName, Annotation("@Param", values.map { "\"$it\"" }))
50+
)
51+
}
52+
53+
fun annotationsForProperty(line: String): List<String> {
54+
val annotations = mutableListOf<String>()
55+
for ((propertyName, annotation) in propertyAnnotations) {
56+
val regex = Regex("\\s*(public|private|protected|internal)?\\s*(final|open)?\\s*(val|var)\\s+${Regex.escape(propertyName)}")
57+
if (regex.matchesAt(line, 0)) {
58+
check(!annotation.isUsed)
59+
annotation.isUsed = true
60+
annotations.add(annotation.toCode())
61+
}
62+
}
63+
return annotations
64+
}
65+
66+
fun annotationsForFunction(line: String): List<String> {
67+
val annotations = mutableListOf<String>()
68+
for ((functionName, annotation) in functionAnnotations) {
69+
val regex = Regex("\\s*(public|private|protected|internal)?\\s*(final|open)?\\s*fun\\s+${Regex.escape(functionName)}\\(")
70+
if (regex.matchesAt(line, 0)) {
71+
check(!annotation.isUsed)
72+
annotation.isUsed = true
73+
annotations.add(annotation.toCode())
74+
}
75+
}
76+
return annotations
77+
}
78+
79+
fun replaceClassAnnotation(line: String): String {
1780
val trimmedLine = line.trimStart()
1881
val prefix = line.substring(0, line.length - trimmedLine.length)
19-
return when {
20-
isMeasurementSpecified && trimmedLine.startsWith("@Measurement") ->
21-
"$prefix@Measurement($iterations, $time, $timeUnit)"
22-
else ->
23-
line
82+
for (annotation in classAnnotations) {
83+
if (trimmedLine.startsWith(annotation.name)) {
84+
check(!annotation.isUsed)
85+
annotation.isUsed = true
86+
return prefix + annotation.toCode()
87+
}
2488
}
89+
return line
90+
}
91+
92+
fun checkAllAnnotationsAreUsed() {
93+
classAnnotations.forEach { check(it.isUsed) { "Unused class annotation: $it" } }
94+
propertyAnnotations.forEach { check(it.annotation.isUsed) { "Unused property annotation: $it" } }
95+
functionAnnotations.forEach { check(it.annotation.isUsed) { "Unused function annotation: $it" } }
2596
}
97+
}
98+
99+
private data class AnnotatedMember(
100+
val memberName: String,
101+
val annotation: Annotation
102+
)
103+
104+
private data class Annotation(
105+
val name: String,
106+
val arguments: List<Any?> = emptyList(),
107+
var isUsed: Boolean = false
108+
) {
109+
fun toCode(): String =
110+
"$name${if (arguments.isEmpty()) "" else arguments.joinToString(", ", "(", ")")}"
26111
}

integration/src/main/kotlin/kotlinx/benchmark/integration/Runner.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,40 @@ class Runner(
3636
fun updateAnnotations(filePath: String, annotationsSpecifier: AnnotationsSpecifier.() -> Unit) {
3737
val annotations = AnnotationsSpecifier().also(annotationsSpecifier)
3838
val file = projectDir.resolve(filePath)
39-
val updatedLines = file.readLines().map { annotations.replacementForLine(it) }
39+
40+
val updatedLines = file.readLines().map {
41+
annotations.replaceClassAnnotation(it)
42+
}
43+
annotations.checkAllAnnotationsAreUsed()
44+
45+
file.writeText(updatedLines.joinToString(separator = "\n"))
46+
if (print) {
47+
println(file.readText())
48+
}
49+
}
50+
51+
fun addAnnotation(filePath: String, annotationsSpecifier: AnnotationsSpecifier.() -> Unit) {
52+
val annotations = AnnotationsSpecifier().also(annotationsSpecifier)
53+
val file = projectDir.resolve(filePath)
54+
55+
val updatedLines = mutableListOf<String>()
56+
57+
file.readLines().forEach { line ->
58+
val indentation = " ".repeat(line.length - line.trimStart().length)
59+
annotations.annotationsForFunction(line).forEach { annotation ->
60+
updatedLines.add(indentation + annotation)
61+
}
62+
annotations.annotationsForProperty(line).forEach { annotation ->
63+
updatedLines.add(indentation + annotation)
64+
}
65+
updatedLines.add(line)
66+
}
67+
annotations.checkAllAnnotationsAreUsed()
68+
4069
file.writeText(updatedLines.joinToString(separator = "\n"))
70+
if (print) {
71+
println(file.readText())
72+
}
4173
}
4274

4375
fun generatedDir(targetName: String, filePath: String, fileTestAction: (File) -> Unit) {

0 commit comments

Comments
 (0)