Skip to content

Instantly share code, notes, and snippets.

@teenriot
Last active December 12, 2024 22:16
Show Gist options
  • Save teenriot/d22e95ffdfcd37f09a7916927567fb79 to your computer and use it in GitHub Desktop.
Save teenriot/d22e95ffdfcd37f09a7916927567fb79 to your computer and use it in GitHub Desktop.
HTML-Report generation for kotlinx benchmarks as buildSrc-Plugin - JVM only - doesn't work with org.gradle.configuration-cache=true
repositories {
gradlePluginPortal()
}
dependencies {
// dependency ist only needed by BenchmarkHtmlReportPlugin to extract files from:
// https://github.com/jzillmann/gradle-jmh-report/blob/master/src/main/resources/jmh-visualizer.zip
implementation("io.morethan.jmhreport:io.morethan.jmhreport.gradle.plugin:0.9.6")
}
package deshe.benchmark
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.JavaExec
import org.gradle.internal.os.OperatingSystem
import java.io.File
import java.nio.charset.StandardCharsets.UTF_8
import java.util.zip.ZipInputStream
/* doesn't work with: org.gradle.configuration-cache=true */
class BenchmarkHtmlReportPlugin : Plugin<Project> {
open class Extension {
var reportsDir: String? = "benchmark-reports"
var resourceDirName = "resources"
var openNewReportsInBrowser = false
}
lateinit var extension: Extension
override fun apply(project: Project) {
extension = project.extensions.create("benchmarkHtmlReport", Extension::class.java)
project.finalizeBenchmarkRunsByReportGenerator()
}
// TODO everything below 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!
private fun Project.finalizeBenchmarkRunsByReportGenerator() {
tasks.whenTaskAdded {
if (name=="benchmarkClasses") {
doLast {
val reportGenerator = createReportGenerator() ?: return@doLast
tasks // here we can get benchmark tasks that were created on the fly
.filter{ it.name.startsWith("benchmark") && it.name.endsWith("Benchmark") }
.forEach { it.finalizeBenchmarkRunsByReportGenerator(reportGenerator) }
}
}
}
}
private fun Project.createReportGenerator() : HtmlReportGenerator? {
return HtmlReportGenerator(
file(extension.reportsDir ?: return null),
extension.resourceDirName,
extension.openNewReportsInBrowser
)
}
private fun Task.finalizeBenchmarkRunsByReportGenerator(htmlGenerator : HtmlReportGenerator) {
doLast {
val jsonReportFile = determineJsonReportFile() ?: return@doLast
htmlGenerator.generateHtmlReport(jsonReportFile)
}
}
private fun Task.determineJsonReportFile(): File? {
val benchmarkDescriptionFile = (this as? JavaExec)
?.args
?.getOrNull(0)
?.let(::File)
?.takeIf { it.exists() }
?: return null
return benchmarkDescriptionFile
.readText(UTF_8)
.lineSequence()
.firstOrNull { it.startsWith("reportFile:") }
?.substringAfter("reportFile:")
?.takeIf{ it.endsWith(".json", ignoreCase = true) }
?.let(::File)
?.takeIf { it.exists() }
}
}
/** based on: https://github.com/jzillmann/gradle-jmh-report/blob/master/src/main/kotlin/io/morethan/jmhreport/gradle/task/JmhReportTask.kt */
internal class HtmlReportGenerator(
val outputDir : File,
val resourceDirectoryName : String = "resources",
val openNewReportsInBrowser : Boolean = false
) {
fun generateHtmlReport(jsonFile: File) {
if (!jsonFile.exists()) return
val reportFile = jsonFile.jsonToHtmlReportFile()
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 File.jsonToHtmlReportFile(): 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 = parentFile.parentFile.name
val benchmarkDateTime = parentFile.name
val benchmarkDate = benchmarkDateTime.substringBefore("T")
val benchmarkTime = benchmarkDateTime.substringAfter("T").substringBeforeLast(".").replace('.', '-')
return outputDir.resolve("${benchmarkName}__${benchmarkDate}__${benchmarkTime}.html")
}
private fun generateResourceDirectory(resourceDirectory : File) {
if (resourceDirectory.exists()) return
val zipStream = 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 = 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)
}
}
}
}
plugins {
id("deshe.benchmark.htmlreport")
}
// ##########################################################################
//whole block is optional
benchmarkHtmlReport {
reportsDir = "benchmark-reports"
resourceDirName = "resources"
openNewReportsInBrowser = true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment