Normalize benchmarks and changes in charts (#2820)

This commit is contained in:
LepilkinaElena
2019-03-28 10:40:31 +03:00
committed by GitHub
parent 31892bd82e
commit 7971090461
7 changed files with 163 additions and 17 deletions
@@ -40,6 +40,13 @@ object BintrayConnector : Connector() {
val fileParametersSize = 3 val fileParametersSize = 3
val fileDescription = fileLocation.substringAfter(connectorPrefix) val fileDescription = fileLocation.substringAfter(connectorPrefix)
val fileParameters = fileDescription.split(':', limit = fileParametersSize) val fileParameters = fileDescription.split(':', limit = fileParametersSize)
// Right link to bintray file.
if (fileParameters.size == 1) {
val accessFileUrl = "$bintrayUrl/${fileParameters[0]}"
return sendGetRequest(accessFileUrl, followLocation = true)
}
// Used builds description format.
if (fileParameters.size != fileParametersSize) { if (fileParameters.size != fileParametersSize) {
error("To get file from bintray, please, specify, build number from TeamCity and target" + error("To get file from bintray, please, specify, build number from TeamCity and target" +
" in format bintray:build_number:target:filename") " in format bintray:build_number:target:filename")
@@ -81,26 +88,50 @@ fun getFileContent(fileName: String, user: String? = null): String {
fun getBenchmarkReport(fileName: String, user: String? = null) = fun getBenchmarkReport(fileName: String, user: String? = null) =
BenchmarksReport.create(JsonTreeParser.parse(getFileContent(fileName, user))) BenchmarksReport.create(JsonTreeParser.parse(getFileContent(fileName, user)))
fun parseNormalizeResults(results: String): Map<String, Map<String, Double>> {
val parsedNormalizeResults = mutableMapOf<String, MutableMap<String, Double>>()
val tokensNumber = 3
results.lines().forEach {
if (!it.isEmpty()) {
val tokens = it.split(",").map { it.trim() }
if (tokens.size != tokensNumber) {
error("Data for normalization should include benchmark name, metric name and value. Got $it")
}
parsedNormalizeResults.getOrPut(tokens[0], { mutableMapOf<String, Double>() })[tokens[1]] = tokens[2].toDouble()
}
}
return parsedNormalizeResults
}
// Prints text summary by users request. // Prints text summary by users request.
fun summaryAction(argParser: ArgParser) { fun summaryAction(argParser: ArgParser) {
val benchsReport = SummaryBenchmarksReport(getBenchmarkReport(argParser.get<String>("mainReport")!!, argParser.get<String>("user"))) val benchsReport = SummaryBenchmarksReport(getBenchmarkReport(argParser.get<String>("mainReport")!!, argParser.get<String>("user")))
val results = mutableListOf<String>() val results = mutableListOf<String>()
val executionNormalize = argParser.get<String>("exec-normalize")?.let {
parseNormalizeResults(getFileContent(it))
}
val compileNormalize = argParser.get<String>("compile-normalize")?.let {
parseNormalizeResults(getFileContent(it))
}
val codesizeNormalize = argParser.get<String>("codesize-normalize")?.let {
parseNormalizeResults(getFileContent(it))
}
results.apply { results.apply {
add(benchsReport.failedBenchmarks.size.toString()) add(benchsReport.failedBenchmarks.size.toString())
argParser.getAll<String>("exec-samples")?. let { argParser.getAll<String>("exec-samples")?. let {
val filter = if (it.first() == "all") null else it val filter = if (it.first() == "all") null else it
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.EXECUTION_TIME, add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.EXECUTION_TIME,
argParser.get<String>("exec")!! == "geomean", filter).joinToString(";")) argParser.get<String>("exec")!! == "geomean", filter, executionNormalize).joinToString(";"))
} }
argParser.getAll<String>("compile-samples")?. let { argParser.getAll<String>("compile-samples")?. let {
val filter = if (it.first() == "all") null else it val filter = if (it.first() == "all") null else it
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.COMPILE_TIME, add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.COMPILE_TIME,
argParser.get<String>("compile")!! == "geomean", filter).joinToString(";")) argParser.get<String>("compile")!! == "geomean", filter, compileNormalize).joinToString(";"))
} }
argParser.getAll<String>("codesize-samples")?. let { argParser.getAll<String>("codesize-samples")?. let {
val filter = if (it.first() == "all") null else it val filter = if (it.first() == "all") null else it
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.CODE_SIZE, add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.CODE_SIZE,
argParser.get<String>("codesize")!! == "geomean", filter).joinToString(";")) argParser.get<String>("codesize")!! == "geomean", filter, codesizeNormalize).joinToString(";"))
} }
} }
println(results.joinToString()) println(results.joinToString())
@@ -117,16 +148,22 @@ fun main(args: Array<String>) {
OptionDescriptor(ArgType.String(), "exec-samples", OptionDescriptor(ArgType.String(), "exec-samples",
description = "Samples used for execution time metric (value 'all' allows use all samples)", description = "Samples used for execution time metric (value 'all' allows use all samples)",
delimiter = ","), delimiter = ","),
OptionDescriptor(ArgType.String(), "exec-normalize",
description = "File with golden results which should be used for normalization"),
OptionDescriptor(ArgType.Choice(listOf("samples", "geomean")), "compile", OptionDescriptor(ArgType.Choice(listOf("samples", "geomean")), "compile",
description = "Compile time way of calculation", defaultValue = "geomean"), description = "Compile time way of calculation", defaultValue = "geomean"),
OptionDescriptor(ArgType.String(), "compile-samples", OptionDescriptor(ArgType.String(), "compile-samples",
description = "Samples used for compile time metric (value 'all' allows use all samples)", description = "Samples used for compile time metric (value 'all' allows use all samples)",
delimiter = ","), delimiter = ","),
OptionDescriptor(ArgType.String(), "compile-normalize",
description = "File with golden results which should be used for normalization"),
OptionDescriptor(ArgType.Choice(listOf("samples", "geomean")), "codesize", OptionDescriptor(ArgType.Choice(listOf("samples", "geomean")), "codesize",
description = "Code size way of calculation", defaultValue = "geomean"), description = "Code size way of calculation", defaultValue = "geomean"),
OptionDescriptor(ArgType.String(), "codesize-samples", OptionDescriptor(ArgType.String(), "codesize-samples",
description = "Samples used for code size metric (value 'all' allows use all samples)", description = "Samples used for code size metric (value 'all' allows use all samples)",
delimiter = ","), delimiter = ","),
OptionDescriptor(ArgType.String(), "codesize-normalize",
description = "File with golden results which should be used for normalization"),
OptionDescriptor(ArgType.String(), "user", "u", "User access information for authorization") OptionDescriptor(ArgType.String(), "user", "u", "User access information for authorization")
), listOf(ArgDescriptor(ArgType.String(), "mainReport", "Main report for analysis")) ), listOf(ArgDescriptor(ArgType.String(), "mainReport", "Main report for analysis"))
) )
@@ -137,7 +174,7 @@ fun main(args: Array<String>) {
OptionDescriptor(ArgType.String(), "output", "o", "Output file"), OptionDescriptor(ArgType.String(), "output", "o", "Output file"),
OptionDescriptor(ArgType.Double(), "eps", "e", "Meaningful performance changes", "1.0"), OptionDescriptor(ArgType.Double(), "eps", "e", "Meaningful performance changes", "1.0"),
OptionDescriptor(ArgType.Boolean(), "short", "s", "Show short version of report", "false"), OptionDescriptor(ArgType.Boolean(), "short", "s", "Show short version of report", "false"),
OptionDescriptor(ArgType.Choice(listOf("text", "html", "teamcity", "statistics")), OptionDescriptor(ArgType.Choice(listOf("text", "html", "teamcity", "statistics", "metrics")),
"renders", "r", "Renders for showing information", "text", isMultiple = true), "renders", "r", "Renders for showing information", "text", isMultiple = true),
OptionDescriptor(ArgType.String(), "user", "u", "User access information for authorization") OptionDescriptor(ArgType.String(), "user", "u", "User access information for authorization")
) )
@@ -133,7 +133,8 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
} }
} }
fun getResultsByMetric(metric: BenchmarkResult.Metric, getGeoMean: Boolean = true, filter: List<String>? = null): List<Double> { fun getResultsByMetric(metric: BenchmarkResult.Metric, getGeoMean: Boolean = true, filter: List<String>? = null,
normalizeData: Map<String, Map<String, Double>>? = null): List<Double> {
val benchmarks = filter?.let { val benchmarks = filter?.let {
mergedReport.filter { entry -> mergedReport.filter { entry ->
filter.find { filter.find {
@@ -148,7 +149,14 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
if (filteredBenchmarks.isEmpty()) { if (filteredBenchmarks.isEmpty()) {
error("There is no benchmarks for metric $metric") error("There is no benchmarks for metric $metric")
} }
val results = filteredBenchmarks.map { entry -> entry.key.removeSuffix(metric.suffix) to entry.value.first!!.meanBenchmark.score }.toMap() val results = filteredBenchmarks.map { entry ->
val score = entry.value.first!!.meanBenchmark.score
val name = entry.key.removeSuffix(metric.suffix)
val value = normalizeData?.let {
it.get(name)?.get("$metric")?.let { score / it }
?: error("No normalization data for benchmark $name and metric $metric")
} ?: score
name to value }.toMap()
if (getGeoMean) { if (getGeoMean) {
return listOf(geometricMean(results.values)) return listOf(geometricMean(results.values))
} }
@@ -0,0 +1,40 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.BenchmarkResult
// Report render to text format.
class MetricResultsRender: Render() {
override val name: String
get() = "metrics"
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
val results = report.mergedReport.map { entry ->
buildString {
val metric = entry.value.first!!.meanBenchmark.metric
append("{ \"benchmarkName\": \"${entry.key.removeSuffix(metric.suffix)}\",")
append("\"metric\": \"${metric}\",")
append("\"value\": \"${entry.value.first!!.meanBenchmark.score}\" }")
}
}.joinToString(", ")
return "[ $results ]"
}
}
@@ -32,6 +32,7 @@ abstract class Render {
"html" -> HTMLRender() "html" -> HTMLRender()
"teamcity" -> TeamCityStatisticsRender() "teamcity" -> TeamCityStatisticsRender()
"statistics" -> StatisticsRender() "statistics" -> StatisticsRender()
"metrics" -> MetricResultsRender()
else -> error("Unknown render $name") else -> error("Unknown render $name")
} }
} }
+1 -1
View File
@@ -50,7 +50,7 @@ sealed class ArgType(val hasParameter: kotlin.Boolean) {
get() = "{ Value should be one of $values }" get() = "{ Value should be one of $values }"
override fun check(value: kotlin.String, name: kotlin.String) { override fun check(value: kotlin.String, name: kotlin.String) {
if (value !in values) error("Option $name is expected to be obe of $values. $value is provided.") if (value !in values) error("Option $name is expected to be one of $values. $value is provided.")
} }
} }
} }
@@ -24,6 +24,7 @@ const val teamCityUrl = "https://buildserver.labs.intellij.net/app/rest"
const val downloadBintrayUrl = "https://dl.bintray.com/content/lepilkinaelena/KotlinNativePerformance" const val downloadBintrayUrl = "https://dl.bintray.com/content/lepilkinaelena/KotlinNativePerformance"
const val uploadBintrayUrl = "https://api.bintray.com/content/lepilkinaelena/KotlinNativePerformance" const val uploadBintrayUrl = "https://api.bintray.com/content/lepilkinaelena/KotlinNativePerformance"
const val buildsFileName = "buildsSummary.csv" const val buildsFileName = "buildsSummary.csv"
const val goldenResultsFileName = "goldenResults.csv"
const val bintrayPackage = "builds" const val bintrayPackage = "builds"
const val buildsInfoPartsNumber = 11 const val buildsInfoPartsNumber = 11
@@ -109,6 +110,9 @@ object LocalCache {
} }
} }
data class GoldenResult(val benchmarkName: String, val metric: String, val value: Double)
data class GoldenResultsInfo(val bintrayUser: String, val bintrayPassword: String, val goldenResults: Array<GoldenResult>)
// Build information provided from request. // Build information provided from request.
data class BuildInfo(val buildNumber: String, val branch: String, val startTime: String, data class BuildInfo(val buildNumber: String, val branch: String, val startTime: String,
val finishTime: String) val finishTime: String)
@@ -197,11 +201,7 @@ fun checkBuildType(currentType: String, targetType: String): Boolean {
fun prepareBuildsResponse(builds: Collection<String>, type: String, branch: String, buildNumber: String? = null): List<Build> { fun prepareBuildsResponse(builds: Collection<String>, type: String, branch: String, buildNumber: String? = null): List<Build> {
val buildsObjects = mutableListOf<Build>() val buildsObjects = mutableListOf<Build>()
builds.forEach { builds.forEach {
val tokens = it.split(",").map { it.trim() } val tokens = buildDescriptionToTokens(it)
if (tokens.size != buildsInfoPartsNumber) {
error("Build description $it doesn't contain all necessary information. " +
"File with data could be corrupted.")
}
if ((checkBuildType(tokens[5], type) || type == "day") && (branch == tokens[3] || branch == "all") if ((checkBuildType(tokens[5], type) || type == "day") && (branch == tokens[3] || branch == "all")
|| tokens[0] == buildNumber) { || tokens[0] == buildNumber) {
buildsObjects.add(Build(tokens[0], tokens[1], tokens[2], tokens[3], buildsObjects.add(Build(tokens[0], tokens[1], tokens[2], tokens[3],
@@ -212,6 +212,15 @@ fun prepareBuildsResponse(builds: Collection<String>, type: String, branch: Stri
return buildsObjects return buildsObjects
} }
fun buildDescriptionToTokens(buildDescription: String): List<String> {
val tokens = buildDescription.split(",").map { it.trim() }
if (tokens.size != buildsInfoPartsNumber) {
error("Build description $buildDescription doesn't contain all necessary information. " +
"File with data could be corrupted.")
}
return tokens
}
// Routing of requests to current server. // Routing of requests to current server.
fun router() { fun router() {
val express = require("express") val express = require("express")
@@ -262,6 +271,20 @@ fun router() {
response.sendStatus(200) response.sendStatus(200)
}) })
// Register golden results to normalize on Bintray.
router.post("/registerGolden", { request, response ->
val goldenResultsInfo = JSON.parse<GoldenResultsInfo>(JSON.stringify(request.body))
val buildsDescription = StringBuilder(sendGetRequest("$downloadBintrayUrl/$goldenResultsFileName"))
goldenResultsInfo.goldenResults.forEach {
buildsDescription.append("${it.benchmarkName}, ${it.metric}, ${it.value}\n")
}
// Upload new version of file.
val uploadUrl = "$uploadBintrayUrl/$bintrayPackage/latest/$goldenResultsFileName?publish=1&override=1"
sendUploadRequest(uploadUrl, buildsDescription.toString(), goldenResultsInfo.bintrayUser, goldenResultsInfo.bintrayPassword)
// Send response.
response.sendStatus(200)
})
// Get list of builds. // Get list of builds.
router.get("/builds/:target/:type/:branch/:id", { request, response -> router.get("/builds/:target/:type/:branch/:id", { request, response ->
val builds = LocalCache[request.params.target, request.params.id] val builds = LocalCache[request.params.target, request.params.id]
@@ -273,6 +296,16 @@ fun router() {
response.json(prepareBuildsResponse(builds, request.params.type, request.params.branch)) response.json(prepareBuildsResponse(builds, request.params.type, request.params.branch))
}) })
router.get("/branches/:target", { request, response ->
val builds = LocalCache[request.params.target]
response.json(builds.map { buildDescriptionToTokens(it)[3] }.distinct())
})
router.get("/buildsNumbers/:target", { request, response ->
val builds = LocalCache[request.params.target]
response.json(builds.map { buildDescriptionToTokens(it)[0] }.distinct())
})
router.get("/clean", { _, response -> router.get("/clean", { _, response ->
LocalCache.clean() LocalCache.clean()
response.sendStatus(200) response.sendStatus(200)
@@ -19,6 +19,7 @@ import org.w3c.xhr.*
import org.jetbrains.report.json.* import org.jetbrains.report.json.*
import org.jetbrains.build.Build import org.jetbrains.build.Build
import kotlin.js.* import kotlin.js.*
import kotlin.math.ceil
import org.w3c.dom.* import org.w3c.dom.*
// API for interop with JS library Chartist. // API for interop with JS library Chartist.
@@ -30,6 +31,7 @@ external class ChartistPlugins {
external object Chartist { external object Chartist {
class Svg(form: String, parameters: dynamic, chartArea: String) class Svg(form: String, parameters: dynamic, chartArea: String)
val plugins: ChartistPlugins val plugins: ChartistPlugins
val Interpolation: dynamic
fun Line(query: String, data: dynamic, options: dynamic): dynamic fun Line(query: String, data: dynamic, options: dynamic): dynamic
} }
@@ -87,6 +89,15 @@ fun getChartOptions(samples: Array<String>, yTitle: String, classNames: Array<St
chartOptions["chartPadding"] = paddingObject chartOptions["chartPadding"] = paddingObject
val axisXObject: dynamic = object{} val axisXObject: dynamic = object{}
axisXObject["offset"] = 40 axisXObject["offset"] = 40
axisXObject["labelInterpolationFnc"] = { value, index, labels ->
val labelsCount = 30
val skipNumber = ceil((labels.length as Int).toDouble() / labelsCount).toInt()
if (skipNumber > 1) {
if (index % skipNumber == 0) value else null
} else {
value
}
}
chartOptions["axisX"] = axisXObject chartOptions["axisX"] = axisXObject
val axisYObject: dynamic = object{} val axisYObject: dynamic = object{}
axisYObject["offset"] = 90 axisYObject["offset"] = 90
@@ -105,6 +116,9 @@ fun getChartOptions(samples: Array<String>, yTitle: String, classNames: Array<St
axisYTitle["textAnchor"] = "middle" axisYTitle["textAnchor"] = "middle"
axisYTitle["flipTitle"] = true axisYTitle["flipTitle"] = true
titleObject["axisY"] = axisYTitle titleObject["axisY"] = axisYTitle
val interpolationObject: dynamic = {}
interpolationObject["fillHoles"] = true
chartOptions["lineSmooth"] = Chartist.Interpolation.simple(interpolationObject)
chartOptions["plugins"] = arrayOf(Chartist.plugins.legend(legendObject), Chartist.plugins.ctAxisTitle(titleObject)) chartOptions["plugins"] = arrayOf(Chartist.plugins.legend(legendObject), Chartist.plugins.ctAxisTitle(titleObject))
return chartOptions return chartOptions
} }
@@ -148,6 +162,7 @@ fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynam
else ""}" else ""}"
val information = buildString { val information = buildString {
append("<a href=\"$linkToDetailedInfo\">${currentBuild.buildNumber}</a><br>") append("<a href=\"$linkToDetailedInfo\">${currentBuild.buildNumber}</a><br>")
append("Value: ${data.value.y.toFixed(4)}<br>")
if (currentBuild.failuresNumber > 0) { if (currentBuild.failuresNumber > 0) {
append("failures: ${currentBuild.failuresNumber}<br>") append("failures: ${currentBuild.failuresNumber}<br>")
} }
@@ -180,7 +195,7 @@ fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynam
} }
fun main(args: Array<String>) { fun main(args: Array<String>) {
val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg/builds" val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg"
// Get parameters from request. // Get parameters from request.
val url = window.location.href val url = window.location.href
@@ -196,7 +211,7 @@ fun main(args: Array<String>) {
// Get builds. // Get builds.
val buildsUrl = buildString { val buildsUrl = buildString {
append("$serverUrl") append("$serverUrl/builds")
append("/${parameters["target"]}") append("/${parameters["target"]}")
append("/${parameters["type"]}") append("/${parameters["type"]}")
append("/${parameters["branch"]}") append("/${parameters["branch"]}")
@@ -208,10 +223,16 @@ fun main(args: Array<String>) {
if (data !is JsonArray) { if (data !is JsonArray) {
error("Response is expected to be an array.") error("Response is expected to be an array.")
} }
val builds = data.jsonArray.map { Build.create(it as JsonObject) } val builds = data.jsonArray.map { Build.create(it as JsonObject) }.sortedBy { it.buildNumber.substringAfterLast("-") }
val branchesUrl = "$serverUrl/branches/${parameters["target"]}"
val branches: Array<String> = JSON.parse(sendGetRequest(branchesUrl))
val releaseBranches = branches.filter { "v\\d+\\.\\d+\\.\\d+-fixes".toRegex().find(it) != null }
// Fill autocomplete list. // Fill autocomplete list.
val buildsNumbers = builds.map { json("value" to it.buildNumber, "data" to it.buildNumber) }.toTypedArray() val buildsNumbersUrl = "$serverUrl/buildsNumbers/${parameters["target"]}"
val buildsNumbers: Array<String> = JSON.parse(sendGetRequest(buildsNumbersUrl))
// Change inputs values connected with parameters and add events listeners. // Change inputs values connected with parameters and add events listeners.
document.querySelector("#inputGroupTarget [value=\"${parameters["target"]}\"]")?.setAttribute("selected", "true") document.querySelector("#inputGroupTarget [value=\"${parameters["target"]}\"]")?.setAttribute("selected", "true")
@@ -245,6 +266,12 @@ fun main(args: Array<String>) {
} }
}) })
// Add release branches to selector.
releaseBranches.forEach {
val option = Option(it, it)
js("$('#inputGroupBranch')").append(js("$(option)"))
}
val autocompleteParameters: dynamic = object{} val autocompleteParameters: dynamic = object{}
autocompleteParameters["lookup"] = buildsNumbers autocompleteParameters["lookup"] = buildsNumbers
autocompleteParameters["onSelect"] = { suggestion -> autocompleteParameters["onSelect"] = { suggestion ->
@@ -294,7 +321,7 @@ fun main(args: Array<String>) {
getChartOptions(compileTime.keys.toTypedArray(), "Time, milliseconds")) getChartOptions(compileTime.keys.toTypedArray(), "Time, milliseconds"))
val codeSizeChart = Chartist.Line("#codesize_chart", getChartData(labels, codeSize.values, sizeClassName), val codeSizeChart = Chartist.Line("#codesize_chart", getChartData(labels, codeSize.values, sizeClassName),
getChartOptions(codeSize.keys.toTypedArray(), "Size, KB", arrayOf("ct-series-2"))) getChartOptions(codeSize.keys.toTypedArray(), "Size, KB", arrayOf("ct-series-2")))
val bundleSizeChart = Chartist.Line("#bundlesize_chart", getChartData(labels, listOf(bundleSize), "ct-series-d"), val bundleSizeChart = Chartist.Line("#bundlesize_chart", getChartData(labels, listOf(bundleSize), sizeClassName),
getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-2"))) getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-2")))
// Tooltips and higlights. // Tooltips and higlights.