Use ElasticSearch as data storage for performance monitoring system (#4302)

This commit is contained in:
LepilkinaElena
2020-08-26 09:23:32 +03:00
committed by GitHub
parent c817ae9f4e
commit 33dc51b0d3
57 changed files with 2726 additions and 1519 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ dependencies {
}
sourceSets["main"].withConvention(KotlinSourceSet::class) {
kotlin.srcDir("$projectDir/../tools/benchmarks/shared/src")
kotlin.srcDir("$projectDir/../tools/benchmarks/shared/src/main/kotlin/report")
}
gradlePlugin {
@@ -6,46 +6,31 @@
package org.jetbrains.kotlin
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.jetbrains.report.json.*
import java.io.FileInputStream
import java.io.OutputStreamWriter
import java.io.BufferedReader
import java.io.FileInputStream
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import java.util.Properties
import java.util.*
typealias performanceAdditionalResult = Triple<String, String, String>
/**
* Task to produce regressions report and send it to slack. Requires a report with current benchmarks result
* and path to analyzer tool
* Task to save benchmarks results on server.
*
* @property currentBenchmarksReportFile path to file with becnhmarks result
* @property analyzer path to analyzer tool
* @property bundleSize size of build
* @property onlyBranch register only builds for branch
* @property fileWithResult json file with benchmarks run results
*/
open class BuildRegister : DefaultTask() {
@Input
lateinit var currentBenchmarksReportFile: String
@Input
lateinit var analyzer: String
var onlyBranch: String? = null
var bundleSize: Int? = null
var fileWithResult: String = "nativeReport.json"
val buildInfoTokens: Int = 4
val additionalInfoTokens: Int = 3
val compileTimeSamplesNumber: Int = 2
val buildNumberRegex: Regex = "\\d+(\\.\\d+)+(-M\\d)?-(\\w+)-\\d+".toRegex()
val performanceServer = "https://kotlin-native-perf-summary.labs.jb.gg"
private fun sendPostRequest(url: String, body: String) : String {
private fun sendPostRequest(url: String, body: String): String {
val connection = URL(url).openConnection() as HttpURLConnection
return connection.apply {
setRequestProperty("Content-Type", "application/json; charset=utf-8")
@@ -71,131 +56,33 @@ open class BuildRegister : DefaultTask() {
}
}
fun getAdditionalInfo(analyzer: String, reportFile: String, alwaysExists: Boolean, benchmarkName: String,
showedName: String = benchmarkName) :
performanceAdditionalResult? {
val output = arrayOf(analyzer, "summary", "--compile", "samples",
"--compile-samples", benchmarkName, "--codesize-samples", benchmarkName,
"--codesize-normalize", "artifactory:builds/goldenResults.csv", reportFile)
.runCommand()
val buildInfoParts = output.split(',')
if (buildInfoParts.size != additionalInfoTokens) {
val message = "Problems with getting summary information using $analyzer and $reportFile. $output"
if (!alwaysExists) {
println(message)
return null
}
error(message)
}
val (failures, compileTime, codeSize) = buildInfoParts.map { it.trim() }
val codeSizeInfo = "$showedName-$codeSize"
val compileTimeInfo = "$showedName-$compileTime"
return performanceAdditionalResult(failures, compileTimeInfo, codeSizeInfo)
}
@TaskAction
fun run() {
// Get TeamCity properties.
val teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") ?:
error("Can't load teamcity config!")
val teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") ?: error("Can't load teamcity config!")
val buildProperties = Properties()
buildProperties.load(FileInputStream(teamcityConfig))
val buildId = buildProperties.getProperty("teamcity.build.id")
val teamCityUser = buildProperties.getProperty("teamcity.auth.userId")
val teamCityPassword = buildProperties.getProperty("teamcity.auth.password")
val buildNumber = buildProperties.getProperty("build.number")
val apiKey = buildProperties.getProperty("artifactory.apikey")
// Get branch.
val currentBuild = getBuild("id:$buildId", teamCityUser, teamCityPassword)
val branch = getBuildProperty(currentBuild,"branchName")
val target = System.getProperty("os.name").replace("\\s".toRegex(), "")
// Get summary information.
val output = arrayOf("$analyzer", "summary", "--exec-samples", "all", "--compile", "samples",
"--compile-samples", "HelloWorld,Videoplayer", "--codesize-samples", "all",
"--exec-normalize", "artifactory:builds/goldenResults.csv",
"--codesize-normalize", "artifactory:builds/goldenResults.csv", "$currentBenchmarksReportFile")
.runCommand()
// Postprocess information.
val buildInfoParts = output.split(',')
if (buildInfoParts.size != buildInfoTokens) {
error("Problems with getting summary information using $analyzer and $currentBenchmarksReportFile. $output")
}
val (failures, executionTime, compileTime, codeSize) = buildInfoParts.map { it.trim() }
var failuresNumber = failures.toInt()
// Add legends.
val geometricMean = "Geometric Mean-"
val executionTimeInfo = "$geometricMean$executionTime"
var codeSizeInfo = "$geometricMean$codeSize"
val compileTimeSamples = compileTime.split(';')
if (compileTimeSamples.size != compileTimeSamplesNumber) {
error("Problems with getting compile time samples value. Expected at least $compileTimeSamplesNumber samples, got ${compileTimeSamples.size}")
}
val (helloWorldCompile, videoplayerCompile) = compileTimeSamples
var compileTimeInfo = "HelloWorld-$helloWorldCompile;Videoplayer-$videoplayerCompile"
// Collect framework run details.
if (target == "MacOSX") {
val frameworkResults = getAdditionalInfo(analyzer, currentBenchmarksReportFile, true,
"FrameworkBenchmarksAnalyzer")
frameworkResults?.let {
val (_, frameworkCompileTime, frameworkCodeSize) = it
codeSizeInfo += ";$frameworkCodeSize"
compileTimeInfo += ";$frameworkCompileTime"
}
val spaceResults = getAdditionalInfo(analyzer,
"artifactory:$buildNumber:$target:spaceFrameworkReport.json", false,
"circlet_iosX64", "SpaceFramework_iosX64")
spaceResults?.let {
val (failures, frameworkCompileTime, frameworkCodeSize) = it
failuresNumber += failures.toInt()
codeSizeInfo += ";$frameworkCodeSize"
compileTimeInfo += ";$frameworkCompileTime"
}
}
if (target == "Linux") {
val coroutinesResults = getAdditionalInfo(analyzer,
"artifactory:$buildNumber:$target:externalReport.json", false,
"kotlinx.coroutines")
coroutinesResults?.let {
val (failures, libraryCompileTime, libraryCodeSize) = it
failuresNumber += failures.toInt()
codeSizeInfo += ";$libraryCodeSize"
compileTimeInfo += ";$libraryCompileTime"
}
}
val matchResult = buildNumberRegex.find(buildNumber) ?: error("Wrong format of build number $buildNumber.")
val buildType = matchResult.groups[3]!!.value
val branch = getBuildProperty(currentBuild, "branchName")
// Send post request to register build.
val requestBody = buildString {
append("{\"buildId\":\"$buildId\",")
append("\"teamCityUser\":\"$teamCityUser\",")
append("\"teamCityPassword\":\"$teamCityPassword\",")
append("\"artifactoryApiKey\":\"$apiKey\",")
append("\"target\": \"$target\",")
append("\"buildType\": \"$buildType\",")
append("\"failuresNumber\": $failuresNumber,")
append("\"executionTime\": \"$executionTimeInfo\",")
append("\"compileTime\": \"$compileTimeInfo\",")
append("\"codeSize\": \"$codeSizeInfo\",")
append("\"bundleSize\": ${bundleSize?.let {"\"$bundleSize\""} ?: bundleSize}}")
append("\"fileWithResult\":\"$fileWithResult\",")
append("\"bundleSize\": ${bundleSize?.let { "\"$bundleSize\"" } ?: bundleSize}}")
}
if (onlyBranch == null || onlyBranch == branch) {
println(sendPostRequest("$performanceServer/register", requestBody))
} else {
println("Skipping registration. Current branch $branch, need registration for $onlyBranch!")
}
}
}
@@ -104,7 +104,7 @@ fun createJsonReport(projectProperties: Map<String, Any>): String {
val kotlin = Compiler(backend, getValue("kotlinVersion"))
val benchDesc = getValue("benchmarks")
val benchmarksArray = JsonTreeParser.parse(benchDesc)
val benchmarks = BenchmarksReport.parseBenchmarksArray(benchmarksArray)
val benchmarks = parseBenchmarksArray(benchmarksArray)
.union(projectProperties["compileTime"] as List<BenchmarkResult>).union(
listOf(projectProperties["codeSize"] as? BenchmarkResult).filterNotNull()).toList()
val report = BenchmarksReport(env, benchmarks, kotlin)
+17 -14
View File
@@ -56,7 +56,8 @@ private String findAnalyzerBinary() {
// Produce and send slack report.
task slackReport(type: RegressionsReporter) {
def analyzerBinary = findAnalyzerBinary()
def analyzerBinary = MPPTools.findFile("${analyzerTool}${MPPTools.getNativeProgramExtension()}",
"${rootBuildDirectory}/${analyzerToolDirectory}")
def teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE")
if (teamcityConfig && analyzerBinary != null) {
// Create folder for report (root Kotlin project settings make create report in separate folder).
@@ -159,21 +160,23 @@ task registerExternalBenchmarks {
}
task registerBuild(type: BuildRegister) {
def analyzerBinary = findAnalyzerBinary()
if (analyzerBinary != null) {
onlyBranch = project.findProperty('kotlin.register.branch')
// Get bundle size.
bundleSize = null
if (project.findProperty('kotlin.bundleBuild') != null) {
def dist = findProperty('kotlin.native.home') ?: 'dist'
dist = (new File(dist)).isAbsolute() ? dist : "${project.getProjectDir()}/$dist"
bundleSize = (new File(dist)).directorySize()
}
currentBenchmarksReportFile = "${buildDir.absolutePath}/${nativeJson}"
analyzer = analyzerBinary
onlyBranch = project.findProperty('kotlin.register.branch')
// Get bundle size.
bundleSize = null
if (project.findProperty('kotlin.bundleBuild') != null) {
def dist = findProperty('kotlin.native.home') ?: 'dist'
dist = (new File(dist)).isAbsolute() ? dist : "${project.getProjectDir()}/$dist"
bundleSize = (new File(dist)).directorySize()
}
}
task registerExternalBuild(type: BuildRegister) {
onlyBranch = project.findProperty('kotlin.register.branch')
fileWithResult = externalBenchmarksReport
}
registerExternalBenchmarks.finalizedBy registerExternalBuild
def mergeReports(String fileName) {
def reports = []
subprojects.each {
@@ -265,4 +268,4 @@ task videoplayer {
task KotlinVsSwift {
dependsOn 'clean'
dependsOn 'KotlinVsSwift:konanRun'
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ val defaultBuildType = NativeBuildType.RELEASE
benchmark {
applicationName = "Cinterop"
commonSrcDirs = listOf("../../tools/benchmarks/shared/src", "src/main/kotlin", "../shared/src/main/kotlin")
commonSrcDirs = listOf("../../tools/benchmarks/shared/src/main/kotlin/report", "src/main/kotlin", "../shared/src/main/kotlin")
jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/mingw")
+1 -1
View File
@@ -17,7 +17,7 @@ val defaultBuildType = NativeBuildType.RELEASE
benchmark {
applicationName = "Numerical"
commonSrcDirs = listOf("src/main/kotlin", "../../tools/benchmarks/shared/src", "../shared/src/main/kotlin")
commonSrcDirs = listOf("src/main/kotlin", "../../tools/benchmarks/shared/src/main/kotlin/report", "../shared/src/main/kotlin")
jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("../shared/src/main/kotlin-native/mingw")
+1 -1
View File
@@ -17,7 +17,7 @@ val defaultBuildType = NativeBuildType.RELEASE
benchmark {
applicationName = "ObjCInterop"
commonSrcDirs = listOf("../../tools/benchmarks/shared/src", "src/main/kotlin", "../shared/src/main/kotlin")
commonSrcDirs = listOf("../../tools/benchmarks/shared/src/main/kotlin/report", "src/main/kotlin", "../shared/src/main/kotlin")
jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/mingw")
+1 -1
View File
@@ -13,7 +13,7 @@ val defaultBuildType = NativeBuildType.RELEASE
benchmark {
applicationName = "Ring"
commonSrcDirs = listOf("../../tools/benchmarks/shared/src", "src/main/kotlin", "../shared/src/main/kotlin")
commonSrcDirs = listOf("../../tools/benchmarks/shared/src/main/kotlin/report", "src/main/kotlin", "../shared/src/main/kotlin")
jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/mingw")
+1 -1
View File
@@ -15,7 +15,7 @@ val defaultBuildType = NativeBuildType.RELEASE
benchmark {
applicationName = "Startup"
commonSrcDirs = listOf("../../tools/benchmarks/shared/src", "src/main/kotlin", "../shared/src/main/kotlin")
commonSrcDirs = listOf("../../tools/benchmarks/shared/src/main/kotlin/report", "src/main/kotlin", "../shared/src/main/kotlin")
jvmSrcDirs = listOf("../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("../shared/src/main/kotlin-native/mingw")
+1 -1
View File
@@ -15,7 +15,7 @@ val defaultBuildType = NativeBuildType.RELEASE
swiftBenchmark {
applicationName = "swiftInterop"
commonSrcDirs = listOf("$toolsPath/benchmarks/shared/src/main/kotlin", "src", "../shared/src/main/kotlin")
commonSrcDirs = listOf("$toolsPath/benchmarks/shared/src/main/kotlin/report", "src", "../shared/src/main/kotlin")
nativeSrcDirs = listOf("../shared/src/main/kotlin-native/common", "../shared/src/main/kotlin-native/posix")
swiftSources = listOf("$projectDir/swiftSrc/benchmarks.swift", "$projectDir/swiftSrc/main.swift")
compileTasks = listOf("compileKotlinNative", "linkBenchmarkReleaseFrameworkNative")
@@ -1,20 +1,8 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
// Report with changes of different fields.
@@ -0,0 +1,131 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.MeanVariance
import org.jetbrains.report.MeanVarianceBenchmark
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
val MeanVariance.description: String
get() {
val format = { number: Double -> number.format(2) }
return "${format(mean)} ± ${format(variance)}"
}
val MeanVarianceBenchmark.description: String
get() = "${score.format()} ± ${variance.format()}"
// Calculate difference in percentage compare to another.
fun MeanVarianceBenchmark.calcPercentageDiff(other: MeanVarianceBenchmark): MeanVariance {
assert(other.score >= 0 &&
other.variance >= 0 &&
other.score - other.variance != 0.0,
{ "Mean and variance should be positive and not equal!" })
// Analyze intervals. Calculate difference between border points.
val (bigValue, smallValue) = if (score > other.score) Pair(this, other) else Pair(other, this)
val bigValueIntervalStart = bigValue.score - bigValue.variance
val bigValueIntervalEnd = bigValue.score + bigValue.variance
val smallValueIntervalStart = smallValue.score - smallValue.variance
val smallValueIntervalEnd = smallValue.score + smallValue.variance
if (smallValueIntervalEnd > bigValueIntervalStart) {
// Interval intersect.
return MeanVariance(0.0, 0.0)
}
val mean = ((smallValueIntervalEnd - bigValueIntervalStart) / bigValueIntervalStart) *
(if (score > other.score) -1 else 1)
val maxValueChange = ((bigValueIntervalEnd - smallValueIntervalEnd) / bigValueIntervalEnd)
val minValueChange = ((bigValueIntervalStart - smallValueIntervalStart) / bigValueIntervalStart)
val variance = abs(abs(mean) - max(minValueChange, maxValueChange))
return MeanVariance(mean * 100, variance * 100)
}
// Calculate ratio value compare to another.
fun MeanVarianceBenchmark.calcRatio(other: MeanVarianceBenchmark): MeanVariance {
assert(other.score >= 0 &&
other.variance >= 0 &&
other.score - other.variance != 0.0,
{ "Mean and variance should be positive and not equal!" })
val mean = score / other.score
val minRatio = (score - variance) / (other.score + other.variance)
val maxRatio = (score + variance) / (other.score - other.variance)
val ratioConfInt = min(abs(minRatio - mean), abs(maxRatio - mean))
return MeanVariance(mean, ratioConfInt)
}
fun geometricMean(values: Collection<Double>, totalNumber: Int = values.size) =
with(values.asSequence().filter { it != 0.0 }) {
if (count() == 0) {
0.0
} else {
map { it.pow(1.0 / totalNumber) }.reduce { a, b -> a * b }
}
}
fun computeMeanVariance(samples: List<Double>): MeanVariance {
val zStar = 1.67 // Critical point for 90% confidence of normal distribution.
val mean = samples.sum() / samples.size
val variance = samples.indices.sumByDouble { (samples[it] - mean) * (samples[it] - mean) } / samples.size
val confidenceInterval = sqrt(variance / samples.size) * zStar
return MeanVariance(mean, confidenceInterval)
}
// Calculate average results for benchmarks (each benchmark can be run several times).
fun collectMeanResults(benchmarks: Map<String, List<BenchmarkResult>>): BenchmarksTable {
return benchmarks.map { (name, resultsSet) ->
val repeatedSequence = IntArray(resultsSet.size)
var metric = BenchmarkResult.Metric.EXECUTION_TIME
var currentStatus = BenchmarkResult.Status.PASSED
var currentWarmup = -1
// Results can be already processed.
if (resultsSet[0] is MeanVarianceBenchmark) {
assert(resultsSet.size == 1) { "Several MeanVarianceBenchmark instances." }
name to resultsSet[0] as MeanVarianceBenchmark
} else {
// Collect common benchmark values and check them.
resultsSet.forEachIndexed { index, result ->
// If there was at least one failure, summary is marked as failure.
if (result.status == BenchmarkResult.Status.FAILED) {
currentStatus = result.status
}
repeatedSequence[index] = result.repeat
if (currentWarmup != -1)
if (result.warmup != currentWarmup)
println("Check data consistency. Warmup value for benchmark '${result.name}' differs.")
currentWarmup = result.warmup
metric = result.metric
}
repeatedSequence.sort()
// Check if there are missed loop during running benchmarks.
repeatedSequence.forEachIndexed { index, element ->
if (index != 0)
if ((element - repeatedSequence[index - 1]) != 1)
println("Check data consistency. For benchmark '$name' there is no run" +
" between ${repeatedSequence[index - 1]} and $element.")
}
// Create mean and variance benchmarks result.
val scoreMeanVariance = computeMeanVariance(resultsSet.map { it.score })
val runtimeInUsMeanVariance = computeMeanVariance(resultsSet.map { it.runtimeInUs })
val meanBenchmark = MeanVarianceBenchmark(name, currentStatus, scoreMeanVariance.mean, metric,
runtimeInUsMeanVariance.mean, repeatedSequence[resultsSet.size - 1],
currentWarmup, scoreMeanVariance.variance)
name to meanBenchmark
}
}.toMap()
}
fun collectBenchmarksDurations(benchmarks: Map<String, List<BenchmarkResult>>): Map<String, Double> =
benchmarks.map { (name, resultsSet) ->
name to resultsSet.sumByDouble { it.runtimeInUs }
}.toMap()
@@ -1,27 +1,17 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import kotlin.math.abs
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.Environment
import org.jetbrains.report.Compiler
import org.jetbrains.report.BenchmarksReport
import org.jetbrains.report.Compiler
import org.jetbrains.report.Environment
import org.jetbrains.report.MeanVariance
import org.jetbrains.report.MeanVarianceBenchmark
import kotlin.math.abs
typealias SummaryBenchmark = Pair<MeanVarianceBenchmark?, MeanVarianceBenchmark?>
typealias BenchmarksTable = Map<String, MeanVarianceBenchmark>
@@ -29,12 +19,12 @@ typealias SummaryBenchmarksTable = Map<String, SummaryBenchmark>
typealias ScoreChange = Pair<MeanVariance, MeanVariance>
// Summary report with comparasion of separate benchmarks results.
class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
val previousReport: BenchmarksReport? = null,
val meaningfulChangesValue: Double = 0.5) {
class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
val previousReport: BenchmarksReport? = null,
val meaningfulChangesValue: Double = 0.5) {
// Report created by joining comparing reports.
val mergedReport: Map<String, SummaryBenchmark>
val benchmarksDurations: Map<String, Pair<Double?, Double?>>
private val benchmarksDurations: Map<String, Pair<Double?, Double?>>
// Lists of benchmarks in different status.
private val benchmarksWithChangedStatus = mutableListOf<FieldChange<BenchmarkResult.Status>>()
@@ -56,8 +46,8 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
// Countable properties.
val failedBenchmarks: List<String>
get() = mergedReport.filter { it.value.first?.meanBenchmark?.status == BenchmarkResult.Status.FAILED }
.map { it.key }
get() = mergedReport.filter { it.value.first?.status == BenchmarkResult.Status.FAILED }
.map { it.key }
val addedBenchmarks: List<String>
get() = mergedReport.filter { it.value.second == null }.map { it.key }
@@ -72,7 +62,7 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
get() = mergedReport.filter { it.value.first != null }.map { it.value.first!! }
val currentBenchmarksDuration: Map<String, Double>
get() = benchmarksDurations.filter{ it.value.first != null }.map { it.key to it.value.first!! }.toMap()
get() = benchmarksDurations.filter { it.value.first != null }.map { it.key to it.value.first!! }.toMap()
val maximumRegression: Double
get() = getMaximumChange(regressions)
@@ -133,8 +123,15 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
}
}
// Get benchmark report.
fun getBenchmarksReport(takeMainReport: Boolean = true) =
if (takeMainReport)
BenchmarksReport(environments.first, mergedReport.map { (_, value) -> value.first!! }, compilers.first)
else
BenchmarksReport(environments.second!!, mergedReport.map { (_, value) -> value.second!! }, compilers.second!!)
fun getResultsByMetric(metric: BenchmarkResult.Metric, getGeoMean: Boolean = true, filter: List<String>? = null,
normalizeData: Map<String, Map<String, Double>>? = null): List<Double> {
normalizeData: Map<String, Map<String, Double>>? = null): List<Double?> {
val benchmarks = filter?.let {
mergedReport.filter { entry ->
filter.find {
@@ -142,30 +139,26 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
} != null
}
} ?: mergedReport
if (benchmarks.isEmpty()) {
error("There is no benchmarks from provided list")
}
val filteredBenchmarks = benchmarks.filter { entry -> entry.value.first!!.meanBenchmark.metric == metric }
if (filteredBenchmarks.isEmpty()) {
error("There is no benchmarks for metric $metric")
}
val results = filteredBenchmarks.map { entry ->
val score = entry.value.first!!.meanBenchmark.score
val results = benchmarks.map { entry ->
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 (entry.value.first!!.metric == metric) {
val score = entry.value.first!!.score
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
} else name to null
}.toMap()
if (getGeoMean) {
return listOf(geometricMean(results.values))
return listOf(geometricMean(results.values.filterNotNull()))
}
return filter?.let { it.map { results[it] ?: error("Benchmark $it for metric $metric doesn't exist.") }.toList() } ?: results.values.toList()
return filter?.let { it.map { results[it] }.toList() } ?: results.values.toList()
}
private fun getMaximumChange(bucket: Map<String, ScoreChange>): Double =
// Maps of regressions and improvements are sorted.
if (bucket.isEmpty()) 0.0 else bucket.values.map { it.first.mean }.first()
// Maps of regressions and improvements are sorted.
if (bucket.isEmpty()) 0.0 else bucket.values.map { it.first.mean }.first()
private fun getGeometricMeanOfChanges(bucket: Map<String, ScoreChange>): Double {
if (bucket.isEmpty())
@@ -185,11 +178,9 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
// Create geometric mean.
private fun createGeoMeanBenchmark(benchTable: BenchmarksTable): MeanVarianceBenchmark {
val geoMeanBenchmarkName = "Geometric mean"
val geoMean = geometricMean(benchTable.toList().map { (_, value) -> value.meanBenchmark.score })
val varianceGeoMean = geometricMean(benchTable.toList().map { (_, value) -> value.varianceBenchmark.score })
val meanBenchmark = BenchmarkResult(geoMeanBenchmarkName, geoMean)
val varianceBenchmark = BenchmarkResult(geoMeanBenchmarkName, varianceGeoMean)
return MeanVarianceBenchmark(meanBenchmark, varianceBenchmark)
val geoMean = geometricMean(benchTable.toList().map { (_, value) -> value.score })
val varianceGeoMean = geometricMean(benchTable.toList().map { (_, value) -> value.variance })
return MeanVarianceBenchmark(geoMeanBenchmarkName, geoMean, varianceGeoMean)
}
// Generate map with summary durations of each benchmark.
@@ -209,16 +200,15 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
val mergedTable = mutableMapOf<String, SummaryBenchmark>()
mergedTable.apply {
currentBenchmarks.forEach { (name, current) ->
val currentBenchmark = current.meanBenchmark
// Check existance of benchmark in previous results.
if (previousBenchmarks == null || name !in previousBenchmarks) {
getOrPut(name) { SummaryBenchmark(current, null) }
} else {
val previousBenchmark = previousBenchmarks.getValue(name).meanBenchmark
val previousBenchmark = previousBenchmarks.getValue(name)
getOrPut(name) { SummaryBenchmark(current, previousBenchmarks[name]) }
// Explore change of status.
if (previousBenchmark.status != currentBenchmark.status) {
val statusChange = FieldChange("$name", previousBenchmark.status, currentBenchmark.status)
if (previousBenchmark.status != current.status) {
val statusChange = FieldChange("$name", previousBenchmark.status, current.status)
benchmarksWithChangedStatus.add(statusChange)
}
}
@@ -240,7 +230,7 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
SummaryBenchmark {
// Calculate geometric mean.
val currentGeoMean = createGeoMeanBenchmark(currentBenchmarks)
val previousGeoMean = previousBenchmarks?. let { createGeoMeanBenchmark(previousBenchmarks) }
val previousGeoMean = previousBenchmarks?.let { createGeoMeanBenchmark(previousBenchmarks) }
return SummaryBenchmark(currentGeoMean, previousGeoMean)
}
@@ -261,7 +251,7 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
// Analyze and collect changes in performance between same becnhmarks.
private fun analyzePerformanceChanges() {
val performanceChanges = mergedReport.asSequence().map {(name, element) ->
val performanceChanges = mergedReport.asSequence().map { (name, element) ->
getBenchmarkPerfomanceChange(name, element)
}.filterNotNull().groupBy {
if (it.second.first.mean > 0) "regressions" else "improvements"
@@ -277,8 +267,8 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
// Calculate change for geometric mean.
val (current, previous) = geoMeanBenchmark
geoMeanScoreChange = current?. let {
previous?. let {
geoMeanScoreChange = current?.let {
previous?.let {
Pair(current.calcPercentageDiff(previous), current.calcRatio(previous))
}
}
@@ -0,0 +1,13 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
expect fun readFile(fileName: String): String
expect fun Double.format(decimalNumber: Int = 4): String
expect fun writeToFile(fileName: String, text: String)
expect fun assert(value: Boolean, lazyMessage: () -> Any)
expect fun sendGetRequest(url: String, user: String? = null, password: String? = null,
followLocation: Boolean = false) : String
@@ -1,26 +1,22 @@
/*
* 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.
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.report
import org.jetbrains.report.json.*
interface JsonSerializable {
fun toJson(): String
fun serializeFields(): String
fun toJson(): String {
return """
{
${serializeFields()}
}
"""
}
// Convert iterable objects arrays, lists to json.
fun <T> arrayToJson(data: Iterable<T>): String {
@@ -30,51 +26,63 @@ interface JsonSerializable {
}
}
interface EntityFromJsonFactory<T>: ConvertedFromJson {
interface EntityFromJsonFactory<T> : ConvertedFromJson {
fun create(data: JsonElement): T
}
// Class for benchcmarks report with all information of run.
class BenchmarksReport(val env: Environment, benchmarksList: List<BenchmarkResult>, val compiler: Compiler):
// Parse array with benchmarks to list
fun parseBenchmarksArray(data: JsonElement): List<BenchmarkResult> {
if (data is JsonArray) {
return data.jsonArray.map {
if (MeanVarianceBenchmark.isMeanVarianceBenchmark(it))
MeanVarianceBenchmark.create(it as JsonObject)
else BenchmarkResult.create(it as JsonObject)
}
} else {
error("Benchmarks field is expected to be an array. Please, check origin files.")
}
}
// Class for benchmarks report with all information of run.
open class BenchmarksReport(val env: Environment, benchmarksList: List<BenchmarkResult>, val compiler: Compiler) :
JsonSerializable {
companion object: EntityFromJsonFactory<BenchmarksReport> {
companion object : EntityFromJsonFactory<BenchmarksReport> {
override fun create(data: JsonElement): BenchmarksReport {
if (data is JsonObject) {
val env = Environment.create(data.getRequiredField("env"))
val benchmarksObj = data.getRequiredField("benchmarks")
val compiler = Compiler.create(data.getRequiredField("kotlin"))
val buildNumberField = data.getOptionalField("buildNumber")
val benchmarksList = parseBenchmarksArray(benchmarksObj)
return BenchmarksReport(env, benchmarksList, compiler)
val report = BenchmarksReport(env, benchmarksList, compiler)
buildNumberField?.let { report.buildNumber = (it as JsonLiteral).unquoted() }
return report
} else {
error("Top level entity is expected to be an object. Please, check origin files.")
}
}
// Parse array with benchmarks to list
fun parseBenchmarksArray(data: JsonElement): List<BenchmarkResult> {
if (data is JsonArray) {
return data.jsonArray.map { BenchmarkResult.create(it as JsonObject) }
} else {
error("Benchmarks field is expected to be an array. Please, check origin files.")
}
}
// Made a map of becnhmarks with name as key from list.
private fun structBenchmarks(benchmarksList: List<BenchmarkResult>) =
benchmarksList.groupBy{ it.name }
benchmarksList.groupBy { it.name }
}
val benchmarks: Map<String, List<BenchmarkResult>> = structBenchmarks(benchmarksList)
val benchmarks = structBenchmarks(benchmarksList)
override fun toJson(): String {
var buildNumber: String? = null
override fun serializeFields(): String {
val buildNumberField = buildNumber?.let {
""",
"buildNumber": "$buildNumber"
"""
} ?: ""
return """
{
"env": ${env.toJson()},
"kotlin": ${compiler.toJson()},
"benchmarks": ${arrayToJson(benchmarks.flatMap{it.value})}
}
"""
"benchmarks": ${arrayToJson(benchmarks.flatMap { it.value })}$buildNumberField
""".trimIndent()
}
fun merge(other: BenchmarksReport): BenchmarksReport {
@@ -85,27 +93,27 @@ class BenchmarksReport(val env: Environment, benchmarksList: List<BenchmarkResul
}
}
mergedBenchmarks.putAll(other.benchmarks)
return BenchmarksReport(env, mergedBenchmarks.flatMap{it.value}, compiler)
return BenchmarksReport(env, mergedBenchmarks.flatMap { it.value }, compiler)
}
// Concatenate benchmarks report if they have same environment and compiler.
operator fun plus(other: BenchmarksReport): BenchmarksReport {
if (compiler != other.compiler || env != other.env) {
error ("It's impossible to concat reports from different machines!")
error("It's impossible to concat reports from different machines!")
}
return merge(other)
}
}
// Class for kotlin compiler
data class Compiler(val backend: Backend, val kotlinVersion: String): JsonSerializable {
data class Compiler(val backend: Backend, val kotlinVersion: String) : JsonSerializable {
enum class BackendType(val type: String) {
JVM("jvm"),
NATIVE("native")
}
companion object: EntityFromJsonFactory<Compiler> {
companion object : EntityFromJsonFactory<Compiler> {
override fun create(data: JsonElement): Compiler {
if (data is JsonObject) {
val backend = Backend.create(data.getRequiredField("backend"))
@@ -121,13 +129,14 @@ data class Compiler(val backend: Backend, val kotlinVersion: String): JsonSerial
}
// Class for compiler backend
data class Backend(val type: BackendType, val version: String, val flags: List<String>): JsonSerializable {
companion object: EntityFromJsonFactory<Backend> {
data class Backend(val type: BackendType, val version: String, val flags: List<String>) : JsonSerializable {
companion object : EntityFromJsonFactory<Backend> {
override fun create(data: JsonElement): Backend {
if (data is JsonObject) {
val typeElement = data.getRequiredField("type")
if (typeElement is JsonLiteral) {
val type = backendTypeFromString(typeElement.unquoted()) ?: error("Backend type should be 'jvm' or 'native'")
val type = backendTypeFromString(typeElement.unquoted())
?: error("Backend type should be 'jvm' or 'native'")
val version = elementToString(data.getRequiredField("version"), "version")
val flagsArray = data.getOptionalField("flags")
var flags: List<String> = emptyList()
@@ -144,40 +153,35 @@ data class Compiler(val backend: Backend, val kotlinVersion: String): JsonSerial
}
}
override fun toJson(): String {
override fun serializeFields(): String {
val result = """
{
"type": "${type.type}",
"version": "${version}""""
// Don't print flags field if there is no one.
if (flags.isEmpty()) {
return """$result
}"""
}
else {
"""
} else {
return """
$result,
"flags": ${arrayToJson(flags)}
}
"flags": ${arrayToJson(flags.map { if (it.startsWith("\"")) it else "\"$it\"" })}
"""
}
}
}
override fun toJson(): String {
override fun serializeFields(): String {
return """
{
"backend": ${backend.toJson()},
"kotlinVersion": "${kotlinVersion}"
}
"""
}
}
// Class for description of environment of benchmarks run
data class Environment(val machine: Machine, val jdk: JDKInstance): JsonSerializable {
data class Environment(val machine: Machine, val jdk: JDKInstance) : JsonSerializable {
companion object: EntityFromJsonFactory<Environment> {
companion object : EntityFromJsonFactory<Environment> {
override fun create(data: JsonElement): Environment {
if (data is JsonObject) {
val machine = Machine.create(data.getRequiredField("machine"))
@@ -191,8 +195,8 @@ data class Environment(val machine: Machine, val jdk: JDKInstance): JsonSerializ
}
// Class for description of machine used for benchmarks run.
data class Machine(val cpu: String, val os: String): JsonSerializable {
companion object: EntityFromJsonFactory<Machine> {
data class Machine(val cpu: String, val os: String) : JsonSerializable {
companion object : EntityFromJsonFactory<Machine> {
override fun create(data: JsonElement): Machine {
if (data is JsonObject) {
val cpu = elementToString(data.getRequiredField("cpu"), "cpu")
@@ -205,19 +209,17 @@ data class Environment(val machine: Machine, val jdk: JDKInstance): JsonSerializ
}
}
override fun toJson(): String {
override fun serializeFields(): String {
return """
{
"cpu": "$cpu",
"os": "$os"
}
"""
}
}
// Class for description of jdk used for benchmarks run.
data class JDKInstance(val version: String, val vendor: String): JsonSerializable {
companion object: EntityFromJsonFactory<JDKInstance> {
data class JDKInstance(val version: String, val vendor: String) : JsonSerializable {
companion object : EntityFromJsonFactory<JDKInstance> {
override fun create(data: JsonElement): JDKInstance {
if (data is JsonObject) {
val version = elementToString(data.getRequiredField("version"), "version")
@@ -230,47 +232,44 @@ data class Environment(val machine: Machine, val jdk: JDKInstance): JsonSerializ
}
}
override fun toJson(): String {
override fun serializeFields(): String {
return """
{
"version": "$version",
"vendor": "$vendor"
}
"""
}
}
override fun toJson(): String {
override fun serializeFields(): String {
return """
{
"machine": ${machine.toJson()},
"jdk": ${jdk.toJson()}
}
"""
}
}
class BenchmarkResult(val name: String, val status: Status,
val score: Double, val metric: Metric, val runtimeInUs: Double,
val repeat: Int, val warmup: Int): JsonSerializable {
open class BenchmarkResult(val name: String, val status: Status,
val score: Double, val metric: Metric, val runtimeInUs: Double,
val repeat: Int, val warmup: Int) : JsonSerializable {
enum class Metric(val suffix: String, val value: String) {
EXECUTION_TIME("", "EXECUTION_TIME"),
CODE_SIZE(".codeSize", "CODE_SIZE"),
COMPILE_TIME(".compileTime", "COMPILE_TIME")
COMPILE_TIME(".compileTime", "COMPILE_TIME"),
BUNDLE_SIZE(".bundleSize", "BUNDLE_SIZE")
}
constructor(name: String, score: Double) : this(name, Status.PASSED, score, Metric.EXECUTION_TIME, 0.0, 0, 0)
companion object: EntityFromJsonFactory<BenchmarkResult> {
companion object : EntityFromJsonFactory<BenchmarkResult> {
override fun create(data: JsonElement): BenchmarkResult {
if (data is JsonObject) {
var name = elementToString(data.getRequiredField("name"), "name")
val metricElement = data.getOptionalField("metric")
val metric = if (metricElement != null && metricElement is JsonLiteral)
metricFromString(metricElement.unquoted()) ?: Metric.EXECUTION_TIME
else Metric.EXECUTION_TIME
metricFromString(metricElement.unquoted()) ?: Metric.EXECUTION_TIME
else Metric.EXECUTION_TIME
name += metric.suffix
val statusElement = data.getRequiredField("status")
if (statusElement is JsonLiteral) {
@@ -300,9 +299,8 @@ class BenchmarkResult(val name: String, val status: Status,
FAILED("FAILED")
}
override fun toJson(): String {
override fun serializeFields(): String {
return """
{
"name": "${name.removeSuffix(metric.suffix)}",
"status": "${status.value}",
"score": ${score},
@@ -310,10 +308,44 @@ class BenchmarkResult(val name: String, val status: Status,
"runtimeInUs": ${runtimeInUs},
"repeat": ${repeat},
"warmup": ${warmup}
}
"""
}
val shortName: String
get() = name.removeSuffix(metric.suffix)
}
// Entity to describe avarage values which conssists of mean and variance values.
data class MeanVariance(val mean: Double, val variance: Double)
// Processed benchmark result with calculated mean and variance value.
open class MeanVarianceBenchmark(name: String, status: BenchmarkResult.Status, score: Double, metric: BenchmarkResult.Metric,
runtimeInUs: Double, repeat: Int, warmup: Int, val variance: Double) :
BenchmarkResult(name, status, score, metric, runtimeInUs, repeat, warmup) {
constructor(name: String, score: Double, variance: Double) : this(name, BenchmarkResult.Status.PASSED, score,
BenchmarkResult.Metric.EXECUTION_TIME, 0.0, 0, 0, variance)
companion object : EntityFromJsonFactory<MeanVarianceBenchmark> {
fun isMeanVarianceBenchmark(data: JsonElement) = data is JsonObject && data.getOptionalField("variance") != null
override fun create(data: JsonElement): MeanVarianceBenchmark {
if (data is JsonObject) {
val baseBenchmark = BenchmarkResult.create(data)
val variance = elementToDouble(data.getRequiredField("variance"), "variance")
return MeanVarianceBenchmark(baseBenchmark.name, baseBenchmark.status, baseBenchmark.score, baseBenchmark.metric,
baseBenchmark.runtimeInUs, baseBenchmark.repeat, baseBenchmark.warmup, variance)
} else {
error("Benchmark entity is expected to be an object. Please, check origin files.")
}
}
}
override fun serializeFields(): String {
return """
${super.serializeFields()},
"variance": $variance
"""
}
}
@@ -121,10 +121,12 @@ internal class Parser(val source: String) {
TC_BEGIN_LIST, TC_BEGIN_OBJ, TC_OTHER, TC_STRING, TC_NULL -> true
else -> false
}
@OptIn(ExperimentalStdlibApi::class)
fun takeStr(): String {
if (tc != TC_OTHER && tc != TC_STRING) fail(tokenPos, "Expected string or non-null literal")
val prevStr = if (offset < 0)
String(buf, 0, length) else
buf.concatToString(0, length) else
source.substring(offset, offset + length)
nextToken()
return prevStr
@@ -1,17 +1,6 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
@@ -1,17 +1,6 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
@@ -29,7 +18,7 @@ actual fun readFile(fileName: String): String {
}
actual fun Double.format(decimalNumber: Int): String =
"%.${decimalNumber}f".format(this)
"%.${decimalNumber}f".format(this)
actual fun writeToFile(fileName: String, text: String) {
File(fileName).printWriter().use { out ->
@@ -1,17 +1,6 @@
/*
* Copyright 2010-2017 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
+47 -104
View File
@@ -1,23 +1,12 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
@file:OptIn(ExperimentalCli::class)
import kotlinx.cli.*
import org.jetbrains.analyzer.sendGetRequest
import org.jetbrains.analyzer.readFile
import org.jetbrains.analyzer.SummaryBenchmarksReport
import kotlinx.cli.*
import org.jetbrains.renders.*
import org.jetbrains.report.*
import org.jetbrains.report.json.*
@@ -56,7 +45,7 @@ object ArtifactoryConnector : Connector() {
}
}
object TeamCityConnector: Connector() {
object TeamCityConnector : Connector() {
override val connectorPrefix = "teamcity:"
val teamCityUrl = "http://buildserver.labs.intellij.net"
@@ -76,11 +65,28 @@ object TeamCityConnector: Connector() {
}
}
object DBServerConnector : Connector() {
override val connectorPrefix = ""
val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg"
override fun getFileContent(fileLocation: String, user: String?): String {
val buildNumber = fileLocation.substringBefore(':')
val target = fileLocation.substringAfter(':')
if (target == buildNumber) {
error("To get file from database, please, specify, target and build number" +
" in format target:build_number")
}
val accessFileUrl = "$serverUrl/report/$target/$buildNumber"
return sendGetRequest(accessFileUrl)
}
}
fun getFileContent(fileName: String, user: String? = null): String {
return when {
ArtifactoryConnector.isCompatible(fileName) -> ArtifactoryConnector.getFileContent(fileName, user)
TeamCityConnector.isCompatible(fileName) -> TeamCityConnector.getFileContent(fileName, user)
else -> readFile(fileName)
fileName.endsWith(".json") -> readFile(fileName)
else -> DBServerConnector.getFileContent(fileName, user)
}
}
@@ -108,12 +114,16 @@ fun parseNormalizeResults(results: String): Map<String, Map<String, Double>> {
return parsedNormalizeResults
}
fun mergeCompilerFlags(reports: List<BenchmarksReport>) =
reports.map {
fun mergeCompilerFlags(reports: List<BenchmarksReport>): List<String> {
val flagsMap = mutableMapOf<String, MutableList<String>>()
reports.forEach {
val benchmarks = it.benchmarks.values.flatten().asSequence().filter { it.metric == BenchmarkResult.Metric.COMPILE_TIME }
.map { it.shortName }.distinct().sorted().joinToString()
"${it.compiler.backend.flags.joinToString()} for [$benchmarks]"
.map { it.shortName }.toList()
if (benchmarks.isNotEmpty())
(flagsMap.getOrPut("${it.compiler.backend.flags.joinToString()}") { mutableListOf<String>() }).addAll(benchmarks)
}
return flagsMap.map { (flags, benchmarks) -> "$flags for [${benchmarks.distinct().sorted().joinToString()}]" }
}
fun mergeReportsWithDetailedFlags(reports: List<BenchmarksReport>) =
if (reports.size > 1) {
@@ -129,75 +139,9 @@ fun mergeReportsWithDetailedFlags(reports: List<BenchmarksReport>) =
}
fun main(args: Array<String>) {
class Summary: Subcommand("summary", "Output summary information") {
val exec by option(ArgType.Choice(listOf("samples", "geomean")),
description = "Execution time way of calculation").default("geomean")
val execSamples by option(ArgType.String, "exec-samples",
description = "Samples used for execution time metric (value 'all' allows use all samples)")
.delimiter(",")
val execNormalize by option(ArgType.String, "exec-normalize",
description = "File with golden results which should be used for normalization")
val compile by option(ArgType.Choice(listOf("samples", "geomean")),
description = "Compile time way of calculation").default("geomean")
val compileSamples by option(ArgType.String, "compile-samples",
description = "Samples used for compile time metric (value 'all' allows use all samples)")
.delimiter(",")
val compileNormalize by option(ArgType.String, "compile-normalize",
description = "File with golden results which should be used for normalization")
val codesize by option(ArgType.Choice(listOf("samples", "geomean")),
description = "Code size way of calculation").default("geomean")
val codesizeSamples by option(ArgType.String, "codesize-samples",
description = "Samples used for code size metric (value 'all' allows use all samples)").delimiter(",")
val codesizeNormalize by option(ArgType.String, "codesize-normalize",
description = "File with golden results which should be used for normalization")
val user by option(ArgType.String, shortName = "u", description = "User access information for authorization")
val mainReport by argument(ArgType.String, description = "Main report for analysis")
override fun execute() {
val reportsList = getBenchmarkReport(mainReport, user)
val report = reportsList.reduce { result, it ->
result.merge(it)
}
val benchsReport = SummaryBenchmarksReport(report)
val results = mutableListOf<String>()
val executionNormalize = execNormalize?.let {
parseNormalizeResults(getFileContent(it))
}
val compileNormalize = compileNormalize?.let {
parseNormalizeResults(getFileContent(it))
}
val codesizeNormalize = codesizeNormalize?.let {
parseNormalizeResults(getFileContent(it))
}
results.apply {
add(benchsReport.failedBenchmarks.size.toString())
if (!execSamples.isEmpty()) {
val filterExec = if (execSamples.first() == "all") null else execSamples
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.EXECUTION_TIME,
exec == "geomean", filterExec, executionNormalize).joinToString(";"))
}
if (!compileSamples.isEmpty()) {
val filterCompile = if (compileSamples.first() == "all") null else compileSamples
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.COMPILE_TIME,
compile == "geomean", filterCompile, compileNormalize).joinToString(";"))
}
if (!codesizeSamples.isEmpty()) {
val filterCodesize = if (codesizeSamples.first() == "all") null else codesizeSamples
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.CODE_SIZE,
codesize == "geomean", filterCodesize, codesizeNormalize).joinToString(";"))
}
}
println(results.joinToString())
}
}
val action = Summary()
// Parse args.
val argParser = ArgParser("benchmarksAnalyzer")
argParser.subcommands(action)
val mainReport by argParser.argument(ArgType.String, description = "Main report for analysis")
val compareToReport by argParser.argument(ArgType.String, description = "Report to compare to").optional()
@@ -207,26 +151,25 @@ fun main(args: Array<String>) {
val useShortForm by argParser.option(ArgType.Boolean, "short", "s",
"Show short version of report").default(false)
val renders by argParser.option(ArgType.Choice(listOf("text", "html", "teamcity", "statistics", "metrics")),
shortName = "r", description = "Renders for showing information").multiple().default(listOf("text"))
shortName = "r", description = "Renders for showing information").multiple().default(listOf("text"))
val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization")
if (argParser.parse(args).commandName == "benchmarksAnalyzer") {
// Read contents of file.
val mainBenchsReport = mergeReportsWithDetailedFlags(getBenchmarkReport(mainReport, user))
argParser.parse(args)
// Read contents of file.
val mainBenchsReport = mergeReportsWithDetailedFlags(getBenchmarkReport(mainReport, user))
var compareToBenchsReport = compareToReport?.let {
mergeReportsWithDetailedFlags(getBenchmarkReport(it, user))
}
var compareToBenchsReport = compareToReport?.let {
mergeReportsWithDetailedFlags(getBenchmarkReport(it, user))
}
// Generate comparasion report.
val summaryReport = SummaryBenchmarksReport(mainBenchsReport,
compareToBenchsReport,
epsValue)
// Generate comparasion report.
val summaryReport = SummaryBenchmarksReport(mainBenchsReport,
compareToBenchsReport,
epsValue)
var outputFile = output
renders.forEach {
Render.getRenderByName(it).print(summaryReport, useShortForm, outputFile)
outputFile = null
}
var outputFile = output
renders.forEach {
Render.getRenderByName(it).print(summaryReport, useShortForm, outputFile)
outputFile = null
}
}
@@ -1,145 +0,0 @@
/*
* 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.analyzer
import org.jetbrains.report.BenchmarkResult
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
// Entity to describe avarage values which conssists of mean and variance values.
data class MeanVariance(val mean: Double, val variance: Double) {
override fun toString(): String {
val format = { number: Double -> number.format(2)}
return "${format(mean)} ± ${format(variance)}"
}
}
// Composite benchmark which descibes avarage result for several runs and contains mean and variance value.
data class MeanVarianceBenchmark(val meanBenchmark: BenchmarkResult, val varianceBenchmark: BenchmarkResult) {
// Calculate difference in percentage compare to another.
fun calcPercentageDiff(other: MeanVarianceBenchmark): MeanVariance {
assert(other.meanBenchmark.score >= 0 &&
other.varianceBenchmark.score >= 0 &&
other.meanBenchmark.score - other.varianceBenchmark.score != 0.0,
{ "Mean and variance should be positive and not equal!" })
val exactMean = (meanBenchmark.score - other.meanBenchmark.score) / other.meanBenchmark.score
// Analyze intervals. Calculate difference between border points.
val (bigValue, smallValue) = if (meanBenchmark.score > other.meanBenchmark.score) Pair(this, other) else Pair(other, this)
val bigValueIntervalStart = bigValue.meanBenchmark.score - bigValue.varianceBenchmark.score
val bigValueIntervalEnd = bigValue.meanBenchmark.score + bigValue.varianceBenchmark.score
val smallValueIntervalStart = smallValue.meanBenchmark.score - smallValue.varianceBenchmark.score
val smallValueIntervalEnd = smallValue.meanBenchmark.score + smallValue.varianceBenchmark.score
if (smallValueIntervalEnd > bigValueIntervalStart) {
// Interval intersect.
return MeanVariance(0.0, 0.0)
}
val mean = ((smallValueIntervalEnd - bigValueIntervalStart) / bigValueIntervalStart) *
(if (meanBenchmark.score > other.meanBenchmark.score) -1 else 1)
val maxValueChange = ((bigValueIntervalEnd - smallValueIntervalEnd) / bigValueIntervalEnd)
val minValueChange = ((bigValueIntervalStart - smallValueIntervalStart) / bigValueIntervalStart)
val variance = abs(abs(mean) - max(minValueChange, maxValueChange))
return MeanVariance(mean * 100, variance * 100)
}
// Calculate ratio value compare to another.
fun calcRatio(other: MeanVarianceBenchmark): MeanVariance {
assert(other.meanBenchmark.score >= 0 &&
other.varianceBenchmark.score >= 0 &&
other.meanBenchmark.score - other.varianceBenchmark.score != 0.0,
{ "Mean and variance should be positive and not equal!" })
val mean = meanBenchmark.score / other.meanBenchmark.score
val minRatio = (meanBenchmark.score - varianceBenchmark.score) / (other.meanBenchmark.score + other.varianceBenchmark.score)
val maxRatio = (meanBenchmark.score + varianceBenchmark.score) / (other.meanBenchmark.score - other.varianceBenchmark.score)
val ratioConfInt = min(abs(minRatio - mean), abs(maxRatio - mean))
return MeanVariance(mean, ratioConfInt)
}
override fun toString(): String =
"${meanBenchmark.score.format()} ± ${varianceBenchmark.score.format()}"
}
fun geometricMean(values: Collection<Double>, totalNumber: Int = values.size) =
with(values.asSequence().filter { it != 0.0 }) {
if (count() == 0) {
0.0
} else {
map { it.pow(1.0 / totalNumber) }.reduce { a, b -> a * b }
}
}
fun computeMeanVariance(samples: List<Double>): MeanVariance {
val zStar = 1.67 // Critical point for 90% confidence of normal distribution.
val mean = samples.sum() / samples.size
val variance = samples.indices.sumByDouble { (samples[it] - mean) * (samples[it] - mean) } / samples.size
val confidenceInterval = sqrt(variance / samples.size) * zStar
return MeanVariance(mean, confidenceInterval)
}
// Calculate avarage results for bencmarks (each becnhmark can be run several times).
fun collectMeanResults(benchmarks: Map<String, List<BenchmarkResult>>): BenchmarksTable {
return benchmarks.map {(name, resultsSet) ->
val repeatedSequence = IntArray(resultsSet.size)
var metric = BenchmarkResult.Metric.EXECUTION_TIME
var currentStatus = BenchmarkResult.Status.PASSED
var currentWarmup = -1
// Collect common becnhmark values and check them.
resultsSet.forEachIndexed { index, result ->
// If there was at least one failure, summary is marked as failure.
if (result.status == BenchmarkResult.Status.FAILED) {
currentStatus = result.status
}
repeatedSequence[index] = result.repeat
if (currentWarmup != -1)
if (result.warmup != currentWarmup)
println("Check data consistency. Warmup value for benchmark '${result.name}' differs.")
currentWarmup = result.warmup
metric = result.metric
}
repeatedSequence.sort()
// Check if there are missed loop during running benchmarks.
repeatedSequence.forEachIndexed { index, element ->
if (index != 0)
if ((element - repeatedSequence[index - 1]) != 1)
println("Check data consistency. For benchmark '$name' there is no run" +
" between ${repeatedSequence[index - 1]} and $element.")
}
// Create mean and variance benchmarks result.
val scoreMeanVariance = computeMeanVariance(resultsSet.map { it.score })
val runtimeInUsMeanVariance = computeMeanVariance(resultsSet.map { it.runtimeInUs })
val meanBenchmark = BenchmarkResult(name, currentStatus, scoreMeanVariance.mean, metric,
runtimeInUsMeanVariance.mean, repeatedSequence[resultsSet.size - 1],
currentWarmup)
val varianceBenchmark = BenchmarkResult(name, currentStatus, scoreMeanVariance.variance, metric,
runtimeInUsMeanVariance.variance, repeatedSequence[resultsSet.size - 1],
currentWarmup)
name to MeanVarianceBenchmark(meanBenchmark, varianceBenchmark)
}.toMap()
}
fun collectBenchmarksDurations(benchmarks: Map<String, List<BenchmarkResult>>): Map<String, Double> =
benchmarks.map { (name, resultsSet) ->
name to resultsSet.sumByDouble { it.runtimeInUs }
}.toMap()
@@ -1,24 +0,0 @@
/*
* 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.analyzer
expect fun readFile(fileName: String): String
expect fun Double.format(decimalNumber: Int = 4): String
expect fun writeToFile(fileName: String, text: String)
expect fun assert(value: Boolean, lazyMessage: () -> Any)
expect fun sendGetRequest(url: String, user: String? = null, password: String? = null,
followLocation: Boolean = false) : String
@@ -1,20 +1,8 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
@@ -556,7 +544,7 @@ class HTMLRender: Render() {
bucket: Map<String, ScoreChange>? = null, rowStyle: String? = null) {
if (bucket != null && !bucket.isEmpty()) {
// Find max ratio.
val maxRatio = bucket.values.map { it.second.mean }.max()!!
val maxRatio = bucket.values.map { it.second.mean }.maxOrNull()!!
// There are changes in performance.
// Output changed benchmarks.
for ((name, change) in bucket) {
@@ -565,19 +553,19 @@ class HTMLRender: Render() {
attributes["style"] = rowStyle
}
th { +name }
td { +"${fullSet.getValue(name).first}" }
td { +"${fullSet.getValue(name).second}" }
td { +"${fullSet.getValue(name).first?.description}" }
td { +"${fullSet.getValue(name).second?.description}" }
td {
attributes["bgcolor"] = ColoredCell(if (bucket.values.first().first.mean == 0.0) null
else change.first.mean / abs(bucket.values.first().first.mean))
.backgroundStyle
+"${change.first.toString() + " %"}"
+"${change.first.description + " %"}"
}
td {
val scaledRatio = if (maxRatio == 0.0) null else change.second.mean / maxRatio
attributes["bgcolor"] = ColoredCell(scaledRatio,
borderPositive = { cellValue -> cellValue > 1.0 / maxRatio }).backgroundStyle
+"${change.second}"
+"${change.second.description}"
}
}
}
@@ -587,8 +575,8 @@ class HTMLRender: Render() {
for ((name, value) in fullSet) {
tr {
th { +name }
td { +"${value.first?.toString() ?: placeholder}" }
td { +"${value.second?.toString() ?: placeholder}" }
td { +"${value.first?.description ?: placeholder}" }
td { +"${value.second?.description ?: placeholder}" }
td { +placeholder }
td { +placeholder }
}
@@ -619,11 +607,11 @@ class HTMLRender: Render() {
}
}
val geoMeanChangeMap = report.geoMeanScoreChange?.
let { mapOf(report.geoMeanBenchmark.first!!.meanBenchmark.name to report.geoMeanScoreChange!!) }
let { mapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanScoreChange!!) }
tbody {
renderBenchmarksDetails(
mutableMapOf(report.geoMeanBenchmark.first!!.meanBenchmark.name to report.geoMeanBenchmark),
mutableMapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanBenchmark),
geoMeanChangeMap, "border-bottom: 2.3pt solid black; border-top: 2.3pt solid black")
renderBenchmarksDetails(report.mergedReport, report.regressions)
renderBenchmarksDetails(report.mergedReport, report.improvements)
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.*
// Report render to text format.
class JsonResultsRender: Render() {
override val name: String
get() = "json"
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean) =
report.getBenchmarksReport().toJson()
}
@@ -1,20 +1,8 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
@@ -28,10 +16,10 @@ class MetricResultsRender: Render() {
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
val results = report.mergedReport.map { entry ->
buildString {
val metric = entry.value.first!!.meanBenchmark.metric
val metric = entry.value.first!!.metric
append("{ \"benchmarkName\": \"${entry.key.removeSuffix(metric.suffix)}\",")
append("\"metric\": \"${metric}\",")
append("\"value\": \"${entry.value.first!!.meanBenchmark.score}\" }")
append("\"value\": \"${entry.value.first!!.score}\" }")
}
}.joinToString(", ")
return "[ $results ]"
@@ -1,24 +1,12 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.*
import kotlin.math.abs
@@ -150,23 +138,23 @@ class TextRender: Render() {
private fun printBenchmarksDetails(fullSet: Map<String, SummaryBenchmark>,
bucket: Map<String, ScoreChange>? = null) {
val placeholder = "-"
if (bucket != null) {
// There are changes in performance.
// Output changed benchmarks.
for ((name, change) in bucket) {
append(formatColumn(name, true) +
formatColumn(fullSet.getValue(name).first.toString()) +
formatColumn(fullSet.getValue(name).second.toString()) +
formatColumn(change.first.toString() + " %") +
formatColumn(change.second.toString()))
formatColumn(fullSet.getValue(name).first?.description ?: placeholder) +
formatColumn(fullSet.getValue(name).second?.description ?: placeholder) +
formatColumn(change.first.description + " %") +
formatColumn(change.second.description))
}
} else {
// Output all values without performance changes.
val placeholder = "-"
for ((name, value) in fullSet) {
append(formatColumn(name, true) +
formatColumn(value.first?.toString() ?: placeholder) +
formatColumn(value.second?.toString() ?: placeholder) +
formatColumn(value.first?.description ?: placeholder) +
formatColumn(value.second?.description ?: placeholder) +
formatColumn(placeholder) +
formatColumn(placeholder))
}
@@ -201,9 +189,9 @@ class TextRender: Render() {
val tableWidth = printPerformanceTableHeader()
// Print geometric mean.
val geoMeanChangeMap = report.geoMeanScoreChange?.
let { mapOf(report.geoMeanBenchmark.first!!.meanBenchmark.name to report.geoMeanScoreChange!!) }
let { mapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanScoreChange!!) }
printBenchmarksDetails(
mutableMapOf(report.geoMeanBenchmark.first!!.meanBenchmark.name to report.geoMeanBenchmark),
mutableMapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanBenchmark),
geoMeanChangeMap)
printTableLineSeparator(tableWidth)
printBenchmarksDetails(report.mergedReport, report.regressions)
@@ -1,20 +1,8 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
@@ -1,24 +1,13 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.MeanVarianceBenchmark
// Report render to text format.
class TeamCityStatisticsRender: Render() {
@@ -34,9 +23,8 @@ class TeamCityStatisticsRender: Render() {
// For current benchmarks print score as TeamCity Test Metadata
report.currentMeanVarianceBenchmarks.forEach { benchmark ->
renderBenchmark(benchmark.meanBenchmark, currentDurations[benchmark.meanBenchmark.name]!!)
renderSummaryBecnhmarkValue(benchmark.meanBenchmark, "Mean")
renderSummaryBecnhmarkValue(benchmark.varianceBenchmark, "Variance")
renderBenchmark(benchmark, currentDurations[benchmark.name]!!)
renderSummaryBecnhmarkValue(benchmark)
}
content.append("##teamcity[testSuiteFinished name='Benchmarks']\n")
@@ -46,9 +34,12 @@ class TeamCityStatisticsRender: Render() {
return content.toString()
}
private fun renderSummaryBecnhmarkValue(benchmark: BenchmarkResult, metric: String) =
content.append("##teamcity[testMetadata testName='${benchmark.name}' name='$metric'" +
" type='number' value='${benchmark.score}']\n")
private fun renderSummaryBecnhmarkValue(benchmark: MeanVarianceBenchmark) {
content.append("##teamcity[testMetadata testName='${benchmark.name}' name='Mean'" +
" type='number' value='${benchmark.score}']\n")
content.append("##teamcity[testMetadata testName='${benchmark.name}' name='Variance'" +
" type='number' value='${benchmark.variance}']\n")
}
// Produce benchmark as test in TeamCity
private fun renderBenchmark(benchmark: BenchmarkResult , duration: Double) {
@@ -61,7 +52,7 @@ class TeamCityStatisticsRender: Render() {
}
private fun renderGeometricMean(geoMeanBenchmark: MeanVarianceBenchmark) {
content.append("##teamcity[buildStatisticValue key='Geometric mean' value='${geoMeanBenchmark.meanBenchmark.score}']\n")
content.append("##teamcity[buildStatisticValue key='Geometric mean variance' value='${geoMeanBenchmark.varianceBenchmark.score}']\n")
content.append("##teamcity[buildStatisticValue key='Geometric mean' value='${geoMeanBenchmark.score}']\n")
content.append("##teamcity[buildStatisticValue key='Geometric mean variance' value='${geoMeanBenchmark.variance}']\n")
}
}
@@ -1,37 +1,21 @@
/*
* 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import kotlin.test.*
import kotlin.math.abs
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.MeanVarianceBenchmark
class AnalyzerTests {
private val eps = 0.000001
private fun createMeanVarianceBenchmarks(): Pair<MeanVarianceBenchmark, MeanVarianceBenchmark> {
val firstMean = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 9.0, BenchmarkResult.Metric.EXECUTION_TIME, 9.0, 10, 10)
val firstVariance = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 0.0001, BenchmarkResult.Metric.EXECUTION_TIME, 0.0001, 10, 10)
val first = MeanVarianceBenchmark(firstMean, firstVariance)
val secondMean = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 10.0, BenchmarkResult.Metric.EXECUTION_TIME, 10.0, 10, 10)
val secondVariance = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 0.0001, BenchmarkResult.Metric.EXECUTION_TIME, 0.0001, 10, 10)
val second = MeanVarianceBenchmark(secondMean, secondVariance)
val first = MeanVarianceBenchmark("testBenchmark", BenchmarkResult.Status.PASSED, 9.0, BenchmarkResult.Metric.EXECUTION_TIME, 9.0, 10, 10, 0.0001)
val second = MeanVarianceBenchmark("testBenchmark", BenchmarkResult.Status.PASSED, 10.0, BenchmarkResult.Metric.EXECUTION_TIME, 10.0, 10, 10, 0.0001)
return Pair(first, second)
}
+30 -17
View File
@@ -18,6 +18,8 @@ buildscript {
}
}
apply plugin: 'kotlin-multiplatform'
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
@@ -30,22 +32,33 @@ repositories {
}
}
apply plugin: 'kotlin2js'
kotlin {
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
}
kotlin.srcDir '../benchmarks/shared/src'
}
jsMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion"
implementation(npm("aws-sdk", "~2.670.0"))
}
kotlin.srcDir 'src/main/kotlin'
kotlin.srcDir 'src/main/kotlin-js'
kotlin.srcDir 'shared/src/main/kotlin'
}
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion"
}
sourceSets {
main.resources.srcDirs += "resources"
main.kotlin.srcDirs += "src"
main.kotlin.srcDirs += "../benchmarks/shared/src"
main.kotlin.srcDirs += "shared/src"
}
compileKotlin2Js {
kotlinOptions.outputFile = "${projectDir}/server/app.js"
kotlinOptions.moduleKind = "commonjs"
kotlinOptions.sourceMap = true
targets {
fromPreset(presets.js, 'js') {
nodejs()
compilations.main.kotlinOptions {
outputFile = "${projectDir}/server/app.js"
moduleKind = "commonjs"
sourceMap = true
}
}
}
}
Binary file not shown.
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+100
View File
@@ -0,0 +1,100 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+93 -203
View File
@@ -4,32 +4,6 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/concat-stream": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz",
"integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=",
"requires": {
"@types/node": "*"
}
},
"@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
"integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "9.6.44",
"resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.44.tgz",
"integrity": "sha512-hR+jK4kSOX+u5aQ6Zj6az5N4l1AeXH37/sOenbgxJvkO450C5qQL4/1twdufIMLC3o3hNfJNrPfQn0ivMdKfPg=="
},
"@types/qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-47kAAs3yV/hROraCTQYDMh4p/6zI9+gtssjD0kq9OWsGdLcBge59rl49FnCuJ+iWxEKiqFz6KXzeGH5DRVjNJA=="
},
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
@@ -44,15 +18,26 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
"aws-sdk": {
"version": "2.670.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.670.0.tgz",
"integrity": "sha512-hGRnZtp1wDUh6hZRBHO0Ki7thx/xbRlIEiTKlWes+f/0E1Nhm3KpelsBZ3L/Q6y1ragwkQd4Q720AmWEqemLyA==",
"requires": {
"buffer": "4.9.1",
"events": "1.1.1",
"ieee754": "1.1.13",
"jmespath": "0.15.0",
"querystring": "0.2.0",
"sax": "1.2.1",
"url": "0.10.3",
"uuid": "3.3.2",
"xml2js": "0.4.19"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"body-parser": {
"version": "1.18.3",
@@ -81,40 +66,21 @@
}
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
"buffer": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4",
"isarray": "^1.0.0"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"combined-stream": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
@@ -135,11 +101,6 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -155,11 +116,6 @@
}
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -195,6 +151,11 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
},
"express": {
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
@@ -276,16 +237,6 @@
}
}
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -296,24 +247,6 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
"integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw="
},
"http-basic": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-7.0.0.tgz",
"integrity": "sha1-gvClBr6UJzLsje6+6A50bvVzbbo=",
"requires": {
"@types/concat-stream": "^1.6.0",
"@types/node": "^9.4.1",
"caseless": "~0.12.0",
"concat-stream": "^1.4.6",
"http-response-object": "^3.0.1",
"parse-cache-control": "^1.0.1"
}
},
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
@@ -325,14 +258,6 @@
"statuses": ">= 1.4.0 < 2"
}
},
"http-response-object": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.1.tgz",
"integrity": "sha512-6L0Fkd6TozA8kFSfh9Widst0wfza3U1Ex2RjJ6zNDK0vR1U1auUR6jY4Nn2Xl7CCy0ikFmxW1XcspVpb9RvwTg==",
"requires": {
"@types/node": "^9.3.0"
}
},
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
@@ -341,6 +266,11 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
@@ -356,10 +286,15 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"jmespath": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
"kotlin": {
"version": "1.3.21",
"resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.3.21.tgz",
"integrity": "sha512-JqxY9quNzwWl8iYvNj/ycfNBEbSFDKvmeK2rDVU89q0/X8BRjVzibJ5RFOq/9efvvvi+VBnIRhL7GSOGzmP2nQ=="
"version": "1.4.0-M3",
"resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.4.0-M3.tgz",
"integrity": "sha512-y6dl1t36tI1hAJlJTxlOB/fj/0iOga+XC8+eg9hUSTu5BDJ9MIUR7XnA8Ld+tQA9IakAPaUmgZZfX2lsHaAdIw=="
},
"media-typer": {
"version": "0.3.0",
@@ -404,6 +339,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -412,11 +352,6 @@
"ee-first": "1.1.1"
}
},
"parse-cache-control": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
"integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104="
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
@@ -427,19 +362,6 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
},
"promise": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/promise/-/promise-8.0.2.tgz",
"integrity": "sha512-EIyzM39FpVOMbqgzEHhxdrEhtOSDOtjMZQ0M6iVfCE+kWNgCkAyOdnuCWqfmflylftfadU6FkiMgHZA2kUzwRw==",
"requires": {
"asap": "~2.0.6"
}
},
"proxy-addr": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
@@ -449,11 +371,21 @@
"ipaddr.js": "1.8.0"
}
},
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
@@ -470,20 +402,6 @@
"unpipe": "1.0.0"
}
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -494,6 +412,11 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
"integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
@@ -550,57 +473,6 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"sync-request": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.0.0.tgz",
"integrity": "sha512-jGNIAlCi9iU4X3Dm4oQnNQshDD3h0/1A7r79LyqjbjUnj69sX6mShAXlhRXgImsfVKtTcnra1jfzabdZvp+Lmw==",
"requires": {
"http-response-object": "^3.0.1",
"sync-rpc": "^1.2.1",
"then-request": "^6.0.0"
}
},
"sync-rpc": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.4.tgz",
"integrity": "sha512-Iug+t1ICVFenUcTnDu8WXFnT+k8IVoLKGh8VA3eXUtl2Rt9SjKX3YEv33OenABqpTPL9QEaHv1+CNn2LK8vMow==",
"requires": {
"get-port": "^3.1.0"
}
},
"then-request": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.0.tgz",
"integrity": "sha512-xA+7uEMc+jsQIoyySJ93Ad08Kuqnik7u6jLS5hR91Z3smAoCfL3M8/MqMlobAa9gzBfO9pA88A/AntfepkkMJQ==",
"requires": {
"@types/concat-stream": "^1.6.0",
"@types/form-data": "0.0.33",
"@types/node": "^8.0.0",
"@types/qs": "^6.2.31",
"caseless": "~0.12.0",
"concat-stream": "^1.6.0",
"form-data": "^2.2.0",
"http-basic": "^7.0.0",
"http-response-object": "^3.0.1",
"promise": "^8.0.0",
"qs": "^6.4.0"
},
"dependencies": {
"@types/node": {
"version": "8.10.42",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.42.tgz",
"integrity": "sha512-8LCqostMfYwQs9by1k21/P4KZp9uFQk3Q528y3qtPKQnCJmKz0Em3YzgeNjTNV1FVrG/7n/6j12d4UKg9zSgDw=="
}
}
},
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
@@ -610,30 +482,48 @@
"mime-types": "~2.1.18"
}
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
"url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
}
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~9.0.1"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
}
}
}
+4 -3
View File
@@ -6,11 +6,12 @@
"start": "node server/app.js"
},
"dependencies": {
"aws-sdk": "~2.670.0",
"body-parser": "~1.18.3",
"debug": "~4.1.1",
"ejs": "~2.6.1",
"express": "~4.16.4",
"kotlin": "^1.3.21",
"sync-request": "~6.0.0",
"ejs": "~2.6.1"
"kotlin": "~1.4.0-M3",
"node-fetch": "~2.6.0"
}
}
@@ -1,45 +1,26 @@
/*
* Copyright 2010-2019 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.build
package org.jetbrains.buildInfo
import org.jetbrains.report.*
import org.jetbrains.report.json.*
data class Build(val buildNumber: String, val startTime: String, val finishTime: String, val branch: String,
val commits: String, val buildType: String, val failuresNumber: Int, val executionTime: String,
val compileTime: String, val codeSize: String, val bundleSize: String?) {
val commits: String, val failuresNumber: Int) {
companion object: EntityFromJsonFactory<Build> {
companion object : EntityFromJsonFactory<Build> {
override fun create(data: JsonElement): Build {
if (data is JsonObject) {
val buildNumber = elementToString(data.getRequiredField("buildNumber"), "buildNumber")
val startTime = elementToString(data.getRequiredField("startTime"), "startTime")
val finishTime = elementToString(data.getRequiredField("finishTime"), "finishTime")
val branch = elementToString(data.getRequiredField("branch"), "branch")
val buildNumber = elementToString(data.getRequiredField("buildNumber"), "buildNumber").replace("\"", "")
val startTime = elementToString(data.getRequiredField("startTime"), "startTime").replace("\"", "")
val finishTime = elementToString(data.getRequiredField("finishTime"), "finishTime").replace("\"", "")
val branch = elementToString(data.getRequiredField("branch"), "branch").replace("\"", "")
val commits = elementToString(data.getRequiredField("commits"), "commits")
val buildType = elementToString(data.getRequiredField("buildType"), "buildType")
val failuresNumber = elementToInt(data.getRequiredField("failuresNumber"), "failuresNumber")
val executionTime = elementToString(data.getRequiredField("executionTime"), "executionTime")
val compileTime = elementToString(data.getRequiredField("compileTime"), "compileTime")
val codeSize = elementToString(data.getRequiredField("codeSize"), "codeSize")
val bundleSize = elementToStringOrNull(data.getRequiredField("bundleSize"), "bundleSize")
return Build(buildNumber, startTime, finishTime, branch, commits, buildType, failuresNumber, executionTime,
compileTime, codeSize, bundleSize)
return Build(buildNumber, startTime, finishTime, branch, commits, failuresNumber)
} else {
error("Top level entity is expected to be an object. Please, check origin files.")
}
@@ -58,6 +39,7 @@ data class Build(val buildNumber: String, val startTime: String, val finishTime:
return "${if (hours < 10) "0$hours" else "$hours"}:${matchResult[2]}"
} ?: error { "Wrong format of time $startTime" }
}
val date: String by lazy {
val matchResult = "^(\\d{4})(\\d{2})(\\d{2})".toRegex().find(startTime)?.groupValues
matchResult?.let { "${matchResult[3]}/${matchResult[2]}/${matchResult[1]}" }
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.elastic
import kotlin.js.Promise // TODO - migrate to multiplatform.
import org.jetbrains.report.json.*
import org.jetbrains.network.*
// Connector with InfluxDB.
class ElasticSearchConnector(private val connector: NetworkConnector,
private val user: String? = null, private val password: String? = null) {
// Execute ElasticSearch request.
fun request(method: RequestMethod, path: String, acceptJsonContentType: Boolean = true, body: String? = null) =
connector.sendRequest(method, path, user, password, acceptJsonContentType, body)
}
@@ -0,0 +1,224 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.elastic
import org.jetbrains.report.*
import org.jetbrains.report.json.*
import org.jetbrains.report.MeanVarianceBenchmark
import org.jetbrains.network.*
import kotlin.js.Promise // TODO - migrate to multiplatform.
data class Commit(val revision: String, val developer: String) : JsonSerializable {
override fun toString() = "$revision by $developer"
override fun serializeFields() = """
"revision": "$revision",
"developer": "$developer"
"""
companion object : EntityFromJsonFactory<Commit> {
fun parse(description: String) = if (description != "...") {
description.split(" by ").let {
val (currentRevision, currentDeveloper) = it
Commit(currentRevision, currentDeveloper)
}
} else {
Commit("unknown", "unknown")
}
override fun create(data: JsonElement): Commit {
if (data is JsonObject) {
val revision = elementToString(data.getRequiredField("revision"), "revision")
val developer = elementToString(data.getRequiredField("developer"), "developer")
return Commit(revision, developer)
} else {
error("Top level entity is expected to be an object. Please, check origin files.")
}
}
}
}
// List of commits.
class CommitsList : ConvertedFromJson, JsonSerializable {
val commits: List<Commit>
constructor(data: JsonElement) {
if (data !is JsonObject) {
error("Commits description is expected to be a JSON object!")
}
val changesElement = data.getOptionalField("change")
commits = changesElement?.let {
if (changesElement !is JsonArray) {
error("Change field is expected to be an array. Please, check source.")
}
changesElement.jsonArray.map {
with(it as JsonObject) {
Commit(elementToString(getRequiredField("version"), "version"),
elementToString(getRequiredField("username"), "username")
)
}
}
} ?: listOf<Commit>()
}
constructor(_commits: List<Commit>) {
commits = _commits
}
override fun toString(): String =
commits.toString()
companion object {
fun parse(description: String) = CommitsList(description.split(";").filter { it.isNotEmpty() }.map {
Commit.parse(it)
})
}
override fun serializeFields() = """
"commits": ${arrayToJson(commits)}
"""
}
data class BuildInfo(val buildNumber: String, val startTime: String, val endTime: String, val commitsList: CommitsList,
val branch: String,
val agentInfo: String /* Important agent information often used in requests.*/) : JsonSerializable {
override fun serializeFields() = """
"buildNumber": "$buildNumber",
"startTime": "$startTime",
"endTime": "$endTime",
${commitsList.serializeFields()},
"branch": "$branch",
"agentInfo": "$agentInfo"
"""
companion object : EntityFromJsonFactory<BuildInfo> {
override fun create(data: JsonElement): BuildInfo {
if (data is JsonObject) {
val buildNumber = elementToString(data.getRequiredField("buildNumber"), "buildNumber")
val startTime = elementToString(data.getRequiredField("startTime"), "startTime")
val endTime = elementToString(data.getRequiredField("endTime"), "endTime")
val branch = elementToString(data.getRequiredField("branch"), "branch")
val commitsList = data.getRequiredField("commits")
val commits = if (commitsList is JsonArray) {
commitsList.jsonArray.map { Commit.create(it as JsonObject) }
} else {
error("benchmarksSets field is expected to be an array. Please, check origin files.")
}
val agentInfoElement = data.getOptionalField("agentInfo")
val agentInfo = agentInfoElement?.let {
elementToString(agentInfoElement, "agentInfo")
} ?: ""
return BuildInfo(buildNumber, startTime, endTime, CommitsList(commits), branch, agentInfo)
} else {
error("Top level entity is expected to be an object. Please, check origin files.")
}
}
}
}
enum class ElasticSearchType {
TEXT, KEYWORD, DATE, LONG, DOUBLE, BOOLEAN, OBJECT, NESTED
}
abstract class ElasticSearchIndex(val indexName: String, val connector: ElasticSearchConnector) {
var nextId = 0L
// Insert data.
fun insert(data: JsonSerializable): Promise<String> {
val description = data.toJson()
val writePath = "$indexName/_doc/$nextId?pretty"
nextId++
return connector.request(RequestMethod.PUT, writePath, body = description)
}
// Delete data.
fun delete(data: String): Promise<String> {
val writePath = "$indexName/_delete_by_query"
return connector.request(RequestMethod.POST, writePath, body = data)
}
// Make search request.
fun search(requestJson: String, filterPathes: List<String> = emptyList()): Promise<String> {
val path = "$indexName/_search?pretty${if (filterPathes.isNotEmpty())
"&filter_path=" + filterPathes.joinToString(",") else ""}"
return connector.request(RequestMethod.POST, path, body = requestJson)
}
init {
// Get latest id for index.
val queryBody = """{
"_source": ["_id"],
"size": 1,
"query": {
"match_all": {}
}
}"""
search(queryBody, listOf("hits.total.value")).then { responseString ->
val response = JsonTreeParser.parse(responseString).jsonObject
val value = response.getObjectOrNull("hits")?.getObjectOrNull("total")?.getPrimitiveOrNull("value")?.content
?: error("Error response from ElasticSearch:\n$responseString")
nextId = value.toLong()
}.catch { errorMessage ->
error(errorMessage.message ?: "Failed getting next id for index $indexName")
}
}
abstract val mapping: Map<String, ElasticSearchType>
val mappingDescription: String
get() = """
{
"mappings": {
"properties": {
${mapping.map { (property, type) ->
"\"${property}\": { \"type\": \"${type.name.toLowerCase()}\"${if (type == ElasticSearchType.DATE) "," +
"\"format\": \"basic_date_time_no_millis\"" else ""} }"
}.joinToString()}}
}
}
""".trimIndent()
fun createMapping() =
connector.request(RequestMethod.PUT, indexName, body = mappingDescription)
}
class BenchmarksIndex(name: String, connector: ElasticSearchConnector) : ElasticSearchIndex(name, connector) {
override val mapping: Map<String, ElasticSearchType>
get() = mapOf("buildNumber" to ElasticSearchType.KEYWORD,
"benchmarks" to ElasticSearchType.NESTED,
"env" to ElasticSearchType.NESTED,
"kotlin" to ElasticSearchType.NESTED)
}
class GoldenResultsIndex(connector: ElasticSearchConnector) : ElasticSearchIndex("golden", connector) {
override val mapping: Map<String, ElasticSearchType>
get() = mapOf("buildNumber" to ElasticSearchType.KEYWORD,
"benchmarks" to ElasticSearchType.NESTED,
"env" to ElasticSearchType.NESTED,
"kotlin" to ElasticSearchType.NESTED)
}
class BuildInfoIndex(connector: ElasticSearchConnector) : ElasticSearchIndex("builds", connector) {
override val mapping: Map<String, ElasticSearchType>
get() = mapOf("buildNumber" to ElasticSearchType.KEYWORD,
"startTime" to ElasticSearchType.DATE,
"endTime" to ElasticSearchType.DATE,
"commits" to ElasticSearchType.NESTED)
}
// Processed benchmark result with calculated mean, variance and normalized reult.
class NormalizedMeanVarianceBenchmark(name: String, status: BenchmarkResult.Status, score: Double, metric: BenchmarkResult.Metric,
runtimeInUs: Double, repeat: Int, warmup: Int, variance: Double, val normalizedScore: Double) :
MeanVarianceBenchmark(name, status, score, metric, runtimeInUs, repeat, warmup, variance) {
override fun serializeFields(): String {
return """
${super.serializeFields()},
"normalizedScore": $normalizedScore
"""
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.network
import kotlin.js.Promise // TODO - migrate to multiplatform.
import kotlin.js.json // TODO - migrate to multiplatform.
// Now implemenation for network connection only for Node.js. TODO - multiplatform.
external fun require(module: String): dynamic
enum class RequestMethod {
POST, GET, PUT
}
// Abstract class for working with network.
abstract class NetworkConnector {
fun getAuth(user: String, password: String): String {
val buffer = js("Buffer").from(user + ":" + password)
val based64String = buffer.toString("base64")
return "Basic " + based64String
}
protected abstract fun <T : String?> sendBaseRequest(method: RequestMethod, path: String, user: String? = null,
password: String? = null, acceptJsonContentType: Boolean = true,
body: String? = null,
errorHandler: (url: String, response: dynamic) -> Nothing?): Promise<T>
open fun sendRequest(method: RequestMethod, path: String, user: String? = null, password: String? = null,
acceptJsonContentType: Boolean = true, body: String? = null): Promise<String> =
sendBaseRequest<String>(method, path, user, password, acceptJsonContentType, body) { url, response ->
error("Error during getting response from $url\n$response")
}
open fun sendOptionalRequest(method: RequestMethod, path: String, user: String? = null, password: String? = null,
acceptJsonContentType: Boolean = true, body: String? = null): Promise<String?> =
sendBaseRequest<String?>(method, path, user, password, acceptJsonContentType, body) { url, response ->
println("Error during getting response from $url\n$response")
null
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.network
import kotlin.js.Promise // TODO - migrate to multiplatform.
import kotlin.js.json // TODO - migrate to multiplatform.
// Network connector to work with basic url requests.
class UrlNetworkConnector(private val host: String, private val port: Int? = null) : NetworkConnector() {
private val url = "$host${port?.let { ":$port" } ?: ""}"
override fun <T : String?> sendBaseRequest(method: RequestMethod, path: String, user: String?, password: String?,
acceptJsonContentType: Boolean, body: String?,
errorHandler: (url: String, response: dynamic) -> Nothing?): Promise<T> {
val fullUrl = "$url/$path"
val request = require("node-fetch")
val headers = mutableListOf<Pair<String, String>>()
if (user != null && password != null) {
headers.add("Authorization" to getAuth(user, password))
}
if (acceptJsonContentType) {
headers.add("Accept" to "application/json")
headers.add("Content-Type" to "application/json")
}
return request(fullUrl,
json(
"method" to method.toString(),
"headers" to json(*(headers.toTypedArray())),
"body" to body
)
).then { response ->
if (!response.ok) {
errorHandler(fullUrl, response)
} else {
response.text()
}
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import org.w3c.xhr.*
import kotlin.browser.*
import kotlin.js.*
actual fun readFile(fileName: String): String {
error("Reading from local file for JS isn't supported")
}
actual fun writeToFile(fileName: String, text: String) {
error("Writing to local file for JS isn't supported")
}
actual fun Double.format(decimalNumber: Int): String =
this.asDynamic().toFixed(decimalNumber)
actual fun assert(value: Boolean, lazyMessage: () -> Any) {
if (!value) error(lazyMessage)
}
actual fun sendGetRequest(url: String, user: String?, password: String?, followLocation: Boolean) : String {
error("Unsupported")
}
@@ -0,0 +1,365 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.database
import kotlin.js.Promise
import org.jetbrains.elastic.*
import org.jetbrains.utils.*
import org.jetbrains.report.json.*
import org.jetbrains.report.*
fun <T> Iterable<T>.isEmpty() = count() == 0
fun <T> Iterable<T>.isNotEmpty() = !isEmpty()
inline fun <T: Any> T?.str(block: (T) -> String): String =
if (this != null) block(this)
else ""
// Dispatcher to create and control benchmarks indexes separated by some feature.
// Feature can be choosen as often used as filtering entity in case there is no need in separate indexes.
// Default behaviour of dispatcher is working with one index (case when separating isn't needed).
class BenchmarksIndexesDispatcher(connector: ElasticSearchConnector, val feature: String,
featureValues: Iterable<String> = emptyList()) {
// Becnhmarks indexes to work with in case of existing feature values.
private val benchmarksIndexes =
if (featureValues.isNotEmpty())
featureValues.map { it to BenchmarksIndex("benchmarks_${it.replace(" ", "_").toLowerCase()}", connector) }
.toMap()
else emptyMap()
// Single benchmark index.
private val benchmarksSingleInstance =
if (featureValues.isEmpty()) BenchmarksIndex("benchmarks", connector) else null
// Get right index in ES.
private fun getIndex(featureValue: String = "") =
benchmarksSingleInstance ?: benchmarksIndexes[featureValue]
?: error("Used wrong feature value $featureValue. Indexes are separated using next values: ${benchmarksIndexes.keys}")
// Used filter to get data with needed feature value.
var featureFilter: ((String) -> String)? = null
// Get benchmark reports corresponding to needed build number.
fun getBenchmarksReports(buildNumber: String, featureValue: String): Promise<List<String>> {
val queryDescription = """
{
"size": 1000,
"query": {
"bool": {
"must": [
{ "match": { "buildNumber": "$buildNumber" } }
]
}
}
}
"""
return getIndex(featureValue).search(queryDescription, listOf("hits.hits._source")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits")?.let { results ->
results.map {
val element = it as JsonObject
element.getObject("_source").toString()
}
} ?: emptyList()
}
}
// Get benchmarkes names corresponding to needed build number.
fun getBenchmarksList(buildNumber: String, featureValue: String): Promise<List<String>> {
return getBenchmarksReports(buildNumber, featureValue).then { reports ->
reports.map {
val dbResponse = JsonTreeParser.parse(it).jsonObject
parseBenchmarksArray(dbResponse.getArray("benchmarks"))
.map { it.name }
}.flatten()
}
}
// Delete benchmarks from database.
fun deleteBenchmarks(featureValue: String, buildNumber: String? = null): Promise<String> {
// Delete all or for choosen build number.
val matchQuery = buildNumber?.let {
""""match": { "buildNumber": "$it" }"""
} ?: """"match_all": {}"""
val queryDescription = """
{
"query": {
$matchQuery
}
}
""".trimIndent()
return getIndex(featureValue).delete(queryDescription)
}
// Get benchmarks values of needed metric for choosen build number.
fun getSamples(metricName: String, featureValue: String = "", samples: List<String>,
buildNumbers: Iterable<String>? = null,
normalize: Boolean = false): Promise<List<Pair<String, Array<Double?>>>> {
val queryDescription = """
{
"_source": ["buildNumber"],
"size": 1000,
"query": {
"bool": {
"must": [
${buildNumbers.str { builds ->
"""
{ "terms" : { "buildNumber" : [${builds.map { "\"$it\"" }.joinToString()}] } },""" }
}
${featureFilter.str { "${it(featureValue)}," } }
{"nested" : {
"path" : "benchmarks",
"query" : {
"bool": {
"must": [
{ "match": { "benchmarks.metric": "$metricName" } },
{ "terms": { "benchmarks.name": [${samples.map { "\"${it.toLowerCase()}\"" }.joinToString()}] }}
]
}
}, "inner_hits": {
"size": ${samples.size},
"_source": ["benchmarks.name",
"benchmarks.${if (normalize) "normalizedScore" else "score"}"]
}
}
}
]
}
}
}"""
return getIndex(featureValue).search(queryDescription, listOf("hits.hits._source", "hits.hits.inner_hits"))
.then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
val results = dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits")
?: error("Wrong response:\n$responseString")
// Get indexes for provided samples.
val indexesMap = samples.mapIndexed { index, it -> it to index }.toMap()
val valuesMap = buildNumbers?.map {
it to arrayOfNulls<Double?>(samples.size)
}?.toMap()?.toMutableMap() ?: mutableMapOf<String, Array<Double?>>()
// Parse and save values in requested order.
results.forEach {
val element = it as JsonObject
val build = element.getObject("_source").getPrimitive("buildNumber").content
buildNumbers?.let { valuesMap.getOrPut(build) { arrayOfNulls<Double?>(samples.size) } }
element
.getObject("inner_hits")
.getObject("benchmarks")
.getObject("hits")
.getArray("hits").forEach {
val source = (it as JsonObject).getObject("_source")
valuesMap[build]!![indexesMap[source.getPrimitive("name").content]!!] =
source.getPrimitive(if (normalize) "normalizedScore" else "score").double
}
}
valuesMap.toList()
}
}
fun insert(data: JsonSerializable, featureValue: String = "") =
getIndex(featureValue).insert(data)
fun delete(data: String, featureValue: String = "") =
getIndex(featureValue).delete(data)
// Get failures number happned during build.
fun getFailuresNumber(featureValue: String = "", buildNumbers: Iterable<String>? = null): Promise<Map<String, Int>> {
val queryDescription = """
{
"_source": false,
${featureFilter.str {
"""
"query": {
"bool": {
"must": [ ${it(featureValue)} ]
}
}, """
} }
${buildNumbers.str { builds ->
"""
"aggs" : {
"builds": {
"filters" : {
"filters": {
${builds.map { "\"$it\": { \"match\" : { \"buildNumber\" : \"$it\" }}" }
.joinToString(",\n")}
}
},"""
} }
"aggs" : {
"metric_build" : {
"nested" : {
"path" : "benchmarks"
},
"aggs" : {
"metric_samples": {
"filters" : {
"filters": { "samples": { "match": { "benchmarks.status": "FAILED" } } }
},
"aggs" : {
"failed_count": {
"value_count": {
"field" : "benchmarks.score"
}
}
}
}
}
${buildNumbers.str {
""" }
}"""
} }
}
}
}
"""
return getIndex(featureValue).search(queryDescription, listOf("aggregations")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
val aggregations = dbResponse.getObjectOrNull("aggregations") ?: error("Wrong response:\n$responseString")
buildNumbers?.let {
// Get failed number for each provided build.
val buckets = aggregations
.getObjectOrNull("builds")
?.getObjectOrNull("buckets")
?: error("Wrong response:\n$responseString")
buildNumbers.map {
it to buckets
.getObject(it)
.getObject("metric_build")
.getObject("metric_samples")
.getObject("buckets")
.getObject("samples")
.getObject("failed_count")
.getPrimitive("value")
.int
}.toMap()
} ?: listOf("golden" to aggregations
.getObject("metric_build")
.getObject("metric_samples")
.getObject("buckets")
.getObject("samples")
.getObject("failed_count")
.getPrimitive("value")
.int
).toMap()
}
}
// Get geometric mean for benchmarks values of needed metric.
fun getGeometricMean(metricName: String, featureValue: String = "",
buildNumbers: Iterable<String>? = null, normalize: Boolean = false,
excludeNames: List<String> = emptyList()): Promise<List<Pair<String, List<Double>>>> {
// Filter only with metric or also with names.
val filterBenchmarks = if (excludeNames.isEmpty())
"""
"match": { "benchmarks.metric": "$metricName" }
"""
else """
"bool": {
"must": { "match": { "benchmarks.metric": "$metricName" } },
"must_not": { "terms" : { "benchmarks.name" : [${excludeNames.map { "\"$it\"" }.joinToString()}] } }
}
""".trimIndent()
val queryDescription = """
{
"_source": false,
${featureFilter.str {
"""
"query": {
"bool": {
"must": [ ${it(featureValue)} ]
}
}, """
} }
${buildNumbers.str { builds ->
"""
"aggs" : {
"builds": {
"filters" : {
"filters": {
${builds.map { "\"$it\": { \"match\" : { \"buildNumber\" : \"$it\" }}" }
.joinToString(",\n")}
}
},"""
} }
"aggs" : {
"metric_build" : {
"nested" : {
"path" : "benchmarks"
},
"aggs" : {
"metric_samples": {
"filters" : {
"filters": { "samples": { $filterBenchmarks } }
},
"aggs" : {
"sum_log_x": {
"sum": {
"field" : "benchmarks.${if (normalize) "normalizedScore" else "score"}",
"script" : {
"source": "if (_value == 0) { 0.0 } else { Math.log(_value) }"
}
}
},
"geom_mean": {
"bucket_script": {
"buckets_path": {
"sum_log_x": "sum_log_x",
"x_cnt": "_count"
},
"script": "Math.exp(params.sum_log_x/params.x_cnt)"
}
}
}
}
}
${buildNumbers.str {
""" }
}"""
} }
}
}
}
"""
return getIndex(featureValue).search(queryDescription, listOf("aggregations")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
val aggregations = dbResponse.getObjectOrNull("aggregations") ?: error("Wrong response:\n$responseString")
buildNumbers?.let {
val buckets = aggregations
.getObjectOrNull("builds")
?.getObjectOrNull("buckets")
?: error("Wrong response:\n$responseString")
buildNumbers.map {
it to listOf(buckets
.getObject(it)
.getObject("metric_build")
.getObject("metric_samples")
.getObject("buckets")
.getObject("samples")
.getObject("geom_mean")
.getPrimitive("value")
.double
)
}
} ?: listOf("golden" to listOf(aggregations
.getObject("metric_build")
.getObject("metric_samples")
.getObject("buckets")
.getObject("samples")
.getObject("geom_mean")
.getPrimitive("value")
.double
)
)
}
}
}
@@ -0,0 +1,133 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.database
import kotlin.js.Promise
import org.jetbrains.elastic.*
import org.jetbrains.utils.*
import org.jetbrains.report.json.*
import org.jetbrains.report.*
// Delete build information from ES index.
internal fun deleteBuildInfo(agentInfo: String, buildInfoIndex: ElasticSearchIndex,
buildNumber: String? = null): Promise<String> {
val queryDescription = """
{
"query": {
"bool": {
"must": [
{ "match": { "agentInfo": "$agentInfo" } }
${buildNumber?.let {
""",
{"match": { "buildNumber": "$it" }}
"""
} ?: ""}
]
}
}
}
""".trimIndent()
return buildInfoIndex.delete(queryDescription)
}
// Get infromation about builds details from database.
internal fun getBuildsDescription(type: String?, branch: String?, agentInfo: String, buildInfoIndex: ElasticSearchIndex,
onlyNumbers: Boolean = false): Promise<JsonArray> {
val queryDescription = """
{ "size": 10000,
${if (onlyNumbers) """"_source": ["buildNumber"],""" else ""}
"sort": {"_id": "desc" },
"query": {
"bool": {
"must": [
{ "match": { "agentInfo": "$agentInfo" } }
${type?.let {
""",
{ "regexp": { "buildNumber": { "value": "${if (it == "release")
".*eap.*|.*release.*|.*rc.*" else ".*dev.*"}" } }
}
"""
} ?: ""}
${branch?.let {
""",
{"match": { "branch": "$it" }}
"""
} ?: ""}
]
}
}
}
""".trimIndent()
return buildInfoIndex.search(queryDescription, listOf("hits.hits._source")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits") ?: error("Wrong response:\n$responseString")
}
}
// Check if current build already exists.
suspend fun buildExists(buildInfo: BuildInfo, buildInfoIndex: ElasticSearchIndex): Boolean {
val queryDescription = """
{ "size": 1,
"_source": ["buildNumber"],
"query": {
"bool": {
"must": [
{ "match": { "buildNumber": "${buildInfo.buildNumber}" } },
{ "match": { "agentInfo": "${buildInfo.agentInfo}" } },
{ "match": { "branch": "${buildInfo.branch}" } }
]
}
}
}
""".trimIndent()
return buildInfoIndex.search(queryDescription, listOf("hits.total.value")).then { responseString ->
val response = JsonTreeParser.parse(responseString).jsonObject
val value = response.getObjectOrNull("hits")?.getObjectOrNull("total")?.getPrimitiveOrNull("value")?.content
?: error("Error response from ElasticSearch:\n$responseString")
value.toInt() > 0
}.await()
}
// Get builds numbers corresponding to machine and branch.
fun getBuildsNumbers(type: String?, branch: String?, agentInfo: String, buildInfoIndex: ElasticSearchIndex) =
getBuildsDescription(type, branch, agentInfo, buildInfoIndex, true).then { responseArray ->
responseArray.map { (it as JsonObject).getObject("_source").getPrimitive("buildNumber").content }
}
// Get full builds information corresponding to machine and branch.
fun getBuildsInfo(type: String?, branch: String?, agentInfo: String, buildInfoIndex: ElasticSearchIndex) =
getBuildsDescription(type, branch, agentInfo, buildInfoIndex).then { responseArray ->
responseArray.map { BuildInfo.create((it as JsonObject).getObject("_source")) }
}
// Get golden results from database.
fun getGoldenResults(goldenResultsIndex: GoldenResultsIndex): Promise<Map<String, List<BenchmarkResult>>> {
return goldenResultsIndex.search("", listOf("hits.hits._source")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits")?.map {
val reportDescription = (it as JsonObject).getObject("_source")
BenchmarksReport.create(reportDescription).benchmarks
}?.reduce { acc, it -> acc + it } ?: error("Wrong format of response:\n $responseString")
}
}
// Get distinct values for needed field from database.
fun distinctValues(field: String, index: ElasticSearchIndex): Promise<List<String>> {
val queryDescription = """
{
"aggs": {
"unique": {"terms": {"field": "$field", "size": 1000}}
}
}
""".trimIndent()
return index.search(queryDescription, listOf("aggregations.unique.buckets")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
dbResponse.getObjectOrNull("aggregations")?.getObjectOrNull("unique")?.getArrayOrNull("buckets")
?.map { (it as JsonObject).getPrimitiveOrNull("key")?.content }?.filterNotNull()
?: error("Wrong response:\n$responseString")
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
@file:OptIn(ExperimentalTime::class)
package org.jetbrains.network
import kotlin.js.Promise // TODO - migrate to multiplatform.
import kotlin.time.*
// Response saved in cache.
data class CachedResponse(val cachedResult: Any, val time: TimeMark)
// Dispatcher for work with cachable responses.
object CachableResponseDispatcher {
// Storage of cached responses.
private val cachedResponses = mutableMapOf<String, CachedResponse>()
// Get response. If response isn't cached, use provided action to get response.
fun getResponse(request: dynamic, response: dynamic,
action: (success: (result: Any) -> Unit, reject: () -> Unit) -> Unit) {
cachedResponses[request.url]?.let {
// Update cache value if needed. Update only if last result was get later than 2 minutes.
if (it.time.elapsedNow().inMinutes > 2.0) {
println("Cache update for ${request.url}...")
action({ result: Any ->
cachedResponses[request.url] = CachedResponse(result, TimeSource.Monotonic.markNow())
}, { println("Cache update for ${request.url} failed!") })
}
response.json(it.cachedResult)
} ?: run {
action({ result: Any ->
cachedResponses[request.url] = CachedResponse(result, TimeSource.Monotonic.markNow())
response.json(result)
}, { response.sendStatus(400) })
}
}
fun clear(): Unit {
cachedResponses.clear()
}
}
@@ -0,0 +1,101 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.network
import kotlin.js.Promise // TODO - migrate to multiplatform.
import kotlin.js.json // TODO - migrate to multiplatform.
import kotlin.js.Date
import org.jetbrains.report.json.*
// Placeholder as analog for indexable type in TS.
external interface `T$0` {
@nativeGetter
operator fun get(key: String): String?
@nativeSetter
operator fun set(key: String, value: String)
}
@JsModule("aws-sdk")
@JsNonModule
external object AWSInstance {
// Replace dynamic with some real type
class Endpoint(domain: String)
open class HttpRequest(endpoint: Endpoint, region: String) {
open fun pathname(): String
open var search: String
open var body: String?
open var endpoint: Endpoint
open var headers: `T$0`
open var method: String
open var path: String
}
class HttpClient() {
val handleRequest: dynamic
}
class SharedIniFileCredentials(options: Map<String, String>) {
val accessKeyId: String
}
class Signers() {
class V4(request: HttpRequest, subsystem: String) {
fun addAuthorization(credentials: SharedIniFileCredentials, date: Date)
}
}
}
// Network connector to work with AWS resources.
class AWSNetworkConnector : NetworkConnector() {
val AWSDomain = "vpc-kotlin-perf-service-5e6ldakkdv526ii5hbclzcmpny.eu-west-1.es.amazonaws.com"
val AWSRegion = "eu-west-1"
override fun <T : String?> sendBaseRequest(method: RequestMethod, path: String, user: String?, password: String?,
acceptJsonContentType: Boolean, body: String?,
errorHandler: (url: String, response: dynamic) -> Nothing?): Promise<T> {
val AWSEndpoint = AWSInstance.Endpoint(AWSDomain)
var request = AWSInstance.HttpRequest(AWSEndpoint, AWSRegion)
request.method = method.toString()
request.path += path
request.body = body
request.headers["host"] = this.AWSDomain
if (acceptJsonContentType) {
request.headers["Content-Type"] = "application/json"
request.headers["Content-Length"] = js("Buffer").byteLength(request.body)
}
val credentials = AWSInstance.SharedIniFileCredentials(mapOf<String, String>())
val signer = AWSInstance.Signers.V4(request, "es")
signer.addAuthorization(credentials, Date())
val client = AWSInstance.HttpClient()
return Promise { resolve, reject ->
client.handleRequest(request, null, { response ->
var responseBody = ""
response.on("data") { chunk ->
responseBody += chunk
chunk
}
response.on("end") { _ ->
val dbResponse = JsonTreeParser.parse(responseBody).jsonObject
// Response can fail and return 400 error for ES.
if (dbResponse.getPrimitiveOrNull("status")?.let { it.content != "200" } ?: false) {
println(dbResponse)
val errorMessage = dbResponse.getObject("error").toString()
reject(Throwable(errorMessage))
}
resolve(responseBody as T)
}
}, { error ->
reject(error)
})
}
}
}
@@ -1,330 +1,551 @@
/*
* Copyright 2010-2019 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
import org.w3c.xhr.*
import kotlin.js.json
import kotlin.js.Date
import kotlin.js.Promise
import org.jetbrains.database.*
import org.jetbrains.report.json.*
import org.jetbrains.build.Build
import org.jetbrains.elastic.*
import org.jetbrains.network.*
import org.jetbrains.buildInfo.Build
import org.jetbrains.analyzer.*
import org.jetbrains.report.*
import org.jetbrains.utils.*
// TODO - create DSL for ES requests?
const val teamCityUrl = "https://buildserver.labs.intellij.net/app/rest"
const val artifactoryUrl = "https://repo.labs.intellij.net/kotlin-native-benchmarks"
const val buildsFileName = "buildsSummary.csv"
const val goldenResultsFileName = "goldenResults.csv"
const val artifactoryBuildsDirectory = "builds"
const val buildsInfoPartsNumber = 11
operator fun <K, V> Map<K, V>?.get(key: K) = this?.get(key)
fun getArtifactoryHeader(artifactoryApiKey: String) = Pair("X-JFrog-Art-Api", artifactoryApiKey)
// Local cache for saving information about builds got from Artifactory.
object LocalCache {
private val knownTargets = listOf("Linux", "MacOSX", "Windows10")
private val buildsInfo = mutableMapOf<String, MutableMap<String, String>>()
fun clean(onlyTarget: String? = null) {
onlyTarget?.let {
buildsInfo[onlyTarget]?.clear()
} ?: buildsInfo.clear()
// Convert saved old report to expected new format.
internal fun convertToNewFormat(data: JsonObject): List<Any> {
val env = Environment.create(data.getRequiredField("env"))
val benchmarksObj = data.getRequiredField("benchmarks")
val compilerDescription = data.getRequiredField("kotlin")
val compiler = Compiler.create(compilerDescription)
val backend = (compilerDescription as JsonObject).getRequiredField("backend")
val flagsArray = (backend as JsonObject).getOptionalField("flags")
var flags: List<String> = emptyList()
if (flagsArray != null && flagsArray is JsonArray) {
flags = flagsArray.jsonArray.map { (it as JsonLiteral).unquoted() }
}
val benchmarksList = parseBenchmarksArray(benchmarksObj)
fun fill(onlyTarget: String? = null) {
onlyTarget?.let {
val buildsDescription = getBuildsInfoFromArtifactory(onlyTarget).lines().drop(1)
buildsInfo[onlyTarget] = mutableMapOf<String, String>()
buildsDescription.forEach {
if (!it.isEmpty()) {
val buildNumber = it.substringBefore(',')
if (!"\\d+(\\.\\d+)+(-M\\d)?-\\w+-\\d+".toRegex().matches(buildNumber)) {
error("Build number $buildNumber differs from expected format. File with data for " +
"target $onlyTarget could be corrupted.")
}
buildsInfo[onlyTarget]!![buildNumber] = it
}
}
} ?: run {
knownTargets.forEach {
fill(it)
}
}
}
fun buildExists(target: String, buildNumber: String) =
buildsInfo[target][buildNumber]?.let { true } ?: false
fun delete(target: String, builds: Iterable<String>, apiKey: String): Boolean {
// Delete from Artifactory.
val buildsDescription = getBuildsInfoFromArtifactory(target).lines()
val newBuildsDescription = buildsDescription.filter {
val buildNumber = it.substringBefore(',')
buildNumber !in builds
}
if (newBuildsDescription.size < buildsDescription.size) {
// Upload new version of file.
val uploadUrl = "$artifactoryUrl/$artifactoryBuildsDirectory/$target/$buildsFileName"
sendUploadRequest(uploadUrl, newBuildsDescription.joinToString("\n"),
extraHeaders = listOf(getArtifactoryHeader(apiKey)))
// Reload values.
clean(target)
fill(target)
return true
}
return false
}
private fun getBuilds(target: String, buildNumber: String? = null) =
buildsInfo[target]?.let { buildsList ->
buildNumber?.let {
// Check if interesting build id is in cache.
buildsList[it]?.let { buildsList.values }
} ?: buildsList.values
}
operator fun get(target: String, buildId: String? = null): Collection<String> {
val builds = getBuilds(target, buildId)
if (builds.isNullOrEmpty()) {
// No suitable builds were found.
// Refill cache.
clean(target)
fill(target)
return getBuilds(target, buildId) ?: listOf<String>()
}
return builds
}
return listOf(env, compiler, benchmarksList, flags)
}
// Convert data results to expected format.
internal fun convert(json: String, buildNumber: String, target: String): List<BenchmarksReport> {
val data = JsonTreeParser.parse(json)
val reports = if (data is JsonArray) {
data.map { convertToNewFormat(it as JsonObject) }
} else listOf(convertToNewFormat(data as JsonObject))
// Restored flags for old reports.
val knownFlags = mapOf(
"Cinterop" to listOf("-opt"),
"FrameworkBenchmarksAnalyzer" to listOf("-g"),
"HelloWorld" to if (target == "Mac OS X")
listOf("-Xcache-directory=/Users/teamcity/buildAgent/work/c104dee5223a31c5/test_dist/klib/cache/macos_x64-gSTATIC", "-g")
else listOf("-g"),
"Numerical" to listOf("-opt"),
"ObjCInterop" to listOf("-opt"),
"Ring" to listOf("-opt"),
"Startup" to listOf("-opt"),
"swiftInterop" to listOf("-opt"),
"Videoplayer" to if (target == "Mac OS X")
listOf("-Xcache-directory=/Users/teamcity/buildAgent/work/c104dee5223a31c5/test_dist/klib/cache/macos_x64-gSTATIC", "-g")
else listOf("-g")
)
return reports.map { elements ->
val benchmarks = (elements[2] as List<BenchmarkResult>).groupBy { it.name.substringBefore('.').substringBefore(':') }
val parsedFlags = elements[3] as List<String>
benchmarks.map { (setName, results) ->
val flags = if (parsedFlags.isNotEmpty() && parsedFlags[0] == "-opt") knownFlags[setName]!! else parsedFlags
val savedCompiler = elements[1] as Compiler
val compiler = Compiler(Compiler.Backend(savedCompiler.backend.type, savedCompiler.backend.version, flags),
savedCompiler.kotlinVersion)
val newReport = BenchmarksReport(elements[0] as Environment, results, compiler)
newReport.buildNumber = buildNumber
newReport
}
}.flatten()
}
// Golden result value used to get normalized results.
data class GoldenResult(val benchmarkName: String, val metric: String, val value: Double)
data class GoldenResultsInfo(val apiKey: String, val goldenResults: Array<GoldenResult>)
data class GoldenResultsInfo(val goldenResults: Array<GoldenResult>)
// Convert information about golden results to benchmarks report format.
fun GoldenResultsInfo.toBenchmarksReport(): BenchmarksReport {
val benchmarksSamples = goldenResults.map {
BenchmarkResult(it.benchmarkName, BenchmarkResult.Status.PASSED,
it.value, BenchmarkResult.metricFromString(it.metric)!!, it.value, 1, 0)
}
val compiler = Compiler(Compiler.Backend(Compiler.BackendType.NATIVE, "golden", emptyList()), "golden")
val environment = Environment(Environment.Machine("golden", "golden"), Environment.JDKInstance("golden", "golden"))
return BenchmarksReport(environment,
benchmarksSamples, compiler)
}
// Build information provided from request.
data class BuildInfo(val buildNumber: String, val branch: String, val startTime: String,
val finishTime: String)
data class TCBuildInfo(val buildNumber: String, val branch: String, val startTime: String,
val finishTime: String)
data class BuildRegister(val buildId: String, val teamCityUser: String, val teamCityPassword: String,
val artifactoryApiKey: String, val target: String, val buildType: String, val failuresNumber: Int,
val executionTime: String, val compileTime: String, val codeSize: String,
val bundleSize: String?) {
val bundleSize: String?, val fileWithResult: String) {
companion object {
fun create(json: String): BuildRegister {
val requestDetails = JSON.parse<BuildRegister>(json)
// Parse method doesn't create real instance with all methods. So create it by hands.
return BuildRegister(requestDetails.buildId, requestDetails.teamCityUser, requestDetails.teamCityPassword,
requestDetails.artifactoryApiKey, requestDetails.target, requestDetails.buildType,
requestDetails.failuresNumber, requestDetails.executionTime, requestDetails.compileTime,
requestDetails.codeSize, requestDetails.bundleSize)
requestDetails.bundleSize, requestDetails.fileWithResult)
}
}
private val teamCityBuildUrl: String by lazy { "$teamCityUrl/builds/id:$buildId" }
private val teamCityBuildUrl: String by lazy { "builds/id:$buildId" }
val changesListUrl: String by lazy {
"$teamCityUrl/changes/?locator=build:id:$buildId"
"changes/?locator=build:id:$buildId"
}
private fun sendTeamCityRequest(url: String) = sendGetRequest(url, teamCityUser, teamCityPassword)
val teamCityArtifactsUrl: String by lazy { "builds/id:$buildId/artifacts/content/$fileWithResult" }
fun sendTeamCityRequest(url: String, json: Boolean = false) =
UrlNetworkConnector(teamCityUrl).sendRequest(RequestMethod.GET, url, teamCityUser, teamCityPassword, json)
private fun format(timeValue: Int): String =
if (timeValue < 10) "0$timeValue" else "$timeValue"
fun getBuildInformation(): BuildInfo {
val buildNumber = sendTeamCityRequest("$teamCityBuildUrl/number")
val branch = sendTeamCityRequest("$teamCityBuildUrl/branchName")
val startTime = sendTeamCityRequest("$teamCityBuildUrl/startDate")
val currentTime = Date()
val timeZone = currentTime.getTimezoneOffset() / -60 // Convert to hours.
// Get finish time as current time, because buid on TeamCity isn't finished.
val finishTime = "${format(currentTime.getUTCFullYear())}" +
"${format(currentTime.getUTCMonth() + 1)}" +
"${format(currentTime.getUTCDate())}" +
"T${format(currentTime.getUTCHours())}" +
"${format(currentTime.getUTCMinutes())}" +
"${format(currentTime.getUTCSeconds())}" +
"${if (timeZone > 0) "+" else "-"}${format(timeZone)}${format(0)}"
return BuildInfo(buildNumber, branch, startTime, finishTime)
}
}
data class Commit(val revision: String, val developer: String)
// List of commits.
class CommitsList(data: JsonElement): ConvertedFromJson {
val commits: List<Commit>
init {
if (data !is JsonObject) {
error("Commits description is expected to be a json object!")
}
val changesElement = data.getOptionalField("change")
commits = changesElement?.let {
if (changesElement !is JsonArray) {
error("Change field is expected to be an array. Please, check source.")
}
changesElement.jsonArray.map {
with(it as JsonObject) {
Commit(elementToString(getRequiredField("version"), "version"),
elementToString(getRequiredField("username"), "username")
)
}
}
} ?: listOf<Commit>()
}
}
fun getBuildsInfoFromArtifactory(target: String) =
sendGetRequest("$artifactoryUrl/$artifactoryBuildsDirectory/$target/$buildsFileName")
fun checkBuildType(currentType: String, targetType: String): Boolean {
val releasesBuildTypes = listOf("release", "eap", "rc1", "rc2")
return if (targetType == "release") currentType in releasesBuildTypes else currentType == targetType
}
// Parse and postprocess result of response with build description.
fun prepareBuildsResponse(builds: Collection<String>, type: String, branch: String, buildNumber: String? = null): List<Build> {
val buildsObjects = mutableListOf<Build>()
builds.forEach {
val tokens = buildDescriptionToTokens(it)
if ((checkBuildType(tokens[5], type) || type == "day") && (branch == tokens[3] || branch == "all")
|| tokens[0] == buildNumber) {
buildsObjects.add(Build(tokens[0], tokens[1], tokens[2], tokens[3],
tokens[4], tokens[5], tokens[6].toInt(), tokens[7], tokens[8], tokens[9],
if (tokens[10] == "-") null else tokens[10]))
fun getBuildInformation(): Promise<TCBuildInfo> {
return Promise.all(arrayOf(sendTeamCityRequest("$teamCityBuildUrl/number"),
sendTeamCityRequest("$teamCityBuildUrl/branchName"),
sendTeamCityRequest("$teamCityBuildUrl/startDate"))).then { results ->
val (buildNumber, branch, startTime) = results
val currentTime = Date()
val timeZone = currentTime.getTimezoneOffset() / -60 // Convert to hours.
// Get finish time as current time, because buid on TeamCity isn't finished.
val finishTime = "${format(currentTime.getUTCFullYear())}" +
"${format(currentTime.getUTCMonth() + 1)}" +
"${format(currentTime.getUTCDate())}" +
"T${format(currentTime.getUTCHours())}" +
"${format(currentTime.getUTCMinutes())}" +
"${format(currentTime.getUTCSeconds())}" +
"${if (timeZone > 0) "+" else "-"}${format(timeZone)}${format(0)}"
TCBuildInfo(buildNumber, branch, startTime, finishTime)
}
}
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
}
// Get builds numbers in right order.
internal fun <T> orderedValues(values: List<T>, buildElement: (T) -> String = { it -> it.toString() },
skipMilestones: Boolean = false) =
values.sortedWith(
compareBy({ buildElement(it).substringBefore(".").toInt() },
{ buildElement(it).substringAfter(".").substringBefore("-").toDouble() },
{
if (skipMilestones) 0
else if (buildElement(it).substringAfter("-").startsWith("M"))
buildElement(it).substringAfter("M").substringBefore("-").toInt()
else
Int.MAX_VALUE
},
{ buildElement(it).substringAfterLast("-").toDouble() }
)
)
// ElasticSearch connector for work with custom instance.
internal val localHostElasticConnector = UrlNetworkConnector("http://localhost", 9200)
// ElasticSearch connector for work with AWS instance.
internal val awsElasticConnector = AWSNetworkConnector()
internal val networkConnector = awsElasticConnector
fun urlParameterToBaseFormat(value: dynamic) =
value.toString().replace("_", " ")
// Routing of requests to current server.
fun router() {
val express = require("express")
val router = express.Router()
val connector = ElasticSearchConnector(networkConnector)
val benchmarksDispatcher = BenchmarksIndexesDispatcher(connector, "env.machine.os",
listOf("Linux", "Mac OS X", "Windows 10")
)
val goldenIndex = GoldenResultsIndex(connector)
val buildInfoIndex = BuildInfoIndex(connector)
router.get("/createMapping") { request, response ->
buildInfoIndex.createMapping().then { _ ->
response.sendStatus(200)
}.catch { _ ->
response.sendStatus(400)
}
}
// Get consistent build information in cases of rerunning the same build.
suspend fun getConsistentBuildInfo(buildInfoInstance: BuildInfo, reports: List<BenchmarksReport>,
rerunNumber: Int = 1): BuildInfo {
var currentBuildInfo = buildInfoInstance
if (buildExists(currentBuildInfo, buildInfoIndex)) {
// Check if benchmarks aren't repeated.
val existingBecnhmarks = benchmarksDispatcher.getBenchmarksList(currentBuildInfo.buildNumber,
currentBuildInfo.agentInfo).await()
val benchmarksToRegister = reports.map { it.benchmarks.keys }.flatten()
if (existingBecnhmarks.toTypedArray().intersect(benchmarksToRegister).isNotEmpty()) {
// Build was rerun.
val buildNumber = "${currentBuildInfo.buildNumber}.$rerunNumber"
currentBuildInfo = BuildInfo(buildNumber, currentBuildInfo.startTime, currentBuildInfo.endTime,
currentBuildInfo.commitsList, currentBuildInfo.branch, currentBuildInfo.agentInfo)
return getConsistentBuildInfo(currentBuildInfo, reports, rerunNumber + 1)
}
}
return currentBuildInfo
}
// Register build on Artifactory.
router.post("/register", { request, response ->
val maxCommitsNumber = 5
router.post("/register") { request, response ->
val register = BuildRegister.create(JSON.stringify(request.body))
// Get information from TeamCity.
val buildInfo = register.getBuildInformation()
val changes = sendGetRequest(register.changesListUrl, register.teamCityUser,
register.teamCityPassword, true)
val commitsList = CommitsList(JsonTreeParser.parse(changes))
val commitsDescription = buildString {
if (commitsList.commits.size > maxCommitsNumber) {
append("${commitsList.commits.get(0).revision} by ${commitsList.commits.get(0).developer};")
append("${commitsList.commits.get(1).revision} by ${commitsList.commits.get(1).developer};")
append("...;")
val beforeLast = commitsList.commits.lastIndex - 1
append("${commitsList.commits.get(beforeLast).revision} by ${commitsList.commits.get(beforeLast).developer};")
append("${commitsList.commits.last().revision} by ${commitsList.commits.last().developer};")
} else {
commitsList.commits.forEach {
append("${it.revision} by ${it.developer};")
register.getBuildInformation().then { buildInfo ->
register.sendTeamCityRequest(register.changesListUrl, true).then { changes ->
val commitsList = CommitsList(JsonTreeParser.parse(changes))
// Get artifact.
register.sendTeamCityRequest(register.teamCityArtifactsUrl).then { resultsContent ->
launch {
val reportData = JsonTreeParser.parse(resultsContent)
val reports = if (reportData is JsonArray) {
reportData.map { BenchmarksReport.create(it as JsonObject) }
} else listOf(BenchmarksReport.create(reportData as JsonObject))
val goldenResultPromise = getGoldenResults(goldenIndex)
val goldenResults = goldenResultPromise.await()
// Register build information.
var buildInfoInstance = getConsistentBuildInfo(
BuildInfo(buildInfo.buildNumber, buildInfo.startTime, buildInfo.finishTime,
commitsList, buildInfo.branch, reports[0].env.machine.os),
reports
)
if (register.bundleSize != null) {
// Add bundle size.
val bundleSizeBenchmark = BenchmarkResult("KotlinNative",
BenchmarkResult.Status.PASSED, register.bundleSize.toDouble(),
BenchmarkResult.Metric.BUNDLE_SIZE, 0.0, 1, 0)
val bundleSizeReport = BenchmarksReport(reports[0].env,
listOf(bundleSizeBenchmark), reports[0].compiler)
bundleSizeReport.buildNumber = buildInfoInstance.buildNumber
benchmarksDispatcher.insert(bundleSizeReport, reports[0].env.machine.os).then { _ ->
println("[BUNDLE] Success insert ${buildInfoInstance.buildNumber}")
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse)
}
}
val insertResults = reports.map {
val benchmarksReport = SummaryBenchmarksReport(it).getBenchmarksReport()
.normalizeBenchmarksSet(goldenResults)
benchmarksReport.buildNumber = buildInfoInstance.buildNumber
// Save results in database.
benchmarksDispatcher.insert(benchmarksReport, benchmarksReport.env.machine.os)
}
if (!buildExists(buildInfoInstance, buildInfoIndex)) {
buildInfoIndex.insert(buildInfoInstance).then { _ ->
println("Success insert build information for ${buildInfoInstance.buildNumber}")
}.catch {
response.sendStatus(400)
}
}
Promise.all(insertResults.toTypedArray()).then { _ ->
response.sendStatus(200)
}.catch {
response.sendStatus(400)
}
}
}
}
}
// Get summary file from Artifactory.
var buildsDescription = getBuildsInfoFromArtifactory(register.target)
// Add information about new build.
//var buildsDescription = "build, start time, finish time, branch, commits, type, failuresNumber, execution time, compile time, code size, bundle size\n"
buildsDescription += "${buildInfo.buildNumber}, ${buildInfo.startTime}, ${buildInfo.finishTime}, " +
"${buildInfo.branch}, $commitsDescription, ${register.buildType}, ${register.failuresNumber}, " +
"${register.executionTime}, ${register.compileTime}, ${register.codeSize}, " +
"${register.bundleSize ?: "-"}\n"
// Upload new version of file.
val uploadUrl = "$artifactoryUrl/$artifactoryBuildsDirectory/${register.target}/${buildsFileName}"
sendUploadRequest(uploadUrl, buildsDescription, extraHeaders = listOf(getArtifactoryHeader(register.artifactoryApiKey)))
LocalCache.clean(register.target)
LocalCache.fill(register.target)
// Send response.
response.sendStatus(200)
})
}
// Register golden results to normalize on Artifactory.
router.post("/registerGolden", { request, response ->
val goldenResultsInfo = JSON.parse<GoldenResultsInfo>(JSON.stringify(request.body))
val buildsDescription = StringBuilder(sendGetRequest("$artifactoryUrl/$artifactoryBuildsDirectory/$goldenResultsFileName"))
goldenResultsInfo.goldenResults.forEach {
buildsDescription.append("${it.benchmarkName}, ${it.metric}, ${it.value}\n")
val goldenResultsInfo: GoldenResultsInfo = JSON.parse<GoldenResultsInfo>(JSON.stringify(request.body))
val goldenReport = goldenResultsInfo.toBenchmarksReport()
goldenIndex.insert(goldenReport).then { _ ->
response.sendStatus(200)
}.catch {
response.sendStatus(400)
}
// Upload new version of file.
val uploadUrl = "$artifactoryUrl/$artifactoryBuildsDirectory/$goldenResultsFileName"
sendUploadRequest(uploadUrl, buildsDescription.toString(),
extraHeaders = listOf(getArtifactoryHeader(goldenResultsInfo.apiKey)))
// Send response.
response.sendStatus(200)
})
// Get list of builds.
router.get("/builds/:target/:type/:branch/:id", { request, response ->
val builds = LocalCache[request.params.target, request.params.id]
response.json(prepareBuildsResponse(builds, request.params.type, request.params.branch, request.params.id))
// Get builds description with additional information.
router.get("/buildsDesc/:target", { request, response ->
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
val target = request.params.target.toString().replace('_', ' ')
var branch: String? = null
var type: String? = null
if (request.query != undefined) {
if (request.query.branch != undefined) {
branch = request.query.branch
}
if (request.query.type != undefined) {
type = request.query.type
}
}
getBuildsInfo(type, branch, target, buildInfoIndex).then { buildsInfo ->
val buildNumbers = buildsInfo.map { it.buildNumber }
// Get number of failed benchmarks for each build.
benchmarksDispatcher.getFailuresNumber(target, buildNumbers).then { failures ->
success(orderedValues(buildsInfo, { it -> it.buildNumber }, branch == "master").map {
Build(it.buildNumber, it.startTime, it.endTime, it.branch,
it.commitsList.serializeFields(), failures[it.buildNumber] ?: 0)
})
}.catch { errorResponse ->
println("Error during getting failures numbers")
println(errorResponse)
reject()
}
}.catch {
reject()
}
}
})
router.get("/builds/:target/:type/:branch", { request, response ->
val builds = LocalCache[request.params.target]
response.json(prepareBuildsResponse(builds, request.params.type, request.params.branch))
// Get values of current metric.
router.get("/metricValue/:target/:metric", { request, response ->
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
val metric = request.params.metric
val target = request.params.target.toString().replace('_', ' ')
var samples: List<String> = emptyList()
var aggregation = "geomean"
var normalize = false
var branch: String? = null
var type: String? = null
var excludeNames: List<String> = emptyList()
// Parse parameters from request if it exists.
if (request.query != undefined) {
if (request.query.samples != undefined) {
samples = request.query.samples.toString().split(",").map { it.trim() }
}
if (request.query.agr != undefined) {
aggregation = request.query.agr.toString()
}
if (request.query.normalize != undefined) {
normalize = true
}
if (request.query.branch != undefined) {
branch = request.query.branch
}
if (request.query.type != undefined) {
type = request.query.type
}
if (request.query.exclude != undefined) {
excludeNames = request.query.exclude.toString().split(",").map { it.trim() }
}
}
getBuildsNumbers(type, branch, target, buildInfoIndex).then { buildNumbers ->
if (aggregation == "geomean") {
// Get geometric mean for samples.
benchmarksDispatcher.getGeometricMean(metric, target, buildNumbers, normalize,
excludeNames).then { geoMeansValues ->
success(orderedValues(geoMeansValues, { it -> it.first }, branch == "master"))
}.catch { errorResponse ->
println("Error during getting geometric mean")
println(errorResponse)
reject()
}
} else {
benchmarksDispatcher.getSamples(metric, target, samples, buildNumbers, normalize).then { geoMeansValues ->
success(orderedValues(geoMeansValues, { it -> it.first }, branch == "master"))
}.catch {
println("Error during getting samples")
reject()
}
}
}.catch {
println("Error during getting builds information")
reject()
}
}
})
router.get("/branches/:target", { request, response ->
val builds = LocalCache[request.params.target]
response.json(builds.map { buildDescriptionToTokens(it)[3] }.distinct())
// Get branches for [target].
router.get("/branches", { request, response ->
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
distinctValues("branch", buildInfoIndex).then { results ->
success(results)
}.catch { errorMessage ->
error(errorMessage.message ?: "Failed getting branches list.")
reject()
}
}
})
// Get build numbers for [target].
router.get("/buildsNumbers/:target", { request, response ->
val builds = LocalCache[request.params.target]
response.json(builds.map { buildDescriptionToTokens(it)[0] }.distinct())
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
distinctValues("buildNumber", buildInfoIndex).then { results ->
success(results)
}.catch { errorMessage ->
println(errorMessage.message ?: "Failed getting branches list.")
reject()
}
}
})
router.get("/clean", { _, response ->
LocalCache.clean()
response.sendStatus(200)
})
// Conert data and migrate it from Artifactory to DB.
router.get("/migrate/:target", { request, response ->
val target = urlParameterToBaseFormat(request.params.target)
val targetPathName = target.replace(" ", "")
var buildNumber: String? = null
if (request.query != undefined) {
if (request.query.buildNumber != undefined) {
buildNumber = request.query.buildNumber
}
}
getBuildsInfoFromArtifactory(targetPathName).then { buildInfo ->
launch {
val buildsDescription = buildInfo.lines().drop(1)
var shouldConvert = buildNumber?.let { false } ?: true
val goldenResultPromise = getGoldenResults(goldenIndex)
val goldenResults = goldenResultPromise.await()
val buildsSet = mutableSetOf<String>()
buildsDescription.forEach {
if (!it.isEmpty()) {
val currentBuildNumber = it.substringBefore(',')
if (!"\\d+(\\.\\d+)+(-M\\d)?-\\w+-\\d+(\\.\\d+)?".toRegex().matches(currentBuildNumber)) {
error("Build number $currentBuildNumber differs from expected format. File with data for " +
"target $target could be corrupted.")
}
if (!shouldConvert && buildNumber != null && buildNumber == currentBuildNumber) {
shouldConvert = true
}
if (shouldConvert) {
// Save data from Artifactory into database.
val artifactoryUrlConnector = UrlNetworkConnector(artifactoryUrl)
val fileName = "nativeReport.json"
val accessFileUrl = "$targetPathName/$currentBuildNumber/$fileName"
val extrenalFileName = if (target == "Linux") "externalReport.json" else "spaceFrameworkReport.json"
val accessExternalFileUrl = "$targetPathName/$currentBuildNumber/$extrenalFileName"
val infoParts = it.split(", ")
if ((infoParts[3] == "master" || "eap" in currentBuildNumber || "release" in currentBuildNumber) &&
currentBuildNumber !in buildsSet) {
try {
buildsSet.add(currentBuildNumber)
val jsonReport = artifactoryUrlConnector.sendRequest(RequestMethod.GET, accessFileUrl).await()
var reports = convert(jsonReport, currentBuildNumber, target)
val buildInfoRecord = BuildInfo(currentBuildNumber, infoParts[1], infoParts[2],
CommitsList.parse(infoParts[4]), infoParts[3], target)
router.get("/fill", { _, response ->
LocalCache.fill()
response.sendStatus(200)
val externalJsonReport = artifactoryUrlConnector.sendOptionalRequest(RequestMethod.GET, accessExternalFileUrl).await()
buildInfoIndex.insert(buildInfoRecord).then { _ ->
externalJsonReport?.let {
var externalReports = convert(externalJsonReport, currentBuildNumber, target)
externalReports.forEach { externalReport ->
val extrenalAdditionalReport = SummaryBenchmarksReport(externalReport)
.getBenchmarksReport().normalizeBenchmarksSet(goldenResults)
extrenalAdditionalReport.buildNumber = currentBuildNumber
benchmarksDispatcher.insert(extrenalAdditionalReport, target).then { _ ->
println("[External] Success insert ${buildInfoRecord.buildNumber}")
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse)
}
}
}
val bundleSize = if (infoParts[10] != "-") infoParts[10] else null
if (bundleSize != null) {
// Add bundle size.
val bundleSizeBenchmark = BenchmarkResult("KotlinNative",
BenchmarkResult.Status.PASSED, bundleSize.toDouble(),
BenchmarkResult.Metric.BUNDLE_SIZE, 0.0, 1, 0)
val bundleSizeReport = BenchmarksReport(reports[0].env,
listOf(bundleSizeBenchmark), reports[0].compiler)
bundleSizeReport.buildNumber = currentBuildNumber
benchmarksDispatcher.insert(bundleSizeReport, target).then { _ ->
println("[BUNDLE] Success insert ${buildInfoRecord.buildNumber}")
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse)
}
}
reports.forEach { report ->
val summaryReport = SummaryBenchmarksReport(report).getBenchmarksReport()
.normalizeBenchmarksSet(goldenResults)
summaryReport.buildNumber = currentBuildNumber
// Save results in database.
benchmarksDispatcher.insert(summaryReport, target).then { _ ->
println("Success insert ${buildInfoRecord.buildNumber}")
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse.message)
}
}
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse)
}
} catch (e: Exception) {
println(e)
}
}
}
}
}
}
response.sendStatus(200)
}.catch {
response.sendStatus(400)
}
})
router.get("/delete/:target", { request, response ->
val buildsToDelete: List<String> = request.query.builds.toString().split(",").map { it.trim() }
val result = LocalCache.delete(request.params.target, buildsToDelete, request.query.key)
if (result) {
response.sendStatus(200)
} else {
response.sendStatus(404)
val target = urlParameterToBaseFormat(request.params.target)
var buildNumber: String? = null
if (request.query != undefined) {
if (request.query.buildNumber != undefined) {
buildNumber = request.query.buildNumber
}
}
benchmarksDispatcher.deleteBenchmarks(target, buildNumber).then {
deleteBuildInfo(target, buildInfoIndex, buildNumber).then {
response.sendStatus(200)
}.catch {
response.sendStatus(400)
}
}.catch {
response.sendStatus(400)
}
})
router.get("/report/:target/:buildNumber", { request, response ->
val target = urlParameterToBaseFormat(request.params.target)
val buildNumber = request.params.buildNumber.toString()
benchmarksDispatcher.getBenchmarksReports(buildNumber, target).then { reports ->
response.send(reports.joinToString(", ", "[", "]"))
}.catch {
response.sendStatus(400)
}
})
router.get("/clear", { _, response ->
CachableResponseDispatcher.clear()
response.sendStatus(200)
})
// Main page.
@@ -335,52 +556,20 @@ fun router() {
return router
}
fun getAuth(user: String, password: String): String {
val buffer = js("Buffer").from(user + ":" + password)
val based64String = buffer.toString("base64")
return "Basic " + based64String
fun getBuildsInfoFromArtifactory(target: String): Promise<String> {
val buildsFileName = "buildsSummary.csv"
val artifactoryBuildsDirectory = "builds"
return UrlNetworkConnector(artifactoryUrl).sendRequest(RequestMethod.GET,
"$artifactoryBuildsDirectory/$target/$buildsFileName")
}
fun sendGetRequest(url: String, user: String? = null, password: String? = null, jsonContentType: Boolean = false) : String {
val request = require("sync-request")
val headers = mutableListOf<Pair<String, String>>()
if (user != null && password != null) {
headers.add("Authorization" to getAuth(user, password))
}
if (jsonContentType) {
headers.add("Accept" to "application/json")
}
val response = request("GET", url,
json(
"headers" to json(*(headers.toTypedArray()))
)
)
if (response.statusCode != 200) {
error("Error during getting response from $url\n" +
"${response.getBody()}")
}
return response.getBody().toString()
}
fun sendUploadRequest(url: String, fileContent: String, user: String? = null, password: String? = null,
extraHeaders: List<Pair<String, String>> = emptyList()) {
val request = require("sync-request")
val headers = mutableListOf<Pair<String, String>>("Content-type" to "text/plain")
if (user != null && password != null) {
headers.add("Authorization" to getAuth(user, password))
}
extraHeaders.forEach {
headers.add(it.first to it.second)
}
val response = request("PUT", url,
json(
"headers" to json(*(headers.toTypedArray())),
"body" to fileContent
)
)
if (response.statusCode != 201) {
error("Error during uploading to $url\n" +
"${response}")
}
fun BenchmarksReport.normalizeBenchmarksSet(dataForNormalization: Map<String, List<BenchmarkResult>>): BenchmarksReport {
val resultBenchmarksList = benchmarks.map { benchmarksList ->
benchmarksList.value.map {
NormalizedMeanVarianceBenchmark(it.name, it.status, it.score, it.metric,
it.runtimeInUs, it.repeat, it.warmup, (it as MeanVarianceBenchmark).variance,
dataForNormalization[benchmarksList.key]?.get(0)?.score?.let { golden -> it.score / golden } ?: 0.0)
}
}.flatten()
return BenchmarksReport(env, resultBenchmarksList, compiler)
}
@@ -0,0 +1,20 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.utils
import kotlin.coroutines.*
import kotlin.js.Promise
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
then({ cont.resume(it) }, { cont.resumeWithException(it) })
}
fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) =
block.startCoroutine(Continuation(context) { result ->
result.onFailure { exception ->
throw exception
}
})
+38 -32
View File
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
buildscript {
ext.rootBuildDirectory = file('../../..')
@@ -11,6 +13,12 @@ buildscript {
maven {
url kotlinCompilerRepo
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-eap"
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-dev"
}
}
dependencies {
@@ -28,42 +36,40 @@ repositories {
maven {
url buildKotlinCompilerRepo
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-eap"
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-dev"
}
}
apply plugin: 'kotlin2js'
apply plugin: 'kotlin-multiplatform'
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion"
}
compileKotlin2Js {
kotlinOptions.outputFile = "${projectDir}/js/main.js"
kotlinOptions.main = "call"
}
sourceSets {
main.kotlin.srcDirs += "src"
main.kotlin.srcDirs += "../../benchmarks/shared/src"
main.kotlin.srcDirs += "../shared/src"
main.output.resourcesDir = "build/js/resources"
}
task copyResources(type: Copy) {
from sourceSets.main.resources.srcDirs
into file(buildDir.path + "/js")
}
build.doLast {
configurations.compile.each { File file ->
copy {
includeEmptyDirs = false
from zipTree(file.absolutePath)
into "${projectDir}/lib/kotlin"
include { fileTreeElement ->
def path = fileTreeElement.path
path.endsWith(".js") && (path.startsWith("META-INF/resources/") || !path.startsWith("META-INF/"))
kotlin {
js {
browser {
binaries.executable()
distribution {
directory = new File("$projectDir/js/")
}
}
}
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
}
kotlin.srcDir '../../benchmarks/shared/src'
}
jsMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion"
}
kotlin.srcDir 'src/main/kotlin'
kotlin.srcDir '../shared/src/main/kotlin'
kotlin.srcDir '../src/main/kotlin-js'
}
}
}
@@ -68,11 +68,6 @@
border-color: darkcyan;
}
.ct-legend .ct-series-5:before {
background-color: #453d3f;
border-color: #453d3f;
}
.ct-series-b .ct-line,
.ct-series-b .ct-point {
/* Set the colour of this series line */
+3 -4
View File
@@ -33,8 +33,8 @@
</div>
<select class="custom-select" id="inputGroupTarget">
<option value="Linux">Linux</option>
<option value="MacOSX">Mac OS X</option>
<option value="Windows10">Windows 10</option>
<option value="Mac_OS_X">Mac OS X</option>
<option value="Windows_10">Windows 10</option>
</select>
</div>
</div>
@@ -110,7 +110,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartist-plugin-legend/0.6.2/chartist-plugin-legend.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.devbridge-autocomplete/1.4.9/jquery.autocomplete.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartist-plugin-axistitle@0.0.4/dist/chartist-plugin-axistitle.min.js"></script>
<script src="lib/kotlin/kotlin.js"></script>
<script src="js/main.js"></script>
<script src="js/ui.js"></script>
</body>
</html>
@@ -1,23 +1,12 @@
/*
* Copyright 2010-2019 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.
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
import kotlin.browser.*
import org.w3c.xhr.*
import org.w3c.fetch.*
import org.jetbrains.report.json.*
import org.jetbrains.build.Build
import org.jetbrains.buildInfo.Build
import kotlin.js.*
import kotlin.math.ceil
import org.w3c.dom.*
@@ -30,63 +19,40 @@ external class ChartistPlugins {
external object Chartist {
class Svg(form: String, parameters: dynamic, chartArea: String)
val plugins: ChartistPlugins
val Interpolation: dynamic
fun Line(query: String, data: dynamic, options: dynamic): dynamic
}
fun sendGetRequest(url: String) : String {
val request = XMLHttpRequest()
data class Commit(val revision: String, val developer: String)
request.open("GET", url, false)
request.send()
if (request.status == 200.toShort()) {
return request.responseText
fun sendGetRequest(url: String) = window.fetch(url, RequestInit("GET")).then { response ->
if (!response.ok)
error("Error during getting response from $url\n" +
"${response}")
else
response.text()
}.then { text -> text }
// Get groups of builds for different zoom values.
fun getBuildsGroup(builds: List<Build?>) = buildsNumberToShow?.let {
val buildsGroups = builds.chunked(buildsNumberToShow!!)
val expectedGroup = buildsGroups.size - 1 + stageToShow
val index = when {
expectedGroup < 0 -> 0
expectedGroup >= buildsGroups.size -> buildsGroups.size - 1
else -> expectedGroup
}
error("Request to $url has status ${request.status}")
}
// Parse description with values for metrics.
fun <T : Any> separateValues(values: String, valuesContainer: MutableMap<String, MutableList<T?>>, convert: (String) -> T = { it as T }) {
val existingSamples = mutableListOf<String>()
val splittedValues = values.split(";")
val insertedList = valuesContainer.values.firstOrNull()?.let { MutableList<T?>(it.size) { null } } ?: mutableListOf<T?>()
splittedValues.forEach { it ->
val valueParts = it.split("-", limit = 2)
if (valueParts.size != 2) {
error("Wrong format of value $it.")
}
val (sampleName, value) = valueParts
existingSamples.add(sampleName)
val currentList = mutableListOf<T?>()
currentList.addAll(insertedList)
valuesContainer.getOrPut(sampleName) { currentList }.add(convert(value))
}
// Check if there are other keys that are absent in current record.
val missedSamples = valuesContainer.keys - existingSamples
missedSamples.forEach {
valuesContainer[it]!!.add(null)
}
}
fun getBuildsGroup(builds: List<Build>) = buildsNumberToShow?.let {
val buildsGroups = builds.chunked(buildsNumberToShow!!)
val expectedGroup = buildsGroups.size - 1 + stageToShow
val index = when {
expectedGroup < 0 -> 0
expectedGroup >= buildsGroups.size -> buildsGroups.size - 1
else -> expectedGroup
}
buildsGroups[index]
} ?: builds
buildsGroups[index]
} ?: builds
// Get data for chart in needed format.
fun getChartData(labels: List<String>, valuesList: Collection<List<*>>, stageToShow: Int = 0,
buildsNumber: Int? = null, classNames: Array<String>? = null): dynamic {
val chartData: dynamic = object{}
val chartData: dynamic = object {}
// Show only some part of data.
val (labelsData, valuesData) = buildsNumber?.let {
println("Divide on ${labels.size / buildsNumber}")
val labelsGroups = labels.chunked(buildsNumber)
val valuesListGroups = valuesList.map { it.chunked(buildsNumber) }
val expectedGroup = labelsGroups.size - 1 + stageToShow
@@ -95,11 +61,11 @@ fun getChartData(labels: List<String>, valuesList: Collection<List<*>>, stageToS
expectedGroup >= labelsGroups.size -> labelsGroups.size - 1
else -> expectedGroup
}
Pair(labelsGroups[index], valuesListGroups.map {it[index]})
Pair(labelsGroups[index], valuesListGroups.map { it[index] })
} ?: Pair(labels, valuesList)
chartData["labels"] = labelsData.toTypedArray()
chartData["series"] = valuesData.mapIndexed { index, it ->
val series: dynamic = object{}
val series: dynamic = object {}
series["data"] = it.toTypedArray()
classNames?.let { series["className"] = classNames[index] }
series
@@ -107,32 +73,33 @@ fun getChartData(labels: List<String>, valuesList: Collection<List<*>>, stageToS
return chartData
}
// Create object with options of chart.
fun getChartOptions(samples: Array<String>, yTitle: String, classNames: Array<String>? = null): dynamic {
val chartOptions: dynamic = object{}
val chartOptions: dynamic = object {}
chartOptions["fullWidth"] = true
val paddingObject: dynamic = object{}
val paddingObject: dynamic = object {}
paddingObject["right"] = 40
chartOptions["chartPadding"] = paddingObject
val axisXObject: dynamic = object{}
val axisXObject: dynamic = object {}
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
if (index % skipNumber == 0) value else null
} else {
value
}
}
chartOptions["axisX"] = axisXObject
val axisYObject: dynamic = object{}
val axisYObject: dynamic = object {}
axisYObject["offset"] = 90
chartOptions["axisY"] = axisYObject
val legendObject: dynamic = object{}
val legendObject: dynamic = object {}
legendObject["legendNames"] = samples
classNames?.let { legendObject["classNames"] = classNames }
val titleObject: dynamic = object{}
val axisYTitle: dynamic = object{}
classNames?.let { legendObject["classNames"] = classNames.sliceArray(0 until samples.size) }
val titleObject: dynamic = object {}
val axisYTitle: dynamic = object {}
axisYTitle["axisTitle"] = yTitle
axisYTitle["axisClass"] = "ct-axis-title"
val titleOffset: dynamic = {}
@@ -153,7 +120,8 @@ fun redirect(url: String) {
window.location.href = url
}
fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynamic, builds: List<Build>,
// Set customizations rules for chart.
fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynamic, builds: List<Build?>,
parameters: Map<String, String>) {
chart.on("draw", { data ->
var element = data.element
@@ -161,59 +129,74 @@ fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynam
val buildsGroup = getBuildsGroup(builds)
val pointSize = 12
val currentBuild = buildsGroup.get(data.index)
// Higlight builds with failures.
if (currentBuild.failuresNumber > 0) {
val svgParameters: dynamic = object{}
svgParameters["d"] = arrayOf("M", data.x, data.y - pointSize,
"L", data.x - pointSize, data.y + pointSize/2,
"L", data.x + pointSize, data.y + pointSize/2, "z").joinToString(" ")
svgParameters["style"] = "fill:rgb(255,0,0);stroke-width:0"
val triangle = Chartist.Svg("path", svgParameters, chartContainer)
element = data.element.replace(triangle)
} else if (currentBuild.buildNumber == parameters["build"]) {
// Higlight choosen build.
val svgParameters: dynamic = object{}
svgParameters["x"] = data.x - pointSize/2
svgParameters["y"] = data.y - pointSize/2
svgParameters["height"] = pointSize
svgParameters["width"] = pointSize
svgParameters["style"] = "fill:rgb(0,0,255);stroke-width:0"
val rectangle = Chartist.Svg("rect", svgParameters, "ct-point")
element = data.element.replace(rectangle)
}
// Add tooltips.
val linkToDetailedInfo = "https://kotlin-native-performance.labs.jb.gg/?report=artifactory:" +
"${currentBuild.buildNumber}:${parameters["target"]}:nativeReport.json" +
"${if (data.index - 1 >= 0)
"&compareTo=artifactory:${buildsGroup.get(data.index - 1).buildNumber}:${parameters["target"]}:nativeReport.json"
else ""}"
val information = buildString {
append("<a href=\"$linkToDetailedInfo\">${currentBuild.buildNumber}</a><br>")
append("Value: ${data.value.y.toFixed(4)}<br>")
currentBuild?.let { currentBuild ->
// Higlight builds with failures.
if (currentBuild.failuresNumber > 0) {
append("failures: ${currentBuild.failuresNumber}<br>")
val svgParameters: dynamic = object {}
svgParameters["d"] = arrayOf("M", data.x, data.y - pointSize,
"L", data.x - pointSize, data.y + pointSize / 2,
"L", data.x + pointSize, data.y + pointSize / 2, "z").joinToString(" ")
svgParameters["style"] = "fill:rgb(255,0,0);stroke-width:0"
val triangle = Chartist.Svg("path", svgParameters, chartContainer)
element = data.element.replace(triangle)
} else if (currentBuild.buildNumber == parameters["build"]) {
// Higlight choosen build.
val svgParameters: dynamic = object {}
svgParameters["x"] = data.x - pointSize / 2
svgParameters["y"] = data.y - pointSize / 2
svgParameters["height"] = pointSize
svgParameters["width"] = pointSize
svgParameters["style"] = "fill:rgb(0,0,255);stroke-width:0"
val rectangle = Chartist.Svg("rect", svgParameters, "ct-point")
element = data.element.replace(rectangle)
}
append("branch: ${currentBuild.branch}<br>")
append("date: ${currentBuild.date}<br>")
append("time: ${currentBuild.formattedStartTime}-${currentBuild.formattedFinishTime}<br>")
append("Commits:<br>")
val commitsList = currentBuild.commits.split(";")
commitsList.forEach {
if (!it.isEmpty())
append("${it.substringBefore("by").substring(0, 7)} by ${it.substringAfter("by ")}<br>")
// Add tooltips.
var shift = 1
var previousBuild: Build? = null
while (previousBuild == null && data.index - shift >= 0) {
previousBuild = buildsGroup.get(data.index - shift)
shift++
}
val linkToDetailedInfo = "https://kotlin-native-performance.labs.jb.gg/?report=" +
"${currentBuild.buildNumber}:${parameters["target"]}" +
"${previousBuild?.let {
"&compareTo=${previousBuild.buildNumber}:${parameters["target"]}"
} ?: ""}"
val information = buildString {
append("<a href=\"$linkToDetailedInfo\">${currentBuild.buildNumber}</a><br>")
append("Value: ${data.value.y.toFixed(4)}<br>")
if (currentBuild.failuresNumber > 0) {
append("failures: ${currentBuild.failuresNumber}<br>")
}
append("branch: ${currentBuild.branch}<br>")
append("date: ${currentBuild.date}<br>")
append("time: ${currentBuild.formattedStartTime}-${currentBuild.formattedFinishTime}<br>")
append("Commits:<br>")
val commitsList = (JsonTreeParser.parse("{${currentBuild.commits}}") as JsonObject).getArray("commits").map {
Commit(
(it as JsonObject).getPrimitive("revision").content,
(it as JsonObject).getPrimitive("developer").content
)
}
val commits = if (commitsList.size > 3) commitsList.slice(0..2) else commitsList
commits.forEach {
append("${it.revision.substring(0, 7)} by ${it.developer}<br>")
}
if (commitsList.size > 3) {
append("...")
}
}
element._node.setAttribute("title", information)
element._node.setAttribute("data-chart-tooltip", chartContainer)
element._node.addEventListener("click", {
redirect(linkToDetailedInfo)
})
}
element._node.setAttribute("title", information)
element._node.setAttribute("data-chart-tooltip", chartContainer)
element._node.addEventListener("click", {
redirect(linkToDetailedInfo)
})
}
})
chart.on("created", {
val currentChart = jquerySelector
val parameters: dynamic = object{}
val parameters: dynamic = object {}
parameters["selector"] = "[data-chart-tooltip=\"$chartContainer\"]"
parameters["container"] = "#$chartContainer"
parameters["html"] = true
@@ -225,6 +208,12 @@ var stageToShow = 0
var buildsNumberToShow: Int? = null
fun waitForElements(elements: Iterable<Any?>, action: () -> Unit) {
if (elements.filterNotNull().size == elements.count())
action()
else window.setTimeout(waitForElements(elements, action), 500)
}
fun main(args: Array<String>) {
val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg"
buildsNumberToShow = null
@@ -243,45 +232,52 @@ fun main(args: Array<String>) {
}
}
// Get builds.
val buildsUrl = buildString {
append("$serverUrl/builds")
append("/${parameters["target"]}")
append("/${parameters["type"]}")
append("/${parameters["branch"]}")
append("/${parameters["build"]}")
// Get branches.
val branchesUrl = "$serverUrl/branches"
sendGetRequest(branchesUrl).then { response ->
val branches: Array<String> = JSON.parse(response)
// Add release branches to selector.
branches.filter { it != "master" }.forEach {
if ("v(\\d|\\.)+(-M\\d)?-fixes".toRegex().matches(it)) {
val option = Option(it, it)
js("$('#inputGroupBranch')").append(js("$(option)"))
}
}
document.querySelector("#inputGroupBranch [value=\"${parameters["branch"]}\"]")?.setAttribute("selected", "true")
}
val response = sendGetRequest(buildsUrl)
val data = JsonTreeParser.parse(response)
if (data !is JsonArray) {
error("Response is expected to be an array.")
}
val builds = data.jsonArray.map { Build.create(it as JsonObject) }
.sortedWith(compareBy ( { it.buildNumber.substringBefore(".").toInt() }, { it.buildNumber.substringAfter(".").substringBefore("-").toDouble() }, { it.buildNumber.substringAfterLast("-").toInt() }))
val branchesUrl = "$serverUrl/branches/${parameters["target"]}"
val branches: Array<String> = JSON.parse(sendGetRequest(branchesUrl))
val releaseBranches = branches.filter { "^v\\d+(\\.\\d+)+(-M\\d)?-fixes$".toRegex().find(it) != null }
// Fill autocomplete list.
// Fill autocomplete list with build numbers.
val buildsNumbersUrl = "$serverUrl/buildsNumbers/${parameters["target"]}"
val buildsNumbers: Array<String> = JSON.parse(sendGetRequest(buildsNumbersUrl))
// Add release branches to selector.
releaseBranches.forEach {
val option = Option(it, it)
js("$('#inputGroupBranch')").append(js("$(option)"))
sendGetRequest(buildsNumbersUrl).then { response ->
val buildsNumbers: Array<String> = JSON.parse(response)
val autocompleteParameters: dynamic = object {}
autocompleteParameters["lookup"] = buildsNumbers
autocompleteParameters["onSelect"] = { suggestion ->
if (suggestion.value != parameters["build"]) {
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" +
"${if ((suggestion.value as String).isEmpty()) "" else "&build=${suggestion.value}"}"
window.location.href = newLink
}
}
js("$( \"#highligted_build\" )").autocomplete(autocompleteParameters)
js("$('#highligted_build')").change({ value ->
val newValue = js("$(this).val()").toString()
if (newValue.isEmpty() || newValue in buildsNumbers) {
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" +
"${if (newValue.isEmpty()) "" else "&build=$newValue"}"
window.location.href = newLink
}
})
}
// Change inputs values connected with parameters and add events listeners.
document.querySelector("#inputGroupTarget [value=\"${parameters["target"]}\"]")?.setAttribute("selected", "true")
document.querySelector("#inputGroupBuildType [value=\"${parameters["type"]}\"]")?.setAttribute("selected", "true")
document.querySelector("#inputGroupBranch [value=\"${parameters["branch"]}\"]")?.setAttribute("selected", "true")
(document.getElementById("highligted_build") as HTMLInputElement).value = parameters["build"]!!
// Add onChange events for fields.
// Don't use AJAX to have opportunity to share results with simple links.
js("$('#inputGroupTarget')").change({
val newValue = js("$(this).val()")
if (newValue != parameters["target"]) {
@@ -307,69 +303,160 @@ fun main(args: Array<String>) {
}
})
val autocompleteParameters: dynamic = object{}
autocompleteParameters["lookup"] = buildsNumbers
autocompleteParameters["onSelect"] = { suggestion ->
if (suggestion.value != parameters["build"]) {
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" +
"${if ((suggestion.value as String).isEmpty()) "" else "&build=${suggestion.value}"}"
window.location.href = newLink
}
}
js("$( \"#highligted_build\" )").autocomplete(autocompleteParameters)
js("$('#highligted_build')").change({ value ->
val newValue = js("$(this).val()").toString()
if (newValue.isEmpty() || newValue in builds.map {it.buildNumber}) {
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" +
"${if (newValue.isEmpty()) "" else "&build=$newValue"}"
window.location.href = newLink
}
})
val platformSpecificBenchs = if (parameters["target"] == "Mac_OS_X") ",FrameworkBenchmarksAnalyzer,circlet_iosX64" else
if (parameters["target"] == "Linux") ",kotlinx.coroutines" else ""
// Collect information for charts library.
val labels = mutableListOf<String>()
val executionTime = mutableMapOf<String, MutableList<Double?>>()
val compileTime = mutableMapOf<String, MutableList<Double?>>()
val codeSize = mutableMapOf<String, MutableList<Double?>>()
val bundleSize = mutableListOf<Int?>()
val valuesToShow = mapOf("EXECUTION_TIME" to arrayOf(mapOf(
"normalize" to "true"
)),
"COMPILE_TIME" to arrayOf(mapOf(
"samples" to "HelloWorld,Videoplayer$platformSpecificBenchs",
"agr" to "samples"
)),
"CODE_SIZE" to arrayOf(mapOf(
"normalize" to "true",
"exclude" to if (parameters["target"] == "Linux")
"kotlinx.coroutines"
else if (parameters["target"] == "Mac_OS_X")
"circlet_iosX64"
else ""
), mapOf(
"normalize" to "true",
"agr" to "samples",
"samples" to platformSpecificBenchs.removePrefix(",")
)),
"BUNDLE_SIZE" to arrayOf(mapOf("samples" to "KotlinNative",
"agr" to "samples"))
)
builds.forEach {
// Choose labels on x axis.
if (parameters["type"] == "day") {
labels.add(it.date)
} else {
labels.add(it.buildNumber)
}
separateValues(it.executionTime, executionTime) { value -> value.toDouble() }
separateValues(it.compileTime, compileTime) { value -> value.toDouble() / 1000 }
separateValues(it.codeSize, codeSize) { value -> value.toDouble() }
bundleSize.add(it.bundleSize?.toInt()?. let { it / 1024 / 1024 })
}
var execData = listOf<String>() to listOf<List<Double?>>()
var compileData = listOf<String>() to listOf<List<Double?>>()
var codeSizeData = listOf<String>() to listOf<List<Double?>>()
var bundleSizeData = listOf<String>() to listOf<List<Int?>>()
val sizeClassNames = arrayOf("ct-series-e", "ct-series-f", "ct-series-g")
// Draw charts.
val execChart = Chartist.Line("#exec_chart", getChartData(labels, executionTime.values, stageToShow, buildsNumberToShow),
getChartOptions(executionTime.keys.toTypedArray(), "Normalized time"))
val compileChart = Chartist.Line("#compile_chart", getChartData(labels, compileTime.values, stageToShow, buildsNumberToShow),
getChartOptions(compileTime.keys.toTypedArray(), "Time, milliseconds"))
val codeSizeChart = Chartist.Line("#codesize_chart", getChartData(labels, codeSize.values, stageToShow, buildsNumberToShow, sizeClassNames),
getChartOptions(codeSize.keys.toTypedArray(), "Normalized size", arrayOf("ct-series-4", "ct-series-5", "ct-series-6")))
val bundleSizeChart = Chartist.Line("#bundlesize_chart", getChartData(labels, listOf(bundleSize), stageToShow, buildsNumberToShow, sizeClassNames),
getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-4")))
var execChart: dynamic = null
var compileChart: dynamic = null
var codeSizeChart: dynamic = null
var bundleSizeChart: dynamic = null
// Tooltips and higlights.
customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters)
customizeChart(compileChart, "compile_chart", js("$(\"#compile_chart\")"), builds, parameters)
customizeChart(codeSizeChart, "codesize_chart", js("$(\"#codesize_chart\")"), builds, parameters)
customizeChart(bundleSizeChart, "bundlesize_chart", js("$(\"#bundlesize_chart\")"), builds, parameters)
val descriptionUrl = "$serverUrl/buildsDesc/${parameters["target"]}?type=${parameters["type"]}" +
"${if (parameters["branch"] != "all") "&branch=${parameters["branch"]}" else ""}"
val metricUrl = "$serverUrl/metricValue/${parameters["target"]}/"
// Send requests to get all needed metric values.
valuesToShow.map { (metric, arrayOfSettings) ->
val resultValues = arrayOfSettings.map { settings ->
val getParameters = with(StringBuilder()) {
if (settings.isNotEmpty()) {
append("?")
}
var prefix = ""
settings.forEach { (key, value) ->
if (value.isNotEmpty()) {
append("$prefix$key=$value")
prefix = "&"
}
}
toString()
}
val branchParameter = if (parameters["branch"] != "all")
(if (getParameters.isEmpty()) "?" else "&") + "branch=${parameters["branch"]}"
else ""
val url = "$metricUrl$metric$getParameters$branchParameter${
if (parameters["type"] != "all")
(if (getParameters.isEmpty() && branchParameter.isEmpty()) "?" else "&") + "type=${parameters["type"]}"
else ""
}"
sendGetRequest(url)
}.toTypedArray()
// Get metrics values for charts.
Promise.all(resultValues).then { responses ->
val valuesList = responses.map { response ->
val results = (JsonTreeParser.parse(response) as JsonArray).map {
(it as JsonObject).getPrimitive("first").content to
it.getArray("second").map { (it as JsonPrimitive).doubleOrNull }
}
val labels = results.map { it.first }
val values = results[0]?.second?.size?.let { (0..it - 1).map { i -> results.map { it.second[i] } } }
?: emptyList()
labels to values
}
val labels = valuesList[0].first
val values = valuesList.map { it.second }.reduce { acc, valuesPart -> acc + valuesPart }
when (metric) {
// Update chart with gotten data.
"COMPILE_TIME" -> {
compileData = labels to values.map { it.map { it?.let { it / 1000 } } }
compileChart = Chartist.Line("#compile_chart",
getChartData(labels, compileData.second, stageToShow, buildsNumberToShow),
getChartOptions(valuesToShow["COMPILE_TIME"]!![0]!!["samples"]!!.split(',').toTypedArray(),
"Time, milliseconds"))
}
"EXECUTION_TIME" -> {
execData = labels to values
execChart = Chartist.Line("#exec_chart",
getChartData(labels, execData.second, stageToShow, buildsNumberToShow),
getChartOptions(arrayOf("Geometric Mean"), "Normalized time"))
}
"CODE_SIZE" -> {
codeSizeData = labels to values
codeSizeChart = Chartist.Line("#codesize_chart",
getChartData(labels, codeSizeData.second,
stageToShow, buildsNumberToShow, sizeClassNames),
getChartOptions(arrayOf("Geometric Mean") + platformSpecificBenchs.split(',')
.filter { it.isNotEmpty() },
"Normalized size",
arrayOf("ct-series-4", "ct-series-5", "ct-series-6")))
}
"BUNDLE_SIZE" -> {
bundleSizeData = labels to values.map { it.map { it?.let { it.toInt() / 1024 / 1024 } } }
bundleSizeChart = Chartist.Line("#bundlesize_chart",
getChartData(labels,
bundleSizeData.second, stageToShow,
buildsNumberToShow, sizeClassNames),
getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-4")))
}
else -> error("No chart for metric $metric")
}
true
}
}
// Update all charts with using same data.
val updateAllCharts: () -> Unit = {
execChart.update(getChartData(labels, executionTime.values, stageToShow, buildsNumberToShow))
compileChart.update(getChartData(labels, compileTime.values, stageToShow, buildsNumberToShow))
codeSizeChart.update(getChartData(labels, codeSize.values, stageToShow, buildsNumberToShow, sizeClassNames))
bundleSizeChart.update(getChartData(labels, listOf(bundleSize), stageToShow, buildsNumberToShow, sizeClassNames))
execChart.update(getChartData(execData.first, execData.second, stageToShow, buildsNumberToShow))
compileChart.update(getChartData(compileData.first, compileData.second, stageToShow, buildsNumberToShow))
codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, stageToShow, buildsNumberToShow, sizeClassNames))
bundleSizeChart.update(getChartData(bundleSizeData.first, bundleSizeData.second, stageToShow, buildsNumberToShow, sizeClassNames))
}
// Get builds description.
sendGetRequest(descriptionUrl).then { response ->
val buildsInfo = response as String
val data = JsonTreeParser.parse(buildsInfo)
if (data !is JsonArray) {
error("Response is expected to be an array.")
}
val builds = data.jsonArray.map {
val element = it as JsonElement
if (element.isNull) null else Build.create(element as JsonObject)
}
waitForElements(listOf(execChart, compileChart, codeSizeChart, bundleSizeChart)) {
customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters)
customizeChart(compileChart, "compile_chart", js("$(\"#compile_chart\")"), builds, parameters)
customizeChart(codeSizeChart, "codesize_chart", js("$(\"#codesize_chart\")"), builds, parameters)
customizeChart(bundleSizeChart, "bundlesize_chart", js("$(\"#bundlesize_chart\")"), builds, parameters)
updateAllCharts()
}
}
js("$('#plusBtn')").click({
@@ -379,14 +466,13 @@ fun main(args: Array<String>) {
} else {
it
}
} ?: labels.size / zoomRatio
println(buildsNumberToShow)
} ?: execData.first.size / zoomRatio
updateAllCharts()
})
js("$('#minusBtn')").click({
buildsNumberToShow = buildsNumberToShow?.let {
if (it * zoomRatio <= labels.size) {
if (it * zoomRatio <= execData.first.size) {
it * zoomRatio
} else {
null
@@ -397,11 +483,11 @@ fun main(args: Array<String>) {
js("$('#prevBtn')").click({
buildsNumberToShow?.let {
val bottomBorder = -labels.size / (buildsNumberToShow as Int)
val bottomBorder = -execData.first.size / (buildsNumberToShow as Int)
if (stageToShow - 1 > bottomBorder) {
stageToShow--
}
} ?: run { stageToShow = 0}
} ?: run { stageToShow = 0 }
updateAllCharts()
})