Skip to content

Instantly share code, notes, and snippets.

@teenriot
Last active December 12, 2024 22:51
Show Gist options
  • Save teenriot/7fcfde4ad498a46c99602f24a969460c to your computer and use it in GitHub Desktop.
Save teenriot/7fcfde4ad498a46c99602f24a969460c to your computer and use it in GitHub Desktop.
HTML-Report generation for kotlinx benchmarks - JVM only
import kotlinx.benchmark.gradle.BenchmarkConfiguration
import java.io.File
import java.nio.charset.StandardCharsets.UTF_8
import java.util.zip.ZipInputStream
import kotlin.text.replace
plugins {
// dependency ist only needed by HtmlReportGenerator to extract files from:
// https://github.com/jzillmann/gradle-jmh-report/blob/master/src/main/resources/jmh-visualizer.zip
id("io.morethan.jmhreport") version "0.9.0"
}
// ##########################################################################
val benchmarkHtmlReportGenerator = HtmlReportGenerator(
outputDir = project.file("benchmark-reports"),
resourceDirectoryName = "resources",
openNewReportsInBrowser = true
)
// TODO this is a hack
// how to correctly start HTML-Generator after a benchmark run produced a json report file?
// Kotlinx benchmark tasks are created on the fly and have no output!
tasks["benchmarkClasses"].doLast {
// here we can get benchmark tasks that were created on the fly
tasks.filter{ it.name.startsWith("benchmark") && it.name.endsWith("Benchmark") }.forEach { task ->
task.doLast {
val benchmarkDescriptionFile = (task as? JavaExec)
?.args?.getOrNull(0)
?.let(::File)?.takeIf { it.exists() }
val jsonReportFile = benchmarkDescriptionFile
?.readText(UTF_8)?.lineSequence()
?.firstOrNull { it.startsWith("reportFile:") }
?.substringAfter("reportFile:")
?.takeIf{ it.endsWith(".json", ignoreCase = true) }
?.let(::File)?.takeIf { it.exists() }
if (jsonReportFile!=null) {
benchmarkHtmlReportGenerator.generateHtmlReport(jsonReportFile)
}
}
}
}
/** based on: https://github.com/jzillmann/gradle-jmh-report/blob/master/src/main/kotlin/io/morethan/jmhreport/gradle/task/JmhReportTask.kt */
class HtmlReportGenerator(
val outputDir : File,
val resourceDirectoryName : String,
val openNewReportsInBrowser : Boolean
) {
private fun jsonToHtmlReportFile(jsonReportFile: File): File {
// TODO: UNSAFE since it depends on a file & directory name structure
// that may change in future or become configurable by end user
val benchmarkName = jsonReportFile.parentFile.parentFile.name
val benchmarkDateTime = jsonReportFile.parentFile.name
val benchmarkDate = benchmarkDateTime.substringBefore("T")
val benchmarkTime = benchmarkDateTime.substringAfter("T").substringBeforeLast(".").replace('.', '-')
return outputDir.resolve("${benchmarkName}__${benchmarkDate}__${benchmarkTime}.html")
}
fun generateHtmlReport(jsonFile: File) {
if (!jsonFile.exists()) return
val reportFile = jsonToHtmlReportFile(jsonFile)
if (reportFile.exists()) return
val resourceDirectory = reportFile.parentFile.resolve(resourceDirectoryName)
val json = jsonFile.readText(UTF_8)
generateResourceDirectory(resourceDirectory)
generateReportFile(json, reportFile, resourceDirectory)
println("HTML-Report generated: ${reportFile.asConsoleLink}")
println()
if (openNewReportsInBrowser) {
reportFile.openInBrowser()
}
}
private fun generateResourceDirectory(resourceDirectory : File) {
if (resourceDirectory.exists()) return
val zipStream = this.javaClass.getResourceAsStream("/jmh-visualizer.zip")
?: error("'jmh-visualizer.zip' not found!")
ZipInputStream(zipStream).use {
it.extractTo(resourceDirectory)
}
}
private fun generateReportFile(json: String, reportFile: File, resourceDirectory: File) {
val originalHtml = resourceDirectory.resolve("index.html").readText(UTF_8)
val html = originalHtml
.replace(
"src=\"provided.js\">",
""">
var providedBenchmarks = ['Benchmark'];
var providedBenchmarkStore = { 'Benchmark': $json };
""".trimIndent()
)
.replace("href=\"", "href=\"$resourceDirectoryName/")
.replace("src=\"", "src=\"$resourceDirectoryName/")
reportFile.parentFile.mkdirs()
reportFile.writeText(html, UTF_8)
}
// H E L P E R
private fun File.openInBrowser() {
val os = org.gradle.internal.os.OperatingSystem.current()
val uri = toURI()
when {
os?.isWindows == true -> Runtime.getRuntime().exec(arrayOf("cmd", "/c", "start $uri"))
os?.isMacOsX == true -> Runtime.getRuntime().exec(arrayOf("open", "$uri"))
else -> error("open files in browser is not implemented for OS: ${os.name ?: "unknown"}")
}
}
private val File.asConsoleLink : String
get() = "file:///${absolutePath.replace('\\', '/')}"
private fun ZipInputStream.extractTo(directory: File) {
directory.mkdirs()
while (true) {
val zipEntry = getNextEntry() ?: break
directory.resolve(zipEntry.name).apply {
if (zipEntry.isDirectory) mkdirs()
else outputStream().use(::copyTo)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment