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) { sourceSets["main"].withConvention(KotlinSourceSet::class) {
kotlin.srcDir("$projectDir/../tools/benchmarks/shared/src") kotlin.srcDir("$projectDir/../tools/benchmarks/shared/src/main/kotlin/report")
} }
gradlePlugin { gradlePlugin {
@@ -6,46 +6,31 @@
package org.jetbrains.kotlin package org.jetbrains.kotlin
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction 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.BufferedReader
import java.io.FileInputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL 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 * Task to save benchmarks results on server.
* and path to analyzer tool
* *
* @property currentBenchmarksReportFile path to file with becnhmarks result
* @property analyzer path to analyzer tool
* @property bundleSize size of build * @property bundleSize size of build
* @property onlyBranch register only builds for branch * @property onlyBranch register only builds for branch
* @property fileWithResult json file with benchmarks run results
*/ */
open class BuildRegister : DefaultTask() { open class BuildRegister : DefaultTask() {
@Input
lateinit var currentBenchmarksReportFile: String
@Input
lateinit var analyzer: String
var onlyBranch: String? = null var onlyBranch: String? = null
var bundleSize: Int? = 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" 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 val connection = URL(url).openConnection() as HttpURLConnection
return connection.apply { return connection.apply {
setRequestProperty("Content-Type", "application/json; charset=utf-8") 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 @TaskAction
fun run() { fun run() {
// Get TeamCity properties. // Get TeamCity properties.
val teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") ?: val teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") ?: error("Can't load teamcity config!")
error("Can't load teamcity config!")
val buildProperties = Properties() val buildProperties = Properties()
buildProperties.load(FileInputStream(teamcityConfig)) buildProperties.load(FileInputStream(teamcityConfig))
val buildId = buildProperties.getProperty("teamcity.build.id") val buildId = buildProperties.getProperty("teamcity.build.id")
val teamCityUser = buildProperties.getProperty("teamcity.auth.userId") val teamCityUser = buildProperties.getProperty("teamcity.auth.userId")
val teamCityPassword = buildProperties.getProperty("teamcity.auth.password") val teamCityPassword = buildProperties.getProperty("teamcity.auth.password")
val buildNumber = buildProperties.getProperty("build.number")
val apiKey = buildProperties.getProperty("artifactory.apikey")
// Get branch. // Get branch.
val currentBuild = getBuild("id:$buildId", teamCityUser, teamCityPassword) val currentBuild = getBuild("id:$buildId", teamCityUser, teamCityPassword)
val branch = getBuildProperty(currentBuild,"branchName") 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
// Send post request to register build. // Send post request to register build.
val requestBody = buildString { val requestBody = buildString {
append("{\"buildId\":\"$buildId\",") append("{\"buildId\":\"$buildId\",")
append("\"teamCityUser\":\"$teamCityUser\",") append("\"teamCityUser\":\"$teamCityUser\",")
append("\"teamCityPassword\":\"$teamCityPassword\",") append("\"teamCityPassword\":\"$teamCityPassword\",")
append("\"artifactoryApiKey\":\"$apiKey\",") append("\"fileWithResult\":\"$fileWithResult\",")
append("\"target\": \"$target\",") append("\"bundleSize\": ${bundleSize?.let { "\"$bundleSize\"" } ?: bundleSize}}")
append("\"buildType\": \"$buildType\",")
append("\"failuresNumber\": $failuresNumber,")
append("\"executionTime\": \"$executionTimeInfo\",")
append("\"compileTime\": \"$compileTimeInfo\",")
append("\"codeSize\": \"$codeSizeInfo\",")
append("\"bundleSize\": ${bundleSize?.let {"\"$bundleSize\""} ?: bundleSize}}")
} }
if (onlyBranch == null || onlyBranch == branch) { if (onlyBranch == null || onlyBranch == branch) {
println(sendPostRequest("$performanceServer/register", requestBody)) println(sendPostRequest("$performanceServer/register", requestBody))
} else { } else {
println("Skipping registration. Current branch $branch, need registration for $onlyBranch!") 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 kotlin = Compiler(backend, getValue("kotlinVersion"))
val benchDesc = getValue("benchmarks") val benchDesc = getValue("benchmarks")
val benchmarksArray = JsonTreeParser.parse(benchDesc) val benchmarksArray = JsonTreeParser.parse(benchDesc)
val benchmarks = BenchmarksReport.parseBenchmarksArray(benchmarksArray) val benchmarks = parseBenchmarksArray(benchmarksArray)
.union(projectProperties["compileTime"] as List<BenchmarkResult>).union( .union(projectProperties["compileTime"] as List<BenchmarkResult>).union(
listOf(projectProperties["codeSize"] as? BenchmarkResult).filterNotNull()).toList() listOf(projectProperties["codeSize"] as? BenchmarkResult).filterNotNull()).toList()
val report = BenchmarksReport(env, benchmarks, kotlin) val report = BenchmarksReport(env, benchmarks, kotlin)
+16 -13
View File
@@ -56,7 +56,8 @@ private String findAnalyzerBinary() {
// Produce and send slack report. // Produce and send slack report.
task slackReport(type: RegressionsReporter) { task slackReport(type: RegressionsReporter) {
def analyzerBinary = findAnalyzerBinary() def analyzerBinary = MPPTools.findFile("${analyzerTool}${MPPTools.getNativeProgramExtension()}",
"${rootBuildDirectory}/${analyzerToolDirectory}")
def teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") def teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE")
if (teamcityConfig && analyzerBinary != null) { if (teamcityConfig && analyzerBinary != null) {
// Create folder for report (root Kotlin project settings make create report in separate folder). // Create folder for report (root Kotlin project settings make create report in separate folder).
@@ -159,21 +160,23 @@ task registerExternalBenchmarks {
} }
task registerBuild(type: BuildRegister) { task registerBuild(type: BuildRegister) {
def analyzerBinary = findAnalyzerBinary() onlyBranch = project.findProperty('kotlin.register.branch')
if (analyzerBinary != null) { // Get bundle size.
onlyBranch = project.findProperty('kotlin.register.branch') bundleSize = null
// Get bundle size. if (project.findProperty('kotlin.bundleBuild') != null) {
bundleSize = null def dist = findProperty('kotlin.native.home') ?: 'dist'
if (project.findProperty('kotlin.bundleBuild') != null) { dist = (new File(dist)).isAbsolute() ? dist : "${project.getProjectDir()}/$dist"
def dist = findProperty('kotlin.native.home') ?: 'dist' bundleSize = (new File(dist)).directorySize()
dist = (new File(dist)).isAbsolute() ? dist : "${project.getProjectDir()}/$dist"
bundleSize = (new File(dist)).directorySize()
}
currentBenchmarksReportFile = "${buildDir.absolutePath}/${nativeJson}"
analyzer = analyzerBinary
} }
} }
task registerExternalBuild(type: BuildRegister) {
onlyBranch = project.findProperty('kotlin.register.branch')
fileWithResult = externalBenchmarksReport
}
registerExternalBenchmarks.finalizedBy registerExternalBuild
def mergeReports(String fileName) { def mergeReports(String fileName) {
def reports = [] def reports = []
subprojects.each { subprojects.each {
+1 -1
View File
@@ -14,7 +14,7 @@ val defaultBuildType = NativeBuildType.RELEASE
benchmark { benchmark {
applicationName = "Cinterop" 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") jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common") nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/mingw") 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 { benchmark {
applicationName = "Numerical" 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") jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common") nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("../shared/src/main/kotlin-native/mingw") mingwSrcDirs = listOf("../shared/src/main/kotlin-native/mingw")
+1 -1
View File
@@ -17,7 +17,7 @@ val defaultBuildType = NativeBuildType.RELEASE
benchmark { benchmark {
applicationName = "ObjCInterop" 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") jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common") nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/mingw") 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 { benchmark {
applicationName = "Ring" 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") jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common") nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/mingw") 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 { benchmark {
applicationName = "Startup" 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") jvmSrcDirs = listOf("../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("../shared/src/main/kotlin-native/common") nativeSrcDirs = listOf("../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("../shared/src/main/kotlin-native/mingw") mingwSrcDirs = listOf("../shared/src/main/kotlin-native/mingw")
+1 -1
View File
@@ -15,7 +15,7 @@ val defaultBuildType = NativeBuildType.RELEASE
swiftBenchmark { swiftBenchmark {
applicationName = "swiftInterop" 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") nativeSrcDirs = listOf("../shared/src/main/kotlin-native/common", "../shared/src/main/kotlin-native/posix")
swiftSources = listOf("$projectDir/swiftSrc/benchmarks.swift", "$projectDir/swiftSrc/main.swift") swiftSources = listOf("$projectDir/swiftSrc/benchmarks.swift", "$projectDir/swiftSrc/main.swift")
compileTasks = listOf("compileKotlinNative", "linkBenchmarkReleaseFrameworkNative") compileTasks = listOf("compileKotlinNative", "linkBenchmarkReleaseFrameworkNative")
@@ -1,20 +1,8 @@
/* /*
* Copyright 2010-2018 JetBrains s.r.o. * 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.
* 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 package org.jetbrains.analyzer
// Report with changes of different fields. // 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. * 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.
* 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 package org.jetbrains.analyzer
import kotlin.math.abs
import org.jetbrains.report.BenchmarkResult import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.Environment
import org.jetbrains.report.Compiler
import org.jetbrains.report.BenchmarksReport 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 SummaryBenchmark = Pair<MeanVarianceBenchmark?, MeanVarianceBenchmark?>
typealias BenchmarksTable = Map<String, MeanVarianceBenchmark> typealias BenchmarksTable = Map<String, MeanVarianceBenchmark>
@@ -29,12 +19,12 @@ typealias SummaryBenchmarksTable = Map<String, SummaryBenchmark>
typealias ScoreChange = Pair<MeanVariance, MeanVariance> typealias ScoreChange = Pair<MeanVariance, MeanVariance>
// Summary report with comparasion of separate benchmarks results. // Summary report with comparasion of separate benchmarks results.
class SummaryBenchmarksReport (val currentReport: BenchmarksReport, class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
val previousReport: BenchmarksReport? = null, val previousReport: BenchmarksReport? = null,
val meaningfulChangesValue: Double = 0.5) { val meaningfulChangesValue: Double = 0.5) {
// Report created by joining comparing reports. // Report created by joining comparing reports.
val mergedReport: Map<String, SummaryBenchmark> 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. // Lists of benchmarks in different status.
private val benchmarksWithChangedStatus = mutableListOf<FieldChange<BenchmarkResult.Status>>() private val benchmarksWithChangedStatus = mutableListOf<FieldChange<BenchmarkResult.Status>>()
@@ -56,8 +46,8 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
// Countable properties. // Countable properties.
val failedBenchmarks: List<String> val failedBenchmarks: List<String>
get() = mergedReport.filter { it.value.first?.meanBenchmark?.status == BenchmarkResult.Status.FAILED } get() = mergedReport.filter { it.value.first?.status == BenchmarkResult.Status.FAILED }
.map { it.key } .map { it.key }
val addedBenchmarks: List<String> val addedBenchmarks: List<String>
get() = mergedReport.filter { it.value.second == null }.map { it.key } 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!! } get() = mergedReport.filter { it.value.first != null }.map { it.value.first!! }
val currentBenchmarksDuration: Map<String, Double> 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 val maximumRegression: Double
get() = getMaximumChange(regressions) 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, 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 { val benchmarks = filter?.let {
mergedReport.filter { entry -> mergedReport.filter { entry ->
filter.find { filter.find {
@@ -142,30 +139,26 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
} != null } != null
} }
} ?: mergedReport } ?: mergedReport
if (benchmarks.isEmpty()) { val results = benchmarks.map { entry ->
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 name = entry.key.removeSuffix(metric.suffix) val name = entry.key.removeSuffix(metric.suffix)
val value = normalizeData?.let { if (entry.value.first!!.metric == metric) {
it.get(name)?.get("$metric")?.let { score / it } val score = entry.value.first!!.score
?: error("No normalization data for benchmark $name and metric $metric") val value = normalizeData?.let {
} ?: score it.get(name)?.get("$metric")?.let { score / it }
name to value }.toMap() ?: error("No normalization data for benchmark $name and metric $metric")
} ?: score
name to value
} else name to null
}.toMap()
if (getGeoMean) { 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 = private fun getMaximumChange(bucket: Map<String, ScoreChange>): Double =
// Maps of regressions and improvements are sorted. // Maps of regressions and improvements are sorted.
if (bucket.isEmpty()) 0.0 else bucket.values.map { it.first.mean }.first() if (bucket.isEmpty()) 0.0 else bucket.values.map { it.first.mean }.first()
private fun getGeometricMeanOfChanges(bucket: Map<String, ScoreChange>): Double { private fun getGeometricMeanOfChanges(bucket: Map<String, ScoreChange>): Double {
if (bucket.isEmpty()) if (bucket.isEmpty())
@@ -185,11 +178,9 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
// Create geometric mean. // Create geometric mean.
private fun createGeoMeanBenchmark(benchTable: BenchmarksTable): MeanVarianceBenchmark { private fun createGeoMeanBenchmark(benchTable: BenchmarksTable): MeanVarianceBenchmark {
val geoMeanBenchmarkName = "Geometric mean" val geoMeanBenchmarkName = "Geometric mean"
val geoMean = geometricMean(benchTable.toList().map { (_, value) -> value.meanBenchmark.score }) val geoMean = geometricMean(benchTable.toList().map { (_, value) -> value.score })
val varianceGeoMean = geometricMean(benchTable.toList().map { (_, value) -> value.varianceBenchmark.score }) val varianceGeoMean = geometricMean(benchTable.toList().map { (_, value) -> value.variance })
val meanBenchmark = BenchmarkResult(geoMeanBenchmarkName, geoMean) return MeanVarianceBenchmark(geoMeanBenchmarkName, geoMean, varianceGeoMean)
val varianceBenchmark = BenchmarkResult(geoMeanBenchmarkName, varianceGeoMean)
return MeanVarianceBenchmark(meanBenchmark, varianceBenchmark)
} }
// Generate map with summary durations of each benchmark. // Generate map with summary durations of each benchmark.
@@ -209,16 +200,15 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
val mergedTable = mutableMapOf<String, SummaryBenchmark>() val mergedTable = mutableMapOf<String, SummaryBenchmark>()
mergedTable.apply { mergedTable.apply {
currentBenchmarks.forEach { (name, current) -> currentBenchmarks.forEach { (name, current) ->
val currentBenchmark = current.meanBenchmark
// Check existance of benchmark in previous results. // Check existance of benchmark in previous results.
if (previousBenchmarks == null || name !in previousBenchmarks) { if (previousBenchmarks == null || name !in previousBenchmarks) {
getOrPut(name) { SummaryBenchmark(current, null) } getOrPut(name) { SummaryBenchmark(current, null) }
} else { } else {
val previousBenchmark = previousBenchmarks.getValue(name).meanBenchmark val previousBenchmark = previousBenchmarks.getValue(name)
getOrPut(name) { SummaryBenchmark(current, previousBenchmarks[name]) } getOrPut(name) { SummaryBenchmark(current, previousBenchmarks[name]) }
// Explore change of status. // Explore change of status.
if (previousBenchmark.status != currentBenchmark.status) { if (previousBenchmark.status != current.status) {
val statusChange = FieldChange("$name", previousBenchmark.status, currentBenchmark.status) val statusChange = FieldChange("$name", previousBenchmark.status, current.status)
benchmarksWithChangedStatus.add(statusChange) benchmarksWithChangedStatus.add(statusChange)
} }
} }
@@ -240,7 +230,7 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
SummaryBenchmark { SummaryBenchmark {
// Calculate geometric mean. // Calculate geometric mean.
val currentGeoMean = createGeoMeanBenchmark(currentBenchmarks) val currentGeoMean = createGeoMeanBenchmark(currentBenchmarks)
val previousGeoMean = previousBenchmarks?. let { createGeoMeanBenchmark(previousBenchmarks) } val previousGeoMean = previousBenchmarks?.let { createGeoMeanBenchmark(previousBenchmarks) }
return SummaryBenchmark(currentGeoMean, previousGeoMean) return SummaryBenchmark(currentGeoMean, previousGeoMean)
} }
@@ -261,7 +251,7 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
// Analyze and collect changes in performance between same becnhmarks. // Analyze and collect changes in performance between same becnhmarks.
private fun analyzePerformanceChanges() { private fun analyzePerformanceChanges() {
val performanceChanges = mergedReport.asSequence().map {(name, element) -> val performanceChanges = mergedReport.asSequence().map { (name, element) ->
getBenchmarkPerfomanceChange(name, element) getBenchmarkPerfomanceChange(name, element)
}.filterNotNull().groupBy { }.filterNotNull().groupBy {
if (it.second.first.mean > 0) "regressions" else "improvements" if (it.second.first.mean > 0) "regressions" else "improvements"
@@ -277,8 +267,8 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
// Calculate change for geometric mean. // Calculate change for geometric mean.
val (current, previous) = geoMeanBenchmark val (current, previous) = geoMeanBenchmark
geoMeanScoreChange = current?. let { geoMeanScoreChange = current?.let {
previous?. let { previous?.let {
Pair(current.calcPercentageDiff(previous), current.calcRatio(previous)) 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. * 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.
* 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.report package org.jetbrains.report
import org.jetbrains.report.json.* import org.jetbrains.report.json.*
interface JsonSerializable { interface JsonSerializable {
fun toJson(): String fun serializeFields(): String
fun toJson(): String {
return """
{
${serializeFields()}
}
"""
}
// Convert iterable objects arrays, lists to json. // Convert iterable objects arrays, lists to json.
fun <T> arrayToJson(data: Iterable<T>): String { 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 fun create(data: JsonElement): T
} }
// Class for benchcmarks report with all information of run. // Parse array with benchmarks to list
class BenchmarksReport(val env: Environment, benchmarksList: List<BenchmarkResult>, val compiler: Compiler): 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 { JsonSerializable {
companion object: EntityFromJsonFactory<BenchmarksReport> { companion object : EntityFromJsonFactory<BenchmarksReport> {
override fun create(data: JsonElement): BenchmarksReport { override fun create(data: JsonElement): BenchmarksReport {
if (data is JsonObject) { if (data is JsonObject) {
val env = Environment.create(data.getRequiredField("env")) val env = Environment.create(data.getRequiredField("env"))
val benchmarksObj = data.getRequiredField("benchmarks") val benchmarksObj = data.getRequiredField("benchmarks")
val compiler = Compiler.create(data.getRequiredField("kotlin")) val compiler = Compiler.create(data.getRequiredField("kotlin"))
val buildNumberField = data.getOptionalField("buildNumber")
val benchmarksList = parseBenchmarksArray(benchmarksObj) 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 { } else {
error("Top level entity is expected to be an object. Please, check origin files.") 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. // Made a map of becnhmarks with name as key from list.
private fun structBenchmarks(benchmarksList: List<BenchmarkResult>) = 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 """ return """
{
"env": ${env.toJson()}, "env": ${env.toJson()},
"kotlin": ${compiler.toJson()}, "kotlin": ${compiler.toJson()},
"benchmarks": ${arrayToJson(benchmarks.flatMap{it.value})} "benchmarks": ${arrayToJson(benchmarks.flatMap { it.value })}$buildNumberField
} """.trimIndent()
"""
} }
fun merge(other: BenchmarksReport): BenchmarksReport { fun merge(other: BenchmarksReport): BenchmarksReport {
@@ -85,27 +93,27 @@ class BenchmarksReport(val env: Environment, benchmarksList: List<BenchmarkResul
} }
} }
mergedBenchmarks.putAll(other.benchmarks) 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. // Concatenate benchmarks report if they have same environment and compiler.
operator fun plus(other: BenchmarksReport): BenchmarksReport { operator fun plus(other: BenchmarksReport): BenchmarksReport {
if (compiler != other.compiler || env != other.env) { 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) return merge(other)
} }
} }
// Class for kotlin compiler // 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) { enum class BackendType(val type: String) {
JVM("jvm"), JVM("jvm"),
NATIVE("native") NATIVE("native")
} }
companion object: EntityFromJsonFactory<Compiler> { companion object : EntityFromJsonFactory<Compiler> {
override fun create(data: JsonElement): Compiler { override fun create(data: JsonElement): Compiler {
if (data is JsonObject) { if (data is JsonObject) {
val backend = Backend.create(data.getRequiredField("backend")) 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 // Class for compiler backend
data class Backend(val type: BackendType, val version: String, val flags: List<String>): JsonSerializable { data class Backend(val type: BackendType, val version: String, val flags: List<String>) : JsonSerializable {
companion object: EntityFromJsonFactory<Backend> { companion object : EntityFromJsonFactory<Backend> {
override fun create(data: JsonElement): Backend { override fun create(data: JsonElement): Backend {
if (data is JsonObject) { if (data is JsonObject) {
val typeElement = data.getRequiredField("type") val typeElement = data.getRequiredField("type")
if (typeElement is JsonLiteral) { 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 version = elementToString(data.getRequiredField("version"), "version")
val flagsArray = data.getOptionalField("flags") val flagsArray = data.getOptionalField("flags")
var flags: List<String> = emptyList() 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 = """ val result = """
{
"type": "${type.type}", "type": "${type.type}",
"version": "${version}"""" "version": "${version}""""
// Don't print flags field if there is no one. // Don't print flags field if there is no one.
if (flags.isEmpty()) { if (flags.isEmpty()) {
return """$result return """$result
}""" """
} } else {
else {
return """ return """
$result, $result,
"flags": ${arrayToJson(flags)} "flags": ${arrayToJson(flags.map { if (it.startsWith("\"")) it else "\"$it\"" })}
}
""" """
} }
} }
} }
override fun toJson(): String { override fun serializeFields(): String {
return """ return """
{
"backend": ${backend.toJson()}, "backend": ${backend.toJson()},
"kotlinVersion": "${kotlinVersion}" "kotlinVersion": "${kotlinVersion}"
}
""" """
} }
} }
// Class for description of environment of benchmarks run // 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 { override fun create(data: JsonElement): Environment {
if (data is JsonObject) { if (data is JsonObject) {
val machine = Machine.create(data.getRequiredField("machine")) 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. // Class for description of machine used for benchmarks run.
data class Machine(val cpu: String, val os: String): JsonSerializable { data class Machine(val cpu: String, val os: String) : JsonSerializable {
companion object: EntityFromJsonFactory<Machine> { companion object : EntityFromJsonFactory<Machine> {
override fun create(data: JsonElement): Machine { override fun create(data: JsonElement): Machine {
if (data is JsonObject) { if (data is JsonObject) {
val cpu = elementToString(data.getRequiredField("cpu"), "cpu") 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 """ return """
{
"cpu": "$cpu", "cpu": "$cpu",
"os": "$os" "os": "$os"
}
""" """
} }
} }
// Class for description of jdk used for benchmarks run. // Class for description of jdk used for benchmarks run.
data class JDKInstance(val version: String, val vendor: String): JsonSerializable { data class JDKInstance(val version: String, val vendor: String) : JsonSerializable {
companion object: EntityFromJsonFactory<JDKInstance> { companion object : EntityFromJsonFactory<JDKInstance> {
override fun create(data: JsonElement): JDKInstance { override fun create(data: JsonElement): JDKInstance {
if (data is JsonObject) { if (data is JsonObject) {
val version = elementToString(data.getRequiredField("version"), "version") 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 """ return """
{
"version": "$version", "version": "$version",
"vendor": "$vendor" "vendor": "$vendor"
}
""" """
} }
} }
override fun toJson(): String { override fun serializeFields(): String {
return """ return """
{
"machine": ${machine.toJson()}, "machine": ${machine.toJson()},
"jdk": ${jdk.toJson()} "jdk": ${jdk.toJson()}
}
""" """
} }
} }
class BenchmarkResult(val name: String, val status: Status, open class BenchmarkResult(val name: String, val status: Status,
val score: Double, val metric: Metric, val runtimeInUs: Double, val score: Double, val metric: Metric, val runtimeInUs: Double,
val repeat: Int, val warmup: Int): JsonSerializable { val repeat: Int, val warmup: Int) : JsonSerializable {
enum class Metric(val suffix: String, val value: String) { enum class Metric(val suffix: String, val value: String) {
EXECUTION_TIME("", "EXECUTION_TIME"), EXECUTION_TIME("", "EXECUTION_TIME"),
CODE_SIZE(".codeSize", "CODE_SIZE"), 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) 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 { override fun create(data: JsonElement): BenchmarkResult {
if (data is JsonObject) { if (data is JsonObject) {
var name = elementToString(data.getRequiredField("name"), "name") var name = elementToString(data.getRequiredField("name"), "name")
val metricElement = data.getOptionalField("metric") val metricElement = data.getOptionalField("metric")
val metric = if (metricElement != null && metricElement is JsonLiteral) val metric = if (metricElement != null && metricElement is JsonLiteral)
metricFromString(metricElement.unquoted()) ?: Metric.EXECUTION_TIME metricFromString(metricElement.unquoted()) ?: Metric.EXECUTION_TIME
else Metric.EXECUTION_TIME else Metric.EXECUTION_TIME
name += metric.suffix name += metric.suffix
val statusElement = data.getRequiredField("status") val statusElement = data.getRequiredField("status")
if (statusElement is JsonLiteral) { if (statusElement is JsonLiteral) {
@@ -300,9 +299,8 @@ class BenchmarkResult(val name: String, val status: Status,
FAILED("FAILED") FAILED("FAILED")
} }
override fun toJson(): String { override fun serializeFields(): String {
return """ return """
{
"name": "${name.removeSuffix(metric.suffix)}", "name": "${name.removeSuffix(metric.suffix)}",
"status": "${status.value}", "status": "${status.value}",
"score": ${score}, "score": ${score},
@@ -310,10 +308,44 @@ class BenchmarkResult(val name: String, val status: Status,
"runtimeInUs": ${runtimeInUs}, "runtimeInUs": ${runtimeInUs},
"repeat": ${repeat}, "repeat": ${repeat},
"warmup": ${warmup} "warmup": ${warmup}
}
""" """
} }
val shortName: String val shortName: String
get() = name.removeSuffix(metric.suffix) 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 TC_BEGIN_LIST, TC_BEGIN_OBJ, TC_OTHER, TC_STRING, TC_NULL -> true
else -> false else -> false
} }
@OptIn(ExperimentalStdlibApi::class)
fun takeStr(): String { fun takeStr(): String {
if (tc != TC_OTHER && tc != TC_STRING) fail(tokenPos, "Expected string or non-null literal") if (tc != TC_OTHER && tc != TC_STRING) fail(tokenPos, "Expected string or non-null literal")
val prevStr = if (offset < 0) val prevStr = if (offset < 0)
String(buf, 0, length) else buf.concatToString(0, length) else
source.substring(offset, offset + length) source.substring(offset, offset + length)
nextToken() nextToken()
return prevStr return prevStr
@@ -1,17 +1,6 @@
/* /*
* Copyright 2010-2018 JetBrains s.r.o. * 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.
* 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 package org.jetbrains.analyzer
@@ -1,17 +1,6 @@
/* /*
* Copyright 2010-2018 JetBrains s.r.o. * 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.
* 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 package org.jetbrains.analyzer
@@ -29,7 +18,7 @@ actual fun readFile(fileName: String): String {
} }
actual fun Double.format(decimalNumber: Int): String = actual fun Double.format(decimalNumber: Int): String =
"%.${decimalNumber}f".format(this) "%.${decimalNumber}f".format(this)
actual fun writeToFile(fileName: String, text: String) { actual fun writeToFile(fileName: String, text: String) {
File(fileName).printWriter().use { out -> File(fileName).printWriter().use { out ->
@@ -1,17 +1,6 @@
/* /*
* Copyright 2010-2017 JetBrains s.r.o. * 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.
* 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 package org.jetbrains.analyzer
+47 -104
View File
@@ -1,23 +1,12 @@
/* /*
* Copyright 2010-2018 JetBrains s.r.o. * 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.
* 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.
*/ */
@file:OptIn(ExperimentalCli::class)
import kotlinx.cli.*
import org.jetbrains.analyzer.sendGetRequest import org.jetbrains.analyzer.sendGetRequest
import org.jetbrains.analyzer.readFile import org.jetbrains.analyzer.readFile
import org.jetbrains.analyzer.SummaryBenchmarksReport import org.jetbrains.analyzer.SummaryBenchmarksReport
import kotlinx.cli.*
import org.jetbrains.renders.* import org.jetbrains.renders.*
import org.jetbrains.report.* import org.jetbrains.report.*
import org.jetbrains.report.json.* import org.jetbrains.report.json.*
@@ -56,7 +45,7 @@ object ArtifactoryConnector : Connector() {
} }
} }
object TeamCityConnector: Connector() { object TeamCityConnector : Connector() {
override val connectorPrefix = "teamcity:" override val connectorPrefix = "teamcity:"
val teamCityUrl = "http://buildserver.labs.intellij.net" 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 { fun getFileContent(fileName: String, user: String? = null): String {
return when { return when {
ArtifactoryConnector.isCompatible(fileName) -> ArtifactoryConnector.getFileContent(fileName, user) ArtifactoryConnector.isCompatible(fileName) -> ArtifactoryConnector.getFileContent(fileName, user)
TeamCityConnector.isCompatible(fileName) -> TeamCityConnector.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 return parsedNormalizeResults
} }
fun mergeCompilerFlags(reports: List<BenchmarksReport>) = fun mergeCompilerFlags(reports: List<BenchmarksReport>): List<String> {
reports.map { val flagsMap = mutableMapOf<String, MutableList<String>>()
reports.forEach {
val benchmarks = it.benchmarks.values.flatten().asSequence().filter { it.metric == BenchmarkResult.Metric.COMPILE_TIME } val benchmarks = it.benchmarks.values.flatten().asSequence().filter { it.metric == BenchmarkResult.Metric.COMPILE_TIME }
.map { it.shortName }.distinct().sorted().joinToString() .map { it.shortName }.toList()
"${it.compiler.backend.flags.joinToString()} for [$benchmarks]" 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>) = fun mergeReportsWithDetailedFlags(reports: List<BenchmarksReport>) =
if (reports.size > 1) { if (reports.size > 1) {
@@ -129,75 +139,9 @@ fun mergeReportsWithDetailedFlags(reports: List<BenchmarksReport>) =
} }
fun main(args: Array<String>) { 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. // Parse args.
val argParser = ArgParser("benchmarksAnalyzer") val argParser = ArgParser("benchmarksAnalyzer")
argParser.subcommands(action)
val mainReport by argParser.argument(ArgType.String, description = "Main report for analysis") 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() 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", val useShortForm by argParser.option(ArgType.Boolean, "short", "s",
"Show short version of report").default(false) "Show short version of report").default(false)
val renders by argParser.option(ArgType.Choice(listOf("text", "html", "teamcity", "statistics", "metrics")), 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") val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization")
if (argParser.parse(args).commandName == "benchmarksAnalyzer") { argParser.parse(args)
// Read contents of file. // Read contents of file.
val mainBenchsReport = mergeReportsWithDetailedFlags(getBenchmarkReport(mainReport, user)) val mainBenchsReport = mergeReportsWithDetailedFlags(getBenchmarkReport(mainReport, user))
var compareToBenchsReport = compareToReport?.let { var compareToBenchsReport = compareToReport?.let {
mergeReportsWithDetailedFlags(getBenchmarkReport(it, user)) mergeReportsWithDetailedFlags(getBenchmarkReport(it, user))
} }
// Generate comparasion report. // Generate comparasion report.
val summaryReport = SummaryBenchmarksReport(mainBenchsReport, val summaryReport = SummaryBenchmarksReport(mainBenchsReport,
compareToBenchsReport, compareToBenchsReport,
epsValue) epsValue)
var outputFile = output var outputFile = output
renders.forEach { renders.forEach {
Render.getRenderByName(it).print(summaryReport, useShortForm, outputFile) Render.getRenderByName(it).print(summaryReport, useShortForm, outputFile)
outputFile = null 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. * 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.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package org.jetbrains.renders package org.jetbrains.renders
import org.jetbrains.analyzer.* import org.jetbrains.analyzer.*
@@ -556,7 +544,7 @@ class HTMLRender: Render() {
bucket: Map<String, ScoreChange>? = null, rowStyle: String? = null) { bucket: Map<String, ScoreChange>? = null, rowStyle: String? = null) {
if (bucket != null && !bucket.isEmpty()) { if (bucket != null && !bucket.isEmpty()) {
// Find max ratio. // 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. // There are changes in performance.
// Output changed benchmarks. // Output changed benchmarks.
for ((name, change) in bucket) { for ((name, change) in bucket) {
@@ -565,19 +553,19 @@ class HTMLRender: Render() {
attributes["style"] = rowStyle attributes["style"] = rowStyle
} }
th { +name } th { +name }
td { +"${fullSet.getValue(name).first}" } td { +"${fullSet.getValue(name).first?.description}" }
td { +"${fullSet.getValue(name).second}" } td { +"${fullSet.getValue(name).second?.description}" }
td { td {
attributes["bgcolor"] = ColoredCell(if (bucket.values.first().first.mean == 0.0) null attributes["bgcolor"] = ColoredCell(if (bucket.values.first().first.mean == 0.0) null
else change.first.mean / abs(bucket.values.first().first.mean)) else change.first.mean / abs(bucket.values.first().first.mean))
.backgroundStyle .backgroundStyle
+"${change.first.toString() + " %"}" +"${change.first.description + " %"}"
} }
td { td {
val scaledRatio = if (maxRatio == 0.0) null else change.second.mean / maxRatio val scaledRatio = if (maxRatio == 0.0) null else change.second.mean / maxRatio
attributes["bgcolor"] = ColoredCell(scaledRatio, attributes["bgcolor"] = ColoredCell(scaledRatio,
borderPositive = { cellValue -> cellValue > 1.0 / maxRatio }).backgroundStyle borderPositive = { cellValue -> cellValue > 1.0 / maxRatio }).backgroundStyle
+"${change.second}" +"${change.second.description}"
} }
} }
} }
@@ -587,8 +575,8 @@ class HTMLRender: Render() {
for ((name, value) in fullSet) { for ((name, value) in fullSet) {
tr { tr {
th { +name } th { +name }
td { +"${value.first?.toString() ?: placeholder}" } td { +"${value.first?.description ?: placeholder}" }
td { +"${value.second?.toString() ?: placeholder}" } td { +"${value.second?.description ?: placeholder}" }
td { +placeholder } td { +placeholder }
td { +placeholder } td { +placeholder }
} }
@@ -619,11 +607,11 @@ class HTMLRender: Render() {
} }
} }
val geoMeanChangeMap = report.geoMeanScoreChange?. val geoMeanChangeMap = report.geoMeanScoreChange?.
let { mapOf(report.geoMeanBenchmark.first!!.meanBenchmark.name to report.geoMeanScoreChange!!) } let { mapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanScoreChange!!) }
tbody { tbody {
renderBenchmarksDetails( 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") geoMeanChangeMap, "border-bottom: 2.3pt solid black; border-top: 2.3pt solid black")
renderBenchmarksDetails(report.mergedReport, report.regressions) renderBenchmarksDetails(report.mergedReport, report.regressions)
renderBenchmarksDetails(report.mergedReport, report.improvements) 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. * 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.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package org.jetbrains.renders package org.jetbrains.renders
import org.jetbrains.analyzer.* import org.jetbrains.analyzer.*
@@ -28,10 +16,10 @@ class MetricResultsRender: Render() {
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String { override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
val results = report.mergedReport.map { entry -> val results = report.mergedReport.map { entry ->
buildString { buildString {
val metric = entry.value.first!!.meanBenchmark.metric val metric = entry.value.first!!.metric
append("{ \"benchmarkName\": \"${entry.key.removeSuffix(metric.suffix)}\",") append("{ \"benchmarkName\": \"${entry.key.removeSuffix(metric.suffix)}\",")
append("\"metric\": \"${metric}\",") append("\"metric\": \"${metric}\",")
append("\"value\": \"${entry.value.first!!.meanBenchmark.score}\" }") append("\"value\": \"${entry.value.first!!.score}\" }")
} }
}.joinToString(", ") }.joinToString(", ")
return "[ $results ]" return "[ $results ]"
@@ -1,24 +1,12 @@
/* /*
* Copyright 2010-2018 JetBrains s.r.o. * 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.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package org.jetbrains.renders package org.jetbrains.renders
import org.jetbrains.analyzer.* import org.jetbrains.analyzer.*
import org.jetbrains.report.BenchmarkResult import org.jetbrains.report.*
import kotlin.math.abs import kotlin.math.abs
@@ -150,23 +138,23 @@ class TextRender: Render() {
private fun printBenchmarksDetails(fullSet: Map<String, SummaryBenchmark>, private fun printBenchmarksDetails(fullSet: Map<String, SummaryBenchmark>,
bucket: Map<String, ScoreChange>? = null) { bucket: Map<String, ScoreChange>? = null) {
val placeholder = "-"
if (bucket != null) { if (bucket != null) {
// There are changes in performance. // There are changes in performance.
// Output changed benchmarks. // Output changed benchmarks.
for ((name, change) in bucket) { for ((name, change) in bucket) {
append(formatColumn(name, true) + append(formatColumn(name, true) +
formatColumn(fullSet.getValue(name).first.toString()) + formatColumn(fullSet.getValue(name).first?.description ?: placeholder) +
formatColumn(fullSet.getValue(name).second.toString()) + formatColumn(fullSet.getValue(name).second?.description ?: placeholder) +
formatColumn(change.first.toString() + " %") + formatColumn(change.first.description + " %") +
formatColumn(change.second.toString())) formatColumn(change.second.description))
} }
} else { } else {
// Output all values without performance changes. // Output all values without performance changes.
val placeholder = "-"
for ((name, value) in fullSet) { for ((name, value) in fullSet) {
append(formatColumn(name, true) + append(formatColumn(name, true) +
formatColumn(value.first?.toString() ?: placeholder) + formatColumn(value.first?.description ?: placeholder) +
formatColumn(value.second?.toString() ?: placeholder) + formatColumn(value.second?.description ?: placeholder) +
formatColumn(placeholder) + formatColumn(placeholder) +
formatColumn(placeholder)) formatColumn(placeholder))
} }
@@ -201,9 +189,9 @@ class TextRender: Render() {
val tableWidth = printPerformanceTableHeader() val tableWidth = printPerformanceTableHeader()
// Print geometric mean. // Print geometric mean.
val geoMeanChangeMap = report.geoMeanScoreChange?. val geoMeanChangeMap = report.geoMeanScoreChange?.
let { mapOf(report.geoMeanBenchmark.first!!.meanBenchmark.name to report.geoMeanScoreChange!!) } let { mapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanScoreChange!!) }
printBenchmarksDetails( printBenchmarksDetails(
mutableMapOf(report.geoMeanBenchmark.first!!.meanBenchmark.name to report.geoMeanBenchmark), mutableMapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanBenchmark),
geoMeanChangeMap) geoMeanChangeMap)
printTableLineSeparator(tableWidth) printTableLineSeparator(tableWidth)
printBenchmarksDetails(report.mergedReport, report.regressions) printBenchmarksDetails(report.mergedReport, report.regressions)
@@ -1,20 +1,8 @@
/* /*
* Copyright 2010-2018 JetBrains s.r.o. * 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.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package org.jetbrains.renders package org.jetbrains.renders
import org.jetbrains.analyzer.* import org.jetbrains.analyzer.*
@@ -1,24 +1,13 @@
/* /*
* Copyright 2010-2018 JetBrains s.r.o. * 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.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package org.jetbrains.renders package org.jetbrains.renders
import org.jetbrains.analyzer.* import org.jetbrains.analyzer.*
import org.jetbrains.report.BenchmarkResult import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.MeanVarianceBenchmark
// Report render to text format. // Report render to text format.
class TeamCityStatisticsRender: Render() { class TeamCityStatisticsRender: Render() {
@@ -34,9 +23,8 @@ class TeamCityStatisticsRender: Render() {
// For current benchmarks print score as TeamCity Test Metadata // For current benchmarks print score as TeamCity Test Metadata
report.currentMeanVarianceBenchmarks.forEach { benchmark -> report.currentMeanVarianceBenchmarks.forEach { benchmark ->
renderBenchmark(benchmark.meanBenchmark, currentDurations[benchmark.meanBenchmark.name]!!) renderBenchmark(benchmark, currentDurations[benchmark.name]!!)
renderSummaryBecnhmarkValue(benchmark.meanBenchmark, "Mean") renderSummaryBecnhmarkValue(benchmark)
renderSummaryBecnhmarkValue(benchmark.varianceBenchmark, "Variance")
} }
content.append("##teamcity[testSuiteFinished name='Benchmarks']\n") content.append("##teamcity[testSuiteFinished name='Benchmarks']\n")
@@ -46,9 +34,12 @@ class TeamCityStatisticsRender: Render() {
return content.toString() return content.toString()
} }
private fun renderSummaryBecnhmarkValue(benchmark: BenchmarkResult, metric: String) = private fun renderSummaryBecnhmarkValue(benchmark: MeanVarianceBenchmark) {
content.append("##teamcity[testMetadata testName='${benchmark.name}' name='$metric'" + content.append("##teamcity[testMetadata testName='${benchmark.name}' name='Mean'" +
" type='number' value='${benchmark.score}']\n") " 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 // Produce benchmark as test in TeamCity
private fun renderBenchmark(benchmark: BenchmarkResult , duration: Double) { private fun renderBenchmark(benchmark: BenchmarkResult , duration: Double) {
@@ -61,7 +52,7 @@ class TeamCityStatisticsRender: Render() {
} }
private fun renderGeometricMean(geoMeanBenchmark: MeanVarianceBenchmark) { private fun renderGeometricMean(geoMeanBenchmark: MeanVarianceBenchmark) {
content.append("##teamcity[buildStatisticValue key='Geometric mean' value='${geoMeanBenchmark.meanBenchmark.score}']\n") content.append("##teamcity[buildStatisticValue key='Geometric mean' value='${geoMeanBenchmark.score}']\n")
content.append("##teamcity[buildStatisticValue key='Geometric mean variance' value='${geoMeanBenchmark.varianceBenchmark.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. * 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.
* 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 package org.jetbrains.analyzer
import kotlin.test.* import kotlin.test.*
import kotlin.math.abs import kotlin.math.abs
import org.jetbrains.report.BenchmarkResult import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.MeanVarianceBenchmark
class AnalyzerTests { class AnalyzerTests {
private val eps = 0.000001 private val eps = 0.000001
private fun createMeanVarianceBenchmarks(): Pair<MeanVarianceBenchmark, MeanVarianceBenchmark> { private fun createMeanVarianceBenchmarks(): Pair<MeanVarianceBenchmark, MeanVarianceBenchmark> {
val firstMean = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 9.0, BenchmarkResult.Metric.EXECUTION_TIME, 9.0, 10, 10) val first = MeanVarianceBenchmark("testBenchmark", BenchmarkResult.Status.PASSED, 9.0, BenchmarkResult.Metric.EXECUTION_TIME, 9.0, 10, 10, 0.0001)
val firstVariance = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 0.0001, BenchmarkResult.Metric.EXECUTION_TIME, 0.0001, 10, 10) val second = MeanVarianceBenchmark("testBenchmark", BenchmarkResult.Status.PASSED, 10.0, BenchmarkResult.Metric.EXECUTION_TIME, 10.0, 10, 10, 0.0001)
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)
return Pair(first, second) return Pair(first, second)
} }
+30 -17
View File
@@ -18,6 +18,8 @@ buildscript {
} }
} }
apply plugin: 'kotlin-multiplatform'
repositories { repositories {
maven { maven {
url 'https://cache-redirector.jetbrains.com/jcenter' 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 { targets {
compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion" fromPreset(presets.js, 'js') {
} nodejs()
compilations.main.kotlinOptions {
outputFile = "${projectDir}/server/app.js"
sourceSets { moduleKind = "commonjs"
main.resources.srcDirs += "resources" sourceMap = true
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
} }
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, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "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": { "accepts": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", "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", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
}, },
"asap": { "aws-sdk": {
"version": "2.0.6", "version": "2.670.0",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.670.0.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" "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": { "base64-js": {
"version": "0.4.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
}, },
"body-parser": { "body-parser": {
"version": "1.18.3", "version": "1.18.3",
@@ -81,40 +66,21 @@
} }
} }
}, },
"buffer-from": { "buffer": {
"version": "1.1.1", "version": "4.9.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4",
"isarray": "^1.0.0"
}
}, },
"bytes": { "bytes": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" "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": { "content-disposition": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", "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", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" "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": { "debug": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "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": { "depd": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "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", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
}, },
"events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
},
"express": { "express": {
"version": "4.16.4", "version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", "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": { "forwarded": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", "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", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" "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": { "http-errors": {
"version": "1.6.3", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
@@ -325,14 +258,6 @@
"statuses": ">= 1.4.0 < 2" "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": { "iconv-lite": {
"version": "0.4.23", "version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
@@ -341,6 +266,11 @@
"safer-buffer": ">= 2.1.2 < 3" "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": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "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", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" "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": { "kotlin": {
"version": "1.3.21", "version": "1.4.0-M3",
"resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.3.21.tgz", "resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.4.0-M3.tgz",
"integrity": "sha512-JqxY9quNzwWl8iYvNj/ycfNBEbSFDKvmeK2rDVU89q0/X8BRjVzibJ5RFOq/9efvvvi+VBnIRhL7GSOGzmP2nQ==" "integrity": "sha512-y6dl1t36tI1hAJlJTxlOB/fj/0iOga+XC8+eg9hUSTu5BDJ9MIUR7XnA8Ld+tQA9IakAPaUmgZZfX2lsHaAdIw=="
}, },
"media-typer": { "media-typer": {
"version": "0.3.0", "version": "0.3.0",
@@ -404,6 +339,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" "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": { "on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -412,11 +352,6 @@
"ee-first": "1.1.1" "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": { "parseurl": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", "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", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" "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": { "proxy-addr": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
@@ -449,11 +371,21 @@
"ipaddr.js": "1.8.0" "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": { "qs": {
"version": "6.5.2", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" "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": { "range-parser": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
@@ -470,20 +402,6 @@
"unpipe": "1.0.0" "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": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "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", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sax": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
"integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
},
"send": { "send": {
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", "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", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" "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": { "type-is": {
"version": "1.6.16", "version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
@@ -610,30 +482,48 @@
"mime-types": "~2.1.18" "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": { "unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
}, },
"util-deprecate": { "url": {
"version": "1.0.2", "version": "0.10.3",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
}
}, },
"utils-merge": { "utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" "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": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" "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" "start": "node server/app.js"
}, },
"dependencies": { "dependencies": {
"aws-sdk": "~2.670.0",
"body-parser": "~1.18.3", "body-parser": "~1.18.3",
"debug": "~4.1.1", "debug": "~4.1.1",
"ejs": "~2.6.1",
"express": "~4.16.4", "express": "~4.16.4",
"kotlin": "^1.3.21", "kotlin": "~1.4.0-M3",
"sync-request": "~6.0.0", "node-fetch": "~2.6.0"
"ejs": "~2.6.1"
} }
} }
@@ -1,45 +1,26 @@
/* /*
* Copyright 2010-2019 JetBrains s.r.o. * 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.
* 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.buildInfo
package org.jetbrains.build
import org.jetbrains.report.* import org.jetbrains.report.*
import org.jetbrains.report.json.* import org.jetbrains.report.json.*
data class Build(val buildNumber: String, val startTime: String, val finishTime: String, val branch: String, 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 commits: String, val failuresNumber: Int) {
val compileTime: String, val codeSize: String, val bundleSize: String?) {
companion object: EntityFromJsonFactory<Build> { companion object : EntityFromJsonFactory<Build> {
override fun create(data: JsonElement): Build { override fun create(data: JsonElement): Build {
if (data is JsonObject) { if (data is JsonObject) {
val buildNumber = elementToString(data.getRequiredField("buildNumber"), "buildNumber") val buildNumber = elementToString(data.getRequiredField("buildNumber"), "buildNumber").replace("\"", "")
val startTime = elementToString(data.getRequiredField("startTime"), "startTime") val startTime = elementToString(data.getRequiredField("startTime"), "startTime").replace("\"", "")
val finishTime = elementToString(data.getRequiredField("finishTime"), "finishTime") val finishTime = elementToString(data.getRequiredField("finishTime"), "finishTime").replace("\"", "")
val branch = elementToString(data.getRequiredField("branch"), "branch") val branch = elementToString(data.getRequiredField("branch"), "branch").replace("\"", "")
val commits = elementToString(data.getRequiredField("commits"), "commits") val commits = elementToString(data.getRequiredField("commits"), "commits")
val buildType = elementToString(data.getRequiredField("buildType"), "buildType")
val failuresNumber = elementToInt(data.getRequiredField("failuresNumber"), "failuresNumber") val failuresNumber = elementToInt(data.getRequiredField("failuresNumber"), "failuresNumber")
val executionTime = elementToString(data.getRequiredField("executionTime"), "executionTime") return Build(buildNumber, startTime, finishTime, branch, commits, failuresNumber)
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)
} else { } else {
error("Top level entity is expected to be an object. Please, check origin files.") 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]}" return "${if (hours < 10) "0$hours" else "$hours"}:${matchResult[2]}"
} ?: error { "Wrong format of time $startTime" } } ?: error { "Wrong format of time $startTime" }
} }
val date: String by lazy { val date: String by lazy {
val matchResult = "^(\\d{4})(\\d{2})(\\d{2})".toRegex().find(startTime)?.groupValues val matchResult = "^(\\d{4})(\\d{2})(\\d{2})".toRegex().find(startTime)?.groupValues
matchResult?.let { "${matchResult[3]}/${matchResult[2]}/${matchResult[1]}" } 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. * 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.
* 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.
*/ */
import org.w3c.xhr.* import org.w3c.xhr.*
import kotlin.js.json import kotlin.js.json
import kotlin.js.Date import kotlin.js.Date
import kotlin.js.Promise
import org.jetbrains.database.*
import org.jetbrains.report.json.* 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 teamCityUrl = "https://buildserver.labs.intellij.net/app/rest"
const val artifactoryUrl = "https://repo.labs.intellij.net/kotlin-native-benchmarks" 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) operator fun <K, V> Map<K, V>?.get(key: K) = this?.get(key)
fun getArtifactoryHeader(artifactoryApiKey: String) = Pair("X-JFrog-Art-Api", artifactoryApiKey) fun getArtifactoryHeader(artifactoryApiKey: String) = Pair("X-JFrog-Art-Api", artifactoryApiKey)
// Local cache for saving information about builds got from Artifactory. // Convert saved old report to expected new format.
object LocalCache { internal fun convertToNewFormat(data: JsonObject): List<Any> {
private val knownTargets = listOf("Linux", "MacOSX", "Windows10") val env = Environment.create(data.getRequiredField("env"))
private val buildsInfo = mutableMapOf<String, MutableMap<String, String>>() val benchmarksObj = data.getRequiredField("benchmarks")
val compilerDescription = data.getRequiredField("kotlin")
fun clean(onlyTarget: String? = null) { val compiler = Compiler.create(compilerDescription)
onlyTarget?.let { val backend = (compilerDescription as JsonObject).getRequiredField("backend")
buildsInfo[onlyTarget]?.clear() val flagsArray = (backend as JsonObject).getOptionalField("flags")
} ?: buildsInfo.clear() 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) { return listOf(env, compiler, benchmarksList, flags)
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
}
} }
// 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 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. // Build information provided from request.
data class BuildInfo(val buildNumber: String, val branch: String, val startTime: String, data class TCBuildInfo(val buildNumber: String, val branch: String, val startTime: String,
val finishTime: String) val finishTime: String)
data class BuildRegister(val buildId: String, val teamCityUser: String, val teamCityPassword: 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 bundleSize: String?, val fileWithResult: String) {
val executionTime: String, val compileTime: String, val codeSize: String,
val bundleSize: String?) {
companion object { companion object {
fun create(json: String): BuildRegister { fun create(json: String): BuildRegister {
val requestDetails = JSON.parse<BuildRegister>(json) val requestDetails = JSON.parse<BuildRegister>(json)
// Parse method doesn't create real instance with all methods. So create it by hands. // Parse method doesn't create real instance with all methods. So create it by hands.
return BuildRegister(requestDetails.buildId, requestDetails.teamCityUser, requestDetails.teamCityPassword, return BuildRegister(requestDetails.buildId, requestDetails.teamCityUser, requestDetails.teamCityPassword,
requestDetails.artifactoryApiKey, requestDetails.target, requestDetails.buildType, requestDetails.bundleSize, requestDetails.fileWithResult)
requestDetails.failuresNumber, requestDetails.executionTime, requestDetails.compileTime,
requestDetails.codeSize, requestDetails.bundleSize)
} }
} }
private val teamCityBuildUrl: String by lazy { "$teamCityUrl/builds/id:$buildId" } private val teamCityBuildUrl: String by lazy { "builds/id:$buildId" }
val changesListUrl: String by lazy { 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 = private fun format(timeValue: Int): String =
if (timeValue < 10) "0$timeValue" else "$timeValue" if (timeValue < 10) "0$timeValue" else "$timeValue"
fun getBuildInformation(): BuildInfo { fun getBuildInformation(): Promise<TCBuildInfo> {
val buildNumber = sendTeamCityRequest("$teamCityBuildUrl/number") return Promise.all(arrayOf(sendTeamCityRequest("$teamCityBuildUrl/number"),
val branch = sendTeamCityRequest("$teamCityBuildUrl/branchName") sendTeamCityRequest("$teamCityBuildUrl/branchName"),
val startTime = sendTeamCityRequest("$teamCityBuildUrl/startDate") sendTeamCityRequest("$teamCityBuildUrl/startDate"))).then { results ->
val currentTime = Date() val (buildNumber, branch, startTime) = results
val timeZone = currentTime.getTimezoneOffset() / -60 // Convert to hours. val currentTime = Date()
// Get finish time as current time, because buid on TeamCity isn't finished. val timeZone = currentTime.getTimezoneOffset() / -60 // Convert to hours.
val finishTime = "${format(currentTime.getUTCFullYear())}" + // Get finish time as current time, because buid on TeamCity isn't finished.
"${format(currentTime.getUTCMonth() + 1)}" + val finishTime = "${format(currentTime.getUTCFullYear())}" +
"${format(currentTime.getUTCDate())}" + "${format(currentTime.getUTCMonth() + 1)}" +
"T${format(currentTime.getUTCHours())}" + "${format(currentTime.getUTCDate())}" +
"${format(currentTime.getUTCMinutes())}" + "T${format(currentTime.getUTCHours())}" +
"${format(currentTime.getUTCSeconds())}" + "${format(currentTime.getUTCMinutes())}" +
"${if (timeZone > 0) "+" else "-"}${format(timeZone)}${format(0)}" "${format(currentTime.getUTCSeconds())}" +
return BuildInfo(buildNumber, branch, startTime, finishTime) "${if (timeZone > 0) "+" else "-"}${format(timeZone)}${format(0)}"
} TCBuildInfo(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]))
} }
} }
return buildsObjects
} }
fun buildDescriptionToTokens(buildDescription: String): List<String> { // Get builds numbers in right order.
val tokens = buildDescription.split(",").map { it.trim() } internal fun <T> orderedValues(values: List<T>, buildElement: (T) -> String = { it -> it.toString() },
if (tokens.size != buildsInfoPartsNumber) { skipMilestones: Boolean = false) =
error("Build description $buildDescription doesn't contain all necessary information. " + values.sortedWith(
"File with data could be corrupted.") compareBy({ buildElement(it).substringBefore(".").toInt() },
} { buildElement(it).substringAfter(".").substringBefore("-").toDouble() },
return tokens {
} 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. // Routing of requests to current server.
fun router() { fun router() {
val express = require("express") val express = require("express")
val router = express.Router() 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. // Register build on Artifactory.
router.post("/register", { request, response -> router.post("/register") { request, response ->
val maxCommitsNumber = 5
val register = BuildRegister.create(JSON.stringify(request.body)) val register = BuildRegister.create(JSON.stringify(request.body))
// Get information from TeamCity. // Get information from TeamCity.
val buildInfo = register.getBuildInformation() register.getBuildInformation().then { buildInfo ->
val changes = sendGetRequest(register.changesListUrl, register.teamCityUser, register.sendTeamCityRequest(register.changesListUrl, true).then { changes ->
register.teamCityPassword, true) val commitsList = CommitsList(JsonTreeParser.parse(changes))
val commitsList = CommitsList(JsonTreeParser.parse(changes)) // Get artifact.
val commitsDescription = buildString { register.sendTeamCityRequest(register.teamCityArtifactsUrl).then { resultsContent ->
if (commitsList.commits.size > maxCommitsNumber) { launch {
append("${commitsList.commits.get(0).revision} by ${commitsList.commits.get(0).developer};") val reportData = JsonTreeParser.parse(resultsContent)
append("${commitsList.commits.get(1).revision} by ${commitsList.commits.get(1).developer};") val reports = if (reportData is JsonArray) {
append("...;") reportData.map { BenchmarksReport.create(it as JsonObject) }
val beforeLast = commitsList.commits.lastIndex - 1 } else listOf(BenchmarksReport.create(reportData as JsonObject))
append("${commitsList.commits.get(beforeLast).revision} by ${commitsList.commits.get(beforeLast).developer};") val goldenResultPromise = getGoldenResults(goldenIndex)
append("${commitsList.commits.last().revision} by ${commitsList.commits.last().developer};") val goldenResults = goldenResultPromise.await()
} else { // Register build information.
commitsList.commits.forEach { var buildInfoInstance = getConsistentBuildInfo(
append("${it.revision} by ${it.developer};") 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. // Register golden results to normalize on Artifactory.
router.post("/registerGolden", { request, response -> router.post("/registerGolden", { request, response ->
val goldenResultsInfo = JSON.parse<GoldenResultsInfo>(JSON.stringify(request.body)) val goldenResultsInfo: GoldenResultsInfo = JSON.parse<GoldenResultsInfo>(JSON.stringify(request.body))
val buildsDescription = StringBuilder(sendGetRequest("$artifactoryUrl/$artifactoryBuildsDirectory/$goldenResultsFileName")) val goldenReport = goldenResultsInfo.toBenchmarksReport()
goldenResultsInfo.goldenResults.forEach { goldenIndex.insert(goldenReport).then { _ ->
buildsDescription.append("${it.benchmarkName}, ${it.metric}, ${it.value}\n") 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. // Get builds description with additional information.
router.get("/builds/:target/:type/:branch/:id", { request, response -> router.get("/buildsDesc/:target", { request, response ->
val builds = LocalCache[request.params.target, request.params.id] CachableResponseDispatcher.getResponse(request, response) { success, reject ->
response.json(prepareBuildsResponse(builds, request.params.type, request.params.branch, request.params.id)) 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 -> // Get values of current metric.
val builds = LocalCache[request.params.target] router.get("/metricValue/:target/:metric", { request, response ->
response.json(prepareBuildsResponse(builds, request.params.type, request.params.branch)) 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 -> // Get branches for [target].
val builds = LocalCache[request.params.target] router.get("/branches", { request, response ->
response.json(builds.map { buildDescriptionToTokens(it)[3] }.distinct()) 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 -> router.get("/buildsNumbers/:target", { request, response ->
val builds = LocalCache[request.params.target] CachableResponseDispatcher.getResponse(request, response) { success, reject ->
response.json(builds.map { buildDescriptionToTokens(it)[0] }.distinct()) distinctValues("buildNumber", buildInfoIndex).then { results ->
success(results)
}.catch { errorMessage ->
println(errorMessage.message ?: "Failed getting branches list.")
reject()
}
}
}) })
router.get("/clean", { _, response -> // Conert data and migrate it from Artifactory to DB.
LocalCache.clean() router.get("/migrate/:target", { request, response ->
response.sendStatus(200) 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 -> val externalJsonReport = artifactoryUrlConnector.sendOptionalRequest(RequestMethod.GET, accessExternalFileUrl).await()
LocalCache.fill() buildInfoIndex.insert(buildInfoRecord).then { _ ->
response.sendStatus(200) 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 -> router.get("/delete/:target", { request, response ->
val buildsToDelete: List<String> = request.query.builds.toString().split(",").map { it.trim() } val target = urlParameterToBaseFormat(request.params.target)
val result = LocalCache.delete(request.params.target, buildsToDelete, request.query.key) var buildNumber: String? = null
if (result) { if (request.query != undefined) {
response.sendStatus(200) if (request.query.buildNumber != undefined) {
} else { buildNumber = request.query.buildNumber
response.sendStatus(404) }
} }
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. // Main page.
@@ -335,52 +556,20 @@ fun router() {
return router return router
} }
fun getAuth(user: String, password: String): String { fun getBuildsInfoFromArtifactory(target: String): Promise<String> {
val buffer = js("Buffer").from(user + ":" + password) val buildsFileName = "buildsSummary.csv"
val based64String = buffer.toString("base64") val artifactoryBuildsDirectory = "builds"
return "Basic " + based64String return UrlNetworkConnector(artifactoryUrl).sendRequest(RequestMethod.GET,
"$artifactoryBuildsDirectory/$target/$buildsFileName")
} }
fun sendGetRequest(url: String, user: String? = null, password: String? = null, jsonContentType: Boolean = false) : String { fun BenchmarksReport.normalizeBenchmarksSet(dataForNormalization: Map<String, List<BenchmarkResult>>): BenchmarksReport {
val request = require("sync-request") val resultBenchmarksList = benchmarks.map { benchmarksList ->
val headers = mutableListOf<Pair<String, String>>() benchmarksList.value.map {
if (user != null && password != null) { NormalizedMeanVarianceBenchmark(it.name, it.status, it.score, it.metric,
headers.add("Authorization" to getAuth(user, password)) it.runtimeInUs, it.repeat, it.warmup, (it as MeanVarianceBenchmark).variance,
} dataForNormalization[benchmarksList.key]?.get(0)?.score?.let { golden -> it.score / golden } ?: 0.0)
if (jsonContentType) { }
headers.add("Accept" to "application/json") }.flatten()
} return BenchmarksReport(env, resultBenchmarksList, compiler)
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}")
}
} }
@@ -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 { buildscript {
ext.rootBuildDirectory = file('../../..') ext.rootBuildDirectory = file('../../..')
@@ -11,6 +13,12 @@ buildscript {
maven { maven {
url kotlinCompilerRepo url kotlinCompilerRepo
} }
maven {
url "http://dl.bintray.com/kotlin/kotlin-eap"
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-dev"
}
} }
dependencies { dependencies {
@@ -28,42 +36,40 @@ repositories {
maven { maven {
url buildKotlinCompilerRepo 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 { kotlin {
compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion" js {
} browser {
binaries.executable()
compileKotlin2Js { distribution {
kotlinOptions.outputFile = "${projectDir}/js/main.js" directory = new File("$projectDir/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/"))
} }
} }
} }
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; border-color: darkcyan;
} }
.ct-legend .ct-series-5:before {
background-color: #453d3f;
border-color: #453d3f;
}
.ct-series-b .ct-line, .ct-series-b .ct-line,
.ct-series-b .ct-point { .ct-series-b .ct-point {
/* Set the colour of this series line */ /* Set the colour of this series line */
+3 -4
View File
@@ -33,8 +33,8 @@
</div> </div>
<select class="custom-select" id="inputGroupTarget"> <select class="custom-select" id="inputGroupTarget">
<option value="Linux">Linux</option> <option value="Linux">Linux</option>
<option value="MacOSX">Mac OS X</option> <option value="Mac_OS_X">Mac OS X</option>
<option value="Windows10">Windows 10</option> <option value="Windows_10">Windows 10</option>
</select> </select>
</div> </div>
</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/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://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="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/ui.js"></script>
<script src="js/main.js"></script>
</body> </body>
</html> </html>
@@ -1,23 +1,12 @@
/* /*
* Copyright 2010-2019 JetBrains s.r.o. * 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.
* 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.
*/ */
import kotlin.browser.* import kotlin.browser.*
import org.w3c.xhr.* import org.w3c.fetch.*
import org.jetbrains.report.json.* import org.jetbrains.report.json.*
import org.jetbrains.build.Build import org.jetbrains.buildInfo.Build
import kotlin.js.* import kotlin.js.*
import kotlin.math.ceil import kotlin.math.ceil
import org.w3c.dom.* import org.w3c.dom.*
@@ -30,63 +19,40 @@ external class ChartistPlugins {
external object Chartist { external object Chartist {
class Svg(form: String, parameters: dynamic, chartArea: String) class Svg(form: String, parameters: dynamic, chartArea: String)
val plugins: ChartistPlugins val plugins: ChartistPlugins
val Interpolation: dynamic val Interpolation: dynamic
fun Line(query: String, data: dynamic, options: dynamic): dynamic fun Line(query: String, data: dynamic, options: dynamic): dynamic
} }
fun sendGetRequest(url: String) : String { data class Commit(val revision: String, val developer: String)
val request = XMLHttpRequest()
request.open("GET", url, false) fun sendGetRequest(url: String) = window.fetch(url, RequestInit("GET")).then { response ->
request.send() if (!response.ok)
if (request.status == 200.toShort()) { error("Error during getting response from $url\n" +
return request.responseText "${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}") buildsGroups[index]
} } ?: builds
// 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
// Get data for chart in needed format.
fun getChartData(labels: List<String>, valuesList: Collection<List<*>>, stageToShow: Int = 0, fun getChartData(labels: List<String>, valuesList: Collection<List<*>>, stageToShow: Int = 0,
buildsNumber: Int? = null, classNames: Array<String>? = null): dynamic { buildsNumber: Int? = null, classNames: Array<String>? = null): dynamic {
val chartData: dynamic = object{} val chartData: dynamic = object {}
// Show only some part of data. // Show only some part of data.
val (labelsData, valuesData) = buildsNumber?.let { val (labelsData, valuesData) = buildsNumber?.let {
println("Divide on ${labels.size / buildsNumber}")
val labelsGroups = labels.chunked(buildsNumber) val labelsGroups = labels.chunked(buildsNumber)
val valuesListGroups = valuesList.map { it.chunked(buildsNumber) } val valuesListGroups = valuesList.map { it.chunked(buildsNumber) }
val expectedGroup = labelsGroups.size - 1 + stageToShow 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 expectedGroup >= labelsGroups.size -> labelsGroups.size - 1
else -> expectedGroup else -> expectedGroup
} }
Pair(labelsGroups[index], valuesListGroups.map {it[index]}) Pair(labelsGroups[index], valuesListGroups.map { it[index] })
} ?: Pair(labels, valuesList) } ?: Pair(labels, valuesList)
chartData["labels"] = labelsData.toTypedArray() chartData["labels"] = labelsData.toTypedArray()
chartData["series"] = valuesData.mapIndexed { index, it -> chartData["series"] = valuesData.mapIndexed { index, it ->
val series: dynamic = object{} val series: dynamic = object {}
series["data"] = it.toTypedArray() series["data"] = it.toTypedArray()
classNames?.let { series["className"] = classNames[index] } classNames?.let { series["className"] = classNames[index] }
series series
@@ -107,32 +73,33 @@ fun getChartData(labels: List<String>, valuesList: Collection<List<*>>, stageToS
return chartData return chartData
} }
// Create object with options of chart.
fun getChartOptions(samples: Array<String>, yTitle: String, classNames: Array<String>? = null): dynamic { fun getChartOptions(samples: Array<String>, yTitle: String, classNames: Array<String>? = null): dynamic {
val chartOptions: dynamic = object{} val chartOptions: dynamic = object {}
chartOptions["fullWidth"] = true chartOptions["fullWidth"] = true
val paddingObject: dynamic = object{} val paddingObject: dynamic = object {}
paddingObject["right"] = 40 paddingObject["right"] = 40
chartOptions["chartPadding"] = paddingObject chartOptions["chartPadding"] = paddingObject
val axisXObject: dynamic = object{} val axisXObject: dynamic = object {}
axisXObject["offset"] = 40 axisXObject["offset"] = 40
axisXObject["labelInterpolationFnc"] = { value, index, labels -> axisXObject["labelInterpolationFnc"] = { value, index, labels ->
val labelsCount = 30 val labelsCount = 30
val skipNumber = ceil((labels.length as Int).toDouble() / labelsCount).toInt() val skipNumber = ceil((labels.length as Int).toDouble() / labelsCount).toInt()
if (skipNumber > 1) { if (skipNumber > 1) {
if (index % skipNumber == 0) value else null if (index % skipNumber == 0) value else null
} else { } else {
value value
} }
} }
chartOptions["axisX"] = axisXObject chartOptions["axisX"] = axisXObject
val axisYObject: dynamic = object{} val axisYObject: dynamic = object {}
axisYObject["offset"] = 90 axisYObject["offset"] = 90
chartOptions["axisY"] = axisYObject chartOptions["axisY"] = axisYObject
val legendObject: dynamic = object{} val legendObject: dynamic = object {}
legendObject["legendNames"] = samples legendObject["legendNames"] = samples
classNames?.let { legendObject["classNames"] = classNames } classNames?.let { legendObject["classNames"] = classNames.sliceArray(0 until samples.size) }
val titleObject: dynamic = object{} val titleObject: dynamic = object {}
val axisYTitle: dynamic = object{} val axisYTitle: dynamic = object {}
axisYTitle["axisTitle"] = yTitle axisYTitle["axisTitle"] = yTitle
axisYTitle["axisClass"] = "ct-axis-title" axisYTitle["axisClass"] = "ct-axis-title"
val titleOffset: dynamic = {} val titleOffset: dynamic = {}
@@ -153,7 +120,8 @@ fun redirect(url: String) {
window.location.href = url 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>) { parameters: Map<String, String>) {
chart.on("draw", { data -> chart.on("draw", { data ->
var element = data.element var element = data.element
@@ -161,59 +129,74 @@ fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynam
val buildsGroup = getBuildsGroup(builds) val buildsGroup = getBuildsGroup(builds)
val pointSize = 12 val pointSize = 12
val currentBuild = buildsGroup.get(data.index) val currentBuild = buildsGroup.get(data.index)
// Higlight builds with failures. currentBuild?.let { currentBuild ->
if (currentBuild.failuresNumber > 0) { // Higlight builds with failures.
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>")
if (currentBuild.failuresNumber > 0) { 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>") // Add tooltips.
append("date: ${currentBuild.date}<br>") var shift = 1
append("time: ${currentBuild.formattedStartTime}-${currentBuild.formattedFinishTime}<br>") var previousBuild: Build? = null
append("Commits:<br>") while (previousBuild == null && data.index - shift >= 0) {
val commitsList = currentBuild.commits.split(";") previousBuild = buildsGroup.get(data.index - shift)
commitsList.forEach { shift++
if (!it.isEmpty())
append("${it.substringBefore("by").substring(0, 7)} by ${it.substringAfter("by ")}<br>")
} }
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", { chart.on("created", {
val currentChart = jquerySelector val currentChart = jquerySelector
val parameters: dynamic = object{} val parameters: dynamic = object {}
parameters["selector"] = "[data-chart-tooltip=\"$chartContainer\"]" parameters["selector"] = "[data-chart-tooltip=\"$chartContainer\"]"
parameters["container"] = "#$chartContainer" parameters["container"] = "#$chartContainer"
parameters["html"] = true parameters["html"] = true
@@ -225,6 +208,12 @@ var stageToShow = 0
var buildsNumberToShow: Int? = null 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>) { fun main(args: Array<String>) {
val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg" val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg"
buildsNumberToShow = null buildsNumberToShow = null
@@ -243,45 +232,52 @@ fun main(args: Array<String>) {
} }
} }
// Get builds. // Get branches.
val buildsUrl = buildString { val branchesUrl = "$serverUrl/branches"
append("$serverUrl/builds") sendGetRequest(branchesUrl).then { response ->
append("/${parameters["target"]}") val branches: Array<String> = JSON.parse(response)
append("/${parameters["type"]}") // Add release branches to selector.
append("/${parameters["branch"]}") branches.filter { it != "master" }.forEach {
append("/${parameters["build"]}") 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"]}" // Fill autocomplete list with build numbers.
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.
val buildsNumbersUrl = "$serverUrl/buildsNumbers/${parameters["target"]}" val buildsNumbersUrl = "$serverUrl/buildsNumbers/${parameters["target"]}"
val buildsNumbers: Array<String> = JSON.parse(sendGetRequest(buildsNumbersUrl)) sendGetRequest(buildsNumbersUrl).then { response ->
val buildsNumbers: Array<String> = JSON.parse(response)
// Add release branches to selector. val autocompleteParameters: dynamic = object {}
releaseBranches.forEach { autocompleteParameters["lookup"] = buildsNumbers
val option = Option(it, it) autocompleteParameters["onSelect"] = { suggestion ->
js("$('#inputGroupBranch')").append(js("$(option)")) 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. // Change inputs values connected with parameters and add events listeners.
document.querySelector("#inputGroupTarget [value=\"${parameters["target"]}\"]")?.setAttribute("selected", "true") document.querySelector("#inputGroupTarget [value=\"${parameters["target"]}\"]")?.setAttribute("selected", "true")
document.querySelector("#inputGroupBuildType [value=\"${parameters["type"]}\"]")?.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"]!! (document.getElementById("highligted_build") as HTMLInputElement).value = parameters["build"]!!
// Add onChange events for fields. // Add onChange events for fields.
// Don't use AJAX to have opportunity to share results with simple links.
js("$('#inputGroupTarget')").change({ js("$('#inputGroupTarget')").change({
val newValue = js("$(this).val()") val newValue = js("$(this).val()")
if (newValue != parameters["target"]) { if (newValue != parameters["target"]) {
@@ -307,69 +303,160 @@ fun main(args: Array<String>) {
} }
}) })
val autocompleteParameters: dynamic = object{} val platformSpecificBenchs = if (parameters["target"] == "Mac_OS_X") ",FrameworkBenchmarksAnalyzer,circlet_iosX64" else
autocompleteParameters["lookup"] = buildsNumbers if (parameters["target"] == "Linux") ",kotlinx.coroutines" else ""
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
}
})
// Collect information for charts library. // Collect information for charts library.
val labels = mutableListOf<String>() val valuesToShow = mapOf("EXECUTION_TIME" to arrayOf(mapOf(
val executionTime = mutableMapOf<String, MutableList<Double?>>() "normalize" to "true"
val compileTime = mutableMapOf<String, MutableList<Double?>>() )),
val codeSize = mutableMapOf<String, MutableList<Double?>>() "COMPILE_TIME" to arrayOf(mapOf(
val bundleSize = mutableListOf<Int?>() "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 { var execData = listOf<String>() to listOf<List<Double?>>()
// Choose labels on x axis. var compileData = listOf<String>() to listOf<List<Double?>>()
if (parameters["type"] == "day") { var codeSizeData = listOf<String>() to listOf<List<Double?>>()
labels.add(it.date) var bundleSizeData = listOf<String>() to listOf<List<Int?>>()
} 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 })
}
val sizeClassNames = arrayOf("ct-series-e", "ct-series-f", "ct-series-g") val sizeClassNames = arrayOf("ct-series-e", "ct-series-f", "ct-series-g")
// Draw charts. // Draw charts.
val execChart = Chartist.Line("#exec_chart", getChartData(labels, executionTime.values, stageToShow, buildsNumberToShow), var execChart: dynamic = null
getChartOptions(executionTime.keys.toTypedArray(), "Normalized time")) var compileChart: dynamic = null
val compileChart = Chartist.Line("#compile_chart", getChartData(labels, compileTime.values, stageToShow, buildsNumberToShow), var codeSizeChart: dynamic = null
getChartOptions(compileTime.keys.toTypedArray(), "Time, milliseconds")) var bundleSizeChart: dynamic = null
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")))
// Tooltips and higlights. val descriptionUrl = "$serverUrl/buildsDesc/${parameters["target"]}?type=${parameters["type"]}" +
customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters) "${if (parameters["branch"] != "all") "&branch=${parameters["branch"]}" else ""}"
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 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 = { val updateAllCharts: () -> Unit = {
execChart.update(getChartData(labels, executionTime.values, stageToShow, buildsNumberToShow)) execChart.update(getChartData(execData.first, execData.second, stageToShow, buildsNumberToShow))
compileChart.update(getChartData(labels, compileTime.values, stageToShow, buildsNumberToShow)) compileChart.update(getChartData(compileData.first, compileData.second, stageToShow, buildsNumberToShow))
codeSizeChart.update(getChartData(labels, codeSize.values, stageToShow, buildsNumberToShow, sizeClassNames)) codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, stageToShow, buildsNumberToShow, sizeClassNames))
bundleSizeChart.update(getChartData(labels, listOf(bundleSize), 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({ js("$('#plusBtn')").click({
@@ -379,14 +466,13 @@ fun main(args: Array<String>) {
} else { } else {
it it
} }
} ?: labels.size / zoomRatio } ?: execData.first.size / zoomRatio
println(buildsNumberToShow)
updateAllCharts() updateAllCharts()
}) })
js("$('#minusBtn')").click({ js("$('#minusBtn')").click({
buildsNumberToShow = buildsNumberToShow?.let { buildsNumberToShow = buildsNumberToShow?.let {
if (it * zoomRatio <= labels.size) { if (it * zoomRatio <= execData.first.size) {
it * zoomRatio it * zoomRatio
} else { } else {
null null
@@ -397,11 +483,11 @@ fun main(args: Array<String>) {
js("$('#prevBtn')").click({ js("$('#prevBtn')").click({
buildsNumberToShow?.let { buildsNumberToShow?.let {
val bottomBorder = -labels.size / (buildsNumberToShow as Int) val bottomBorder = -execData.first.size / (buildsNumberToShow as Int)
if (stageToShow - 1 > bottomBorder) { if (stageToShow - 1 > bottomBorder) {
stageToShow-- stageToShow--
} }
} ?: run { stageToShow = 0} } ?: run { stageToShow = 0 }
updateAllCharts() updateAllCharts()
}) })