Skip to content

Commit ed69605

Browse files
authored
Support report format selection: json(default), csv/scsv or text (#35)
Implement JMH-similar JS/Native CSV/SCSV/TEXT formatters Print summary after suite execution on all platforms
1 parent c2760d9 commit ed69605

File tree

18 files changed

+301
-90
lines changed

18 files changed

+301
-90
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ Available configuration options:
149149
* `mode` – "thrpt" for measuring operations per time, or "avgt" for measuring time per operation
150150
* `include("…")` – regular expression to include benchmarks with fully qualified names matching it, as a substring
151151
* `exclude("…")` – regular expression to exclude benchmarks with fully qualified names matching it, as a substring
152-
* `param("name", "value1", "value2")` – specify a parameter for a public mutable property `name` annotated with `@Param`
152+
* `param("name", "value1", "value2")` – specify a parameter for a public mutable property `name` annotated with `@Param`
153+
* `reportFormat` – format of report, can be `json`(default), `csv`, `scsv` or `text`
153154

154155
Time units can be NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, or their short variants such as "ms" or "ns".
155156

examples/kotlin-multiplatform/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ benchmark {
9999
iterationTimeUnit = "ms" // time in ms per iteration
100100
advanced("forks", 1)
101101
}
102+
103+
csv {
104+
include("Common")
105+
exclude("long")
106+
iterations = 1
107+
iterationTime = 300
108+
iterationTimeUnit = "ms"
109+
reportFormat = "csv" // csv report format
110+
}
102111
}
103112

104113
// Setup configurations

