Last active
December 12, 2024 22:16
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
implementation-class=deshe.benchmark.BenchmarkHtmlReportPlugin |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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