Server and client to show summary performance information (#2765)

This commit is contained in:
LepilkinaElena
2019-03-18 16:02:41 +03:00
committed by GitHub
parent 06f8864b69
commit a16bddb421
25 changed files with 1705 additions and 28 deletions
+3
View File
@@ -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
@@ -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<String>, 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
@@ -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"
@@ -29,6 +29,8 @@ open class RegressionsSummaryReporter : DefaultTask() {
@Input
lateinit var targetsResultFiles: Map<String, String>
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")
}
@@ -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))
}
}
@@ -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<BenchmarkResult> {
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}
@@ -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 {
@@ -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<String>("mainReport")!!, argParser.get<String>("user")))
val results = mutableListOf<String>()
results.apply {
add(benchsReport.failedBenchmarks.size.toString())
argParser.getAll<String>("exec-samples")?. let {
val filter = if (it.first() == "all") null else it
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.EXECUTION_TIME,
argParser.get<String>("exec")!! == "geomean", filter).joinToString(";"))
}
argParser.getAll<String>("compile-samples")?. let {
val filter = if (it.first() == "all") null else it
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.COMPILE_TIME,
argParser.get<String>("compile")!! == "geomean", filter).joinToString(";"))
}
argParser.getAll<String>("codesize-samples")?. let {
val filter = if (it.first() == "all") null else it
add(benchsReport.getResultsByMetric(BenchmarkResult.Metric.CODE_SIZE,
argParser.get<String>("codesize")!! == "geomean", filter).joinToString(";"))
}
}
println(results.joinToString())
}
fun main(args: Array<String>) {
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<String>) {
)
// 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<String>("mainReport")!!, argParser.get<String>("user"))
val mainReportElement = JsonTreeParser.parse(mainBenchsResults)
val mainBenchsReport = BenchmarksReport.create(mainReportElement)
val mainBenchsReport = getBenchmarkReport(argParser.get<String>("mainReport")!!, argParser.get<String>("user"))
var compareToBenchsReport = argParser.get<String>("compareToReport")?.let {
val compareToResults = getFileContent(it, argParser.get<String>("user"))
val compareToReportElement = JsonTreeParser.parse(compareToResults)
BenchmarksReport.create(compareToReportElement)
getBenchmarkReport(it, argParser.get<String>("user"))
}
val renders = argParser.getAll<String>("renders")
@@ -82,7 +82,7 @@ data class MeanVarianceBenchmark(val meanBenchmark: BenchmarkResult, val varianc
}
fun geometricMean(values: List<Double>, totalNumber: Int = values.size) =
fun geometricMean(values: Collection<Double>, 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<Double>): MeanVariance {
@@ -97,6 +97,7 @@ fun computeMeanVariance(samples: List<Double>): MeanVariance {
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
@@ -111,6 +112,7 @@ fun collectMeanResults(benchmarks: Map<String, List<BenchmarkResult>>): 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<String, List<BenchmarkResult>>): 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)
@@ -133,6 +133,28 @@ class SummaryBenchmarksReport (val currentReport: BenchmarksReport,
}
}
fun getResultsByMetric(metric: BenchmarkResult.Metric, getGeoMean: Boolean = true, filter: List<String>? = null): List<Double> {
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<String, ScoreChange>): Double =
// Maps of regressions and improvements are sorted.
if (bucket.isEmpty()) 0.0 else bucket.values.map { it.first.mean }.first()
@@ -25,12 +25,12 @@ class AnalyzerTests {
private val eps = 0.000001
private fun createMeanVarianceBenchmarks(): Pair<MeanVarianceBenchmark, MeanVarianceBenchmark> {
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)
+13 -1
View File
@@ -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<OptionDescriptor>, argsList: List<ArgDescriptor> = listOf<ArgDescriptor>(),
useDefaultHelpShortName: Boolean = true) {
val actions: Map<String, Action> = 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<OptionDescriptor>, argsList: List<ArgDescripto
valuesOrigin = descriptorsKeys.map { it to ValueOrigin.UNSET }.toMap().toMutableMap()
while (index < args.size) {
val arg = args[index]
// Check for actions.
actions.forEach { (name, action) ->
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)
+19
View File
@@ -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" ]
+51
View File
@@ -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
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.native.home=../../dist
+644
View File
@@ -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="
}
}
}
+16
View File
@@ -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"
}
}
@@ -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<String>) {
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
@@ -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 <K, V> Map<K, V>?.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<String, MutableMap<String, String>>()
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<String, String>()
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<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
}
}
// 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<BuildRegister>(json)
// Parse method doesn't create real instance with all methods. So create it by hands.
return BuildRegister(requestDetails.buildId, requestDetails.teamCityUser, requestDetails.teamCityPassword,
requestDetails.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<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 getBuildsInfoFromBintray(target: String) =
sendGetRequest("$downloadBintrayUrl/$target/$buildsFileName")
// Parse and postprocess result of response with build description.
fun prepareBuildsResponse(builds: Collection<String>, type: String, buildNumber: String? = null): List<Build> {
val buildsObjects = mutableListOf<Build>()
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<Pair<String, String>>()
if (user != null && password != null) {
headers.add("Authorization" to getAuth(user, password))
}
if (jsonContentType) {
headers.add("Accept" to "application/json")
}
val response = request("GET", url,
json(
"headers" to json(*(headers.toTypedArray()))
)
)
if (response.statusCode != 200) {
error("Error during getting response from $url\n" +
"${response.getBody()}")
}
return response.getBody().toString()
}
fun sendUploadRequest(url: String, fileContent: String, user: String? = null, password: String? = null) {
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))
}
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}")
}
}
+69
View File
@@ -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/"))
}
}
}
}
+58
View File
@@ -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;
}
+100
View File
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<title>
Benchmarks report
</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/chartist/0.11.0/chartist.min.css">
<link rel="stylesheet" href="css/style.css">
<script src="https://code.jquery.com/jquery-3.3.1.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js">
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js">
</script>
</head>
<body>
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bd-navbar" style="background-color:#161616;">
<img src="https://dashboard.snapcraft.io/site_media/appmedia/2018/04/256px-kotlin-logo-svg.png" style="width:60px;height:60px;">
<span class="navbar-brand mb-0 h1">
Benchmarks report
</span>
</header>
<div class="container-fluid">
<p>
<div class="row">
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupTarget">Target</label>
</div>
<select class="custom-select" id="inputGroupTarget">
<option value="Linux">Linux</option>
<option value="MacOSX">Mac OS X</option>
<option value="Windows10">Windows 10</option>
</select>
</div>
</div>
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupBuildType">Build type</label>
</div>
<select class="custom-select" id="inputGroupBuildType">
<option value="dev">Dev builds</option>
<option value="day">Day</option>
<option value="release">Releases</option>
</select>
</div>
</div>
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">Highlighted build</span>
</div>
<input id="highligted_build" type="text" class="form-control" placeholder="" aria-label="build" aria-describedby="basic-addon1">
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col">
<h4>Execution time</h4>
<div id="exec_chart" class="chart"></div>
</div>
</div>
<p style="margin-top: 40px">
<div class="row">
<div class="col">
<h4>Compile time</h4>
<div id="compile_chart" class="chart"></div>
</div>
</div>
<p style="margin-top: 40px">
<div class="row">
<div class="col">
<h4>Code size</h4>
<div id="codesize_chart" class="chart"></div>
</div>
</div>
<p style="margin-top: 40px">
<div class="row">
<div class="col">
<h4>Bundle size</h4>
<div id="bundlesize_chart" class="chart"></div>
</div>
</div>
<p style="margin-top: 40px">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartist/0.11.0/chartist.min.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="lib/kotlin/kotlin.js"></script>
<script src="js/main.js"></script>
</body>
</html>
@@ -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 <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 getChartData(labels: List<String>, valuesList: Collection<List<*>>): dynamic {
val chartData: dynamic = object{}
chartData["labels"] = labels.toTypedArray()
chartData["series"] = valuesList.map { it.toTypedArray() }.toTypedArray()
return chartData
}
fun getChartOptions(samples: Array<String>): 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<Build>,
parameters: Map<String, String>) {
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("<a href=\"$linkToDetailedInfo\">${currentBuild.buildNumber}</a><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 = currentBuild.commits.split(";")
commitsList.forEach {
if (!it.isEmpty())
append("${it.substringBefore("by").substring(0, 7)} by ${it.substringAfter("by ")}<br>")
}
}
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<String>) {
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<String>()
val executionTime = mutableMapOf<String, MutableList<Double?>>()
val compileTime = mutableMapOf<String, MutableList<Double?>>()
val codeSize = mutableMapOf<String, MutableList<Int?>>()
val bundleSize = mutableListOf<Int?>()
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)
}