plugin/main/src/kotlinx/benchmark/gradle/BenchmarkConfiguration.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ open class BenchmarkConfiguration(val extension: BenchmarksExtension, val name:
1010
var iterationTimeUnit: String? = null
1111
var mode: String? = null
1212
var outputTimeUnit: String? = null
13+
var reportFormat: String? = null
1314

1415
var includes: MutableList<String> = mutableListOf()
1516
var excludes: MutableList<String> = mutableListOf()
@@ -32,9 +33,10 @@ open class BenchmarkConfiguration(val extension: BenchmarksExtension, val name:
3233
fun advanced(name: String, value: Any?) {
3334
advanced[name] = value
3435
}
35-
36+
3637
fun capitalizedName() = if (name == "main") "" else name.capitalize()
3738
fun prefixName(suffix: String) = if (name == "main") suffix else name + suffix.capitalize()
39+
fun reportFileExt(): String = reportFormat?.toLowerCase() ?: "json"
3840
}
3941

4042
open class BenchmarkTarget(val extension: BenchmarksExtension, val name: String) {
@@ -74,4 +76,4 @@ class NativeBenchmarkTarget(
7476
val compilation: KotlinNativeCompilation
7577
) : BenchmarkTarget(extension, name) {
7678

77-
}
79+
}

plugin/main/src/kotlinx/benchmark/gradle/JsNodeTasks.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ fun Project.createJsBenchmarkExecTask(
2020
extensions.extraProperties.set("idea.internal.test", System.getProperty("idea.active"))
2121

2222
val reportsDir = benchmarkReportsDir(config, target)
23-
val reportFile = reportsDir.resolve("${target.name}.json")
23+
val reportFile = reportsDir.resolve("${target.name}.${config.reportFileExt()}")
2424

2525
val executableFile = compilation.compileKotlinTask.outputFile
2626
args("-r", "source-map-support/register")

plugin/main/src/kotlinx/benchmark/gradle/JvmTasks.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ fun Project.createJvmBenchmarkExecTask(
9999

100100
val benchmarkBuildDir = benchmarkBuildDir(target)
101101
val reportsDir = benchmarkReportsDir(config, target)
102-
val reportFile = reportsDir.resolve("${target.name}.json")
102+
val reportFile = reportsDir.resolve("${target.name}.${config.reportFileExt()}")
103103
main = "kotlinx.benchmark.jvm.JvmBenchmarkRunnerKt"
104104

105105
if (target.workingDir != null)

plugin/main/src/kotlinx/benchmark/gradle/NativeMultiplatformTasks.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ fun Project.createNativeBenchmarkExecTask(
114114
onlyIf { linkTask.enabled }
115115

116116
val reportsDir = benchmarkReportsDir(config, target)
117-
val reportFile = reportsDir.resolve("${target.name}.json")
117+
val reportFile = reportsDir.resolve("${target.name}.${config.reportFileExt()}")
118118

119119
val executableFile = linkTask.outputFile.get()
120120
executable = executableFile.absolutePath

plugin/main/src/kotlinx/benchmark/gradle/Utils.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ fun writeParameters(
7777
format: String,
7878
config: BenchmarkConfiguration
7979
): File {
80+
validateConfig(config)
8081
val file = createTempFile("benchmarks")
8182
file.writeText(buildString {
8283
appendln("name:$name")
8384
appendln("reportFile:$reportFile")
8485
appendln("traceFormat:$format")
86+
config.reportFormat?.let { appendln("reportFormat:$it") }
8587
config.iterations?.let { appendln("iterations:$it") }
8688
config.warmups?.let { appendln("warmups:$it") }
8789
config.iterationTime?.let { appendln("iterationTime:$it") }
@@ -105,3 +107,11 @@ fun writeParameters(
105107
return file
106108
}
107109

110+
private fun validateConfig(config: BenchmarkConfiguration) {
111+
config.reportFormat?.let {
112+
require(it.toLowerCase() in setOf("json", "csv", "scsv", "text")) {
113+
"Report format '$it' is not supported."
114+
}
115+
}
116+
117+
}

runtime/commonMain/src/kotlinx/benchmark/BenchmarkProgress.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package kotlinx.benchmark
22

33
abstract class BenchmarkProgress {
44
abstract fun startSuite(suite: String)
5-
abstract fun endSuite(suite: String)
5+
abstract fun endSuite(suite: String, summary: String)
66

77
abstract fun startBenchmark(suite: String, benchmark: String)
88
abstract fun endBenchmark(suite: String, benchmark: String, status: FinishStatus, message: String)
@@ -36,10 +36,11 @@ class IntelliJBenchmarkProgress : BenchmarkProgress() {
3636
println(ijSuiteStart(rootId, suite))
3737
}
3838

39-
override fun endSuite(suite: String) {
39+
override fun endSuite(suite: String, summary: String) {
4040
if (currentClass != "") {
4141
println(ijSuiteFinish(suite, currentClass, currentStatus))
4242
}
43+
println(ijLogOutput(rootId, suite, "$suite summary:\n$summary\n"))
4344
println(ijSuiteFinish(rootId, suite, suiteStatus))
4445
println(ijSuiteFinish("", rootId, suiteStatus))
4546
}
@@ -85,8 +86,10 @@ class ConsoleBenchmarkProgress : BenchmarkProgress() {
8586

8687
}
8788

88-
override fun endSuite(suite: String) {
89+
override fun endSuite(suite: String, summary: String) {
8990
println()
91+
println("$suite summary:")
92+
println(summary)
9093
}
9194

9295
override fun startBenchmark(suite: String, benchmark: String) {
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package kotlinx.benchmark
2+
3+
import kotlin.math.*
4+
5+
sealed class BenchmarkReportFormatter {
6+
abstract fun format(results: Collection<ReportBenchmarkResult>): String
7+
8+
companion object {
9+
fun create(format: String): BenchmarkReportFormatter = when (format.toLowerCase()) {
10+
"json" -> JsonBenchmarkReportFormatter
11+
"csv" -> CsvBenchmarkReportFormatter(",")
12+
"scsv" -> CsvBenchmarkReportFormatter(";")
13+
"text" -> TextBenchmarkReportFormatter
14+
else -> throw UnsupportedOperationException("Report format $format is not supported.")
15+
}
16+
}
17+
}
18+
19+
internal object TextBenchmarkReportFormatter : BenchmarkReportFormatter() {
20+
private const val padding = 2
21+
22+
override fun format(results: Collection<ReportBenchmarkResult>): String {
23+
fun columnLength(column: String, selector: (ReportBenchmarkResult) -> String): Int =
24+
max(column.length, results.maxOf { selector(it).length })
25+
26+
val shortNames = denseBenchmarkNames(results.map { it.benchmark.name })
27+
val nameLength = columnLength("Benchmark") { shortNames[it.benchmark.name]!! }
28+
val paramNames = results.flatMap { it.params.keys }.toSet()
29+
val paramLengths = paramNames.associateWith { paramName ->
30+
max(paramName.length + 2, results.mapNotNull { it.params[paramName] }.maxOf { it.length }) + padding
31+
}
32+
val modeLength = columnLength("Mode") { it.config.mode.toText() } + padding
33+
val samplesLength = columnLength("Cnt") { it.values.size.toString() } + padding
34+
val scopeLength = columnLength("Score") { it.score.format(3, useGrouping = false) } + padding
35+
val errorLength = columnLength("Error") { it.error.format(3, useGrouping = false) } + padding - 1
36+
val unitsLength = columnLength("Units") { unitText(it.config.mode, it.config.outputTimeUnit) } + padding
37+
38+
return buildString {
39+
appendPaddedAfter("Benchmark", nameLength)
40+
paramNames.forEach {
41+
appendPaddedBefore("($it)", paramLengths[it]!!)
42+
}
43+
appendPaddedBefore("Mode", modeLength)
44+
appendPaddedBefore("Cnt", samplesLength)
45+
appendPaddedBefore("Score", scopeLength)
46+
append(" ")
47+
appendPaddedBefore("Error", errorLength)
48+
appendPaddedBefore("Units", unitsLength)
49+
appendLine()
50+
51+
results.forEach { result ->
52+
appendPaddedAfter(shortNames[result.benchmark.name]!!, nameLength)
53+
paramNames.forEach {
54+
appendPaddedBefore(result.params[it] ?: "N/A", paramLengths[it]!!)
55+
}
56+
appendPaddedBefore(result.config.mode.toText(), modeLength)
57+
appendPaddedBefore(result.values.size.takeIf { it > 1 }?.toString() ?: " ", samplesLength)
58+
appendPaddedBefore(result.score.format(3, useGrouping = false), scopeLength)
59+
if (result.error.isNaNOrZero()) {
60+
append(" ")
61+
appendPaddedBefore("", errorLength)
62+
} else {
63+
append(" \u00B1")
64+
appendPaddedBefore(result.error.format(3, useGrouping = false), errorLength)
65+
}
66+
appendPaddedBefore(unitText(result.config.mode, result.config.outputTimeUnit), unitsLength)
67+
appendLine()
68+
}
69+
}
70+
}
71+
72+
private fun StringBuilder.appendSpace(l: Int): StringBuilder = append(" ".repeat(l))
73+
74+
private fun StringBuilder.appendPaddedBefore(value: String, l: Int): StringBuilder =
75+
appendSpace(l - value.length).append(value)
76+
77+
private fun StringBuilder.appendPaddedAfter(value: String, l: Int): StringBuilder =
78+
append(value).appendSpace(l - value.length)
79+
80+
81+
/**
82+
* Algorithm:
83+
* 1. remove package names, if it is the same for all benchmarks
84+
* 2. if not, shorthand same package names
85+
*
86+
* (jmh similar logic)
87+
*/
88+
private fun denseBenchmarkNames(src: List<String>): Map<String, String> {
89+
if (src.isEmpty()) return emptyMap()
90+
91+
var first = true
92+
var prefixCut = false
93+
94+
val prefix = src.fold(emptyList<String>()) { prefix, s ->
95+
val names = s.split(".")
96+
if (first) {
97+
first = false
98+
names.takeWhile { it.toLowerCase() == it }
99+
} else {
100+
val common = prefix.zip(names).takeWhile { (p, n) -> p == n && n.toLowerCase() == n }
101+
if (prefix.size != common.size) prefixCut = true
102+
prefix.take(common.size)
103+
}
104+
}.map { if (prefixCut) it[0].toString() else "" }
105+
106+
return src.associateWith { s ->
107+
val names = prefix + s.split(".").drop(prefix.size)
108+
names.joinToString("") { if (it.isNotEmpty()) "$it." else "" }.removeSuffix(".")
109+
}
110+
}
111+
}
112+
113+
private class CsvBenchmarkReportFormatter(val delimiter: String) : BenchmarkReportFormatter() {
114+
override fun format(results: Collection<ReportBenchmarkResult>): String = buildString {
115+
val allParams = results.flatMap { it.params.keys }.toSet()
116+
appendHeader(allParams)
117+
results.forEach {
118+
appendResult(allParams, it)
119+
}
120+
}
121+
122+
private fun StringBuilder.appendHeader(params: Set<String>) {
123+
appendEscaped("Benchmark").append(delimiter)
124+
appendEscaped("Mode").append(delimiter)
125+
appendEscaped("Threads").append(delimiter)
126+
appendEscaped("Samples").append(delimiter)
127+
appendEscaped("Score").append(delimiter)
128+
appendEscaped("Score Error (99.9%)").append(delimiter)
129+
appendEscaped("Unit")
130+
params.forEach {
131+
append(delimiter)
132+
appendEscaped("Param: $it")
133+
}
134+
append("\r\n")
135+
}
136+
137+
private fun StringBuilder.appendResult(params: Set<String>, result: ReportBenchmarkResult) {
138+
appendEscaped(result.benchmark.name).append(delimiter)
139+
appendEscaped(result.config.mode.toText()).append(delimiter)
140+
append(1).append(delimiter)
141+
append(result.values.size).append(delimiter)
142+
append(result.score.format(6, useGrouping = false)).append(delimiter)
143+
append(result.error.format(6, useGrouping = false)).append(delimiter)
144+
appendEscaped(unitText(result.config.mode, result.config.outputTimeUnit))
145+
params.forEach {
146+
append(delimiter)
147+
result.params[it]?.let { param ->
148+
appendEscaped(param)
149+
}
150+
}
151+
append("\r\n")
152+
}
153+
154+
private fun StringBuilder.appendEscaped(value: String): StringBuilder =
155+
append("\"").append(value.replace("\"", "\"\"")).append("\"")
156+
157+
}
158+
159+
private object JsonBenchmarkReportFormatter : BenchmarkReportFormatter() {
160+
161+
override fun format(results: Collection<ReportBenchmarkResult>): String =
162+
results.joinToString(",", prefix = "[", postfix = "\n]", transform = this::format)
163+
164+
private fun format(result: ReportBenchmarkResult): String =
165+
"""
166+
{
167+
"benchmark" : "${result.benchmark.name}",
168+
"mode" : "${result.config.mode.toText()}",
169+
"warmupIterations" : ${result.config.warmups},
170+
"warmupTime" : "${result.config.iterationTime} ${result.config.iterationTimeUnit.toText()}",
171+
"measurementIterations" : ${result.config.iterations},
172+
"measurementTime" : "${result.config.iterationTime} ${result.config.iterationTimeUnit.toText()}",
173+
"params" : {
174+
${result.params.entries.joinToString(separator = ",\n ") { "\"${it.key}\" : \"${it.value}\"" }}
175+
},
176+
"primaryMetric" : {
177+
"score": ${result.score},
178+
"scoreError": ${result.error},
179+
"scoreConfidence" : [
180+
${result.confidence.first},
181+
${result.confidence.second}
182+
],
183+
"scorePercentiles" : {
184+
${result.percentiles.entries.joinToString(separator = ",\n ") { "\"${it.key.format(2)}\" : ${it.value}" }}
185+
},
186+
"scoreUnit" : "${unitText(result.config.mode, result.config.outputTimeUnit)}",
187+
"rawData" : [
188+
${
189+
result.values.joinToString(
190+
prefix = "[\n ",
191+
postfix = "\n ]",
192+
separator = ",\n "
193+
)
194+
}
195+
]
196+
},
197+
"secondaryMetrics" : {
198+
}
199+
}"""
200+
201+
}
Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,3 @@
11
package kotlinx.benchmark
22

3-
expect fun saveReport(reportFile: String?, results: Collection<ReportBenchmarkResult>)
4-
5-
fun formatJson(results: Collection<ReportBenchmarkResult>) =
6-
results.joinToString(",", prefix = "[", postfix = "\n]") { result ->
7-
"""
8-
{
9-
"benchmark" : "${result.benchmark.name}",
10-
"mode" : "${result.config.mode.toText()}",
11-
"warmupIterations" : ${result.config.warmups},
12-
"warmupTime" : "${result.config.iterationTime} ${result.config.iterationTimeUnit.toText()}",
13-
"measurementIterations" : ${result.config.iterations},
14-
"measurementTime" : "${result.config.iterationTime} ${result.config.iterationTimeUnit.toText()}",
15-
"params" : {
16-
${result.params.entries.joinToString(separator = ",\n ") {
17-
"\"${it.key}\" : \"${it.value}\""
18-
}}
19-
},
20-
"primaryMetric" : {
21-
"score": ${result.score},
22-
"scoreError": ${result.error},
23-
"scoreConfidence" : [
24-
${result.confidence.first},
25-
${result.confidence.second}
26-
],
27-
"scorePercentiles" : {
28-
${result.percentiles.entries.joinToString(separator = ",\n ") {
29-
"\"${it.key.format(2)}\" : ${it.value}"
30-
}}
31-
},
32-
"scoreUnit" : "${unitText(result.config.mode, result.config.outputTimeUnit)}",
33-
"rawData" : [
34-
${result.values.joinToString(
35-
prefix = "[\n ",
36-
postfix = "\n ]",
37-
separator = ",\n "
38-
)}
39-
]
40-
},
41-
"secondaryMetrics" : {
42-
}
43-
}"""
44-
}
3+
expect fun saveReport(reportFile: String, report: String)

0 commit comments

Comments
 (0)