From 9b727bdd9d85ffcee1c461a00acb6fd61730bf28 Mon Sep 17 00:00:00 2001 From: LepilkinaElena Date: Mon, 7 Sep 2020 14:16:34 +0300 Subject: [PATCH] Deployment fixes for performance monitoring system (#4356) --- .../main/kotlin/org/jetbrains/kotlin/Utils.kt | 4 +- performance/build.gradle | 31 ++- tools/benchmarksAnalyzer/build.gradle | 11 + tools/performance-server/package-lock.json | 6 +- tools/performance-server/package.json | 2 +- .../jetbrains/elastic/ElasticSearchIndex.kt | 26 +-- .../jetbrains/network/UrlNetworkConnector.kt | 1 + .../database/BenchmarksIndexesDispatcher.kt | 18 +- .../main/kotlin/database/DatabaseRequests.kt | 28 ++- .../network/CachableResponseDispatcher.kt | 6 +- .../kotlin/network/aws/AWSNetworkUtils.kt | 14 +- .../src/main/kotlin/routes/route.kt | 49 +++- .../ui/src/main/kotlin/main.kt | 220 +++++++++--------- 13 files changed, 244 insertions(+), 172 deletions(-) diff --git a/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt b/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt index 7a7ee89f3ba..ee556923507 100644 --- a/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt +++ b/build-tools/src/main/kotlin/org/jetbrains/kotlin/Utils.kt @@ -159,7 +159,9 @@ fun Array.runCommand(workingDir: File = File("."), waitFor(timeoutAmount, timeoutUnit) }.inputStream.bufferedReader().readText() } catch (e: Exception) { - error("Couldn't run command $this") + println("Couldn't run command ${this.joinToString(" ")}") + println(e.stackTrace.joinToString("\n")) + error(e.message!!) } } diff --git a/performance/build.gradle b/performance/build.gradle index de781fcb4cb..465bc2ddee7 100644 --- a/performance/build.gradle +++ b/performance/build.gradle @@ -81,16 +81,27 @@ task slackSummary(type: RegressionsSummaryReporter) { 'Windows': "${rootBuildDirectory}/targetsResults/Windows 10.txt".toString()] } -private def uploadBenchmarkResultToArtifactory(String fileName) { +private String getUploadedFile(String fileName) { def teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") if (teamcityConfig) { def buildProperties = new Properties() buildProperties.load(new FileInputStream(teamcityConfig)) - def password = buildProperties.getProperty("artifactory.apikey") def buildNumber = buildProperties.getProperty("build.number") def target = System.getProperty("os.name").replaceAll("\\s", "") + return "${target}/${buildNumber}/${fileName}" + } + return null +} + +private def uploadBenchmarkResultToArtifactory(String fileName) { + def teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") + if (teamcityConfig) { + def uploadedFile = getUploadedFile(fileName) + def buildProperties = new Properties() + buildProperties.load(new FileInputStream(teamcityConfig)) + def password = buildProperties.getProperty("artifactory.apikey") MPPTools.uploadFileToArtifactory("${artifactoryUrl}", "${artifactoryRepo}", - "${target}/${buildNumber}/${fileName}", "${buildDir.absolutePath}/${fileName}", password) + uploadedFile, "${buildDir.absolutePath}/${fileName}", password) } } @@ -161,6 +172,11 @@ task registerExternalBenchmarks { task registerBuild(type: BuildRegister) { onlyBranch = project.findProperty('kotlin.register.branch') + def uploadedFile = getUploadedFile(nativeJson) + if (uploadedFile != null) { + println("Use file from Artifacrory $uploadedFile") + fileWithResult = uploadedFile + } // Get bundle size. bundleSize = null if (project.findProperty('kotlin.bundleBuild') != null) { @@ -172,7 +188,14 @@ task registerBuild(type: BuildRegister) { task registerExternalBuild(type: BuildRegister) { onlyBranch = project.findProperty('kotlin.register.branch') - fileWithResult = externalBenchmarksReport + def uploadedFile = getUploadedFile(externalBenchmarksReport) + if (uploadedFile != null) { + println("Use file from Artifacrory $uploadedFile") + fileWithResult = uploadedFile + } else { + fileWithResult = externalBenchmarksReport + } + } registerExternalBenchmarks.finalizedBy registerExternalBuild diff --git a/tools/benchmarksAnalyzer/build.gradle b/tools/benchmarksAnalyzer/build.gradle index 1e46e464358..5d04a0ae23e 100644 --- a/tools/benchmarksAnalyzer/build.gradle +++ b/tools/benchmarksAnalyzer/build.gradle @@ -154,6 +154,17 @@ kotlin { } } } + + js { + browser { + distribution { + directory = new File("$projectDir/web/") + } + dceTask { + keep 'benchmarksAnalyzer.main_kand9s$' + } + } + } } def getMingwPath() { diff --git a/tools/performance-server/package-lock.json b/tools/performance-server/package-lock.json index a7adb37e4a5..4df8565458c 100644 --- a/tools/performance-server/package-lock.json +++ b/tools/performance-server/package-lock.json @@ -292,9 +292,9 @@ "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, "kotlin": { - "version": "1.4.0-M3", - "resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.4.0-M3.tgz", - "integrity": "sha512-y6dl1t36tI1hAJlJTxlOB/fj/0iOga+XC8+eg9hUSTu5BDJ9MIUR7XnA8Ld+tQA9IakAPaUmgZZfX2lsHaAdIw==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.4.0.tgz", + "integrity": "sha512-q+Ts9Xr72eT3QkmLjHDObPAHbsKbZ/Vn0ozqrNNr7UbtpLxa26+Hn8ILORPLz1UIUTespZyfXztXJ7AO5Xl/Gg==" }, "media-typer": { "version": "0.3.0", diff --git a/tools/performance-server/package.json b/tools/performance-server/package.json index dbbec39fc8d..9efbf916741 100644 --- a/tools/performance-server/package.json +++ b/tools/performance-server/package.json @@ -11,7 +11,7 @@ "debug": "~4.1.1", "ejs": "~2.6.1", "express": "~4.16.4", - "kotlin": "~1.4.0-M3", + "kotlin": "~1.4.0", "node-fetch": "~2.6.0" } } diff --git a/tools/performance-server/shared/src/main/kotlin/org/jetbrains/elastic/ElasticSearchIndex.kt b/tools/performance-server/shared/src/main/kotlin/org/jetbrains/elastic/ElasticSearchIndex.kt index eeb9f9f218a..5d1c09fb9e6 100644 --- a/tools/performance-server/shared/src/main/kotlin/org/jetbrains/elastic/ElasticSearchIndex.kt +++ b/tools/performance-server/shared/src/main/kotlin/org/jetbrains/elastic/ElasticSearchIndex.kt @@ -125,14 +125,11 @@ enum class ElasticSearchType { } abstract class ElasticSearchIndex(val indexName: String, val connector: ElasticSearchConnector) { - var nextId = 0L - // Insert data. fun insert(data: JsonSerializable): Promise { val description = data.toJson() - val writePath = "$indexName/_doc/$nextId?pretty" - nextId++ - return connector.request(RequestMethod.PUT, writePath, body = description) + val writePath = "$indexName/_doc/" + return connector.request(RequestMethod.POST, writePath, body = description) } // Delete data. @@ -148,25 +145,6 @@ abstract class ElasticSearchIndex(val indexName: String, val connector: ElasticS return connector.request(RequestMethod.POST, path, body = requestJson) } - init { - // Get latest id for index. - val queryBody = """{ - "_source": ["_id"], - "size": 1, - "query": { - "match_all": {} - } - }""" - search(queryBody, listOf("hits.total.value")).then { responseString -> - val response = JsonTreeParser.parse(responseString).jsonObject - val value = response.getObjectOrNull("hits")?.getObjectOrNull("total")?.getPrimitiveOrNull("value")?.content - ?: error("Error response from ElasticSearch:\n$responseString") - nextId = value.toLong() - }.catch { errorMessage -> - error(errorMessage.message ?: "Failed getting next id for index $indexName") - } - } - abstract val mapping: Map val mappingDescription: String diff --git a/tools/performance-server/shared/src/main/kotlin/org/jetbrains/network/UrlNetworkConnector.kt b/tools/performance-server/shared/src/main/kotlin/org/jetbrains/network/UrlNetworkConnector.kt index f594570f5ad..e2f047d0b62 100644 --- a/tools/performance-server/shared/src/main/kotlin/org/jetbrains/network/UrlNetworkConnector.kt +++ b/tools/performance-server/shared/src/main/kotlin/org/jetbrains/network/UrlNetworkConnector.kt @@ -35,6 +35,7 @@ class UrlNetworkConnector(private val host: String, private val port: Int? = nul ) ).then { response -> if (!response.ok) { + println(JSON.stringify(response)) errorHandler(fullUrl, response) } else { response.text() diff --git a/tools/performance-server/src/main/kotlin/database/BenchmarksIndexesDispatcher.kt b/tools/performance-server/src/main/kotlin/database/BenchmarksIndexesDispatcher.kt index e5db2561e50..2ae964db6ba 100644 --- a/tools/performance-server/src/main/kotlin/database/BenchmarksIndexesDispatcher.kt +++ b/tools/performance-server/src/main/kotlin/database/BenchmarksIndexesDispatcher.kt @@ -97,13 +97,13 @@ class BenchmarksIndexesDispatcher(connector: ElasticSearchConnector, val feature } // Get benchmarks values of needed metric for choosen build number. - fun getSamples(metricName: String, featureValue: String = "", samples: List, + fun getSamples(metricName: String, featureValue: String = "", samples: List, buildsCountToShow: Int, buildNumbers: Iterable? = null, normalize: Boolean = false): Promise>>> { val queryDescription = """ { "_source": ["buildNumber"], - "size": 1000, + "size": ${samples.size * buildsCountToShow}, "query": { "bool": { "must": [ @@ -255,7 +255,7 @@ class BenchmarksIndexesDispatcher(connector: ElasticSearchConnector, val feature // Get geometric mean for benchmarks values of needed metric. fun getGeometricMean(metricName: String, featureValue: String = "", buildNumbers: Iterable? = null, normalize: Boolean = false, - excludeNames: List = emptyList()): Promise>>> { + excludeNames: List = emptyList()): Promise>>> { // Filter only with metric or also with names. val filterBenchmarks = if (excludeNames.isEmpty()) """ @@ -345,9 +345,9 @@ class BenchmarksIndexesDispatcher(connector: ElasticSearchConnector, val feature .getObject("metric_samples") .getObject("buckets") .getObject("samples") - .getObject("geom_mean") - .getPrimitive("value") - .double + .getObjectOrNull("geom_mean") + ?.getPrimitive("value") + ?.double ) } } ?: listOf("golden" to listOf(aggregations @@ -355,9 +355,9 @@ class BenchmarksIndexesDispatcher(connector: ElasticSearchConnector, val feature .getObject("metric_samples") .getObject("buckets") .getObject("samples") - .getObject("geom_mean") - .getPrimitive("value") - .double + .getObjectOrNull("geom_mean") + ?.getPrimitive("value") + ?.double ) ) } diff --git a/tools/performance-server/src/main/kotlin/database/DatabaseRequests.kt b/tools/performance-server/src/main/kotlin/database/DatabaseRequests.kt index 04f5c97919c..8d247aa221b 100644 --- a/tools/performance-server/src/main/kotlin/database/DatabaseRequests.kt +++ b/tools/performance-server/src/main/kotlin/database/DatabaseRequests.kt @@ -35,11 +35,12 @@ internal fun deleteBuildInfo(agentInfo: String, buildInfoIndex: ElasticSearchInd // Get infromation about builds details from database. internal fun getBuildsDescription(type: String?, branch: String?, agentInfo: String, buildInfoIndex: ElasticSearchIndex, + buildsCountToShow: Int, beforeDate: String?, afterDate: String?, onlyNumbers: Boolean = false): Promise { val queryDescription = """ - { "size": 10000, + { "size": $buildsCountToShow, ${if (onlyNumbers) """"_source": ["buildNumber"],""" else ""} - "sort": {"_id": "desc" }, + "sort": {"startTime": "desc" }, "query": { "bool": { "must": [ @@ -51,6 +52,16 @@ internal fun getBuildsDescription(type: String?, branch: String?, agentInfo: Str } """ } ?: ""} + ${beforeDate?.let { + """, + { "range": { "startTime": { "lt": "$it" } } } + """ + } ?: ""} + ${afterDate?.let { + """, + { "range": { "startTime": { "gt": "$it" } } } + """ + } ?: ""} ${branch?.let { """, {"match": { "branch": "$it" }} @@ -61,6 +72,7 @@ internal fun getBuildsDescription(type: String?, branch: String?, agentInfo: Str } } """.trimIndent() + return buildInfoIndex.search(queryDescription, listOf("hits.hits._source")).then { responseString -> val dbResponse = JsonTreeParser.parse(responseString).jsonObject dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits") ?: error("Wrong response:\n$responseString") @@ -93,14 +105,18 @@ suspend fun buildExists(buildInfo: BuildInfo, buildInfoIndex: ElasticSearchIndex } // Get builds numbers corresponding to machine and branch. -fun getBuildsNumbers(type: String?, branch: String?, agentInfo: String, buildInfoIndex: ElasticSearchIndex) = - getBuildsDescription(type, branch, agentInfo, buildInfoIndex, true).then { responseArray -> +fun getBuildsNumbers(type: String?, branch: String?, agentInfo: String, buildsCountToShow: Int, + buildInfoIndex: ElasticSearchIndex, beforeDate: String? = null, afterDate: String? = null) = + getBuildsDescription(type, branch, agentInfo, buildInfoIndex, buildsCountToShow, beforeDate, afterDate, true) + .then { responseArray -> responseArray.map { (it as JsonObject).getObject("_source").getPrimitive("buildNumber").content } } // Get full builds information corresponding to machine and branch. -fun getBuildsInfo(type: String?, branch: String?, agentInfo: String, buildInfoIndex: ElasticSearchIndex) = - getBuildsDescription(type, branch, agentInfo, buildInfoIndex).then { responseArray -> +fun getBuildsInfo(type: String?, branch: String?, agentInfo: String, buildsCountToShow: Int, + buildInfoIndex: ElasticSearchIndex, beforeDate: String? = null, + afterDate: String? = null) = + getBuildsDescription(type, branch, agentInfo, buildInfoIndex, buildsCountToShow, beforeDate, afterDate).then { responseArray -> responseArray.map { BuildInfo.create((it as JsonObject).getObject("_source")) } } diff --git a/tools/performance-server/src/main/kotlin/network/CachableResponseDispatcher.kt b/tools/performance-server/src/main/kotlin/network/CachableResponseDispatcher.kt index ee86444bfe2..f685d9f08cc 100644 --- a/tools/performance-server/src/main/kotlin/network/CachableResponseDispatcher.kt +++ b/tools/performance-server/src/main/kotlin/network/CachableResponseDispatcher.kt @@ -17,6 +17,8 @@ object CachableResponseDispatcher { // Storage of cached responses. private val cachedResponses = mutableMapOf() + private val cacheMaxSize = 200 + // Get response. If response isn't cached, use provided action to get response. fun getResponse(request: dynamic, response: dynamic, action: (success: (result: Any) -> Unit, reject: () -> Unit) -> Unit) { @@ -31,7 +33,9 @@ object CachableResponseDispatcher { response.json(it.cachedResult) } ?: run { action({ result: Any -> - cachedResponses[request.url] = CachedResponse(result, TimeSource.Monotonic.markNow()) + if (cachedResponses.size >= cacheMaxSize) { + cachedResponses[request.url] = CachedResponse(result, TimeSource.Monotonic.markNow()) + } response.json(result) }, { response.sendStatus(400) }) } diff --git a/tools/performance-server/src/main/kotlin/network/aws/AWSNetworkUtils.kt b/tools/performance-server/src/main/kotlin/network/aws/AWSNetworkUtils.kt index 2e4ad8398a6..d07750d49ae 100644 --- a/tools/performance-server/src/main/kotlin/network/aws/AWSNetworkUtils.kt +++ b/tools/performance-server/src/main/kotlin/network/aws/AWSNetworkUtils.kt @@ -39,13 +39,19 @@ external object AWSInstance { val handleRequest: dynamic } - class SharedIniFileCredentials(options: Map) { + interface Credentials + + class SharedIniFileCredentials(options: Map): Credentials { + val accessKeyId: String + } + + class EnvironmentCredentials(envPrefix: String): Credentials { val accessKeyId: String } class Signers() { class V4(request: HttpRequest, subsystem: String) { - fun addAuthorization(credentials: SharedIniFileCredentials, date: Date) + fun addAuthorization(credentials: Credentials, date: Date) } } } @@ -58,6 +64,7 @@ class AWSNetworkConnector : NetworkConnector() { override fun sendBaseRequest(method: RequestMethod, path: String, user: String?, password: String?, acceptJsonContentType: Boolean, body: String?, errorHandler: (url: String, response: dynamic) -> Nothing?): Promise { + val useEnvironmentCredentials = true // For easy test on localhost change to false. val AWSEndpoint = AWSInstance.Endpoint(AWSDomain) var request = AWSInstance.HttpRequest(AWSEndpoint, AWSRegion) request.method = method.toString() @@ -71,7 +78,8 @@ class AWSNetworkConnector : NetworkConnector() { request.headers["Content-Length"] = js("Buffer").byteLength(request.body) } - val credentials = AWSInstance.SharedIniFileCredentials(mapOf()) + val credentials = if (useEnvironmentCredentials) AWSInstance.EnvironmentCredentials("AWS") + else AWSInstance.SharedIniFileCredentials(mapOf()) val signer = AWSInstance.Signers.V4(request, "es") signer.addAuthorization(credentials, Date()) diff --git a/tools/performance-server/src/main/kotlin/routes/route.kt b/tools/performance-server/src/main/kotlin/routes/route.kt index 5134e238d5e..5a2317c4a36 100644 --- a/tools/performance-server/src/main/kotlin/routes/route.kt +++ b/tools/performance-server/src/main/kotlin/routes/route.kt @@ -25,6 +25,8 @@ operator fun Map?.get(key: K) = this?.get(key) fun getArtifactoryHeader(artifactoryApiKey: String) = Pair("X-JFrog-Art-Api", artifactoryApiKey) +external fun decodeURIComponent(url: String): String + // Convert saved old report to expected new format. internal fun convertToNewFormat(data: JsonObject): List { val env = Environment.create(data.getRequiredField("env")) @@ -183,7 +185,7 @@ fun router() { val goldenIndex = GoldenResultsIndex(connector) val buildInfoIndex = BuildInfoIndex(connector) - router.get("/createMapping") { request, response -> + router.get("/createMapping") { _, response -> buildInfoIndex.createMapping().then { _ -> response.sendStatus(200) }.catch { _ -> @@ -220,7 +222,10 @@ fun router() { register.sendTeamCityRequest(register.changesListUrl, true).then { changes -> val commitsList = CommitsList(JsonTreeParser.parse(changes)) // Get artifact. - register.sendTeamCityRequest(register.teamCityArtifactsUrl).then { resultsContent -> + val content = if(register.fileWithResult.contains("/")) + UrlNetworkConnector(artifactoryUrl).sendRequest(RequestMethod.GET, register.fileWithResult) + else register.sendTeamCityRequest(register.teamCityArtifactsUrl) + content.then { resultsContent -> launch { val reportData = JsonTreeParser.parse(resultsContent) val reports = if (reportData is JsonArray) { @@ -292,6 +297,9 @@ fun router() { var branch: String? = null var type: String? = null + var buildsCountToShow = 200 + var beforeDate: String? = null + var afterDate: String? = null if (request.query != undefined) { if (request.query.branch != undefined) { branch = request.query.branch @@ -299,9 +307,19 @@ fun router() { if (request.query.type != undefined) { type = request.query.type } + if (request.query.count != undefined) { + buildsCountToShow = request.query.count.toString().toInt() + } + if (request.query.before != undefined) { + beforeDate = decodeURIComponent(request.query.before) + } + if (request.query.after != undefined) { + afterDate = decodeURIComponent(request.query.after) + } } - getBuildsInfo(type, branch, target, buildInfoIndex).then { buildsInfo -> + getBuildsInfo(type, branch, target, buildsCountToShow, buildInfoIndex, beforeDate, afterDate) + .then { buildsInfo -> val buildNumbers = buildsInfo.map { it.buildNumber } // Get number of failed benchmarks for each build. benchmarksDispatcher.getFailuresNumber(target, buildNumbers).then { failures -> @@ -331,6 +349,9 @@ fun router() { var branch: String? = null var type: String? = null var excludeNames: List = emptyList() + var buildsCountToShow = 200 + var beforeDate: String? = null + var afterDate: String? = null // Parse parameters from request if it exists. if (request.query != undefined) { @@ -352,9 +373,18 @@ fun router() { if (request.query.exclude != undefined) { excludeNames = request.query.exclude.toString().split(",").map { it.trim() } } + if (request.query.count != undefined) { + buildsCountToShow = request.query.count.toString().toInt() + } + if (request.query.before != undefined) { + beforeDate = decodeURIComponent(request.query.before) + } + if (request.query.after != undefined) { + afterDate = decodeURIComponent(request.query.after) + } } - getBuildsNumbers(type, branch, target, buildInfoIndex).then { buildNumbers -> + getBuildsNumbers(type, branch, target, buildsCountToShow, buildInfoIndex, beforeDate, afterDate).then { buildNumbers -> if (aggregation == "geomean") { // Get geometric mean for samples. benchmarksDispatcher.getGeometricMean(metric, target, buildNumbers, normalize, @@ -366,7 +396,8 @@ fun router() { reject() } } else { - benchmarksDispatcher.getSamples(metric, target, samples, buildNumbers, normalize).then { geoMeansValues -> + benchmarksDispatcher.getSamples(metric, target, samples, buildsCountToShow, buildNumbers, normalize) + .then { geoMeansValues -> success(orderedValues(geoMeansValues, { it -> it.first }, branch == "master")) }.catch { println("Error during getting samples") @@ -412,6 +443,7 @@ fun router() { if (request.query != undefined) { if (request.query.buildNumber != undefined) { buildNumber = request.query.buildNumber + buildNumber = request.query.buildNumber } } getBuildsInfoFromArtifactory(targetPathName).then { buildInfo -> @@ -448,10 +480,13 @@ fun router() { val buildInfoRecord = BuildInfo(currentBuildNumber, infoParts[1], infoParts[2], CommitsList.parse(infoParts[4]), infoParts[3], target) - val externalJsonReport = artifactoryUrlConnector.sendOptionalRequest(RequestMethod.GET, accessExternalFileUrl).await() + val externalJsonReport = artifactoryUrlConnector.sendOptionalRequest(RequestMethod.GET, accessExternalFileUrl) + .await() buildInfoIndex.insert(buildInfoRecord).then { _ -> + println("[BUILD INFO] Success insert build number ${buildInfoRecord.buildNumber}") externalJsonReport?.let { - var externalReports = convert(externalJsonReport, currentBuildNumber, target) + var externalReports = convert(externalJsonReport.replace("circlet_iosX64", "SpaceFramework_iosX64"), + currentBuildNumber, target) externalReports.forEach { externalReport -> val extrenalAdditionalReport = SummaryBenchmarksReport(externalReport) .getBenchmarksReport().normalizeBenchmarksSet(goldenResults) diff --git a/tools/performance-server/ui/src/main/kotlin/main.kt b/tools/performance-server/ui/src/main/kotlin/main.kt index 5b917862322..ce26fc3c05b 100644 --- a/tools/performance-server/ui/src/main/kotlin/main.kt +++ b/tools/performance-server/ui/src/main/kotlin/main.kt @@ -35,36 +35,12 @@ fun sendGetRequest(url: String) = window.fetch(url, RequestInit("GET")).then { r response.text() }.then { text -> text } -// Get groups of builds for different zoom values. -fun getBuildsGroup(builds: List) = buildsNumberToShow?.let { - val buildsGroups = builds.chunked(buildsNumberToShow!!) - val expectedGroup = buildsGroups.size - 1 + stageToShow - val index = when { - expectedGroup < 0 -> 0 - expectedGroup >= buildsGroups.size -> buildsGroups.size - 1 - else -> expectedGroup - } - buildsGroups[index] -} ?: builds - // Get data for chart in needed format. -fun getChartData(labels: List, valuesList: Collection>, stageToShow: Int = 0, - buildsNumber: Int? = null, classNames: Array? = null): dynamic { +fun getChartData(labels: List, valuesList: Collection>, + classNames: Array? = null): dynamic { val chartData: dynamic = object {} - // Show only some part of data. - val (labelsData, valuesData) = buildsNumber?.let { - val labelsGroups = labels.chunked(buildsNumber) - val valuesListGroups = valuesList.map { it.chunked(buildsNumber) } - val expectedGroup = labelsGroups.size - 1 + stageToShow - val index = when { - expectedGroup < 0 -> 0 - expectedGroup >= labelsGroups.size -> labelsGroups.size - 1 - else -> expectedGroup - } - Pair(labelsGroups[index], valuesListGroups.map { it[index] }) - } ?: Pair(labels, valuesList) - chartData["labels"] = labelsData.toTypedArray() - chartData["series"] = valuesData.mapIndexed { index, it -> + chartData["labels"] = labels.toTypedArray() + chartData["series"] = valuesList.mapIndexed { index, it -> val series: dynamic = object {} series["data"] = it.toTypedArray() classNames?.let { series["className"] = classNames[index] } @@ -83,7 +59,7 @@ fun getChartOptions(samples: Array, yTitle: String, classNames: Array - val labelsCount = 30 + val labelsCount = 20 val skipNumber = ceil((labels.length as Int).toDouble() / labelsCount).toInt() if (skipNumber > 1) { if (index % skipNumber == 0) value else null @@ -126,9 +102,8 @@ fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynam chart.on("draw", { data -> var element = data.element if (data.type == "point") { - val buildsGroup = getBuildsGroup(builds) val pointSize = 12 - val currentBuild = buildsGroup.get(data.index) + val currentBuild = builds.get(data.index) currentBuild?.let { currentBuild -> // Higlight builds with failures. if (currentBuild.failuresNumber > 0) { @@ -154,7 +129,7 @@ fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynam var shift = 1 var previousBuild: Build? = null while (previousBuild == null && data.index - shift >= 0) { - previousBuild = buildsGroup.get(data.index - shift) + previousBuild = builds.get(data.index - shift) shift++ } val linkToDetailedInfo = "https://kotlin-native-performance.labs.jb.gg/?report=" + @@ -204,21 +179,19 @@ fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynam }) } -var stageToShow = 0 +var buildsNumberToShow: Int = 200 +var beforeDate: String? = null +var afterDate: String? = null -var buildsNumberToShow: Int? = null +external fun decodeURIComponent(url: String): String +external fun encodeURIComponent(url: String): String -fun waitForElements(elements: Iterable, action: () -> Unit) { - if (elements.filterNotNull().size == elements.count()) - action() - else window.setTimeout(waitForElements(elements, action), 500) -} +fun getDatesComponents() = "${beforeDate?.let {"&before=${encodeURIComponent(it)}"} ?: ""}" + + "${afterDate?.let {"&after=${encodeURIComponent(it)}"} ?: ""}" fun main(args: Array) { val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg" - buildsNumberToShow = null - stageToShow = 0 - val zoomRatio = 3 + val zoomRatio = 2 // Get parameters from request. val url = window.location.href @@ -232,6 +205,10 @@ fun main(args: Array) { } } + buildsNumberToShow = parameters["count"]?.toInt() ?: buildsNumberToShow + beforeDate = parameters["before"]?.let{ decodeURIComponent(it)} + afterDate = parameters["after"]?.let{ decodeURIComponent(it)} + // Get branches. val branchesUrl = "$serverUrl/branches" sendGetRequest(branchesUrl).then { response -> @@ -256,7 +233,8 @@ fun main(args: Array) { autocompleteParameters["onSelect"] = { suggestion -> if (suggestion.value != parameters["build"]) { val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" + - "${if ((suggestion.value as String).isEmpty()) "" else "&build=${suggestion.value}"}" + "${if ((suggestion.value as String).isEmpty()) "" else "&build=${suggestion.value}"}&count=$buildsNumberToShow" + + getDatesComponents() window.location.href = newLink } } @@ -265,7 +243,8 @@ fun main(args: Array) { val newValue = js("$(this).val()").toString() if (newValue.isEmpty() || newValue in buildsNumbers) { val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" + - "${if (newValue.isEmpty()) "" else "&build=$newValue"}" + "${if (newValue.isEmpty()) "" else "&build=$newValue"}&count=$buildsNumberToShow" + + getDatesComponents() window.location.href = newLink } }) @@ -282,7 +261,7 @@ fun main(args: Array) { val newValue = js("$(this).val()") if (newValue != parameters["target"]) { val newLink = "http://${window.location.host}/?target=$newValue&type=${parameters["type"]}&branch=${parameters["branch"]}" + - "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}" + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" window.location.href = newLink } }) @@ -290,7 +269,7 @@ fun main(args: Array) { val newValue = js("$(this).val()") if (newValue != parameters["type"]) { val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=$newValue&branch=${parameters["branch"]}" + - "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}" + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" window.location.href = newLink } }) @@ -298,35 +277,35 @@ fun main(args: Array) { val newValue = js("$(this).val()") if (newValue != parameters["branch"]) { val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=$newValue" + - "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}" + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" window.location.href = newLink } }) - val platformSpecificBenchs = if (parameters["target"] == "Mac_OS_X") ",FrameworkBenchmarksAnalyzer,circlet_iosX64" else + val platformSpecificBenchs = if (parameters["target"] == "Mac_OS_X") ",FrameworkBenchmarksAnalyzer,SpaceFramework_iosX64" else if (parameters["target"] == "Linux") ",kotlinx.coroutines" else "" // Collect information for charts library. - val valuesToShow = mapOf("EXECUTION_TIME" to arrayOf(mapOf( + val valuesToShow = mapOf("EXECUTION_TIME" to listOf(mapOf( "normalize" to "true" )), - "COMPILE_TIME" to arrayOf(mapOf( + "COMPILE_TIME" to listOf(mapOf( "samples" to "HelloWorld,Videoplayer$platformSpecificBenchs", "agr" to "samples" )), - "CODE_SIZE" to arrayOf(mapOf( + "CODE_SIZE" to listOf(mapOf( "normalize" to "true", "exclude" to if (parameters["target"] == "Linux") "kotlinx.coroutines" else if (parameters["target"] == "Mac_OS_X") - "circlet_iosX64" + "SpaceFramework_iosX64" else "" - ), mapOf( + ), if (platformSpecificBenchs.isNotEmpty()) mapOf( "normalize" to "true", "agr" to "samples", "samples" to platformSpecificBenchs.removePrefix(",") - )), - "BUNDLE_SIZE" to arrayOf(mapOf("samples" to "KotlinNative", + ) else null).filterNotNull(), + "BUNDLE_SIZE" to listOf(mapOf("samples" to "KotlinNative", "agr" to "samples")) ) @@ -344,13 +323,27 @@ fun main(args: Array) { var bundleSizeChart: dynamic = null val descriptionUrl = "$serverUrl/buildsDesc/${parameters["target"]}?type=${parameters["type"]}" + - "${if (parameters["branch"] != "all") "&branch=${parameters["branch"]}" else ""}" + "${if (parameters["branch"] != "all") "&branch=${parameters["branch"]}" else ""}&count=$buildsNumberToShow" + + getDatesComponents() val metricUrl = "$serverUrl/metricValue/${parameters["target"]}/" + // Get builds description. + val buildsInfoPromise = sendGetRequest(descriptionUrl).then { response -> + val buildsInfo = response as String + val data = JsonTreeParser.parse(buildsInfo) + if (data !is JsonArray) { + error("Response is expected to be an array.") + } + data.jsonArray.map { + val element = it as JsonElement + if (element.isNull) null else Build.create(element as JsonObject) + } + } + // Send requests to get all needed metric values. - valuesToShow.map { (metric, arrayOfSettings) -> - val resultValues = arrayOfSettings.map { settings -> + valuesToShow.map { (metric, listOfSettings) -> + val resultValues = listOfSettings.map { settings -> val getParameters = with(StringBuilder()) { if (settings.isNotEmpty()) { append("?") @@ -372,7 +365,7 @@ fun main(args: Array) { if (parameters["type"] != "all") (if (getParameters.isEmpty() && branchParameter.isEmpty()) "?" else "&") + "type=${parameters["type"]}" else "" - }" + }&count=$buildsNumberToShow${getDatesComponents()}" sendGetRequest(url) }.toTypedArray() @@ -397,33 +390,47 @@ fun main(args: Array) { "COMPILE_TIME" -> { compileData = labels to values.map { it.map { it?.let { it / 1000 } } } compileChart = Chartist.Line("#compile_chart", - getChartData(labels, compileData.second, stageToShow, buildsNumberToShow), + getChartData(labels, compileData.second), getChartOptions(valuesToShow["COMPILE_TIME"]!![0]!!["samples"]!!.split(',').toTypedArray(), "Time, milliseconds")) + buildsInfoPromise.then { builds -> + customizeChart(compileChart, "compile_chart", js("$(\"#compile_chart\")"), builds, parameters) + compileChart.update(getChartData(compileData.first, compileData.second)) + } } "EXECUTION_TIME" -> { execData = labels to values execChart = Chartist.Line("#exec_chart", - getChartData(labels, execData.second, stageToShow, buildsNumberToShow), + getChartData(labels, execData.second), getChartOptions(arrayOf("Geometric Mean"), "Normalized time")) + buildsInfoPromise.then { builds -> + customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters) + execChart.update(getChartData(execData.first, execData.second)) + } } "CODE_SIZE" -> { codeSizeData = labels to values codeSizeChart = Chartist.Line("#codesize_chart", - getChartData(labels, codeSizeData.second, - stageToShow, buildsNumberToShow, sizeClassNames), + getChartData(labels, codeSizeData.second), getChartOptions(arrayOf("Geometric Mean") + platformSpecificBenchs.split(',') .filter { it.isNotEmpty() }, "Normalized size", arrayOf("ct-series-4", "ct-series-5", "ct-series-6"))) + buildsInfoPromise.then { builds -> + customizeChart(codeSizeChart, "codesize_chart", js("$(\"#codesize_chart\")"), builds, parameters) + codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, sizeClassNames)) + } } "BUNDLE_SIZE" -> { bundleSizeData = labels to values.map { it.map { it?.let { it.toInt() / 1024 / 1024 } } } bundleSizeChart = Chartist.Line("#bundlesize_chart", getChartData(labels, - bundleSizeData.second, stageToShow, - buildsNumberToShow, sizeClassNames), + bundleSizeData.second, sizeClassNames), getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-4"))) + buildsInfoPromise.then { builds -> + customizeChart(bundleSizeChart, "bundlesize_chart", js("$(\"#bundlesize_chart\")"), builds, parameters) + bundleSizeChart.update(getChartData(bundleSizeData.first, bundleSizeData.second, sizeClassNames)) + } } else -> error("No chart for metric $metric") } @@ -433,69 +440,56 @@ fun main(args: Array) { // Update all charts with using same data. val updateAllCharts: () -> Unit = { - execChart.update(getChartData(execData.first, execData.second, stageToShow, buildsNumberToShow)) - compileChart.update(getChartData(compileData.first, compileData.second, stageToShow, buildsNumberToShow)) - codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, stageToShow, buildsNumberToShow, sizeClassNames)) - bundleSizeChart.update(getChartData(bundleSizeData.first, bundleSizeData.second, stageToShow, buildsNumberToShow, sizeClassNames)) - } - - // Get builds description. - sendGetRequest(descriptionUrl).then { response -> - val buildsInfo = response as String - val data = JsonTreeParser.parse(buildsInfo) - if (data !is JsonArray) { - error("Response is expected to be an array.") - } - val builds = data.jsonArray.map { - val element = it as JsonElement - if (element.isNull) null else Build.create(element as JsonObject) - } - waitForElements(listOf(execChart, compileChart, codeSizeChart, bundleSizeChart)) { - customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters) - customizeChart(compileChart, "compile_chart", js("$(\"#compile_chart\")"), builds, parameters) - customizeChart(codeSizeChart, "codesize_chart", js("$(\"#codesize_chart\")"), builds, parameters) - customizeChart(bundleSizeChart, "bundlesize_chart", js("$(\"#bundlesize_chart\")"), builds, parameters) - updateAllCharts() - } + execChart.update(getChartData(execData.first, execData.second)) + compileChart.update(getChartData(compileData.first, compileData.second)) + codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, sizeClassNames)) + bundleSizeChart.update(getChartData(bundleSizeData.first, bundleSizeData.second, sizeClassNames)) } js("$('#plusBtn')").click({ - buildsNumberToShow = buildsNumberToShow?.let { - if (it / zoomRatio > zoomRatio) { - it / zoomRatio + buildsNumberToShow = + if (buildsNumberToShow / zoomRatio > zoomRatio) { + buildsNumberToShow / zoomRatio } else { - it + buildsNumberToShow } - } ?: execData.first.size / zoomRatio - updateAllCharts() + + val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=${parameters["branch"]}" + + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" + + getDatesComponents() + window.location.href = newLink + Unit }) js("$('#minusBtn')").click({ - buildsNumberToShow = buildsNumberToShow?.let { - if (it * zoomRatio <= execData.first.size) { - it * zoomRatio - } else { - null - } - } - updateAllCharts() + buildsNumberToShow = buildsNumberToShow * zoomRatio + val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=${parameters["branch"]}" + + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" + + getDatesComponents() + window.location.href = newLink + Unit }) js("$('#prevBtn')").click({ - buildsNumberToShow?.let { - val bottomBorder = -execData.first.size / (buildsNumberToShow as Int) - if (stageToShow - 1 > bottomBorder) { - stageToShow-- - } - } ?: run { stageToShow = 0 } - updateAllCharts() + buildsInfoPromise.then { builds -> + beforeDate = builds.firstOrNull()?.startTime + afterDate = null + val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=${parameters["branch"]}" + + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" + + "${beforeDate?.let {"&before=${encodeURIComponent(it)}"} ?: ""}" + window.location.href = newLink + } }) js("$('#nextBtn')").click({ - if (stageToShow + 1 <= 0) { - stageToShow++ + buildsInfoPromise.then { builds -> + beforeDate = null + afterDate = builds.lastOrNull()?.startTime + val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=${parameters["branch"]}" + + "${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" + + "${afterDate?.let {"&after=${encodeURIComponent(it)}"} ?: ""}" + window.location.href = newLink } - updateAllCharts() }) // Auto reload.