diff --git a/performance/build.gradle b/performance/build.gradle index 588d69ce678..b5a4e084b64 100644 --- a/performance/build.gradle +++ b/performance/build.gradle @@ -1,95 +1,148 @@ buildscript { + ext.rootBuildDirectory = file('..') + + apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle" + apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle" repositories { maven { - url 'https://cache-redirector.jetbrains.com/maven-central' + url 'https://cache-redirector.jetbrains.com/jcenter' } maven { - url buildKotlinCompilerRepo + url kotlinCompilerRepo } } dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$buildKotlinVersion" - classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:$gradlePluginVersion" + 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 } + } -//TODO: property -def jvmWarmup = 10000 -def nativeWarmup = 10 -def attempts = 10 +private def determinePreset() { + def preset = MPPTools.defaultHostPreset(project) + println("$project has been configured for ${preset.name} platform.") + preset +} -ext."konan.home" = distDir +def hostPreset = determinePreset() -apply plugin: 'kotlin' -apply plugin: 'application' -apply plugin: 'konan' +kotlin { + sourceSets { + commonMain { + dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion" + } + kotlin.srcDir '../tools/benchmarks/shared/src' + kotlin.srcDir 'src/main/kotlin' -checkKonanCompiler.dependsOn ':dist' + } + nativeMain { + kotlin.srcDir 'src/main/kotlin-native' + } + jvmMain { + dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + } + kotlin.srcDir 'src/main/kotlin-jvm' + } + } -konanArtifacts { - program('Ring') { - srcDir 'src/main/kotlin' - srcDir 'src/main/kotlin-native' - enableOptimizations true + targets { + fromPreset(presets.jvm, 'jvm') { + def mainOutput = compilations.main.output + compilations.all { + tasks[compileKotlinTaskName].kotlinOptions { + jvmTarget = '1.8' + } + tasks[compileKotlinTaskName].kotlinOptions.suppressWarnings = true + } + } + + fromPreset(hostPreset, 'native') { + compilations.main.outputKinds('EXECUTABLE') + compilations.main.extraOpts '-opt' + compilations.main.buildTypes = [RELEASE] + } } } -sourceSets { - main.kotlin.srcDir 'src/main/kotlin' - main.kotlin.srcDir 'src/main/kotlin-jvm' -} - -compileKotlin { - kotlinOptions.suppressWarnings = true - kotlinOptions { - jvmTarget = "1.8" +MPPTools.createRunTask(project, 'konanRun', kotlin.targets.native) { + workingDir = project.provider { + kotlin.targets.native.compilations.main.getBinary('EXECUTABLE', buildType).parentFile } + args("$nativeWarmup", "$attempts", "${buildDir.absolutePath}/${nativeBenchResults}") + outputFileName("${buildDir.absolutePath}/${nativeTextReport}") } -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$buildKotlinVersion" -} - -task jvmRun(type: JavaExec) { +task jvmRun(type: JavaExec) { dependsOn 'build' def output = new ByteArrayOutputStream() - classpath sourceSets.main.runtimeClasspath + def runtimeClasspath = files( + kotlin.targets.jvm.compilations.main.output.allOutputs, + project.configurations.getByName(kotlin.targets.jvm.compilations.main.runtimeDependencyConfigurationName) + ) + classpath runtimeClasspath main = "MainKt" - args "$jvmWarmup", "$attempts" + args "$jvmWarmup", "$attempts", "${buildDir.absolutePath}/${jvmBenchResults}" standardOutput = output doLast { - dumpReport('jvmReport', output) + dumpReport("${buildDir.absolutePath}/${jvmTextReport}", output) } } +task konanJsonReport { + doLast { + String benchContents = new File("${buildDir.absolutePath}/${nativeBenchResults}").text + def properties = getCommonProperties() + ['type' : 'native', + 'compilerVersion': "${konanVersion}".toString(), + 'flags' : kotlin.targets.native.compilations.main.extraOpts.collect{ '"' + it + '"'}, + 'benchmarks' : benchContents] + def output = MPPTools.createJsonReport(properties) + new File("${buildDir.absolutePath}/${nativeJson}").write(output) + } +} + +task jvmJsonReport { + doLast { + String benchContents = new File("${buildDir.absolutePath}/${jvmBenchResults}").text + def properties = getCommonProperties() + ['type' : 'jvm', + 'compilerVersion': "${buildKotlinVersion}".toString(), + 'benchmarks' : benchContents] + println(properties['compilerVersion']) + def output = MPPTools.createJsonReport(properties) + new File("${buildDir.absolutePath}/${jvmJson}").write(output) + } +} + +jvmRun.finalizedBy jvmJsonReport +konanRun.finalizedBy konanJsonReport + private void dumpReport(String name, ByteArrayOutputStream output) { - new File("${buildDir.absolutePath}/${name}.txt").withOutputStream { + new File("${name}").withOutputStream { it.write(output.toByteArray()) } } -task konanRun(type: Exec) { - dependsOn 'build' - def output = new ByteArrayOutputStream() - commandLine konanArtifacts.Ring.getByTarget('host').artifact.absolutePath, "$nativeWarmup", "$attempts" - standardOutput = output - doLast { - dumpReport('konanReport', output) - } -} - -startScripts{ - setEnabled(false) +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()] } task bench(type:DefaultTask) { @@ -97,8 +150,8 @@ task bench(type:DefaultTask) { dependsOn konanRun doLast { - def jvmReport = new Report(project.file("build/jvmReport.txt")) - def konanReport = new Report(project.file("build/konanReport.txt")) + def jvmReport = new Report(project.file("${buildDir.absolutePath}/${jvmTextReport}")) + def konanReport = new Report(project.file("${buildDir.absolutePath}/${nativeTextReport}")) def average = "none" def absoluteAverage = "none" jvmReport.report @@ -138,7 +191,7 @@ class Results { } class Report { - def Map report = new TreeMap() + Map report = new TreeMap() Report(File path) { path.readLines().drop(3).findAll { it.split(':').length == 3 }.each { diff --git a/performance/buildSrc/build.gradle b/performance/buildSrc/build.gradle new file mode 100644 index 00000000000..ca1a9e7226e --- /dev/null +++ b/performance/buildSrc/build.gradle @@ -0,0 +1,35 @@ +buildscript { + repositories { + maven { + url 'https://cache-redirector.jetbrains.com/maven-central' + } + maven { + url kotlinCompilerRepo + } + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} + +apply plugin: 'kotlin' + +repositories { + maven { + url kotlinCompilerRepo + } + maven { + url buildKotlinCompilerRepo + } + jcenter() +} + +dependencies { + compileOnly gradleApi() + implementation "org.jetbrains.kotlin:kotlin-gradle-plugin" + implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion" +} + +sourceSets.main.kotlin.srcDirs = ["src", "$projectDir/../../tools/benchmarks/shared/src"] \ No newline at end of file diff --git a/performance/buildSrc/settings.gradle b/performance/buildSrc/settings.gradle new file mode 100644 index 00000000000..5625fa86bbb --- /dev/null +++ b/performance/buildSrc/settings.gradle @@ -0,0 +1,21 @@ +// Reuse Kotlin version from the root project. +File rootProjectGradlePropertiesFile = file("${rootProject.projectDir}/../../gradle.properties") +if (!rootProjectGradlePropertiesFile.isFile()) { + throw new Exception("File $rootProjectGradlePropertiesFile does not exist or is not a file") +} + +Properties rootProjectProperties = new Properties() +rootProjectGradlePropertiesFile.withInputStream { inputStream -> + rootProjectProperties.load(inputStream) + if (!rootProjectProperties.containsKey('kotlinVersion')) { + throw new Exception("No 'kotlinVersion' property in $rootProjectGradlePropertiesFile file") + } +} + +gradle.beforeProject { project -> + rootProjectProperties.forEach { String key, value -> + if (!project.hasProperty(key)) + project.ext[key] = value + } + +} \ No newline at end of file diff --git a/performance/buildSrc/src/main/kotlin/Internals.kt b/performance/buildSrc/src/main/kotlin/Internals.kt new file mode 100644 index 00000000000..466f15e47f5 --- /dev/null +++ b/performance/buildSrc/src/main/kotlin/Internals.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +import org.gradle.api.NamedDomainObjectCollection +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Project +import org.gradle.api.plugins.ExtraPropertiesExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetPreset +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation + +/* + * This file includes internal short-cuts visible only inside of the 'buildSrc' module. + */ + +internal val hostOs by lazy { System.getProperty("os.name") } +internal val userHome by lazy { System.getProperty("user.home") } + +internal val Project.ext: ExtraPropertiesExtension + get() = extensions.getByName("ext") as ExtraPropertiesExtension + +internal val Project.kotlin: KotlinMultiplatformExtension + get() = extensions.getByName("kotlin") as KotlinMultiplatformExtension + +internal val NamedDomainObjectCollection>.macosX64: KotlinTargetPreset<*> + get() = getByName(::macosX64.name) as KotlinTargetPreset<*> + +internal val NamedDomainObjectCollection>.linuxX64: KotlinTargetPreset<*> + get() = getByName(::linuxX64.name) as KotlinTargetPreset<*> + +internal val NamedDomainObjectCollection>.mingwX64: KotlinTargetPreset<*> + get() = getByName(::mingwX64.name) as KotlinTargetPreset<*> + +internal val NamedDomainObjectContainer>.main: KotlinNativeCompilation + get() = getByName(::main.name) as KotlinNativeCompilation diff --git a/performance/buildSrc/src/main/kotlin/MPPTools.kt b/performance/buildSrc/src/main/kotlin/MPPTools.kt new file mode 100644 index 00000000000..15095964b94 --- /dev/null +++ b/performance/buildSrc/src/main/kotlin/MPPTools.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +@file:JvmName("MPPTools") + +import groovy.lang.Closure +import org.gradle.api.Project +import org.gradle.api.Task +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetPreset +import org.jetbrains.report.* +import org.jetbrains.report.json.* +import java.nio.file.Paths + +/* + * This file includes short-cuts that may potentially be implemented in Kotlin MPP Gradle plugin in the future. + */ + +// Short-cuts for detecting the host OS. +@get:JvmName("isMacos") +val isMacos by lazy { hostOs == "Mac OS X" } + +@get:JvmName("isWindows") +val isWindows by lazy { hostOs.startsWith("Windows") } + +@get:JvmName("isLinux") +val isLinux by lazy { hostOs == "Linux" } + +// Short-cuts for mostly used paths. +@get:JvmName("mingwPath") +val mingwPath by lazy { System.getenv("MINGW64_DIR") ?: "c:/msys64/mingw64" } + +@get:JvmName("kotlinNativeDataPath") +val kotlinNativeDataPath by lazy { + System.getenv("KONAN_DATA_DIR") ?: Paths.get(userHome, ".konan").toString() +} + +// A short-cut for evaluation of the default host Kotlin/Native preset. +@JvmOverloads +fun defaultHostPreset( + subproject: Project, + whitelist: List> = listOf(subproject.kotlin.presets.macosX64, subproject.kotlin.presets.linuxX64, subproject.kotlin.presets.mingwX64) +): KotlinTargetPreset<*> { + + if (whitelist.isEmpty()) + throw Exception("Preset whitelist must not be empty in Kotlin/Native ${subproject.displayName}.") + + val presetCandidate = when { + isMacos -> subproject.kotlin.presets.macosX64 + isLinux -> subproject.kotlin.presets.linuxX64 + isWindows -> subproject.kotlin.presets.mingwX64 + else -> null + } + + return if (presetCandidate != null && presetCandidate in whitelist) + presetCandidate + else + throw Exception("Host OS '$hostOs' is not supported in Kotlin/Native ${subproject.displayName}.") +} + +// Create benchmarks json report based on information get from gradle project +fun createJsonReport(projectProperties: Map): String { + fun getValue(key: String): String = projectProperties[key] as? String ?: "uknown" + val machine = Environment.Machine(getValue("cpu"), getValue("os")) + val jdk = Environment.JDKInstance(getValue("jdkVersion"), getValue("jdkVendor")) + val env = Environment(machine, jdk) + val flags = (projectProperties["flags"] ?: emptyList()) as List + val backend = Compiler.Backend(Compiler.backendTypeFromString(getValue("type"))!! , + getValue("compilerVersion"), flags) + val kotlin = Compiler(backend, getValue("kotlinVersion")) + val benchDesc = getValue("benchmarks") + val benchmarksArray = JsonTreeParser.parse(benchDesc) + val benchmarks = BenchmarksReport.parseBenchmarksArray(benchmarksArray) + val report = BenchmarksReport(env, benchmarks, kotlin) + return report.toJson() +} + +// A short-cut to add a Kotlin/Native run task. +@JvmOverloads +fun createRunTask( + subproject: Project, + name: String, + target: KotlinTarget, + configureClosure: Closure? = null +): Task { + val task = subproject.tasks.create(name, RunKotlinNativeTask::class.java, target) + task.configure(configureClosure ?: task.emptyConfigureClosure()) + return task +} diff --git a/performance/buildSrc/src/main/kotlin/RunKotlinNativeTask.kt b/performance/buildSrc/src/main/kotlin/RunKotlinNativeTask.kt new file mode 100644 index 00000000000..a1875cbb220 --- /dev/null +++ b/performance/buildSrc/src/main/kotlin/RunKotlinNativeTask.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +import groovy.lang.Closure +import org.gradle.api.DefaultTask +import org.gradle.api.Task +import org.gradle.api.tasks.TaskAction +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import javax.inject.Inject +import java.io.File + +open class RunKotlinNativeTask @Inject constructor( + private val curTarget: KotlinTarget +): DefaultTask() { + + var buildType = "RELEASE" + var workingDir: Any = project.projectDir + var outputFileName: String? = null + private var curArgs: List = emptyList() + private val curEnvironment: MutableMap = mutableMapOf() + + fun args(vararg args: Any) { + curArgs = args.map { it.toString() } + } + + fun environment(map: Map) { + curEnvironment += map + } + + override fun configure(configureClosure: Closure): Task { + val task = super.configure(configureClosure) + this.dependsOn += curTarget.compilations.main.linkTaskName("EXECUTABLE", buildType) + return task + } + + fun depends(taskName: String) { + this.dependsOn += taskName + } + + private fun executeTask(output: java.io.OutputStream? = null) { + project.exec { + it.executable = curTarget.compilations.main.getBinary("EXECUTABLE", buildType).toString() + it.args = curArgs + it.environment = curEnvironment + it.workingDir(workingDir) + if (output != null) + it.standardOutput = output + } + } + + @TaskAction + fun run() { + if (outputFileName != null) + File(outputFileName).outputStream().use { output -> executeTask(output)} + else + executeTask() + } + + internal fun emptyConfigureClosure() = object : Closure(this) { + override fun call(): RunKotlinNativeTask { + return this@RunKotlinNativeTask + } + } +} diff --git a/performance/gradle.properties b/performance/gradle.properties new file mode 100644 index 00000000000..7d10881186f --- /dev/null +++ b/performance/gradle.properties @@ -0,0 +1,11 @@ +org.jetbrains.kotlin.native.home=../dist +jvmWarmup = 10000 +nativeWarmup = 10 +attempts = 10 +jvmBenchResults = jvmBenchResults.json +nativeBenchResults = nativeBenchResults.json +nativeTextReport = nativeReport.txt +jvmTextReport = jvmReport.txt +nativeJson = nativeReport.json +jvmJson = jvmReport.json + diff --git a/performance/settings.gradle b/performance/settings.gradle new file mode 100644 index 00000000000..e69de29bb2d diff --git a/performance/src/main/kotlin-jvm/cleanup.kt b/performance/src/main/kotlin-jvm/cleanup.kt index c939736e404..4bdbdce2419 100644 --- a/performance/src/main/kotlin-jvm/cleanup.kt +++ b/performance/src/main/kotlin-jvm/cleanup.kt @@ -1,3 +1,3 @@ package org.jetbrains.ring -fun cleanup() { } \ No newline at end of file +actual fun cleanup() { } \ No newline at end of file diff --git a/performance/src/main/kotlin-jvm/org/jetbrains/ring/Utils.kt b/performance/src/main/kotlin-jvm/org/jetbrains/ring/Utils.kt index 5b8081fe47d..2a76d6e98b4 100644 --- a/performance/src/main/kotlin-jvm/org/jetbrains/ring/Utils.kt +++ b/performance/src/main/kotlin-jvm/org/jetbrains/ring/Utils.kt @@ -16,12 +16,14 @@ package org.jetbrains.ring +import java.io.File + //-----------------------------------------------------------------------------// -class Blackhole { - companion object { - var consumer = 0 - fun consume(value: Any) { +actual class Blackhole { + actual companion object { + actual var consumer = 0 + actual fun consume(value: Any) { consumer += value.hashCode() } } @@ -29,18 +31,37 @@ class Blackhole { //-----------------------------------------------------------------------------// -class Random() { - companion object { - var seedInt = 0 - fun nextInt(boundary: Int = 100): Int { +actual class Random actual constructor() { + actual companion object { + actual var seedInt = 0 + actual fun nextInt(boundary: Int): Int { seedInt = (3 * seedInt + 11) % boundary return seedInt } - var seedDouble: Double = 0.1 - fun nextDouble(boundary: Double = 100.0): Double { + actual var seedDouble: Double = 0.1 + actual fun nextDouble(boundary: Double): Double { seedDouble = (7.0 * seedDouble + 7.0) % boundary return seedDouble } } } + +//-----------------------------------------------------------------------------// + +actual fun writeToFile(fileName: String, text: String) { + File(fileName).printWriter().use { out -> + out.println(text) + } +} + +// Wrapper for assert funtion in stdlib +actual fun assert(value: Boolean) { + kotlin.assert(value) +} + +// Wrapper for measureNanoTime funtion in stdlib +actual inline fun measureNanoTime(block: () -> Unit): Long { + return kotlin.system.measureNanoTime(block) +} + diff --git a/performance/src/main/kotlin-native/cleanup.kt b/performance/src/main/kotlin-native/cleanup.kt index 9169f21f2ea..a27ad95cf8d 100644 --- a/performance/src/main/kotlin-native/cleanup.kt +++ b/performance/src/main/kotlin-native/cleanup.kt @@ -2,4 +2,4 @@ package org.jetbrains.ring import kotlin.native.internal.GC -fun cleanup() { GC.collect() } \ No newline at end of file +actual fun cleanup() { GC.collect() } \ No newline at end of file diff --git a/performance/src/main/kotlin-native/org/jetbrains/ring/Utils.kt b/performance/src/main/kotlin-native/org/jetbrains/ring/Utils.kt index ee166e73002..0d2416b9a61 100644 --- a/performance/src/main/kotlin-native/org/jetbrains/ring/Utils.kt +++ b/performance/src/main/kotlin-native/org/jetbrains/ring/Utils.kt @@ -16,13 +16,15 @@ package org.jetbrains.ring +import platform.posix.* + //-----------------------------------------------------------------------------// -class Blackhole { +actual class Blackhole { @kotlin.native.ThreadLocal - companion object { - var consumer = 0 - fun consume(value: Any) { + actual companion object { + actual var consumer = 0 + actual fun consume(value: Any) { consumer += value.hashCode() } } @@ -30,19 +32,41 @@ class Blackhole { //-----------------------------------------------------------------------------// -class Random() { +actual class Random actual constructor() { @kotlin.native.ThreadLocal - companion object { - var seedInt = 0 - fun nextInt(boundary: Int = 100): Int { + actual companion object { + actual var seedInt = 0 + actual fun nextInt(boundary: Int): Int { seedInt = (3 * seedInt + 11) % boundary return seedInt } - var seedDouble: Double = 0.1 - fun nextDouble(boundary: Double = 100.0): Double { + actual var seedDouble: Double = 0.1 + actual fun nextDouble(boundary: Double): Double { seedDouble = (7.0 * seedDouble + 7.0) % boundary return seedDouble } } } + +//-----------------------------------------------------------------------------// + + +actual fun writeToFile(fileName: String, text: String) { + val file = fopen(fileName, "wt") ?: error("Cannot write file '$fileName'") + try { + if (fputs(text, file) == EOF) throw Error("File write error") + } finally { + fclose(file) + } +} + +// Wrapper for assert funtion in stdlib +actual fun assert(value: Boolean) { + kotlin.assert(value) +} + +// Wrapper for measureNanoTime funtion in stdlib +actual inline fun measureNanoTime(block: () -> Unit): Long { + return kotlin.system.measureNanoTime(block) +} diff --git a/performance/src/main/kotlin/cleanup.kt b/performance/src/main/kotlin/cleanup.kt new file mode 100644 index 00000000000..efbe7a1f8fb --- /dev/null +++ b/performance/src/main/kotlin/cleanup.kt @@ -0,0 +1,3 @@ +package org.jetbrains.ring + +expect fun cleanup() \ No newline at end of file diff --git a/performance/src/main/kotlin/main.kt b/performance/src/main/kotlin/main.kt index a98cffe2b36..d85307ceea9 100644 --- a/performance/src/main/kotlin/main.kt +++ b/performance/src/main/kotlin/main.kt @@ -15,10 +15,12 @@ */ import org.jetbrains.ring.Launcher +import org.jetbrains.ring.JsonReportCreator fun main(args: Array) { var numWarmIterations = 0 // Should be 100000 for jdk based run var numberOfAttempts = 10 + var jsonReport: String? = null when (args.size) { 0 -> { } @@ -27,13 +29,20 @@ fun main(args: Array) { numWarmIterations = args[0].toInt() numberOfAttempts = args[1].toInt() } + 3 -> { + numWarmIterations = args[0].toInt() + numberOfAttempts = args[1].toInt() + jsonReport = args[2].toString() + } else -> { - println("Usage: perf [# warmup iterations] [# attempts]") + println("Usage: perf [# warmup iterations] [# attempts] [# path of json report]") return } } println("Ring starting") println(" warmup iterations count: $numWarmIterations") - Launcher(numWarmIterations, numberOfAttempts).runBenchmarks() + val results = Launcher(numWarmIterations, numberOfAttempts).runBenchmarks() + if (jsonReport != null) + JsonReportCreator(results).printJsonReport(jsonReport) } \ No newline at end of file diff --git a/performance/src/main/kotlin/org/jetbrains/ring/JsonReportCreator.kt b/performance/src/main/kotlin/org/jetbrains/ring/JsonReportCreator.kt new file mode 100644 index 00000000000..c7a2033023b --- /dev/null +++ b/performance/src/main/kotlin/org/jetbrains/ring/JsonReportCreator.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.ring + +import org.jetbrains.report.BenchmarkResult + +class JsonReportCreator(val data: Iterable) { + fun printJsonReport(jsonReport: String): Unit { + val reportText = data.joinToString(prefix = "[", postfix = "]") { + it.toJson() + } + writeToFile(jsonReport, reportText) + } +} \ No newline at end of file diff --git a/performance/src/main/kotlin/org/jetbrains/ring/OctoTest/basicTest.kt b/performance/src/main/kotlin/org/jetbrains/ring/OctoTest/basicTest.kt index b6b7cc129db..db6bfc06527 100644 --- a/performance/src/main/kotlin/org/jetbrains/ring/OctoTest/basicTest.kt +++ b/performance/src/main/kotlin/org/jetbrains/ring/OctoTest/basicTest.kt @@ -1,6 +1,9 @@ /** * Created by semoro on 07.07.17. */ + +import org.jetbrains.ring.assert + fun octoTest() { val tree = OctoTree(4) val to = (2 shl tree.depth) diff --git a/performance/src/main/kotlin/org/jetbrains/ring/Utils.kt b/performance/src/main/kotlin/org/jetbrains/ring/Utils.kt new file mode 100644 index 00000000000..4200ed9d692 --- /dev/null +++ b/performance/src/main/kotlin/org/jetbrains/ring/Utils.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.ring + +expect fun writeToFile(fileName: String, text: String) + +expect class Blackhole { + companion object { + var consumer: Int + fun consume(value: Any) + } +} + +expect class Random() { + companion object { + var seedInt: Int + fun nextInt(boundary: Int = 100): Int + + var seedDouble: Double + fun nextDouble(boundary: Double = 100.0): Double + } +} + +expect fun assert(value: Boolean) + +expect inline fun measureNanoTime(block: () -> Unit): Long + diff --git a/performance/src/main/kotlin/org/jetbrains/ring/launcher.kt b/performance/src/main/kotlin/org/jetbrains/ring/launcher.kt index 78e2dbac0ac..ae448972622 100644 --- a/performance/src/main/kotlin/org/jetbrains/ring/launcher.kt +++ b/performance/src/main/kotlin/org/jetbrains/ring/launcher.kt @@ -18,7 +18,7 @@ package org.jetbrains.ring import octoTest import kotlin.math.sqrt -import kotlin.system.measureNanoTime +import org.jetbrains.report.BenchmarkResult val BENCHMARK_SIZE = 100 @@ -28,8 +28,9 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { class Results(val mean: Double, val variance: Double) val results = mutableMapOf() + val benchmarkResults = mutableListOf() - fun launch(benchmark: () -> Any?): Results { // If benchmark runs too long - use coeff to speed it up. + fun launch(benchmark: () -> Any?, name: String) { // If benchmark runs too long - use coeff to speed it up. var i = numWarmIterations while (i-- > 0) benchmark() @@ -58,17 +59,22 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { } cleanup() } - samples[k] = time * 1.0 / autoEvaluatedNumberOfMeasureIteration + val scaledTime = time * 1.0 / autoEvaluatedNumberOfMeasureIteration + samples[k] = scaledTime + // Save benchmark object + benchmarkResults.add(BenchmarkResult(name, BenchmarkResult.Status.PASSED, + scaledTime / 1000, scaledTime / 1000, + k + 1, numWarmIterations)) } val mean = samples.sum() / numberOfAttempts val variance = samples.indices.sumByDouble { (samples[it] - mean) * (samples[it] - mean) } / numberOfAttempts - return Results(mean, variance) + results[name] = Results(mean, variance) } //-------------------------------------------------------------------------// - fun runBenchmarks() { + fun runBenchmarks(): List { runAbstractMethodBenchmark() runClassArrayBenchmark() runClassBaselineBenchmark() @@ -96,6 +102,7 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { runOctoTest() printResultsNormalized() + return benchmarkResults } //-------------------------------------------------------------------------// @@ -135,8 +142,8 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runAbstractMethodBenchmark() { val benchmark = AbstractMethodBenchmark() - results["AbstractMethod.sortStrings"] = launch(benchmark::sortStrings) - results["AbstractMethod.sortStringsWithComparator"] = launch(benchmark::sortStringsWithComparator) + launch(benchmark::sortStrings, "AbstractMethod.sortStrings") + launch(benchmark::sortStringsWithComparator, "AbstractMethod.sortStringsWithComparator") } //-------------------------------------------------------------------------// @@ -145,16 +152,16 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = ClassArrayBenchmark() benchmark.setup() - results["ClassArray.copy"] = launch(benchmark::copy) - results["ClassArray.copyManual"] = launch(benchmark::copyManual) - results["ClassArray.filterAndCount"] = launch(benchmark::filterAndCount) - results["ClassArray.filterAndMap"] = launch(benchmark::filterAndMap) - results["ClassArray.filterAndMapManual"] = launch(benchmark::filterAndMapManual) - results["ClassArray.filter"] = launch(benchmark::filter) - results["ClassArray.filterManual"] = launch(benchmark::filterManual) - results["ClassArray.countFilteredManual"] = launch(benchmark::countFilteredManual) - results["ClassArray.countFiltered"] = launch(benchmark::countFiltered) - results["ClassArray.countFilteredLocal"] = launch(benchmark::countFilteredLocal) + launch(benchmark::copy,"ClassArray.copy") + launch(benchmark::copyManual, "ClassArray.copyManual") + launch(benchmark::filterAndCount, "ClassArray.filterAndCount") + launch(benchmark::filterAndMap, "ClassArray.filterAndMap") + launch(benchmark::filterAndMapManual, "ClassArray.filterAndMapManual") + launch(benchmark::filter, "ClassArray.filter") + launch(benchmark::filterManual, "ClassArray.filterManual") + launch(benchmark::countFilteredManual, "ClassArray.countFilteredManual") + launch(benchmark::countFiltered, "ClassArray.countFiltered") + launch(benchmark::countFilteredLocal, "ClassArray.countFilteredLocal") } //-------------------------------------------------------------------------// @@ -162,13 +169,13 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runClassBaselineBenchmark() { val benchmark = ClassBaselineBenchmark() - results["ClassBaseline.consume"] = launch(benchmark::consume) - results["ClassBaseline.consumeField"] = launch(benchmark::consumeField) - results["ClassBaseline.allocateList"] = launch(benchmark::allocateList) - results["ClassBaseline.allocateArray"] = launch(benchmark::allocateArray) - results["ClassBaseline.allocateListAndFill"] = launch(benchmark::allocateListAndFill) - results["ClassBaseline.allocateListAndWrite"] = launch(benchmark::allocateListAndWrite) - results["ClassBaseline.allocateArrayAndFill"] = launch(benchmark::allocateArrayAndFill) + launch(benchmark::consume, "ClassBaseline.consume") + launch(benchmark::consumeField, "ClassBaseline.consumeField") + launch(benchmark::allocateList, "ClassBaseline.allocateList") + launch(benchmark::allocateArray, "ClassBaseline.allocateArray") + launch(benchmark::allocateListAndFill, "ClassBaseline.allocateListAndFill") + launch(benchmark::allocateListAndWrite, "ClassBaseline.allocateListAndWrite") + launch(benchmark::allocateArrayAndFill, "ClassBaseline.allocateArrayAndFill") } //-------------------------------------------------------------------------// @@ -177,22 +184,22 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = ClassListBenchmark() benchmark.setup() - results["ClassList.copy"] = launch(benchmark::copy) - results["ClassList.copyManual"] = launch(benchmark::copyManual) - results["ClassList.filterAndCount"] = launch(benchmark::filterAndCount) - results["ClassList.filterAndCountWithLambda"] = launch(benchmark::filterAndCountWithLambda) - results["ClassList.filterWithLambda"] = launch(benchmark::filterWithLambda) - results["ClassList.mapWithLambda"] = launch(benchmark::mapWithLambda) - results["ClassList.countWithLambda"] = launch(benchmark::countWithLambda) - results["ClassList.filterAndMapWithLambda"] = launch(benchmark::filterAndMapWithLambda) - results["ClassList.filterAndMapWithLambdaAsSequence"] = launch(benchmark::filterAndMapWithLambdaAsSequence) - results["ClassList.filterAndMap"] = launch(benchmark::filterAndMap) - results["ClassList.filterAndMapManual"] = launch(benchmark::filterAndMapManual) - results["ClassList.filter"] = launch(benchmark::filter) - results["ClassList.filterManual"] = launch(benchmark::filterManual) - results["ClassList.countFilteredManual"] = launch(benchmark::countFilteredManual) - results["ClassList.countFiltered"] = launch(benchmark::countFiltered) - results["ClassList.reduce"] = launch(benchmark::reduce) + launch(benchmark::copy, "ClassList.copy") + launch(benchmark::copyManual, "ClassList.copyManual") + launch(benchmark::filterAndCount, "ClassList.filterAndCount") + launch(benchmark::filterAndCountWithLambda, "ClassList.filterAndCountWithLambda") + launch(benchmark::filterWithLambda, "ClassList.filterWithLambda") + launch(benchmark::mapWithLambda, "ClassList.mapWithLambda") + launch(benchmark::countWithLambda, "ClassList.countWithLambda") + launch(benchmark::filterAndMapWithLambda, "ClassList.filterAndMapWithLambda") + launch(benchmark::filterAndMapWithLambdaAsSequence, "ClassList.filterAndMapWithLambdaAsSequence") + launch(benchmark::filterAndMap, "ClassList.filterAndMap") + launch(benchmark::filterAndMapManual, "ClassList.filterAndMapManual") + launch(benchmark::filter, "ClassList.filter") + launch(benchmark::filterManual, "ClassList.filterManual") + launch(benchmark::countFilteredManual, "ClassList.countFilteredManual") + launch(benchmark::countFiltered, "ClassList.countFiltered") + launch(benchmark::reduce, "ClassList.reduce") } //-------------------------------------------------------------------------// @@ -201,16 +208,16 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = ClassStreamBenchmark() benchmark.setup() - results["ClassStream.copy"] = launch(benchmark::copy) - results["ClassStream.copyManual"] = launch(benchmark::copyManual) - results["ClassStream.filterAndCount"] = launch(benchmark::filterAndCount) - results["ClassStream.filterAndMap"] = launch(benchmark::filterAndMap) - results["ClassStream.filterAndMapManual"] = launch(benchmark::filterAndMapManual) - results["ClassStream.filter"] = launch(benchmark::filter) - results["ClassStream.filterManual"] = launch(benchmark::filterManual) - results["ClassStream.countFilteredManual"] = launch(benchmark::countFilteredManual) - results["ClassStream.countFiltered"] = launch(benchmark::countFiltered) - results["ClassStream.reduce"] = launch(benchmark::reduce) + launch(benchmark::copy, "ClassStream.copy") + launch(benchmark::copyManual, "ClassStream.copyManual") + launch(benchmark::filterAndCount, "ClassStream.filterAndCount") + launch(benchmark::filterAndMap, "ClassStream.filterAndMap") + launch(benchmark::filterAndMapManual, "ClassStream.filterAndMapManual") + launch(benchmark::filter, "ClassStream.filter") + launch(benchmark::filterManual, "ClassStream.filterManual") + launch(benchmark::countFilteredManual, "ClassStream.countFilteredManual") + launch(benchmark::countFiltered, "ClassStream.countFiltered") + launch(benchmark::reduce, "ClassStream.reduce") } //-------------------------------------------------------------------------// @@ -218,8 +225,8 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runCompanionObjectBenchmark() { val benchmark = CompanionObjectBenchmark() - results["CompanionObject.invokeRegularFunction"] = launch(benchmark::invokeRegularFunction) - results["CompanionObject.invokeJvmStaticFunction"] = launch(benchmark::invokeJvmStaticFunction) + launch(benchmark::invokeRegularFunction, "CompanionObject.invokeRegularFunction") + launch(benchmark::invokeJvmStaticFunction, "CompanionObject.invokeJvmStaticFunction") } //-------------------------------------------------------------------------// @@ -228,12 +235,12 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = DefaultArgumentBenchmark() benchmark.setup() - results["DefaultArgument.testOneOfTwo"] = launch(benchmark::testOneOfTwo) - results["DefaultArgument.testTwoOfTwo"] = launch(benchmark::testTwoOfTwo) - results["DefaultArgument.testOneOfFour"] = launch(benchmark::testOneOfFour) - results["DefaultArgument.testFourOfFour"] = launch(benchmark::testFourOfFour) - results["DefaultArgument.testOneOfEight"] = launch(benchmark::testOneOfEight) - results["DefaultArgument.testEightOfEight"] = launch(benchmark::testEightOfEight) + launch(benchmark::testOneOfTwo, "DefaultArgument.testOneOfTwo") + launch(benchmark::testTwoOfTwo, "DefaultArgument.testTwoOfTwo") + launch(benchmark::testOneOfFour, "DefaultArgument.testOneOfFour") + launch(benchmark::testFourOfFour, "DefaultArgument.testFourOfFour") + launch(benchmark::testOneOfEight, "DefaultArgument.testOneOfEight") + launch(benchmark::testEightOfEight, "DefaultArgument.testEightOfEight") } //-------------------------------------------------------------------------// @@ -242,7 +249,7 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = ElvisBenchmark() benchmark.setup() - results["Elvis.testElvis"] = launch(benchmark::testElvis) + launch(benchmark::testElvis, "Elvis.testElvis") } //-------------------------------------------------------------------------// @@ -250,14 +257,14 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runEulerBenchmark() { val benchmark = EulerBenchmark() - results["Euler.problem1bySequence"] = launch(benchmark::problem1bySequence) - results["Euler.problem1"] = launch(benchmark::problem1) - results["Euler.problem2"] = launch(benchmark::problem2) - results["Euler.problem4"] = launch(benchmark::problem4) - results["Euler.problem8"] = launch(benchmark::problem8) - results["Euler.problem9"] = launch(benchmark::problem9) - results["Euler.problem14"] = launch(benchmark::problem14) - results["Euler.problem14full"] = launch(benchmark::problem14full) + launch(benchmark::problem1bySequence, "Euler.problem1bySequence") + launch(benchmark::problem1, "Euler.problem1") + launch(benchmark::problem2, "Euler.problem2") + launch(benchmark::problem4, "Euler.problem4") + launch(benchmark::problem8, "Euler.problem8") + launch(benchmark::problem9, "Euler.problem9") + launch(benchmark::problem14, "Euler.problem14") + launch(benchmark::problem14full, "Euler.problem14full") } @@ -265,10 +272,10 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runFibonacciBenchmark() { val benchmark = FibonacciBenchmark() - results["Fibonacci.calcClassic"] = launch(benchmark::calcClassic) - results["Fibonacci.calc"] = launch(benchmark::calc) - results["Fibonacci.calcWithProgression"] = launch(benchmark::calcWithProgression) - results["Fibonacci.calcSquare"] = launch(benchmark::calcSquare) + launch(benchmark::calcClassic, "Fibonacci.calcClassic") + launch(benchmark::calc, "Fibonacci.calc") + launch(benchmark::calcWithProgression, "Fibonacci.calcWithProgression") + launch(benchmark::calcSquare, "Fibonacci.calcSquare") } //-------------------------------------------------------------------------// @@ -276,27 +283,27 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runForLoopBenchmark() { val benchmark = ForLoopsBenchmark() - results["ForLoops.arrayLoop"] = launch(benchmark::arrayLoop) - results["ForLoops.intArrayLoop"] = launch(benchmark::intArrayLoop) - results["ForLoops.floatArrayLoop"] = launch(benchmark::floatArrayLoop) - results["ForLoops.charArrayLoop"] = launch(benchmark::charArrayLoop) - results["ForLoops.stringLoop"] = launch(benchmark::stringLoop) + launch(benchmark::arrayLoop, "ForLoops.arrayLoop") + launch(benchmark::intArrayLoop, "ForLoops.intArrayLoop") + launch(benchmark::floatArrayLoop, "ForLoops.floatArrayLoop") + launch(benchmark::charArrayLoop, "ForLoops.charArrayLoop") + launch(benchmark::stringLoop, "ForLoops.stringLoop") - results["ForLoops.arrayIndicesLoop"] = launch(benchmark::arrayIndicesLoop) - results["ForLoops.intArrayIndicesLoop"] = launch(benchmark::intArrayIndicesLoop) - results["ForLoops.floatArrayIndicesLoop"] = launch(benchmark::floatArrayIndicesLoop) - results["ForLoops.charArrayIndicesLoop"] = launch(benchmark::charArrayIndicesLoop) - results["ForLoops.stringIndicesLoop"] = launch(benchmark::stringIndicesLoop) + launch(benchmark::arrayIndicesLoop, "ForLoops.arrayIndicesLoop") + launch(benchmark::intArrayIndicesLoop, "ForLoops.intArrayIndicesLoop") + launch(benchmark::floatArrayIndicesLoop, "ForLoops.floatArrayIndicesLoop") + launch(benchmark::charArrayIndicesLoop, "ForLoops.charArrayIndicesLoop") + launch(benchmark::stringIndicesLoop, "ForLoops.stringIndicesLoop") } //-------------------------------------------------------------------------// fun runInlineBenchmark() { val benchmark = InlineBenchmark() - results["Inline.calculate"] = launch(benchmark::calculate) - results["Inline.calculateInline"] = launch(benchmark::calculateInline) - results["Inline.calculateGeneric"] = launch(benchmark::calculateGeneric) - results["Inline.calculateGenericInline"] = launch(benchmark::calculateGenericInline) + launch(benchmark::calculate, "Inline.calculate") + launch(benchmark::calculateInline, "Inline.calculateInline") + launch(benchmark::calculateGeneric, "Inline.calculateGeneric") + launch(benchmark::calculateGenericInline, "Inline.calculateGenericInline") } //-------------------------------------------------------------------------// @@ -305,26 +312,26 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = IntArrayBenchmark() benchmark.setup() - results["IntArray.copy"] = launch(benchmark::copy) - results["IntArray.copyManual"] = launch(benchmark::copyManual) - results["IntArray.filterAndCount"] = launch(benchmark::filterAndCount) - results["IntArray.filterSomeAndCount"] = launch(benchmark::filterSomeAndCount) - results["IntArray.filterAndMap"] = launch(benchmark::filterAndMap) - results["IntArray.filterAndMapManual"] = launch(benchmark::filterAndMapManual) - results["IntArray.filter"] = launch(benchmark::filter) - results["IntArray.filterSome"] = launch(benchmark::filterSome) - results["IntArray.filterPrime"] = launch(benchmark::filterPrime) - results["IntArray.filterManual"] = launch(benchmark::filterManual) - results["IntArray.filterSomeManual"] = launch(benchmark::filterSomeManual) - results["IntArray.countFilteredManual"] = launch(benchmark::countFilteredManual) - results["IntArray.countFilteredSomeManual"] = launch(benchmark::countFilteredSomeManual) - results["IntArray.countFilteredPrimeManual"] = launch(benchmark::countFilteredPrimeManual) - results["IntArray.countFiltered"] = launch(benchmark::countFiltered) - results["IntArray.countFilteredSome"] = launch(benchmark::countFilteredSome) - results["IntArray.countFilteredPrime"] = launch(benchmark::countFilteredPrime) - results["IntArray.countFilteredLocal"] = launch(benchmark::countFilteredLocal) - results["IntArray.countFilteredSomeLocal"] = launch(benchmark::countFilteredSomeLocal) - results["IntArray.reduce"] = launch(benchmark::reduce) + launch(benchmark::copy, "IntArray.copy") + launch(benchmark::copyManual, "IntArray.copyManual") + launch(benchmark::filterAndCount, "IntArray.filterAndCount") + launch(benchmark::filterSomeAndCount, "IntArray.filterSomeAndCount") + launch(benchmark::filterAndMap, "IntArray.filterAndMap") + launch(benchmark::filterAndMapManual, "IntArray.filterAndMapManual") + launch(benchmark::filter, "IntArray.filter") + launch(benchmark::filterSome, "IntArray.filterSome") + launch(benchmark::filterPrime, "IntArray.filterPrime") + launch(benchmark::filterManual, "IntArray.filterManual") + launch(benchmark::filterSomeManual, "IntArray.filterSomeManual") + launch(benchmark::countFilteredManual, "IntArray.countFilteredManual") + launch(benchmark::countFilteredSomeManual, "IntArray.countFilteredSomeManual") + launch(benchmark::countFilteredPrimeManual, "IntArray.countFilteredPrimeManual") + launch(benchmark::countFiltered, "IntArray.countFiltered") + launch(benchmark::countFilteredSome, "IntArray.countFilteredSome") + launch(benchmark::countFilteredPrime, "IntArray.countFilteredPrime") + launch(benchmark::countFilteredLocal, "IntArray.countFilteredLocal") + launch(benchmark::countFilteredSomeLocal, "IntArray.countFilteredSomeLocal") + launch(benchmark::reduce, "IntArray.reduce") } //-------------------------------------------------------------------------// @@ -332,11 +339,11 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runIntBaselineBenchmark() { val benchmark = IntBaselineBenchmark() - results["IntBaseline.consume"] = launch(benchmark::consume) - results["IntBaseline.allocateList"] = launch(benchmark::allocateList) - results["IntBaseline.allocateArray"] = launch(benchmark::allocateArray) - results["IntBaseline.allocateListAndFill"] = launch(benchmark::allocateListAndFill) - results["IntBaseline.allocateArrayAndFill"] = launch(benchmark::allocateArrayAndFill) + launch(benchmark::consume, "IntBaseline.consume") + launch(benchmark::allocateList, "IntBaseline.allocateList") + launch(benchmark::allocateArray, "IntBaseline.allocateArray") + launch(benchmark::allocateListAndFill, "IntBaseline.allocateListAndFill") + launch(benchmark::allocateArrayAndFill, "IntBaseline.allocateArrayAndFill") } //-------------------------------------------------------------------------// @@ -345,17 +352,17 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = IntListBenchmark() benchmark.setup() - results["IntList.copy"] = launch(benchmark::copy) - results["IntList.copyManual"] = launch(benchmark::copyManual) - results["IntList.filterAndCount"] = launch(benchmark::filterAndCount) - results["IntList.filterAndMap"] = launch(benchmark::filterAndMap) - results["IntList.filterAndMapManual"] = launch(benchmark::filterAndMapManual) - results["IntList.filter"] = launch(benchmark::filter) - results["IntList.filterManual"] = launch(benchmark::filterManual) - results["IntList.countFilteredManual"] = launch(benchmark::countFilteredManual) - results["IntList.countFiltered"] = launch(benchmark::countFiltered) - results["IntList.countFilteredLocal"] = launch(benchmark::countFilteredLocal) - results["IntList.reduce"] = launch(benchmark::reduce) + launch(benchmark::copy, "IntList.copy") + launch(benchmark::copyManual, "IntList.copyManual") + launch(benchmark::filterAndCount, "IntList.filterAndCount") + launch(benchmark::filterAndMap, "IntList.filterAndMap") + launch(benchmark::filterAndMapManual, "IntList.filterAndMapManual") + launch(benchmark::filter, "IntList.filter") + launch(benchmark::filterManual, "IntList.filterManual") + launch(benchmark::countFilteredManual, "IntList.countFilteredManual") + launch(benchmark::countFiltered, "IntList.countFiltered") + launch(benchmark::countFilteredLocal, "IntList.countFilteredLocal") + launch(benchmark::reduce, "IntList.reduce") } //-------------------------------------------------------------------------// @@ -364,17 +371,17 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = IntStreamBenchmark() benchmark.setup() - results["IntStream.copy"] = launch(benchmark::copy) - results["IntStream.copyManual"] = launch(benchmark::copyManual) - results["IntStream.filterAndCount"] = launch(benchmark::filterAndCount) - results["IntStream.filterAndMap"] = launch(benchmark::filterAndMap) - results["IntStream.filterAndMapManual"] = launch(benchmark::filterAndMapManual) - results["IntStream.filter"] = launch(benchmark::filter) - results["IntStream.filterManual"] = launch(benchmark::filterManual) - results["IntStream.countFilteredManual"] = launch(benchmark::countFilteredManual) - results["IntStream.countFiltered"] = launch(benchmark::countFiltered) - results["IntStream.countFilteredLocal"] = launch(benchmark::countFilteredLocal) - results["IntStream.reduce"] = launch(benchmark::reduce) + launch(benchmark::copy, "IntStream.copy") + launch(benchmark::copyManual, "IntStream.copyManual") + launch(benchmark::filterAndCount, "IntStream.filterAndCount") + launch(benchmark::filterAndMap, "IntStream.filterAndMap") + launch(benchmark::filterAndMapManual, "IntStream.filterAndMapManual") + launch(benchmark::filter, "IntStream.filter") + launch(benchmark::filterManual, "IntStream.filterManual") + launch(benchmark::countFilteredManual, "IntStream.countFilteredManual") + launch(benchmark::countFiltered, "IntStream.countFiltered") + launch(benchmark::countFilteredLocal, "IntStream.countFilteredLocal") + launch(benchmark::reduce, "IntStream.reduce") } //-------------------------------------------------------------------------// @@ -383,14 +390,14 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = LambdaBenchmark() benchmark.setup() - results["Lambda.noncapturingLambda"] = launch(benchmark::noncapturingLambda) - results["Lambda.noncapturingLambdaNoInline"] = launch(benchmark::noncapturingLambdaNoInline) - results["Lambda.capturingLambda"] = launch(benchmark::capturingLambda) - results["Lambda.capturingLambdaNoInline"] = launch(benchmark::capturingLambdaNoInline) - results["Lambda.mutatingLambda"] = launch(benchmark::mutatingLambda) - results["Lambda.mutatingLambdaNoInline"] = launch(benchmark::mutatingLambdaNoInline) - results["Lambda.methodReference"] = launch(benchmark::methodReference) - results["Lambda.methodReferenceNoInline"] = launch(benchmark::methodReferenceNoInline) + launch(benchmark::noncapturingLambda, "Lambda.noncapturingLambda") + launch(benchmark::noncapturingLambdaNoInline, "Lambda.noncapturingLambdaNoInline") + launch(benchmark::capturingLambda, "Lambda.capturingLambda") + launch(benchmark::capturingLambdaNoInline, "Lambda.capturingLambdaNoInline") + launch(benchmark::mutatingLambda, "Lambda.mutatingLambda") + launch(benchmark::mutatingLambdaNoInline, "Lambda.mutatingLambdaNoInline") + launch(benchmark::methodReference, "Lambda.methodReference") + launch(benchmark::methodReferenceNoInline, "Lambda.methodReferenceNoInline") } //-------------------------------------------------------------------------// @@ -399,13 +406,13 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = LoopBenchmark() benchmark.setup() - results["Loop.arrayLoop"] = launch(benchmark::arrayLoop) - results["Loop.arrayIndexLoop"] = launch(benchmark::arrayIndexLoop) - results["Loop.rangeLoop"] = launch(benchmark::rangeLoop) - results["Loop.arrayListLoop"] = launch(benchmark::arrayListLoop) - results["Loop.arrayWhileLoop"] = launch(benchmark::arrayWhileLoop) - results["Loop.arrayForeachLoop"] = launch(benchmark::arrayForeachLoop) - results["Loop.arrayListForeachLoop"] = launch(benchmark::arrayListForeachLoop) + launch(benchmark::arrayLoop, "Loop.arrayLoop") + launch(benchmark::arrayIndexLoop, "Loop.arrayIndexLoop") + launch(benchmark::rangeLoop, "Loop.rangeLoop") + launch(benchmark::arrayListLoop, "Loop.arrayListLoop") + launch(benchmark::arrayWhileLoop, "Loop.arrayWhileLoop") + launch(benchmark::arrayForeachLoop, "Loop.arrayForeachLoop") + launch(benchmark::arrayListForeachLoop, "Loop.arrayListForeachLoop") } //-------------------------------------------------------------------------// @@ -413,7 +420,7 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runMatrixMapBenchmark() { val benchmark = MatrixMapBenchmark() - results["MatrixMap.add"] = launch(benchmark::add) + launch(benchmark::add, "MatrixMap.add") } //-------------------------------------------------------------------------// @@ -421,12 +428,12 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runParameterNotNullAssertionBenchmark() { val benchmark = ParameterNotNullAssertionBenchmark() - results["ParameterNotNull.invokeOneArgWithNullCheck"] = launch(benchmark::invokeOneArgWithNullCheck) - results["ParameterNotNull.invokeOneArgWithoutNullCheck"] = launch(benchmark::invokeOneArgWithoutNullCheck) - results["ParameterNotNull.invokeTwoArgsWithNullCheck"] = launch(benchmark::invokeTwoArgsWithNullCheck) - results["ParameterNotNull.invokeTwoArgsWithoutNullCheck"] = launch(benchmark::invokeTwoArgsWithoutNullCheck) - results["ParameterNotNull.invokeEightArgsWithNullCheck"] = launch(benchmark::invokeEightArgsWithNullCheck) - results["ParameterNotNull.invokeEightArgsWithoutNullCheck"] = launch(benchmark::invokeEightArgsWithoutNullCheck) + launch(benchmark::invokeOneArgWithNullCheck, "ParameterNotNull.invokeOneArgWithNullCheck") + launch(benchmark::invokeOneArgWithoutNullCheck, "ParameterNotNull.invokeOneArgWithoutNullCheck") + launch(benchmark::invokeTwoArgsWithNullCheck, "ParameterNotNull.invokeTwoArgsWithNullCheck") + launch(benchmark::invokeTwoArgsWithoutNullCheck, "ParameterNotNull.invokeTwoArgsWithoutNullCheck") + launch(benchmark::invokeEightArgsWithNullCheck, "ParameterNotNull.invokeEightArgsWithNullCheck") + launch(benchmark::invokeEightArgsWithoutNullCheck, "ParameterNotNull.invokeEightArgsWithoutNullCheck") } //-------------------------------------------------------------------------// @@ -434,8 +441,8 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { fun runPrimeListBenchmark() { val benchmark = PrimeListBenchmark() - results["PrimeList.calcDirect"] = launch(benchmark::calcDirect) - results["PrimeList.calcEratosthenes"] = launch(benchmark::calcEratosthenes) + launch(benchmark::calcDirect, "PrimeList.calcDirect") + launch(benchmark::calcEratosthenes, "PrimeList.calcEratosthenes") } //-------------------------------------------------------------------------// @@ -444,11 +451,11 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = StringBenchmark() benchmark.setup() - results["String.stringConcat"] = launch(benchmark::stringConcat) - results["String.stringConcatNullable"] = launch(benchmark::stringConcatNullable) - results["String.stringBuilderConcat"] = launch(benchmark::stringBuilderConcat) - results["String.stringBuilderConcatNullable"] = launch(benchmark::stringBuilderConcatNullable) - results["String.summarizeSplittedCsv"] = launch(benchmark::summarizeSplittedCsv) + launch(benchmark::stringConcat, "String.stringConcat") + launch(benchmark::stringConcatNullable, "String.stringConcatNullable") + launch(benchmark::stringBuilderConcat, "String.stringBuilderConcat") + launch(benchmark::stringBuilderConcatNullable, "String.stringBuilderConcatNullable") + launch(benchmark::summarizeSplittedCsv, "String.summarizeSplittedCsv") } //-------------------------------------------------------------------------// @@ -460,15 +467,15 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { benchmark.setupEnums() benchmark.setupSealedClassses() - results["Switch.testSparseIntSwitch"] = launch(benchmark::testSparseIntSwitch) - results["Switch.testDenseIntSwitch"] = launch(benchmark::testDenseIntSwitch) - results["Switch.testConstSwitch"] = launch(benchmark::testConstSwitch) - results["Switch.testObjConstSwitch"] = launch(benchmark::testObjConstSwitch) - results["Switch.testVarSwitch"] = launch(benchmark::testVarSwitch) - results["Switch.testStringsSwitch"] = launch(benchmark::testStringsSwitch) - results["Switch.testEnumsSwitch"] = launch(benchmark::testEnumsSwitch) - results["Switch.testDenseEnumsSwitch"] = launch(benchmark::testDenseEnumsSwitch) - results["Switch.testSealedWhenSwitch"] = launch(benchmark::testSealedWhenSwitch) + launch(benchmark::testSparseIntSwitch, "Switch.testSparseIntSwitch") + launch(benchmark::testDenseIntSwitch, "Switch.testDenseIntSwitch") + launch(benchmark::testConstSwitch, "Switch.testConstSwitch") + launch(benchmark::testObjConstSwitch, "Switch.testObjConstSwitch") + launch(benchmark::testVarSwitch, "Switch.testVarSwitch") + launch(benchmark::testStringsSwitch, "Switch.testStringsSwitch") + launch(benchmark::testEnumsSwitch, "Switch.testEnumsSwitch") + launch(benchmark::testDenseEnumsSwitch, "Switch.testDenseEnumsSwitch") + launch(benchmark::testSealedWhenSwitch, "Switch.testSealedWhenSwitch") } //-------------------------------------------------------------------------// @@ -477,14 +484,14 @@ class Launcher(val numWarmIterations: Int, val numberOfAttempts: Int) { val benchmark = WithIndiciesBenchmark() benchmark.setup() - results["WithIndicies.withIndicies"] = launch(benchmark::withIndicies) - results["WithIndicies.withIndiciesManual"] = launch(benchmark::withIndiciesManual) + launch(benchmark::withIndicies, "WithIndicies.withIndicies") + launch(benchmark::withIndiciesManual, "WithIndicies.withIndiciesManual") } //-------------------------------------------------------------------------// fun runOctoTest() { - results["OctoTest"] = launch(::octoTest) + launch(::octoTest, "OctoTest") } //-------------------------------------------------------------------------// diff --git a/settings.gradle b/settings.gradle index 02423f46739..9033cf52713 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,7 +28,7 @@ include ':common' include ':backend.native:tests' include ':backend.native:debugger-tests' include ':utilities' -include ':performance' +//include ':performance' TODO: return when there is one HostManager class version include ':platformLibs' includeBuild 'shared' diff --git a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/BenchmarksReport.kt b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/BenchmarksReport.kt new file mode 100644 index 00000000000..8448d6b4321 --- /dev/null +++ b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/BenchmarksReport.kt @@ -0,0 +1,325 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.jetbrains.report + +import org.jetbrains.report.json.* + +interface JsonSerializable { + fun toJson(): String + + // Convert iterable objects arrays, lists to json. + fun arrayToJson(data: Iterable): String { + return data.joinToString(prefix = "[", postfix = "]") { + if (it is JsonSerializable) it.toJson() else it.toString() + } + } +} + +// Entity can be created from json description. +interface ConvertedFromJson { + fun getRequiredField(data: JsonObject, fieldName: String): JsonElement { + return data.getOrNull(fieldName) ?: error("Field '$fieldName' doesn't exist in '$data'. Please, check origin files.") + } + + fun getOptionalField(data: JsonObject, fieldName: String): JsonElement? { + return data.getOrNull(fieldName) + } + + // Parse json array to list. + // Takes function to convert elements in array to expected type. + fun arrayToList(array: JsonArray, convert: JsonArray.(Int) -> T?): List { + var results = mutableListOf() + var index = 0 + var current: T? = array.convert(index) + while (current != null) { + results.add(current) + index++ + current = array.convert(index) + } + return results + } + + // Methods for conversion to expected type with checks of possibility of such conversions. + fun elementToDouble(element: JsonElement, name: String): Double = + if (element is JsonPrimitive) + element.double + else + error("Field '$name' in '$element' is expected to be a double number. Please, check origin files.") + + fun elementToInt(element: JsonElement, name: String): Int = + if (element is JsonPrimitive) + element.int + else + error("Field '$name' in '$element' is expected to be an integer number. Please, check origin files.") + + fun elementToString(element: JsonElement, name:String): String = + if (element is JsonLiteral) + element.unquoted() + else + error("Field '$name' in '$element' is expected to be a string. Please, check origin files.") +} + +interface EntityFromJsonFactory: ConvertedFromJson { + fun create(data: JsonElement): T +} + +// Class for benchcmarks report with all information of run. +data class BenchmarksReport(val env: Environment, val benchmarks: List, val compiler: Compiler): + JsonSerializable { + + companion object: EntityFromJsonFactory { + override fun create(data: JsonElement): BenchmarksReport { + if (data is JsonObject) { + val env = Environment.create(getRequiredField(data, "env")) + val benchmarksObj = getRequiredField(data, "benchmarks") + val compiler = Compiler.create(getRequiredField(data, "kotlin")) + val benchmarks = parseBenchmarksArray(benchmarksObj) + return BenchmarksReport(env, benchmarks, compiler) + } else { + error("Top level entity is expected to be an object. Please, check origin files.") + } + } + + // Parse array with benchmarks to list + fun parseBenchmarksArray(data: JsonElement): List { + if (data is JsonArray) { + return arrayToList(data.jsonArray, { index -> + if (this.getObjectOrNull(index) != null) + BenchmarkResult.create(this.getObjectOrNull(index) as JsonObject) + else null + }) + } else { + error("Benchmarks field is expected to be an array. Please, check origin files.") + } + } + } + + override fun toJson(): String { + return """ + { + "env": ${env.toJson()}, + "kotlin": ${compiler.toJson()}, + "benchmarks": ${arrayToJson(benchmarks)} + } + """ + } +} + +// Class for kotlin compiler +data class Compiler(val backend: Backend, val kotlinVersion: String): JsonSerializable { + + enum class BackendType(val type: String) { + JVM("jvm"), + NATIVE("native") + } + + companion object: EntityFromJsonFactory { + override fun create(data: JsonElement): Compiler { + if (data is JsonObject) { + val backend = Backend.create(getRequiredField(data, "backend")) + val kotlinVersion = elementToString(getRequiredField(data, "kotlinVersion"), "kotlinVersion") + + return Compiler(backend, kotlinVersion) + } else { + error("Kotlin entity is expected to be an object. Please, check origin files.") + } + } + + fun backendTypeFromString(s: String): BackendType? = BackendType.values().find { it.type == s } + } + + // Class for compiler backend + data class Backend(val type: BackendType, val version: String, val flags: List): JsonSerializable { + companion object: EntityFromJsonFactory { + override fun create(data: JsonElement): Backend { + if (data is JsonObject) { + val typeElement = getRequiredField(data, "type") + if (typeElement is JsonLiteral) { + val type = backendTypeFromString(typeElement.unquoted()) ?: error("Backend type should be 'jvm' or 'native'") + val version = elementToString(getRequiredField(data, "version"), "version") + val flagsArray = getOptionalField(data, "flags") + var flags: List = emptyList() + if (flagsArray != null && flagsArray is JsonArray) { + flags = arrayToList(flagsArray.jsonArray, { index -> + this.getPrimitiveOrNull(index)?.toString() + }) + } + return Backend(type, version, flags) + } else { + error("Backend type should be string literal.") + } + } else { + error("Backend entity is expected to be an object. Please, check origin files.") + } + } + } + + override fun toJson(): String { + val result = """ + { + "type": "${type.type}", + "version": "${version}"""" + // Don't print flags field if there is no one. + if (flags.isEmpty()) { + return """$result + }""" + } + else { + return """ + $result, + "flags": ${arrayToJson(flags)} + } + """ + } + } + } + + override fun toJson(): String { + return """ + { + "backend": ${backend.toJson()}, + "kotlinVersion": "${kotlinVersion}" + } + """ + } +} + +// Class for description of environment of benchmarks run +data class Environment(val machine: Machine, val jdk: JDKInstance): JsonSerializable { + + companion object: EntityFromJsonFactory { + override fun create(data: JsonElement): Environment { + if (data is JsonObject) { + val machine = Machine.create(getRequiredField(data, "machine")) + val jdk = JDKInstance.create(getRequiredField(data, "jdk")) + + return Environment(machine, jdk) + } else { + error("Environment entity is expected to be an object. Please, check origin files.") + } + } + } + + // Class for description of machine used for benchmarks run. + data class Machine(val cpu: String, val os: String): JsonSerializable { + companion object: EntityFromJsonFactory { + override fun create(data: JsonElement): Machine { + if (data is JsonObject) { + val cpu = elementToString(getRequiredField(data, "cpu"), "cpu") + val os = elementToString(getRequiredField(data, "os"), "os") + + return Machine(cpu, os) + } else { + error("Machine entity is expected to be an object. Please, check origin files.") + } + } + } + + override fun toJson(): String { + return """ + { + "cpu": "$cpu", + "os": "$os" + } + """ + } + } + + // Class for description of jdk used for benchmarks run. + data class JDKInstance(val version: String, val vendor: String): JsonSerializable { + companion object: EntityFromJsonFactory { + override fun create(data: JsonElement): JDKInstance { + if (data is JsonObject) { + val version = elementToString(getRequiredField(data, "version"), "version") + val vendor = elementToString(getRequiredField(data, "vendor"), "vendor") + + return JDKInstance(version, vendor) + } else { + error("JDK entity is expected to be an object. Please, check origin files.") + } + } + } + + override fun toJson(): String { + return """ + { + "version": "$version", + "vendor": "$vendor" + } + """ + } + } + + override fun toJson(): String { + return """ + { + "machine": ${machine.toJson()}, + "jdk": ${jdk.toJson()} + } + """ + } +} + +class BenchmarkResult(val name: String, val status: Status, + val score: Double, val runtimeInUs: Double, + val repeat: Int, val warmup: Int): JsonSerializable { + + companion object: EntityFromJsonFactory { + + override fun create(data: JsonElement): BenchmarkResult { + if (data is JsonObject) { + val name = elementToString(getRequiredField(data, "name"), "name") + val statusElement = getRequiredField(data, "status") + if (statusElement is JsonLiteral) { + val status = statusFromString(statusElement.unquoted()) + ?: error("Status should be PASSED or FAILED") + val score = elementToDouble(getRequiredField(data, "score"), "score") + val runtimeInUs = elementToDouble(getRequiredField(data, "runtimeInUs"), "runtimeInUs") + val repeat = elementToInt(getRequiredField(data, "repeat"), "repeat") + val warmup = elementToInt(getRequiredField(data, "warmup"), "warmup") + + return BenchmarkResult(name, status, score, runtimeInUs, repeat, warmup) + } else { + error("Status should be string literal.") + } + } else { + error("Benchmark entity is expected to be an object. Please, check origin files.") + } + } + + fun statusFromString(s: String): Status? = Status.values().find { it.value == s } + } + + enum class Status(val value: String) { + PASSED("PASSED"), + FAILED("FAILED") + } + + override fun toJson(): String { + return """ + { + "name": "$name", + "status": "${status.value}", + "score": ${score}, + "runtimeInUs": ${runtimeInUs}, + "repeat": ${repeat}, + "warmup": ${warmup} + } + """ + } +} \ No newline at end of file diff --git a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonElement.kt b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonElement.kt new file mode 100644 index 00000000000..18316b41085 --- /dev/null +++ b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonElement.kt @@ -0,0 +1,329 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.report.json + +/** + * Class representing single JSON element. + * Can be [JsonPrimitive], [JsonArray] or [JsonObject]. + * + * [JsonElement.toString] properly prints JSON tree as valid JSON, taking into + * account quoted values and primitives + */ +sealed class JsonElement { + + /** + * Convenience method to get current element as [JsonPrimitive] + * @throws JsonElementTypeMismatchException is current element is not a [JsonPrimitive] + */ + open val primitive: JsonPrimitive + get() = error("JsonLiteral") + + /** + * Convenience method to get current element as [JsonObject] + * @throws JsonElementTypeMismatchException is current element is not a [JsonObject] + */ + open val jsonObject: JsonObject + get() = error("JsonObject") + + /** + * Convenience method to get current element as [JsonArray] + * @throws JsonElementTypeMismatchException is current element is not a [JsonArray] + */ + open val jsonArray: JsonArray + get() = error("JsonArray") + + /** + * Convenience method to get current element as [JsonNull] + * @throws JsonElementTypeMismatchException is current element is not a [JsonNull] + */ + open val jsonNull: JsonNull + get() = error("JsonPrimitive") + + /** + * Checks whether current element is [JsonNull] + */ + val isNull: Boolean + get() = this === JsonNull + + private fun error(element: String): Nothing = + throw JsonElementTypeMismatchException(this::class.toString(), element) +} + +/** + * Class representing JSON primitive value. Can be either [JsonLiteral] or [JsonNull]. + */ +sealed class JsonPrimitive : JsonElement() { + + /** + * Content of given element without quotes. For [JsonNull] this methods returns `"null"` + */ + abstract val content: String + + /** + * Content of the given element without quotes or `null` if current element is [JsonNull] + */ + abstract val contentOrNull: String? + + @Suppress("LeakingThis") + final override val primitive: JsonPrimitive = this + + /** + * Returns content of current element as int + * @throws NumberFormatException if current element is not a valid representation of number + */ + val int: Int get() = content.toInt() + + /** + * Returns content of current element as int or `null` if current element is not a valid representation of number + **/ + val intOrNull: Int? get() = content.toIntOrNull() + + /** + * Returns content of current element as long + * @throws NumberFormatException if current element is not a valid representation of number + */ + val long: Long get() = content.toLong() + + /** + * Returns content of current element as long or `null` if current element is not a valid representation of number + */ + val longOrNull: Long? get() = content.toLongOrNull() + + /** + * Returns content of current element as double + * @throws NumberFormatException if current element is not a valid representation of number + */ + val double: Double get() = content.toDouble() + + /** + * Returns content of current element as double or `null` if current element is not a valid representation of number + */ + val doubleOrNull: Double? get() = content.toDoubleOrNull() + + /** + * Returns content of current element as float + * @throws NumberFormatException if current element is not a valid representation of number + */ + val float: Float get() = content.toFloat() + + /** + * Returns content of current element as float or `null` if current element is not a valid representation of number + */ + val floatOrNull: Float? get() = content.toFloatOrNull() + + /** + * Returns content of current element as boolean + * @throws IllegalStateException if current element doesn't represent boolean + */ + val boolean: Boolean get() = content.toBooleanStrict() + + /** + * Returns content of current element as boolean or `null` if current element is not a valid representation of boolean + */ + val booleanOrNull: Boolean? get() = content.toBooleanStrictOrNull() + + override fun toString() = content +} + +/** + * Class representing JSON literals: numbers, booleans and string. + * Strings are always quoted. + */ +data class JsonLiteral internal constructor( + private val body: Any, + private val isString: Boolean +) : JsonPrimitive() { + + override val content = body.toString() + override val contentOrNull: String = content + + /** + * Creates number literal + */ + constructor(number: Number) : this(number, false) + + /** + * Creates boolean literal + */ + constructor(boolean: Boolean) : this(boolean, false) + + /** + * Creates quoted string literal + */ + constructor(string: String) : this(string, true) + + override fun toString() = + if (isString) buildString { printQuoted(content) } + else content + + fun unquoted() = content +} + +/** + * Class representing JSON `null` value + */ +object JsonNull : JsonPrimitive() { + override val jsonNull: JsonNull = this + override val content: String = "null" + override val contentOrNull: String? = null +} + +/** + * Class representing JSON object, consisting of name-value pairs, where value is arbitrary [JsonElement] + */ +data class JsonObject(val content: Map) : JsonElement(), Map by content { + + override val jsonObject: JsonObject = this + + /** + * Returns [JsonElement] associated with given [key] + * @throws NoSuchElementException if element is not present + */ + override fun get(key: String): JsonElement = content[key] ?: throw NoSuchElementException("Element $key is missing") + + /** + * Returns [JsonElement] associated with given [key] or `null` if element is not present + */ + fun getOrNull(key: String): JsonElement? = content[key] + + /** + * Returns [JsonPrimitive] associated with given [key] + * + * @throws NoSuchElementException if element is not present + * @throws JsonElementTypeMismatchException if element is present, but has invalid type + */ + fun getPrimitive(key: String): JsonPrimitive = get(key) as? JsonPrimitive + ?: unexpectedJson(key, "JsonPrimitive") + + /** + * Returns [JsonObject] associated with given [key] + * + * @throws NoSuchElementException if element is not present + * @throws JsonElementTypeMismatchException if element is present, but has invalid type + */ + fun getObject(key: String): JsonObject = get(key) as? JsonObject + ?: unexpectedJson(key, "JsonObject") + + /** + * Returns [JsonArray] associated with given [key] + * + * @throws NoSuchElementException if element is not present + * @throws JsonElementTypeMismatchException if element is present, but has invalid type + */ + fun getArray(key: String): JsonArray = get(key) as? JsonArray + ?: unexpectedJson(key, "JsonArray") + + /** + * Returns [JsonPrimitive] associated with given [key] or `null` if element + * is not present or has different type + */ + fun getPrimitiveOrNull(key: String): JsonPrimitive? = content[key] as? JsonPrimitive + + /** + * Returns [JsonObject] associated with given [key] or `null` if element + * is not present or has different type + */ + fun getObjectOrNull(key: String): JsonObject? = content[key] as? JsonObject + + /** + * Returns [JsonArray] associated with given [key] or `null` if element + * is not present or has different type + */ + fun getArrayOrNull(key: String): JsonArray? = content[key] as? JsonArray + + /** + * Returns [J] associated with given [key] + * + * @throws NoSuchElementException if element is not present + * @throws JsonElementTypeMismatchException if element is present, but has invalid type + */ + inline fun getAs(key: String): J = get(key) as? J + ?: unexpectedJson(key, J::class.toString()) + + /** + * Returns [J] associated with given [key] or `null` if element + * is not present or has different type + */ + inline fun lookup(key: String): J? = content[key] as? J + + override fun toString(): String { + return content.entries.joinToString( + prefix = "{", + postfix = "}", + transform = {(k, v) -> """"$k": $v"""} + ) + } +} + +data class JsonArray(val content: List) : JsonElement(), List by content { + + override val jsonArray: JsonArray = this + + /** + * Returns [index]-th element of an array as [JsonPrimitive] + * @throws JsonElementTypeMismatchException if element has invalid type + */ + fun getPrimitive(index: Int) = content[index] as? JsonPrimitive + ?: unexpectedJson("at $index", "JsonPrimitive") + + /** + * Returns [index]-th element of an array as [JsonObject] + * @throws JsonElementTypeMismatchException if element has invalid type + */ + fun getObject(index: Int) = content[index] as? JsonObject + ?: unexpectedJson("at $index", "JsonObject") + + /** + * Returns [index]-th element of an array as [JsonArray] + * @throws JsonElementTypeMismatchException if element has invalid type + */ + fun getArray(index: Int) = content[index] as? JsonArray + ?: unexpectedJson("at $index", "JsonArray") + + /** + * Returns [index]-th element of an array as [JsonPrimitive] or `null` if element is missing or has different type + */ + fun getPrimitiveOrNull(index: Int) = content.getOrNull(index) as? JsonPrimitive + + /** + * Returns [index]-th element of an array as [JsonObject] or `null` if element is missing or has different type + */ + fun getObjectOrNull(index: Int) = content.getOrNull(index) as? JsonObject + + /** + * Returns [index]-th element of an array as [JsonArray] or `null` if element is missing or has different type + */ + fun getArrayOrNull(index: Int) = content.getOrNull(index) as? JsonArray + + /** + * Returns [index]-th element of an array as [J] + * @throws JsonElementTypeMismatchException if element has invalid type + */ + inline fun getAs(index: Int): J = content[index] as? J + ?: unexpectedJson("at $index", J::class.toString()) + + /** + * Returns [index]-th element of an array as [J] or `null` if element is missing or has different type + */ + inline fun getAsOrNull(index: Int): J? = content.getOrNull(index) as? J + + override fun toString() = content.joinToString(prefix = "[", postfix = "]") +} + +@PublishedApi +internal fun unexpectedJson(key: String, expected: String): Nothing = + throw JsonElementTypeMismatchException(key, expected) \ No newline at end of file diff --git a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonExceptions.kt b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonExceptions.kt new file mode 100644 index 00000000000..4b7acaa08ec --- /dev/null +++ b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonExceptions.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.report.json + +class JsonInvalidValueInStrictModeException(value: Any, valueDescription: String) : Exception( + "$value is not a valid $valueDescription as per JSON spec.\n" + + "You can disable strict mode to serialize such values" +) { + constructor(floatValue: Float) : this(floatValue, "float") + constructor(doubleValue: Double) : this(doubleValue, "double") +} + +class JsonUnknownKeyException(key: String) : Exception( + "Strict JSON encountered unknown key: $key\n" + + "You can disable strict mode to skip unknown keys" +) + +class JsonParsingException(position: Int, message: String) : Exception("Invalid JSON at $position: $message") + +class JsonElementTypeMismatchException(key: String, expected: String) : Exception("Element $key is not a $expected") diff --git a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonParser.kt b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonParser.kt new file mode 100644 index 00000000000..1f2b13a483f --- /dev/null +++ b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonParser.kt @@ -0,0 +1,289 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.report.json + +import org.jetbrains.report.json.EscapeCharMappings.ESC2C +// special strings +internal const val NULL = "null" + +// special chars +internal const val COMMA = ',' +internal const val COLON = ':' +internal const val BEGIN_OBJ = '{' +internal const val END_OBJ = '}' +internal const val BEGIN_LIST = '[' +internal const val END_LIST = ']' +internal const val STRING = '"' +internal const val STRING_ESC = '\\' +internal const val INVALID = 0.toChar() +internal const val UNICODE_ESC = 'u' +// token classes +internal const val TC_OTHER: Byte = 0 +internal const val TC_STRING: Byte = 1 +internal const val TC_STRING_ESC: Byte = 2 +internal const val TC_WS: Byte = 3 +internal const val TC_COMMA: Byte = 4 +internal const val TC_COLON: Byte = 5 +internal const val TC_BEGIN_OBJ: Byte = 6 +internal const val TC_END_OBJ: Byte = 7 +internal const val TC_BEGIN_LIST: Byte = 8 +internal const val TC_END_LIST: Byte = 9 +internal const val TC_NULL: Byte = 10 +internal const val TC_INVALID: Byte = 11 +internal const val TC_EOF: Byte = 12 +// mapping from chars to token classes +private const val CTC_MAX = 0x7e +// mapping from escape chars real chars +private const val C2ESC_MAX = 0x5d +private const val ESC2C_MAX = 0x75 + +internal val C2TC = ByteArray(CTC_MAX).apply { + for (i in 0..0x20) + initC2TC(i, TC_INVALID) + initC2TC(0x09, TC_WS) + initC2TC(0x0a, TC_WS) + initC2TC(0x0d, TC_WS) + initC2TC(0x20, TC_WS) + initC2TC(COMMA, TC_COMMA) + initC2TC(COLON, TC_COLON) + initC2TC(BEGIN_OBJ, TC_BEGIN_OBJ) + initC2TC(END_OBJ, TC_END_OBJ) + initC2TC(BEGIN_LIST, TC_BEGIN_LIST) + initC2TC(END_LIST, TC_END_LIST) + initC2TC(STRING, TC_STRING) + initC2TC(STRING_ESC, TC_STRING_ESC) +} +// object instead of @SharedImmutable because there is mutual initialization in [initC2ESC] +internal object EscapeCharMappings { + internal val ESC2C = CharArray(ESC2C_MAX) + internal val C2ESC = CharArray(C2ESC_MAX).apply { + for (i in 0x00..0x1f) + initC2ESC(i, UNICODE_ESC) + initC2ESC(0x08, 'b') + initC2ESC(0x09, 't') + initC2ESC(0x0a, 'n') + initC2ESC(0x0c, 'f') + initC2ESC(0x0d, 'r') + initC2ESC('/', '/') + initC2ESC(STRING, STRING) + initC2ESC(STRING_ESC, STRING_ESC) + } + private fun CharArray.initC2ESC(c: Int, esc: Char) { + this[c] = esc + if (esc != UNICODE_ESC) ESC2C[esc.toInt()] = c.toChar() + } + private fun CharArray.initC2ESC(c: Char, esc: Char) = initC2ESC(c.toInt(), esc) +} +private fun ByteArray.initC2TC(c: Int, cl: Byte) { + this[c] = cl +} +private fun ByteArray.initC2TC(c: Char, cl: Byte) { + initC2TC(c.toInt(), cl) +} +internal fun charToTokenClass(c: Char) = if (c.toInt() < CTC_MAX) C2TC[c.toInt()] else TC_OTHER +internal fun escapeToChar(c: Int): Char = if (c < ESC2C_MAX) ESC2C[c] else INVALID +// JSON low level parser +internal class Parser(val source: String) { + var curPos: Int = 0 // position in source + private set + // updated by nextToken + var tokenPos: Int = 0 + private set + var tc: Byte = TC_EOF + private set + // update by nextString/nextLiteral + private var offset = -1 // when offset >= 0 string is in source, otherwise in buf + private var length = 0 // length of string + private var buf = CharArray(16) // only used for strings with escapes + init { + nextToken() + } + internal inline fun requireTc(expected: Byte, lazyErrorMsg: () -> String) { + if (tc != expected) + fail(tokenPos, lazyErrorMsg()) + } + val canBeginValue: Boolean + get() = when (tc) { + TC_BEGIN_LIST, TC_BEGIN_OBJ, TC_OTHER, TC_STRING, TC_NULL -> true + else -> false + } + fun takeStr(): String { + if (tc != TC_OTHER && tc != TC_STRING) fail(tokenPos, "Expected string or non-null literal") + val prevStr = if (offset < 0) + String(buf, 0, length) else + source.substring(offset, offset + length) + nextToken() + return prevStr + } + private fun append(ch: Char) { + if (length >= buf.size) buf = buf.copyOf(2 * buf.size) + buf[length++] = ch + } + // initializes buf usage upon the first encountered escaped char + private fun appendRange(source: String, fromIndex: Int, toIndex: Int) { + val addLen = toIndex - fromIndex + val oldLen = length + val newLen = oldLen + addLen + if (newLen > buf.size) buf = buf.copyOf(newLen.coerceAtLeast(2 * buf.size)) + for (i in 0 until addLen) buf[oldLen + i] = source[fromIndex + i] + length += addLen + } + fun nextToken() { + val source = source + var curPos = curPos + val maxLen = source.length + while (true) { + if (curPos >= maxLen) { + tokenPos = curPos + tc = TC_EOF + return + } + val ch = source[curPos] + val tc = charToTokenClass(ch) + when (tc) { + TC_WS -> curPos++ // skip whitespace + TC_OTHER -> { + nextLiteral(source, curPos) + return + } + TC_STRING -> { + nextString(source, curPos) + return + } + else -> { + this.tokenPos = curPos + this.tc = tc + this.curPos = curPos + 1 + return + } + } + } + } + private fun nextLiteral(source: String, startPos: Int) { + tokenPos = startPos + offset = startPos + var curPos = startPos + val maxLen = source.length + while (true) { + curPos++ + if (curPos >= maxLen || charToTokenClass(source[curPos]) != TC_OTHER) break + } + this.curPos = curPos + length = curPos - offset + tc = if (rangeEquals(source, offset, length, NULL)) TC_NULL else TC_OTHER + } + private fun nextString(source: String, startPos: Int) { + tokenPos = startPos + length = 0 // in buffer + var curPos = startPos + 1 + var lastPos = curPos + val maxLen = source.length + parse@ while (true) { + if (curPos >= maxLen) fail(curPos, "Unexpected end in string") + if (source[curPos] == STRING) { + break@parse + } else if (source[curPos] == STRING_ESC) { + appendRange(source, lastPos, curPos) + val newPos = appendEsc(source, curPos + 1) + curPos = newPos + lastPos = newPos + } else { + curPos++ + } + } + if (lastPos == startPos + 1) { + // there was no escaped chars + this.offset = lastPos + this.length = curPos - lastPos + } else { + // some escaped chars were there + appendRange(source, lastPos, curPos) + this.offset = -1 + } + this.curPos = curPos + 1 + tc = TC_STRING + } + private fun appendEsc(source: String, startPos: Int): Int { + var curPos = startPos + require(curPos < source.length, curPos) { "Unexpected end after escape char" } + val curChar = source[curPos++] + if (curChar == UNICODE_ESC) { + curPos = appendHex(source, curPos) + } else { + val c = escapeToChar(curChar.toInt()) + require(c != INVALID, curPos) { "Invalid escaped char '$curChar'" } + append(c) + } + return curPos + } + private fun appendHex(source: String, startPos: Int): Int { + var curPos = startPos + append( + ((fromHexChar(source, curPos++) shl 12) + + (fromHexChar(source, curPos++) shl 8) + + (fromHexChar(source, curPos++) shl 4) + + fromHexChar(source, curPos++)).toChar() + ) + return curPos + } + fun skipElement() { + if (tc != TC_BEGIN_OBJ && tc != TC_BEGIN_LIST) { + nextToken() + return + } + val tokenStack = mutableListOf() + do { + when (tc) { + TC_BEGIN_LIST, TC_BEGIN_OBJ -> tokenStack.add(tc) + TC_END_LIST -> { + if (tokenStack.last() != TC_BEGIN_LIST) throw JsonParsingException(curPos, "found ] instead of }") + tokenStack.removeAt(tokenStack.size - 1) + } + TC_END_OBJ -> { + if (tokenStack.last() != TC_BEGIN_OBJ) throw JsonParsingException(curPos, "found } instead of ]") + tokenStack.removeAt(tokenStack.size - 1) + } + } + nextToken() + } while (tokenStack.isNotEmpty()) + } +} +// Utility functions +private fun fromHexChar(source: String, curPos: Int): Int { + require(curPos < source.length, curPos) { "Unexpected end in unicode escape" } + val curChar = source[curPos] + return when (curChar) { + in '0'..'9' -> curChar.toInt() - '0'.toInt() + in 'a'..'f' -> curChar.toInt() - 'a'.toInt() + 10 + in 'A'..'F' -> curChar.toInt() - 'A'.toInt() + 10 + else -> fail(curPos, "Invalid toHexChar char '$curChar' in unicode escape") + } +} +private fun rangeEquals(source: String, start: Int, length: Int, str: String): Boolean { + val n = str.length + if (length != n) return false + for (i in 0 until n) if (source[start + i] != str[i]) return false + return true +} +internal inline fun require(condition: Boolean, pos: Int, msg: () -> String) { + if (!condition) + fail(pos, msg()) +} +@Suppress("NOTHING_TO_INLINE") +internal inline fun fail(pos: Int, msg: String): Nothing { + throw JsonParsingException(pos, msg) +} \ No newline at end of file diff --git a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonTreeParser.kt b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonTreeParser.kt new file mode 100644 index 00000000000..23f3ccbd151 --- /dev/null +++ b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/JsonTreeParser.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +// A bit changed part of kotlinx.serialization plugin +package org.jetbrains.report.json + +class JsonTreeParser internal constructor(private val p: Parser) { + + companion object { + fun parse(input: String): JsonElement = JsonTreeParser(input).readFully() + } + + constructor(input: String) : this(Parser(input)) + + private fun readObject(): JsonElement { + p.requireTc(TC_BEGIN_OBJ) { "Expected start of object" } + p.nextToken() + val result: MutableMap = hashMapOf() + while (true) { + if (p.tc == TC_COMMA) p.nextToken() + if (!p.canBeginValue) break + val key = p.takeStr() + p.requireTc(TC_COLON) { "Expected ':'" } + p.nextToken() + val elem = read() + result[key] = elem + } + p.requireTc(TC_END_OBJ) { "Expected end of object" } + p.nextToken() + return JsonObject(result) + } + + private fun readValue(isString: Boolean): JsonElement { + val str = p.takeStr() + return JsonLiteral(str, isString) + } + + private fun readArray(): JsonElement { + p.requireTc(TC_BEGIN_LIST) { "Expected start of array" } + p.nextToken() + val result: MutableList = arrayListOf() + while (true) { + if (p.tc == TC_COMMA) p.nextToken() + if (!p.canBeginValue) break + val elem = read() + result.add(elem) + } + p.requireTc(TC_END_LIST) { "Expected end of array" } + p.nextToken() + return JsonArray(result) + } + + fun read(): JsonElement { + if (!p.canBeginValue) fail(p.curPos, "Can't begin reading value from here") + val tc = p.tc + return when (tc) { + TC_NULL -> JsonNull.also { p.nextToken() } + TC_STRING -> readValue(isString = true) + TC_OTHER -> readValue(isString = false) + TC_BEGIN_OBJ -> readObject() + TC_BEGIN_LIST -> readArray() + else -> fail(p.curPos, "Can't begin reading element") + } + } + + fun readFully(): JsonElement { + val r = read() + p.requireTc(TC_EOF) { "Input wasn't consumed fully" } + return r + } +} \ No newline at end of file diff --git a/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/StringOps.kt b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/StringOps.kt new file mode 100644 index 00000000000..9602c5f2ee6 --- /dev/null +++ b/tools/benchmarks/shared/src/main/kotlin/org/jetbrains/report/json/StringOps.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.report.json + +private fun toHexChar(i: Int) : Char { + val d = i and 0xf + return if (d < 10) (d + '0'.toInt()).toChar() + else (d - 10 + 'a'.toInt()).toChar() +} + +private val ESCAPE_CHARS: Array = arrayOfNulls(128).apply { + for (c in 0..0x1f) { + val c1 = toHexChar(c shr 12) + val c2 = toHexChar(c shr 8) + val c3 = toHexChar(c shr 4) + val c4 = toHexChar(c) + this[c] = "\\u$c1$c2$c3$c4" + } + this['"'.toInt()] = "\\\"" + this['\\'.toInt()] = "\\\\" + this['\t'.toInt()] = "\\t" + this['\b'.toInt()] = "\\b" + this['\n'.toInt()] = "\\n" + this['\r'.toInt()] = "\\r" + this[0x0c] = "\\f" +} + +internal fun StringBuilder.printQuoted(value: String) { + append(STRING) + var lastPos = 0 + val length = value.length + for (i in 0 until length) { + val c = value[i].toInt() + // Do not replace this constant with C2ESC_MAX (which is smaller than ESCAPE_CHARS size), + // otherwise JIT won't eliminate range check and won't vectorize this loop + if (c >= ESCAPE_CHARS.size) continue // no need to escape + val esc = ESCAPE_CHARS[c] ?: continue + append(value, lastPos, i) // flush prev + append(esc) + lastPos = i + 1 + } + append(value, lastPos, length) + append(STRING) +} + +/** + * Returns `true` if the contents of this string is equal to the word "true", ignoring case, `false` if content equals "false", + * and throws [IllegalStateException] otherwise. + */ +fun String.toBooleanStrict(): Boolean = toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean") + +/** + * Returns `true` if the contents of this string is equal to the word "true", ignoring case, `false` if content equals "false", + * and returns `null` otherwise. + */ +fun String.toBooleanStrictOrNull(): Boolean? = when { + this.equals("true", ignoreCase = true) -> true + this.equals("false", ignoreCase = true) -> false + else -> null +} \ No newline at end of file