From a16bddb421e4b443fa06b38935f1f9ed7e01b69d Mon Sep 17 00:00:00 2001 From: LepilkinaElena Date: Mon, 18 Mar 2019 16:02:41 +0300 Subject: [PATCH] Server and client to show summary performance information (#2765) --- .gitignore | 3 + .../buildSrc/src/main/kotlin/MPPTools.kt | 10 +- .../src/main/kotlin/RegressionsReporter.kt | 7 +- .../main/kotlin/RegressionsSummaryReporter.kt | 3 + .../jetbrains/benchmarksLauncher/launcher.kt | 2 +- .../org/jetbrains/report/BenchmarksReport.kt | 24 +- .../report/json/ConvertedFromJson.kt | 7 + .../src/main/kotlin/main.kt | 64 +- .../org/jetbrains/analyzer/Statistics.kt | 8 +- .../analyzer/SummaryBenchmarksReport.kt | 22 + .../src/tests/AnalyzerTests.kt | 8 +- tools/kliopt/org/jetbrains/kliopt/KliOpt.kt | 14 +- tools/performance-server/Dockerfile | 19 + tools/performance-server/build.gradle | 51 ++ tools/performance-server/gradle.properties | 1 + tools/performance-server/package-lock.json | 644 ++++++++++++++++++ tools/performance-server/package.json | 16 + tools/performance-server/settings.gradle | 0 .../src/main/kotlin/main.kt | 49 ++ .../src/main/kotlin/routes/route.kt | 284 ++++++++ tools/performance-server/ui/build.gradle | 69 ++ tools/performance-server/ui/css/style.css | 58 ++ tools/performance-server/ui/index.ejs | 100 +++ tools/performance-server/ui/settings.gradle | 0 .../ui/src/main/kotlin/main.kt | 270 ++++++++ 25 files changed, 1705 insertions(+), 28 deletions(-) create mode 100644 tools/performance-server/Dockerfile create mode 100644 tools/performance-server/build.gradle create mode 100644 tools/performance-server/gradle.properties create mode 100644 tools/performance-server/package-lock.json create mode 100644 tools/performance-server/package.json create mode 100644 tools/performance-server/settings.gradle create mode 100644 tools/performance-server/src/main/kotlin/main.kt create mode 100644 tools/performance-server/src/main/kotlin/routes/route.kt create mode 100644 tools/performance-server/ui/build.gradle create mode 100644 tools/performance-server/ui/css/style.css create mode 100644 tools/performance-server/ui/index.ejs create mode 100644 tools/performance-server/ui/settings.gradle create mode 100644 tools/performance-server/ui/src/main/kotlin/main.kt diff --git a/.gitignore b/.gitignore index ea71f7cef8a..03ba8168856 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ samples/androidNativeActivity/Polyhedron # CMake llvmCoverageMappingC/CMakeLists.txt +tools/performance-server/node_modules +tools/performance-server/server +tools/performance-server/ui/js diff --git a/performance/buildSrc/src/main/kotlin/MPPTools.kt b/performance/buildSrc/src/main/kotlin/MPPTools.kt index 6050190d188..b958a4f6544 100644 --- a/performance/buildSrc/src/main/kotlin/MPPTools.kt +++ b/performance/buildSrc/src/main/kotlin/MPPTools.kt @@ -86,9 +86,9 @@ fun getFileSize(filePath: String): Long? { fun getCodeSizeBenchmark(programName: String, filePath: String): BenchmarkResult { val codeSize = getFileSize(filePath) - return BenchmarkResult("$programName.codeSize", + return BenchmarkResult("$programName", codeSize?. let { BenchmarkResult.Status.PASSED } ?: run { BenchmarkResult.Status.FAILED }, - codeSize?.toDouble() ?: 0.0, codeSize?.toDouble() ?: 0.0, 1, 0) + codeSize?.toDouble() ?: 0.0, BenchmarkResult.Metric.CODE_SIZE, codeSize?.toDouble() ?: 0.0, 1, 0) } // Create benchmarks json report based on information get from gradle project @@ -184,7 +184,7 @@ fun getCompileBenchmarkTime(programName: String, tasksNames: Iterable, r status = if (exitCodes["$it$number"] != 0) BenchmarkResult.Status.FAILED else status } - BenchmarkResult("$programName.compileTime", status, time, time, number, 0) + BenchmarkResult("$programName", status, time, BenchmarkResult.Metric.COMPILE_TIME, time, number, 0) }.toList() @@ -197,9 +197,9 @@ class TaskTimerListener: TaskExecutionListener { val time = tasksNames.map { tasksTimes[it] ?: 0.0 }.sum() // TODO get this info from gradle plugin with exit code end stacktrace. val status = tasksNames.map { tasksTimes.containsKey(it) }.reduce { a, b -> a && b } - return BenchmarkResult("$programName.compileTime", + return BenchmarkResult("$programName", if (status) BenchmarkResult.Status.PASSED else BenchmarkResult.Status.FAILED, - time, time, 1, 0) + time, BenchmarkResult.Metric.COMPILE_TIME, time, 1, 0) } fun getTime(taskName: String) = tasksTimes[taskName] ?: 0.0 diff --git a/performance/buildSrc/src/main/kotlin/RegressionsReporter.kt b/performance/buildSrc/src/main/kotlin/RegressionsReporter.kt index 0356f0f90e8..297ed211f05 100644 --- a/performance/buildSrc/src/main/kotlin/RegressionsReporter.kt +++ b/performance/buildSrc/src/main/kotlin/RegressionsReporter.kt @@ -222,11 +222,14 @@ open class RegressionsReporter : DefaultTask() { arrayOf("$analyzer", "-r", "statistics", "$currentBenchmarksReportFile", "bintray:$compareToBuildNumber:$target:$bintrayFileName", "-o", "$summaryFile") .runCommand() - val reportLink = "http://kotlin-native-performance.labs.jb.gg/?" + + val reportLink = "https://kotlin-native-perf-summary.labs.jb.gg/?target=$target&build=$buildNumber" + val detailedReportLink = "https://kotlin-native-performance.labs.jb.gg/?" + "report=bintray:$buildNumber:$target:$bintrayFileName&" + "compareTo=bintray:$compareToBuildNumber:$target:$bintrayFileName" - val title = "\n*Performance report for target $target (build $buildNumber)* - $reportLink\n" + val title = "\n*Performance report for target $target (build $buildNumber)* - $reportLink\n" + + "*Detailed info - * $detailedReportLink" + val header = "$title\n$changesInfo\n\nCompare to build $compareToBuildNumber: $compareToBuildLink\n\n" val footer = "*Benchmarks statistics:* $testReportUrl" val message = "$header\n$footer\n" diff --git a/performance/buildSrc/src/main/kotlin/RegressionsSummaryReporter.kt b/performance/buildSrc/src/main/kotlin/RegressionsSummaryReporter.kt index 7cf4a40cba4..10eb9dd3d79 100644 --- a/performance/buildSrc/src/main/kotlin/RegressionsSummaryReporter.kt +++ b/performance/buildSrc/src/main/kotlin/RegressionsSummaryReporter.kt @@ -29,6 +29,8 @@ open class RegressionsSummaryReporter : DefaultTask() { @Input lateinit var targetsResultFiles: Map + const val performanceServer = "https://kotlin-native-perf-summary.labs.jb.gg/" + @TaskAction fun run() { // Get TeamCity properties. @@ -59,6 +61,7 @@ open class RegressionsSummaryReporter : DefaultTask() { } val message = buildString { + append("*Performance summary on charts* - $performanceServer\n") results.forEach { (property, targets) -> append("$property: ${targets.map {(target, value) -> "$value ($target)"}.joinToString(" | ")}\n") } diff --git a/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/launcher.kt b/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/launcher.kt index 0edea21ee13..5ed9c034bc6 100644 --- a/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/launcher.kt +++ b/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/launcher.kt @@ -61,7 +61,7 @@ abstract class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int, v samples[k] = scaledTime // Save benchmark object benchmarkResults.add(BenchmarkResult("$prefix$it", BenchmarkResult.Status.PASSED, - scaledTime / 1000, scaledTime / 1000, + scaledTime / 1000, BenchmarkResult.Metric.EXECUTION_TIME, scaledTime / 1000, k + 1, numWarmIterations)) } } diff --git a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/BenchmarksReport.kt b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/BenchmarksReport.kt index 796d27a4835..626af1dfdfd 100644 --- a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/BenchmarksReport.kt +++ b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/BenchmarksReport.kt @@ -242,26 +242,38 @@ data class Environment(val machine: Machine, val jdk: JDKInstance): JsonSerializ } class BenchmarkResult(val name: String, val status: Status, - val score: Double, val runtimeInUs: Double, + val score: Double, val metric: Metric, val runtimeInUs: Double, val repeat: Int, val warmup: Int): JsonSerializable { - constructor(name: String, score: Double) : this(name, Status.PASSED, score, 0.0, 0, 0) + enum class Metric(val suffix: String, val value: String) { + EXECUTION_TIME("", "EXECUTION_TIME"), + CODE_SIZE(".codeSize", "CODE_SIZE"), + COMPILE_TIME(".compileTime", "COMPILE_TIME") + } + + constructor(name: String, score: Double) : this(name, Status.PASSED, score, Metric.EXECUTION_TIME, 0.0, 0, 0) companion object: EntityFromJsonFactory { override fun create(data: JsonElement): BenchmarkResult { if (data is JsonObject) { - val name = elementToString(data.getRequiredField("name"), "name") + var name = elementToString(data.getRequiredField("name"), "name") + val metricElement = data.getOptionalField("metric") + val metric = if (metricElement != null && metricElement is JsonLiteral) + metricFromString(metricElement.unquoted()) ?: Metric.EXECUTION_TIME + else Metric.EXECUTION_TIME + name += metric.suffix val statusElement = data.getRequiredField("status") if (statusElement is JsonLiteral) { val status = statusFromString(statusElement.unquoted()) ?: error("Status should be PASSED or FAILED") + val score = elementToDouble(data.getRequiredField("score"), "score") val runtimeInUs = elementToDouble(data.getRequiredField("runtimeInUs"), "runtimeInUs") val repeat = elementToInt(data.getRequiredField("repeat"), "repeat") val warmup = elementToInt(data.getRequiredField("warmup"), "warmup") - return BenchmarkResult(name, status, score, runtimeInUs, repeat, warmup) + return BenchmarkResult(name, status, score, metric, runtimeInUs, repeat, warmup) } else { error("Status should be string literal.") } @@ -271,6 +283,7 @@ class BenchmarkResult(val name: String, val status: Status, } fun statusFromString(s: String): Status? = Status.values().find { it.value == s } + fun metricFromString(s: String): Metric? = Metric.values().find { it.value == s } } enum class Status(val value: String) { @@ -281,9 +294,10 @@ class BenchmarkResult(val name: String, val status: Status, override fun toJson(): String { return """ { - "name": "$name", + "name": "${name.removeSuffix(metric.suffix)}", "status": "${status.value}", "score": ${score}, + "metric": "${metric.value}", "runtimeInUs": ${runtimeInUs}, "repeat": ${repeat}, "warmup": ${warmup} diff --git a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/ConvertedFromJson.kt b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/ConvertedFromJson.kt index 85bbb448c51..7f16418693e 100644 --- a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/ConvertedFromJson.kt +++ b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/ConvertedFromJson.kt @@ -36,6 +36,13 @@ interface ConvertedFromJson { element.unquoted() else error("Field '$name' in '$element' is expected to be a string. Please, check origin files.") + + fun elementToStringOrNull(element: JsonElement, name:String): String? = + when (element) { + is JsonLiteral -> element.unquoted() + is JsonNull -> null + else -> error("Field '$name' in '$element' is expected to be a string. Please, check origin files.") + } } fun JsonObject.getRequiredField(fieldName: String): JsonElement { diff --git a/tools/benchmarksAnalyzer/src/main/kotlin/main.kt b/tools/benchmarksAnalyzer/src/main/kotlin/main.kt index 9e4e7b10e56..03e4dd71d98 100644 --- a/tools/benchmarksAnalyzer/src/main/kotlin/main.kt +++ b/tools/benchmarksAnalyzer/src/main/kotlin/main.kt @@ -20,6 +20,7 @@ import org.jetbrains.analyzer.SummaryBenchmarksReport import org.jetbrains.kliopt.* import org.jetbrains.renders.* import org.jetbrains.report.BenchmarksReport +import org.jetbrains.report.BenchmarkResult import org.jetbrains.report.json.JsonTreeParser abstract class Connector { @@ -77,8 +78,61 @@ fun getFileContent(fileName: String, user: String? = null): String { } } +fun getBenchmarkReport(fileName: String, user: String? = null) = + BenchmarksReport.create(JsonTreeParser.parse(getFileContent(fileName, user))) + +// Prints text summary by users request. +fun summaryAction(argParser: ArgParser) { + val benchsReport = SummaryBenchmarksReport(getBenchmarkReport(argParser.get("mainReport")!!, argParser.get("user"))) + val results = mutableListOf() + results.apply { + add(benchsReport.failedBenchmarks.size.toString()) + argParser.getAll("exec-samples")?. let { + val filter = if (it.first() == "all") null else it + add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.EXECUTION_TIME, + argParser.get("exec")!! == "geomean", filter).joinToString(";")) + } + argParser.getAll("compile-samples")?. let { + val filter = if (it.first() == "all") null else it + add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.COMPILE_TIME, + argParser.get("compile")!! == "geomean", filter).joinToString(";")) + } + argParser.getAll("codesize-samples")?. let { + val filter = if (it.first() == "all") null else it + add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.CODE_SIZE, + argParser.get("codesize")!! == "geomean", filter).joinToString(";")) + } + } + println(results.joinToString()) +} + fun main(args: Array) { + val actions = mapOf( "summary" to Action( + ::summaryAction, + ArgParser( + listOf( + OptionDescriptor(ArgType.Choice(listOf("samples", "geomean")), "exec", + description = "Execution time way of calculation", defaultValue = "geomean"), + OptionDescriptor(ArgType.String(), "exec-samples", + description = "Samples used for execution time metric (value 'all' allows use all samples)", + delimiter = ","), + OptionDescriptor(ArgType.Choice(listOf("samples", "geomean")), "compile", + description = "Compile time way of calculation", defaultValue = "geomean"), + OptionDescriptor(ArgType.String(), "compile-samples", + description = "Samples used for compile time metric (value 'all' allows use all samples)", + delimiter = ","), + OptionDescriptor(ArgType.Choice(listOf("samples", "geomean")), "codesize", + description = "Code size way of calculation", defaultValue = "geomean"), + OptionDescriptor(ArgType.String(), "codesize-samples", + description = "Samples used for code size metric (value 'all' allows use all samples)", + delimiter = ","), + OptionDescriptor(ArgType.String(), "user", "u", "User access information for authorization") + ), listOf(ArgDescriptor(ArgType.String(), "mainReport", "Main report for analysis")) + ) + ) + ) + val options = listOf( OptionDescriptor(ArgType.String(), "output", "o", "Output file"), OptionDescriptor(ArgType.Double(), "eps", "e", "Meaningful performance changes", "1.0"), @@ -94,16 +148,12 @@ fun main(args: Array) { ) // Parse args. - val argParser = ArgParser(options, arguments) + val argParser = ArgParser(options, arguments, actions) if (argParser.parse(args)) { // Read contents of file. - val mainBenchsResults = getFileContent(argParser.get("mainReport")!!, argParser.get("user")) - val mainReportElement = JsonTreeParser.parse(mainBenchsResults) - val mainBenchsReport = BenchmarksReport.create(mainReportElement) + val mainBenchsReport = getBenchmarkReport(argParser.get("mainReport")!!, argParser.get("user")) var compareToBenchsReport = argParser.get("compareToReport")?.let { - val compareToResults = getFileContent(it, argParser.get("user")) - val compareToReportElement = JsonTreeParser.parse(compareToResults) - BenchmarksReport.create(compareToReportElement) + getBenchmarkReport(it, argParser.get("user")) } val renders = argParser.getAll("renders") diff --git a/tools/benchmarksAnalyzer/src/main/kotlin/org/jetbrains/analyzer/Statistics.kt b/tools/benchmarksAnalyzer/src/main/kotlin/org/jetbrains/analyzer/Statistics.kt index be94b15d1c4..760c3fcc972 100644 --- a/tools/benchmarksAnalyzer/src/main/kotlin/org/jetbrains/analyzer/Statistics.kt +++ b/tools/benchmarksAnalyzer/src/main/kotlin/org/jetbrains/analyzer/Statistics.kt @@ -82,7 +82,7 @@ data class MeanVarianceBenchmark(val meanBenchmark: BenchmarkResult, val varianc } -fun geometricMean(values: List, totalNumber: Int = values.size) = +fun geometricMean(values: Collection, totalNumber: Int = values.size) = values.asSequence().filter{ it != 0.0 }.map { it.pow(1.0 / totalNumber) }.reduce { a, b -> a * b } fun computeMeanVariance(samples: List): MeanVariance { @@ -97,6 +97,7 @@ fun computeMeanVariance(samples: List): MeanVariance { fun collectMeanResults(benchmarks: Map>): 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 @@ -111,6 +112,7 @@ fun collectMeanResults(benchmarks: Map>): Benchmar if (result.warmup != currentWarmup) println("Check data consistency. Warmup value for benchmark '${result.name}' differs.") currentWarmup = result.warmup + metric = result.metric } repeatedSequence.sort() @@ -125,10 +127,10 @@ fun collectMeanResults(benchmarks: Map>): Benchmar // 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, + val meanBenchmark = BenchmarkResult(name, currentStatus, scoreMeanVariance.mean, metric, runtimeInUsMeanVariance.mean, repeatedSequence[resultsSet.size - 1], currentWarmup) - val varianceBenchmark = BenchmarkResult(name, currentStatus, scoreMeanVariance.variance, + val varianceBenchmark = BenchmarkResult(name, currentStatus, scoreMeanVariance.variance, metric, runtimeInUsMeanVariance.variance, repeatedSequence[resultsSet.size - 1], currentWarmup) name to MeanVarianceBenchmark(meanBenchmark, varianceBenchmark) diff --git a/tools/benchmarksAnalyzer/src/main/kotlin/org/jetbrains/analyzer/SummaryBenchmarksReport.kt b/tools/benchmarksAnalyzer/src/main/kotlin/org/jetbrains/analyzer/SummaryBenchmarksReport.kt index 84c0478216d..f47d4570a56 100644 --- a/tools/benchmarksAnalyzer/src/main/kotlin/org/jetbrains/analyzer/SummaryBenchmarksReport.kt +++ b/tools/benchmarksAnalyzer/src/main/kotlin/org/jetbrains/analyzer/SummaryBenchmarksReport.kt @@ -133,6 +133,28 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport, } } + fun getResultsByMetric(metric: BenchmarkResult.Metric, getGeoMean: Boolean = true, filter: List? = null): List { + val benchmarks = filter?.let { + mergedReport.filter { entry -> + filter.find { + entry.key.startsWith(it) + } != null + } + } ?: mergedReport + if (benchmarks.isEmpty()) { + error("There is no benchmarks from provided list") + } + val filteredBenchmarks = benchmarks.filter { entry -> entry.value.first!!.meanBenchmark.metric == metric } + if (filteredBenchmarks.isEmpty()) { + error("There is no benchmarks for metric $metric") + } + val results = filteredBenchmarks.map { entry -> entry.key.removeSuffix(metric.suffix) to entry.value.first!!.meanBenchmark.score }.toMap() + if (getGeoMean) { + return listOf(geometricMean(results.values)) + } + return filter?.let { it.map { results[it] ?: error("Benchmark $it for metric $metric doesn't exist.") }.toList() } ?: results.values.toList() + } + private fun getMaximumChange(bucket: Map): Double = // Maps of regressions and improvements are sorted. if (bucket.isEmpty()) 0.0 else bucket.values.map { it.first.mean }.first() diff --git a/tools/benchmarksAnalyzer/src/tests/AnalyzerTests.kt b/tools/benchmarksAnalyzer/src/tests/AnalyzerTests.kt index 100ad104809..2bbd7f35346 100644 --- a/tools/benchmarksAnalyzer/src/tests/AnalyzerTests.kt +++ b/tools/benchmarksAnalyzer/src/tests/AnalyzerTests.kt @@ -25,12 +25,12 @@ class AnalyzerTests { private val eps = 0.000001 private fun createMeanVarianceBenchmarks(): Pair { - val firstMean = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 9.0, 9.0, 10, 10) - val firstVariance = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 0.0001, 0.0001, 10, 10) + val firstMean = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 9.0, BenchmarkResult.Metric.EXECUTION_TIME, 9.0, 10, 10) + val firstVariance = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 0.0001, BenchmarkResult.Metric.EXECUTION_TIME, 0.0001, 10, 10) val first = MeanVarianceBenchmark(firstMean, firstVariance) - val secondMean = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 10.0, 10.0, 10, 10) - val secondVariance = BenchmarkResult("testBenchmark", BenchmarkResult.Status.PASSED, 0.0001, 0.0001, 10, 10) + 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) diff --git a/tools/kliopt/org/jetbrains/kliopt/KliOpt.kt b/tools/kliopt/org/jetbrains/kliopt/KliOpt.kt index ca5a464cac1..f5539d534d7 100644 --- a/tools/kliopt/org/jetbrains/kliopt/KliOpt.kt +++ b/tools/kliopt/org/jetbrains/kliopt/KliOpt.kt @@ -55,6 +55,8 @@ sealed class ArgType(val hasParameter: kotlin.Boolean) { } } +data class Action(val callback: (parser: ArgParser) -> Unit, val parser: ArgParser) + // Common descriptor both for options and positional arguments. abstract class Descriptor(val type: ArgType, val longName: String, @@ -120,7 +122,7 @@ class ArgDescriptor( // Arguments parser. class ArgParser(optionsList: List, argsList: List = listOf(), - useDefaultHelpShortName: Boolean = true) { + val actions: Map = emptyMap(), useDefaultHelpShortName: Boolean = true) { private val options = optionsList.union(if (useDefaultHelpShortName) listOf(OptionDescriptor(ArgType.Boolean(), "help", "h", "Usage info")) else @@ -212,6 +214,16 @@ class ArgParser(optionsList: List, argsList: List + if (arg == name) { + // Use parser for this action. + val parseResult = action.parser.parse(args.slice(index + 1..args.size - 1).toTypedArray()) + if (parseResult) + action.callback(action.parser) + return false + } + } if (arg.startsWith('-')) { // Option is found. val name = shortNames[arg.substring(1)] ?: arg.substring(1) diff --git a/tools/performance-server/Dockerfile b/tools/performance-server/Dockerfile new file mode 100644 index 00000000000..565061efc56 --- /dev/null +++ b/tools/performance-server/Dockerfile @@ -0,0 +1,19 @@ +FROM node:8 + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN npm install +# If you are building your code for production +# RUN npm install --only=production + +# Bundle app source +COPY . . + +EXPOSE 3000 +CMD [ "npm", "start" ] \ No newline at end of file diff --git a/tools/performance-server/build.gradle b/tools/performance-server/build.gradle new file mode 100644 index 00000000000..4648999932c --- /dev/null +++ b/tools/performance-server/build.gradle @@ -0,0 +1,51 @@ +buildscript { + ext.rootBuildDirectory = file('../..') + + apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle" + apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle" + + repositories { + maven { + url 'https://cache-redirector.jetbrains.com/jcenter' + } + maven { + url kotlinCompilerRepo + } + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} + +repositories { + maven { + url 'https://cache-redirector.jetbrains.com/jcenter' + } + maven { + url kotlinCompilerRepo + } + maven { + url buildKotlinCompilerRepo + } +} + +apply plugin: 'kotlin2js' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion" +} + + +sourceSets { + main.resources.srcDirs += "resources" + main.kotlin.srcDirs += "src" + main.kotlin.srcDirs += "../benchmarks/shared/src" + main.kotlin.srcDirs += "shared/src" +} + +compileKotlin2Js { + kotlinOptions.outputFile = "${projectDir}/server/app.js" + kotlinOptions.moduleKind = "commonjs" + kotlinOptions.sourceMap = true +} \ No newline at end of file diff --git a/tools/performance-server/gradle.properties b/tools/performance-server/gradle.properties new file mode 100644 index 00000000000..87803bed88a --- /dev/null +++ b/tools/performance-server/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.kotlin.native.home=../../dist \ No newline at end of file diff --git a/tools/performance-server/package-lock.json b/tools/performance-server/package-lock.json new file mode 100644 index 00000000000..3214b093820 --- /dev/null +++ b/tools/performance-server/package-lock.json @@ -0,0 +1,644 @@ +{ + "name": "performance-server", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", + "requires": { + "@types/node": "*" + } + }, + "@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "9.6.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.44.tgz", + "integrity": "sha512-hR+jK4kSOX+u5aQ6Zj6az5N4l1AeXH37/sOenbgxJvkO450C5qQL4/1twdufIMLC3o3hNfJNrPfQn0ivMdKfPg==" + }, + "@types/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-47kAAs3yV/hROraCTQYDMh4p/6zI9+gtssjD0kq9OWsGdLcBge59rl49FnCuJ+iWxEKiqFz6KXzeGH5DRVjNJA==" + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "bower": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/bower/-/bower-1.8.8.tgz", + "integrity": "sha512-1SrJnXnkP9soITHptSO+ahx3QKp3cVzn8poI6ujqc5SeOkg5iqM1pK9H+DSc2OQ8SnO0jC/NG4Ur/UIwy7574A==" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "ejs": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", + "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + }, + "http-basic": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-7.0.0.tgz", + "integrity": "sha1-gvClBr6UJzLsje6+6A50bvVzbbo=", + "requires": { + "@types/concat-stream": "^1.6.0", + "@types/node": "^9.4.1", + "caseless": "~0.12.0", + "concat-stream": "^1.4.6", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "http-response-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.1.tgz", + "integrity": "sha512-6L0Fkd6TozA8kFSfh9Widst0wfza3U1Ex2RjJ6zNDK0vR1U1auUR6jY4Nn2Xl7CCy0ikFmxW1XcspVpb9RvwTg==", + "requires": { + "@types/node": "^9.3.0" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "kotlin": { + "version": "1.3.21", + "resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.3.21.tgz", + "integrity": "sha512-JqxY9quNzwWl8iYvNj/ycfNBEbSFDKvmeK2rDVU89q0/X8BRjVzibJ5RFOq/9efvvvi+VBnIRhL7GSOGzmP2nQ==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "promise": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.2.tgz", + "integrity": "sha512-EIyzM39FpVOMbqgzEHhxdrEhtOSDOtjMZQ0M6iVfCE+kWNgCkAyOdnuCWqfmflylftfadU6FkiMgHZA2kUzwRw==", + "requires": { + "asap": "~2.0.6" + } + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "sync-request": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.0.0.tgz", + "integrity": "sha512-jGNIAlCi9iU4X3Dm4oQnNQshDD3h0/1A7r79LyqjbjUnj69sX6mShAXlhRXgImsfVKtTcnra1jfzabdZvp+Lmw==", + "requires": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" + } + }, + "sync-rpc": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.4.tgz", + "integrity": "sha512-Iug+t1ICVFenUcTnDu8WXFnT+k8IVoLKGh8VA3eXUtl2Rt9SjKX3YEv33OenABqpTPL9QEaHv1+CNn2LK8vMow==", + "requires": { + "get-port": "^3.1.0" + } + }, + "then-request": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.0.tgz", + "integrity": "sha512-xA+7uEMc+jsQIoyySJ93Ad08Kuqnik7u6jLS5hR91Z3smAoCfL3M8/MqMlobAa9gzBfO9pA88A/AntfepkkMJQ==", + "requires": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^7.0.0", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "dependencies": { + "@types/node": { + "version": "8.10.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.42.tgz", + "integrity": "sha512-8LCqostMfYwQs9by1k21/P4KZp9uFQk3Q528y3qtPKQnCJmKz0Em3YzgeNjTNV1FVrG/7n/6j12d4UKg9zSgDw==" + } + } + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/tools/performance-server/package.json b/tools/performance-server/package.json new file mode 100644 index 00000000000..ded496a07ed --- /dev/null +++ b/tools/performance-server/package.json @@ -0,0 +1,16 @@ +{ + "name": "performance-server", + "version": "1.0.0", + "main": "server/app.js", + "scripts": { + "start": "node server/app.js" + }, + "dependencies": { + "body-parser": "~1.18.3", + "debug": "~4.1.1", + "express": "~4.16.4", + "kotlin": "^1.3.21", + "sync-request": "~6.0.0", + "ejs": "~2.6.1" + } +} diff --git a/tools/performance-server/settings.gradle b/tools/performance-server/settings.gradle new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/performance-server/src/main/kotlin/main.kt b/tools/performance-server/src/main/kotlin/main.kt new file mode 100644 index 00000000000..f6933632698 --- /dev/null +++ b/tools/performance-server/src/main/kotlin/main.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +external fun require(module: String): dynamic + +external val process: dynamic +external val __dirname: dynamic + +fun main(args: Array) { + println("Server Starting!") + + val express = require("express") + val app = express() + val path = require("path") + val bodyParser = require("body-parser") + val http = require("http") + // Get port from environment and store in Express. + val port = normalizePort(process.env.PORT) + app.use(bodyParser.json()) + app.set("port", port) + + // View engine setup. + app.set("views", path.join(__dirname, "../ui")) + app.set("view engine", "ejs") + app.use(express.static("ui")) + + val server = http.createServer(app) + app.listen(port, { + println("App listening on port " + port + "!") + }) + + app.use("/", router()) +} + +fun normalizePort(port: Int) = + if (port >= 0) port else 3000 \ No newline at end of file diff --git a/tools/performance-server/src/main/kotlin/routes/route.kt b/tools/performance-server/src/main/kotlin/routes/route.kt new file mode 100644 index 00000000000..d4e59075732 --- /dev/null +++ b/tools/performance-server/src/main/kotlin/routes/route.kt @@ -0,0 +1,284 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.w3c.xhr.* +import kotlin.js.json +import org.jetbrains.report.json.* +import org.jetbrains.build.Build + +const val teamCityUrl = "https://buildserver.labs.intellij.net/app/rest" +const val downloadBintrayUrl = "https://dl.bintray.com/content/lepilkinaelena/KotlinNativePerformance" +const val uploadBintrayUrl = "https://api.bintray.com/content/lepilkinaelena/KotlinNativePerformance" +const val buildsFileName = "buildsSummary.csv" +const val bintrayPackage = "builds" +const val buildsInfoPartsNumber = 11 + +operator fun Map?.get(key: K) = this?.get(key) + +// Local cache for saving information about builds got from Bintray. +object LocalCache { + private val knownTargets = listOf("Linux", "MacOSX", "Windows10") + private val buildsInfo = mutableMapOf>() + + fun clean(onlyTarget: String? = null) { + onlyTarget?.let { + buildsInfo[onlyTarget]?.clear() + } ?: buildsInfo.clear() + } + + fun fill(onlyTarget: String? = null) { + onlyTarget?.let { + val buildsDescription = getBuildsInfoFromBintray(onlyTarget).lines().drop(1) + buildsInfo[onlyTarget] = mutableMapOf() + buildsDescription.forEach { + if (!it.isEmpty()) { + val buildNumber = it.substringBefore(',') + if (!"\\d+(\\.\\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 + + 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 { + val builds = getBuilds(target, buildId) + + if (builds.isNullOrEmpty()) { + // No suitable builds were found. + // Refill cache. + clean(target) + fill(target) + return getBuilds(target, buildId) ?: listOf() + } + + return builds + } +} + +// Build information provided from request. +data class BuildInfo(val buildNumber: String, val branch: String, val startTime: String, + val finishTime: String) + +data class BuildRegister(val buildId: String, val teamCityUser: String, val teamCityPassword: String, + val bintrayUser: String, val bintrayPassword: String, + val target: String, val buildType: String, val failuresNumber: Int, + val executionTime: String, val compileTime: String, val codeSize: String, + val bundleSize: String?) { + companion object { + fun create(json: String): BuildRegister { + val requestDetails = JSON.parse(json) + // Parse method doesn't create real instance with all methods. So create it by hands. + return BuildRegister(requestDetails.buildId, requestDetails.teamCityUser, requestDetails.teamCityPassword, + requestDetails.bintrayUser, requestDetails.bintrayPassword, requestDetails.target, + requestDetails.buildType, requestDetails.failuresNumber, requestDetails.executionTime, requestDetails.compileTime, + requestDetails.codeSize, requestDetails.bundleSize) + } + } + + private val teamCityBuildUrl: String by lazy { "$teamCityUrl/builds/id:$buildId" } + + val changesListUrl: String by lazy { + "$teamCityUrl/changes/?locator=build:id:$buildId" + } + + private fun sendTeamCityRequest(url: String) = sendGetRequest(url, teamCityUser, teamCityPassword) + + fun getBuildInformation(): BuildInfo { + val buildNumber = sendTeamCityRequest("$teamCityBuildUrl/number") + val branch = sendTeamCityRequest("$teamCityBuildUrl/branchName") + val startTime = sendTeamCityRequest("$teamCityBuildUrl/startDate") + val finishTime = sendTeamCityRequest("$teamCityBuildUrl/finishDate") + return BuildInfo(buildNumber, branch, startTime, finishTime) + } +} + +data class Commit(val revision: String, val developer: String) + +// List of commits. +class CommitsList(data: JsonElement): ConvertedFromJson { + + val commits: List + + 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() + } +} + +fun getBuildsInfoFromBintray(target: String) = + sendGetRequest("$downloadBintrayUrl/$target/$buildsFileName") + +// Parse and postprocess result of response with build description. +fun prepareBuildsResponse(builds: Collection, type: String, buildNumber: String? = null): List { + val buildsObjects = mutableListOf() + builds.forEach { + val tokens = it.split(",").map { it.trim() } + if (tokens.size != buildsInfoPartsNumber) { + error("Build description $it doesn't contain all necessary information. " + + "File with data could be corrupted.") + } + if (tokens[5] == type || type == "day" || 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 +} + +// Routing of requests to current server. +fun router() { + val express = require("express") + val router = express.Router() + + // Register build on Bintray. + router.post("/register", { request, response -> + val register = BuildRegister.create(JSON.stringify(request.body)) + + // Get information from TeamCity. + val buildInfo = register.getBuildInformation() + val changes = sendGetRequest(register.changesListUrl, register.teamCityUser, + register.teamCityPassword, true) + val commitsList = CommitsList(JsonTreeParser.parse(changes)) + val commitsDescription = buildString { + commitsList.commits.forEach { + append("${it.revision} by ${it.developer};") + } + } + + // Get summary file from Bintray. + var buildsDescription = getBuildsInfoFromBintray(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 = "$uploadBintrayUrl/$bintrayPackage/latest/${register.target}/${buildsFileName}?publish=1&override=1" + sendUploadRequest(uploadUrl, buildsDescription, register.bintrayUser, register.bintrayPassword) + + // Send response. + response.sendStatus(200) + }) + + // Get list of builds. + router.get("/builds/:target/:type/:id", { request, response -> + val builds = LocalCache[request.params.target, request.params.id] + response.json(prepareBuildsResponse(builds, request.params.type, request.params.id)) + }) + + router.get("/builds/:target/:type", { request, response -> + val builds = LocalCache[request.params.target] + response.json(prepareBuildsResponse(builds, request.params.type)) + }) + + router.get("/clean", { request, response -> + LocalCache.clean() + response.sendStatus(200) + }) + + router.get("/fill", { request, response -> + LocalCache.fill() + response.sendStatus(200) + }) + + // Main page. + router.get("/", { request, response -> + response.render("index") + }) + + return router +} + +fun getAuth(user: String, password: String): String { + val buffer = js("Buffer").from(user + ":" + password) + val based64String = buffer.toString("base64") + return "Basic " + based64String +} + +fun sendGetRequest(url: String, user: String? = null, password: String? = null, jsonContentType: Boolean = false) : String { + val request = require("sync-request") + val headers = mutableListOf>() + if (user != null && password != null) { + headers.add("Authorization" to getAuth(user, password)) + } + if (jsonContentType) { + headers.add("Accept" to "application/json") + } + val response = request("GET", url, + json( + "headers" to json(*(headers.toTypedArray())) + ) + ) + if (response.statusCode != 200) { + error("Error during getting response from $url\n" + + "${response.getBody()}") + } + + return response.getBody().toString() +} + +fun sendUploadRequest(url: String, fileContent: String, user: String? = null, password: String? = null) { + val request = require("sync-request") + val headers = mutableListOf>("Content-type" to "text/plain") + if (user != null && password != null) { + headers.add("Authorization" to getAuth(user, password)) + } + 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}") + } +} \ No newline at end of file diff --git a/tools/performance-server/ui/build.gradle b/tools/performance-server/ui/build.gradle new file mode 100644 index 00000000000..cc94fb1fafd --- /dev/null +++ b/tools/performance-server/ui/build.gradle @@ -0,0 +1,69 @@ +buildscript { + ext.rootBuildDirectory = file('../../..') + + apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle" + apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle" + + repositories { + maven { + url 'https://cache-redirector.jetbrains.com/jcenter' + } + maven { + url kotlinCompilerRepo + } + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} + +repositories { + maven { + url 'https://cache-redirector.jetbrains.com/jcenter' + } + maven { + url kotlinCompilerRepo + } + maven { + url buildKotlinCompilerRepo + } +} + +apply plugin: 'kotlin2js' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion" +} + +compileKotlin2Js { + kotlinOptions.outputFile = "${projectDir}/js/main.js" + kotlinOptions.main = "call" +} + +sourceSets { + main.kotlin.srcDirs += "src" + main.kotlin.srcDirs += "../../benchmarks/shared/src" + main.kotlin.srcDirs += "../shared/src" + main.output.resourcesDir = "build/js/resources" +} + +task copyResources(type: Copy) { + from sourceSets.main.resources.srcDirs + into file(buildDir.path + "/js") +} + +build.doLast { + configurations.compile.each { File file -> + copy { + includeEmptyDirs = false + + from zipTree(file.absolutePath) + into "${projectDir}/lib/kotlin" + include { fileTreeElement -> + def path = fileTreeElement.path + path.endsWith(".js") && (path.startsWith("META-INF/resources/") || !path.startsWith("META-INF/")) + } + } + } +} \ No newline at end of file diff --git a/tools/performance-server/ui/css/style.css b/tools/performance-server/ui/css/style.css new file mode 100644 index 00000000000..bc153f024cd --- /dev/null +++ b/tools/performance-server/ui/css/style.css @@ -0,0 +1,58 @@ +.chart { + height: 400px; +} +.ct-legend { + position: relative; + z-index: 10; + list-style: none; + text-align: center; +} +.ct-legend li { + position: relative; + padding-left: 23px; + margin-right: 10px; + margin-bottom: 3px; + cursor: pointer; + display: inline-block; +} +.ct-legend li:before { + width: 12px; + height: 12px; + position: absolute; + left: 0; + content: ''; + border: 3px solid transparent; + border-radius: 2px; +} +.ct-legend li.inactive:before { + background: transparent; +} +.ct-legend.ct-legend-inside { + position: absolute; + top: 0; + right: 0; +} +.ct-legend.ct-legend-inside li{ + display: block; + margin: 0; +} +.ct-legend .ct-series-0:before { + background-color: #d70206; + border-color: #d70206; +} +.ct-legend .ct-series-1:before { + background-color: #f05b4f; + border-color: #f05b4f; +} +.ct-legend .ct-series-2:before { + background-color: #f4c63d; + border-color: #f4c63d; +} +.ct-legend .ct-series-3:before { + background-color: #d17905; + border-color: #d17905; +} +.ct-legend .ct-series-4:before { + background-color: #453d3f; + border-color: #453d3f; +} \ No newline at end of file diff --git a/tools/performance-server/ui/index.ejs b/tools/performance-server/ui/index.ejs new file mode 100644 index 00000000000..2fcceefa863 --- /dev/null +++ b/tools/performance-server/ui/index.ejs @@ -0,0 +1,100 @@ + + + + + Benchmarks report + + + + + + + + + + + +
+

+

+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ Highlighted build +
+ +
+
+
+
+
+
+
+

Execution time

+
+
+
+

+

+
+

Compile time

+
+
+
+

+

+
+

Code size

+
+
+
+

+ +

+
+

Bundle size

+
+
+
+

+

+ + + + + + + \ No newline at end of file diff --git a/tools/performance-server/ui/settings.gradle b/tools/performance-server/ui/settings.gradle new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/performance-server/ui/src/main/kotlin/main.kt b/tools/performance-server/ui/src/main/kotlin/main.kt new file mode 100644 index 00000000000..112a1e573d3 --- /dev/null +++ b/tools/performance-server/ui/src/main/kotlin/main.kt @@ -0,0 +1,270 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import kotlin.browser.* +import org.w3c.xhr.* +import org.jetbrains.report.json.* +import org.jetbrains.build.Build +import kotlin.js.* +import org.w3c.dom.* + +// API for interop with JS library Chartist. +external class ChartistPlugins { + fun legend(data: dynamic): dynamic +} + +external object Chartist { + class Svg(form: String, parameters: dynamic, chartArea: String) + val plugins: ChartistPlugins + fun Line(query: String, data: dynamic, options: dynamic): dynamic +} + +fun sendGetRequest(url: String) : String { + val request = XMLHttpRequest() + + request.open("GET", url, false) + request.send() + if (request.status == 200.toShort()) { + return request.responseText + } + error("Request to $url has status ${request.status}") +} + +// Parse description with values for metrics. +fun separateValues(values: String, valuesContainer: MutableMap>, convert: (String) -> T = { it as T }) { + val existingSamples = mutableListOf() + val splittedValues = values.split(";") + val insertedList = valuesContainer.values.firstOrNull()?.let { MutableList(it.size) { null } } ?: mutableListOf() + 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() + 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 getChartData(labels: List, valuesList: Collection>): dynamic { + val chartData: dynamic = object{} + chartData["labels"] = labels.toTypedArray() + chartData["series"] = valuesList.map { it.toTypedArray() }.toTypedArray() + return chartData +} + +fun getChartOptions(samples: Array): dynamic { + val chartOptions: dynamic = object{} + chartOptions["fullWidth"] = true + val paddingObject: dynamic = object{} + paddingObject["right"] = 40 + chartOptions["chartPadding"] = paddingObject + val axisXObject: dynamic = object{} + axisXObject["offset"] = 40 + chartOptions["axisX"] = axisXObject + val legendObject: dynamic = object{} + legendObject["legendNames"] = samples + chartOptions["plugins"] = arrayOf(Chartist.plugins.legend(legendObject)) + return chartOptions +} + +fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynamic, builds: List, + parameters: Map) { + chart.on("draw", { data -> + var element = data.element + if (data.type == "point") { + val pointSize = 12 + val currentBuild = builds.get(data.index) + // Higlight builds with failures. + if (currentBuild.failuresNumber > 0) { + val svgParameters: dynamic = object{} + svgParameters["d"] = arrayOf("M", data.x, data.y - pointSize, + "L", data.x - pointSize, data.y + pointSize/2, + "L", data.x + pointSize, data.y + pointSize/2, "z").joinToString(" ") + svgParameters["style"] = "fill:rgb(255,0,0);stroke-width:0" + val triangle = Chartist.Svg("path", svgParameters, chartContainer) + element = data.element._node.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=bintray:" + + "${currentBuild.buildNumber}:${parameters["target"]}:nativeReport.json" + + "${if (data.index - 1 >= 0) + "&compareTo=bintray:${builds.get(data.index - 1).buildNumber}:${parameters["target"]}:nativeReport.json" + else ""}" + val information = buildString { + append("${currentBuild.buildNumber}
") + if (currentBuild.failuresNumber > 0) { + append("failures: ${currentBuild.failuresNumber}
") + } + append("branch: ${currentBuild.branch}
") + append("date: ${currentBuild.date}
") + append("time: ${currentBuild.formattedStartTime}-${currentBuild.formattedFinishTime}
") + append("Commits:
") + val commitsList = currentBuild.commits.split(";") + commitsList.forEach { + if (!it.isEmpty()) + append("${it.substringBefore("by").substring(0, 7)} by ${it.substringAfter("by ")}
") + } + + } + element._node.setAttribute("title", information) + element._node.setAttribute("data-chart-tooltip", chartContainer) + element._node.addEventListener("click", { + window.location.replace(linkToDetailedInfo) + }) + } + }) + chart.on("created", { + val currentChart = jquerySelector + val parameters: dynamic = object{} + parameters["selector"] = "[data-chart-tooltip=\"$chartContainer\"]" + parameters["container"] = "#$chartContainer" + parameters["html"] = true + currentChart.tooltip(parameters) + }) +} + +fun main(args: Array) { + val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg/builds" + + // Get parameters from request. + val url = window.location.href + val parametersPart = url.substringAfter("?").split('&') + val parameters = mutableMapOf("target" to "Linux", "type" to "dev", "build" to "") + parametersPart.forEach { + val parsedParameter = it.split("=", limit = 2) + if (parsedParameter.size == 2) { + val (key, value) = parsedParameter + if (parameters.containsKey(key)) { + parameters[key] = value + } + } + } + + // Get builds. + val buildsUrl = buildString { + append("$serverUrl") + append("/${parameters["target"]}") + append("/${parameters["type"]}") + append("/${parameters["build"]}") + } + 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) } + + // Fill autocomplete list. + val buildsNumbers = builds.map { json("value" to it.buildNumber, "data" to it.buildNumber) }.toTypedArray() + + // Change inputs values connected with parameters and add events listeners. + document.querySelector("#inputGroupTarget [value=\"${parameters["target"]}\"]")?.setAttribute("selected", "true") + document.querySelector("#inputGroupBuildType [value=\"${parameters["type"]}\"]")?.setAttribute("selected", "true") + (document.getElementById("highligted_build") as HTMLInputElement).value = parameters["build"]!! + + // Add onChange events for fields. + js("$('#inputGroupTarget')").change({ + val newValue = js("$(this).val()") + if (newValue != parameters["target"]) { + val newLink = "http://${window.location.host}/?target=$newValue&type=${parameters["type"]}" + + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}" + window.location.replace(newLink) + } + }) + js("$('#inputGroupBuildType')").change({ + val newValue = js("$(this).val()") + if (newValue != parameters["type"]) { + val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=$newValue" + + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}" + window.location.replace(newLink) + } + }) + + val autocompleteParameters: dynamic = object{} + autocompleteParameters["lookup"] = buildsNumbers + autocompleteParameters["onSelect"] = { suggestion -> + if (suggestion.value != parameters["build"]) { + val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" + + "${if (suggestion.value.isEmpty()) "" else "&build=${suggestion.value}"}" + window.location.replace(newLink) + } + } + js("$( \"#highligted_build\" )").autocomplete(autocompleteParameters) + js("$('#highligted_build')").change({ + 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.replace(newLink) + } + }) + + + // Collect information for charts library. + val labels = mutableListOf() + val executionTime = mutableMapOf>() + val compileTime = mutableMapOf>() + val codeSize = mutableMapOf>() + val bundleSize = mutableListOf() + + builds.forEach { + // Choose labels on x axis. + if (parameters["type"] == "day") { + labels.add(it.date) + } else { + labels.add(it.buildNumber) + } + separateValues(it.executionTime, executionTime) { value -> value.toDouble() } + separateValues(it.compileTime, compileTime) { value -> value.toDouble() } + separateValues(it.codeSize, codeSize) { value -> value.toInt() } + bundleSize.add(it.bundleSize?.toInt()) + } + + // Draw charts. + val execChart = Chartist.Line("#exec_chart", getChartData(labels, executionTime.values), + getChartOptions(executionTime.keys.toTypedArray())) + val compileChart = Chartist.Line("#compile_chart", getChartData(labels, compileTime.values), + getChartOptions(compileTime.keys.toTypedArray())) + val codeSizeChart = Chartist.Line("#codesize_chart", getChartData(labels, codeSize.values), + getChartOptions(codeSize.keys.toTypedArray())) + val bundleSizeChart = Chartist.Line("#bundlesize_chart", getChartData(labels, listOf(bundleSize)), + getChartOptions(arrayOf("Bundle size"))) + + // Tooltips and higlights. + customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters) + customizeChart(compileChart, "compile_chart", js("$(\"#compile_chart\")"), builds, parameters) + customizeChart(codeSizeChart, "codesize_chart", js("$(\"#codesize_chart\")"), builds, parameters) + customizeChart(bundleSizeChart, "bundlesize_chart", js("$(\"#bundlesize_chart\")"), builds, parameters) +} \ No newline at end of file