Added framework benchmark collecting code size and compile time (#2839)
This commit is contained in:
@@ -46,7 +46,8 @@ open class BuildRegister : DefaultTask() {
|
|||||||
|
|
||||||
var bundleSize: Int? = null
|
var bundleSize: Int? = null
|
||||||
|
|
||||||
val buildInfoToken: Int = 4
|
val buildInfoTokens: Int = 4
|
||||||
|
val frameworkInfoTokens: Int = 3
|
||||||
val compileTimeSamplesNumber: Int = 2
|
val compileTimeSamplesNumber: Int = 2
|
||||||
val buildNumberTokens: Int = 3
|
val buildNumberTokens: Int = 3
|
||||||
val performanceServer = "https://kotlin-native-perf-summary.labs.jb.gg"
|
val performanceServer = "https://kotlin-native-perf-summary.labs.jb.gg"
|
||||||
@@ -96,30 +97,50 @@ open class BuildRegister : DefaultTask() {
|
|||||||
val currentBuild = getBuild("id:$buildId", teamCityUser, teamCityPassword)
|
val currentBuild = getBuild("id:$buildId", teamCityUser, teamCityPassword)
|
||||||
val branch = getBuildProperty(currentBuild,"branchName")
|
val branch = getBuildProperty(currentBuild,"branchName")
|
||||||
|
|
||||||
|
val target = System.getProperty("os.name").replace("\\s".toRegex(), "")
|
||||||
|
|
||||||
// Get summary information.
|
// Get summary information.
|
||||||
val output = arrayOf("$analyzer", "summary", "-exec-samples", "all", "-compile", "samples",
|
val output = arrayOf("$analyzer", "summary", "-exec-samples", "all", "-compile", "samples",
|
||||||
"-compile-samples", "HelloWorld,Videoplayer", "-codesize-samples", "all",
|
"-compile-samples", "HelloWorld,Videoplayer", "-codesize-samples", "all",
|
||||||
"-exec-normalize", "bintray:goldenResults.csv",
|
"-exec-normalize", "bintray:goldenResults.csv",
|
||||||
"-codesize-normalize", "bintray:goldenResults.csv", "$currentBenchmarksReportFile")
|
"-codesize-normalize", "bintray:goldenResults.csv", "$currentBenchmarksReportFile")
|
||||||
.runCommand()
|
.runCommand()
|
||||||
|
|
||||||
// Postprocess information.
|
// Postprocess information.
|
||||||
val buildInfoParts = output.split(',')
|
val buildInfoParts = output.split(',')
|
||||||
if (buildInfoParts.size != buildInfoToken) {
|
if (buildInfoParts.size != buildInfoTokens) {
|
||||||
error("Problems with getting summary information using $analyzer and $currentBenchmarksReportFile.")
|
error("Problems with getting summary information using $analyzer and $currentBenchmarksReportFile. $output")
|
||||||
}
|
}
|
||||||
|
|
||||||
val (failures, executionTime, compileTime, codeSize) = buildInfoParts.map { it.trim() }
|
val (failures, executionTime, compileTime, codeSize) = buildInfoParts.map { it.trim() }
|
||||||
// Add legends.
|
// Add legends.
|
||||||
val geometricMean = "Geometric Mean-"
|
val geometricMean = "Geometric Mean-"
|
||||||
val executionTimeInfo = "$geometricMean$executionTime"
|
val executionTimeInfo = "$geometricMean$executionTime"
|
||||||
val codeSizeInfo = "$geometricMean$codeSize"
|
var codeSizeInfo = "$geometricMean$codeSize"
|
||||||
val compileTimeSamples = compileTime.split(';')
|
val compileTimeSamples = compileTime.split(';')
|
||||||
if (compileTimeSamples.size != compileTimeSamplesNumber) {
|
if (compileTimeSamples.size != compileTimeSamplesNumber) {
|
||||||
error("Problems with getting compile time samples value. Expected at least $compileTimeSamplesNumber samples, got ${compileTimeSamples.size}")
|
error("Problems with getting compile time samples value. Expected at least $compileTimeSamplesNumber samples, got ${compileTimeSamples.size}")
|
||||||
}
|
}
|
||||||
val (helloWorldCompile, videoplayerCompile) = compileTimeSamples
|
val (helloWorldCompile, videoplayerCompile) = compileTimeSamples
|
||||||
val compileTimeInfo = "HelloWorld-$helloWorldCompile;Videoplayer-$videoplayerCompile"
|
var compileTimeInfo = "HelloWorld-$helloWorldCompile;Videoplayer-$videoplayerCompile"
|
||||||
val target = System.getProperty("os.name").replace("\\s".toRegex(), "")
|
|
||||||
|
// Collect framework run details.
|
||||||
|
if (target == "MacOSX") {
|
||||||
|
|
||||||
|
val frameworkOutput = arrayOf("$analyzer", "summary", "-compile", "samples",
|
||||||
|
"-compile-samples", "FrameworkBenchmarksAnalyzer", "-codesize-samples", "FrameworkBenchmarksAnalyzer",
|
||||||
|
"-codesize-normalize", "bintray:goldenResults.csv", "$currentBenchmarksReportFile")
|
||||||
|
.runCommand()
|
||||||
|
|
||||||
|
val buildInfoPartsFramework = frameworkOutput.split(',')
|
||||||
|
if (buildInfoPartsFramework.size != frameworkInfoTokens) {
|
||||||
|
error("Problems with getting summary information using $analyzer and $currentBenchmarksReportFile. $frameworkOutput")
|
||||||
|
}
|
||||||
|
val (_, frameworkCompileTime, frameworkCodeSize) = buildInfoPartsFramework.map { it.trim() }
|
||||||
|
codeSizeInfo += ";FrameworkBenchmarksAnalyzer-$frameworkCodeSize"
|
||||||
|
compileTimeInfo += ";FrameworkBenchmarksAnalyzer-$frameworkCompileTime"
|
||||||
|
}
|
||||||
|
|
||||||
val buildNumberParts = buildNumber.split("-")
|
val buildNumberParts = buildNumber.split("-")
|
||||||
if (buildNumberParts.size != buildNumberTokens) {
|
if (buildNumberParts.size != buildNumberTokens) {
|
||||||
error("Wrong format of build number $buildNumber.")
|
error("Wrong format of build number $buildNumber.")
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ fun mergeReports(reports: List<File>): String {
|
|||||||
BenchmarksReport.create(reportElement)
|
BenchmarksReport.create(reportElement)
|
||||||
|
|
||||||
}
|
}
|
||||||
return reportsToMerge.reduce { result, it -> result + it }.toJson()
|
return if (reportsToMerge.isEmpty()) "" else reportsToMerge.reduce { result, it -> result + it }.toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find file with set name in directory.
|
// Find file with set name in directory.
|
||||||
@@ -172,8 +172,10 @@ fun createRunTask(
|
|||||||
fun getJvmCompileTime(programName: String): BenchmarkResult =
|
fun getJvmCompileTime(programName: String): BenchmarkResult =
|
||||||
TaskTimerListener.getBenchmarkResult(programName, listOf("compileKotlinMetadata", "jvmJar"))
|
TaskTimerListener.getBenchmarkResult(programName, listOf("compileKotlinMetadata", "jvmJar"))
|
||||||
|
|
||||||
fun getNativeCompileTime(programName: String): BenchmarkResult =
|
@JvmOverloads
|
||||||
TaskTimerListener.getBenchmarkResult(programName, listOf("compileKotlinNative", "linkMainReleaseExecutableNative"))
|
fun getNativeCompileTime(programName: String,
|
||||||
|
tasks: List<String> = listOf("compileKotlinNative", "linkMainReleaseExecutableNative")): BenchmarkResult =
|
||||||
|
TaskTimerListener.getBenchmarkResult(programName, tasks)
|
||||||
|
|
||||||
fun getCompileBenchmarkTime(programName: String, tasksNames: Iterable<String>, repeats: Int, exitCodes: Map<String, Int>) =
|
fun getCompileBenchmarkTime(programName: String, tasksNames: Iterable<String>, repeats: Int, exitCodes: Map<String, Int>) =
|
||||||
(1..repeats).map { number ->
|
(1..repeats).map { number ->
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin-multiplatform'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url 'https://cache-redirector.jetbrains.com/jcenter'
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url kotlinCompilerRepo
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url buildKotlinCompilerRepo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def toolsPath = '../../tools'
|
||||||
|
def frameworkName = 'benchmarksAnalyzer'
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
macosMain {
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
|
||||||
|
}
|
||||||
|
kotlin.srcDir "$toolsPath/benchmarks/shared/src"
|
||||||
|
kotlin.srcDir "$toolsPath/benchmarksAnalyzer/src/main/kotlin"
|
||||||
|
kotlin.srcDir "$toolsPath/kliopt"
|
||||||
|
kotlin.srcDir "$toolsPath/benchmarksAnalyzer/src/main/kotlin-native"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configure([macosX64("macos")]) {
|
||||||
|
compilations.main.cinterops {
|
||||||
|
libcurl {
|
||||||
|
defFile "$toolsPath/benchmarksAnalyzer/src/nativeInterop/cinterop/libcurl.def"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macosX64("macos").binaries {
|
||||||
|
framework(frameworkName, [RELEASE])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MPPTools.addTimeListener(project)
|
||||||
|
|
||||||
|
task konanRun {
|
||||||
|
if (MPPTools.isMacos()) {
|
||||||
|
dependsOn 'build'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task jvmRun(type: RunJvmTask) {
|
||||||
|
doLast {
|
||||||
|
println("JVM run is unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task konanJsonReport {
|
||||||
|
doLast {
|
||||||
|
if (MPPTools.isMacos()) {
|
||||||
|
def applicationName = "FrameworkBenchmarksAnalyzer"
|
||||||
|
def frameworkPath = kotlin.macosX64("macos").binaries.
|
||||||
|
getFramework(frameworkName, kotlin.macosX64("macos").binaries.RELEASE).outputFile.absolutePath
|
||||||
|
def nativeExecutable = new File("$frameworkPath/$frameworkName").canonicalPath
|
||||||
|
def nativeCompileTime = MPPTools.getNativeCompileTime(applicationName, ['compileKotlinMacos',
|
||||||
|
'linkBenchmarksAnalyzerReleaseFrameworkMacos',
|
||||||
|
'cinteropLibcurlMacos'])
|
||||||
|
def properties = getCommonProperties() + ['type' : 'native',
|
||||||
|
'compilerVersion': "${konanVersion}".toString(),
|
||||||
|
'flags' : [],
|
||||||
|
'benchmarks' : '[]',
|
||||||
|
'compileTime' : [nativeCompileTime],
|
||||||
|
'codeSize' : MPPTools.getCodeSizeBenchmark(applicationName, nativeExecutable)]
|
||||||
|
def output = MPPTools.createJsonReport(properties)
|
||||||
|
new File("${buildDir.absolutePath}/${nativeJson}").write(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task jvmJsonReport {
|
||||||
|
doLast {
|
||||||
|
println("JVM run is unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmRun.finalizedBy jvmJsonReport
|
||||||
|
konanRun.finalizedBy konanJsonReport
|
||||||
|
|
||||||
|
private def getCommonProperties() {
|
||||||
|
return ['cpu': System.getProperty("os.arch"),
|
||||||
|
'os': System.getProperty("os.name"), // OperatingSystem.current().getName()
|
||||||
|
'jdkVersion': System.getProperty("java.version"), // org.gradle.internal.jvm.Jvm.current().javaVersion
|
||||||
|
'jdkVendor': System.getProperty("java.vendor"),
|
||||||
|
'kotlinVersion': "${kotlinVersion}".toString()]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
org.jetbrains.kotlin.native.home=../../dist
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
include ':ring'
|
include ':ring'
|
||||||
include ':cinterop'
|
include ':cinterop'
|
||||||
include ':helloworld'
|
include ':helloworld'
|
||||||
include ':videoplayer'
|
include ':videoplayer'
|
||||||
|
include ':framework'
|
||||||
@@ -72,8 +72,8 @@ abstract class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int, v
|
|||||||
object BenchmarksRunner {
|
object BenchmarksRunner {
|
||||||
fun parse(args: Array<String>): ArgParser {
|
fun parse(args: Array<String>): ArgParser {
|
||||||
val options = listOf(
|
val options = listOf(
|
||||||
OptionDescriptor(ArgType.Int(), "warmup", "w", "Number of warm up iterations", "0"),
|
OptionDescriptor(ArgType.Int(), "warmup", "w", "Number of warm up iterations", "20"),
|
||||||
OptionDescriptor(ArgType.Int(), "repeat", "r", "Number of each becnhmark run", "10"),
|
OptionDescriptor(ArgType.Int(), "repeat", "r", "Number of each becnhmark run", "60"),
|
||||||
OptionDescriptor(ArgType.String(), "prefix", "p", "Prefix added to benchmark name", ""),
|
OptionDescriptor(ArgType.String(), "prefix", "p", "Prefix added to benchmark name", ""),
|
||||||
OptionDescriptor(ArgType.String(), "output", "o", "Output file"),
|
OptionDescriptor(ArgType.String(), "output", "o", "Output file"),
|
||||||
OptionDescriptor(ArgType.String(), "filter", "f", "Benchmark to run", isMultiple = true)
|
OptionDescriptor(ArgType.String(), "filter", "f", "Benchmark to run", isMultiple = true)
|
||||||
|
|||||||
@@ -45,14 +45,18 @@
|
|||||||
border-color: gold;
|
border-color: gold;
|
||||||
}
|
}
|
||||||
.ct-legend .ct-series-2:before {
|
.ct-legend .ct-series-2:before {
|
||||||
|
background-color: limegreen;
|
||||||
|
border-color: limegreen;
|
||||||
|
}
|
||||||
|
.ct-legend .ct-series-3:before {
|
||||||
background-color: mediumorchid;
|
background-color: mediumorchid;
|
||||||
border-color: mediumorchid;
|
border-color: mediumorchid;
|
||||||
}
|
}
|
||||||
.ct-legend .ct-series-3:before {
|
|
||||||
background-color: #d17905;
|
|
||||||
border-color: #d17905;
|
|
||||||
}
|
|
||||||
.ct-legend .ct-series-4:before {
|
.ct-legend .ct-series-4:before {
|
||||||
|
background-color: navy;
|
||||||
|
border-color: navy;
|
||||||
|
}
|
||||||
|
.ct-legend .ct-series-5:before {
|
||||||
background-color: #453d3f;
|
background-color: #453d3f;
|
||||||
border-color: #453d3f;
|
border-color: #453d3f;
|
||||||
}
|
}
|
||||||
@@ -66,16 +70,18 @@
|
|||||||
.ct-series-c .ct-line,
|
.ct-series-c .ct-line,
|
||||||
.ct-series-c .ct-point {
|
.ct-series-c .ct-point {
|
||||||
/* Set the colour of this series line */
|
/* Set the colour of this series line */
|
||||||
stroke: mediumorchid;
|
stroke: limegreen;
|
||||||
}
|
|
||||||
|
|
||||||
.ct-series-d .ct-line {
|
|
||||||
/* Set the colour of this series line */
|
|
||||||
stroke: mediumorchid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ct-series-d .ct-line,
|
||||||
.ct-series-d .ct-point {
|
.ct-series-d .ct-point {
|
||||||
/* Set the colour of this series line */
|
/* Set the colour of this series line */
|
||||||
stroke: mediumorchid;
|
stroke: mediumorchid;
|
||||||
stroke-width: 20;
|
}
|
||||||
}
|
|
||||||
|
.ct-series-e .ct-line,
|
||||||
|
.ct-series-e .ct-point {
|
||||||
|
/* Set the colour of this series line */
|
||||||
|
stroke: navy;
|
||||||
|
}
|
||||||
|
.tooltip { pointer-events: none; }
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js">
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js">
|
||||||
</script>
|
</script>
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js">
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js">
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -69,13 +69,13 @@ fun <T : Any> separateValues(values: String, valuesContainer: MutableMap<String,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChartData(labels: List<String>, valuesList: Collection<List<*>>, className: String? = null): dynamic {
|
fun getChartData(labels: List<String>, valuesList: Collection<List<*>>, classNames: Array<String>? = null): dynamic {
|
||||||
val chartData: dynamic = object{}
|
val chartData: dynamic = object{}
|
||||||
chartData["labels"] = labels.toTypedArray()
|
chartData["labels"] = labels.toTypedArray()
|
||||||
chartData["series"] = valuesList.map {
|
chartData["series"] = valuesList.mapIndexed { index, it ->
|
||||||
val series: dynamic = object{}
|
val series: dynamic = object{}
|
||||||
series["data"] = it.toTypedArray()
|
series["data"] = it.toTypedArray()
|
||||||
className?.let { series["className"] = className }
|
classNames?.let { series["className"] = classNames[index] }
|
||||||
series
|
series
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
return chartData
|
return chartData
|
||||||
@@ -142,7 +142,7 @@ fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynam
|
|||||||
"L", data.x + pointSize, data.y + pointSize/2, "z").joinToString(" ")
|
"L", data.x + pointSize, data.y + pointSize/2, "z").joinToString(" ")
|
||||||
svgParameters["style"] = "fill:rgb(255,0,0);stroke-width:0"
|
svgParameters["style"] = "fill:rgb(255,0,0);stroke-width:0"
|
||||||
val triangle = Chartist.Svg("path", svgParameters, chartContainer)
|
val triangle = Chartist.Svg("path", svgParameters, chartContainer)
|
||||||
element = data.element._node.replace(triangle)
|
element = data.element.replace(triangle)
|
||||||
} else if (currentBuild.buildNumber == parameters["build"]) {
|
} else if (currentBuild.buildNumber == parameters["build"]) {
|
||||||
// Higlight choosen build.
|
// Higlight choosen build.
|
||||||
val svgParameters: dynamic = object{}
|
val svgParameters: dynamic = object{}
|
||||||
@@ -228,7 +228,7 @@ fun main(args: Array<String>) {
|
|||||||
val branchesUrl = "$serverUrl/branches/${parameters["target"]}"
|
val branchesUrl = "$serverUrl/branches/${parameters["target"]}"
|
||||||
|
|
||||||
val branches: Array<String> = JSON.parse(sendGetRequest(branchesUrl))
|
val branches: Array<String> = JSON.parse(sendGetRequest(branchesUrl))
|
||||||
val releaseBranches = branches.filter { "v\\d+\\.\\d+\\.\\d+-fixes".toRegex().find(it) != null }
|
val releaseBranches = branches.filter { "^v\\d+\\.\\d+\\.\\d+-fixes$".toRegex().find(it) != null }
|
||||||
|
|
||||||
// Fill autocomplete list.
|
// Fill autocomplete list.
|
||||||
val buildsNumbersUrl = "$serverUrl/buildsNumbers/${parameters["target"]}"
|
val buildsNumbersUrl = "$serverUrl/buildsNumbers/${parameters["target"]}"
|
||||||
@@ -312,17 +312,17 @@ fun main(args: Array<String>) {
|
|||||||
bundleSize.add(it.bundleSize?.toInt()?. let { it / 1024 / 1024 })
|
bundleSize.add(it.bundleSize?.toInt()?. let { it / 1024 / 1024 })
|
||||||
}
|
}
|
||||||
|
|
||||||
val sizeClassName = "ct-series-c"
|
val sizeClassNames = arrayOf("ct-series-d", "ct-series-e")
|
||||||
|
|
||||||
// Draw charts.
|
// Draw charts.
|
||||||
val execChart = Chartist.Line("#exec_chart", getChartData(labels, executionTime.values),
|
val execChart = Chartist.Line("#exec_chart", getChartData(labels, executionTime.values),
|
||||||
getChartOptions(executionTime.keys.toTypedArray(), "Normalized time"))
|
getChartOptions(executionTime.keys.toTypedArray(), "Normalized time"))
|
||||||
val compileChart = Chartist.Line("#compile_chart", getChartData(labels, compileTime.values),
|
val compileChart = Chartist.Line("#compile_chart", getChartData(labels, compileTime.values),
|
||||||
getChartOptions(compileTime.keys.toTypedArray(), "Time, milliseconds"))
|
getChartOptions(compileTime.keys.toTypedArray(), "Time, milliseconds"))
|
||||||
val codeSizeChart = Chartist.Line("#codesize_chart", getChartData(labels, codeSize.values, sizeClassName),
|
val codeSizeChart = Chartist.Line("#codesize_chart", getChartData(labels, codeSize.values, sizeClassNames),
|
||||||
getChartOptions(codeSize.keys.toTypedArray(), "Normalized size", arrayOf("ct-series-2")))
|
getChartOptions(codeSize.keys.toTypedArray(), "Normalized size", arrayOf("ct-series-3", "ct-series-4")))
|
||||||
val bundleSizeChart = Chartist.Line("#bundlesize_chart", getChartData(labels, listOf(bundleSize), sizeClassName),
|
val bundleSizeChart = Chartist.Line("#bundlesize_chart", getChartData(labels, listOf(bundleSize), sizeClassNames),
|
||||||
getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-2")))
|
getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-3")))
|
||||||
|
|
||||||
// Tooltips and higlights.
|
// Tooltips and higlights.
|
||||||
customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters)
|
customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters)
|
||||||
|
|||||||
Reference in New Issue
Block a user