[MERGE] KT: build-1.5.0-dev-1963 KT/N: 52fe74ede OLD: ece5d9b10

This commit is contained in:
Nikolay Krasko
2021-01-26 17:38:13 +03:00
954 changed files with 10545 additions and 5727 deletions
+7
View File
@@ -1,3 +1,10 @@
# 1.4.30-RC (Jan 2021)
* [KT-44271](https://youtrack.jetbrains.com/issue/KT-44271) Incorrect linking when targeting linux_x64 from mingw_x64 host
* [KT-44219](https://youtrack.jetbrains.com/issue/KT-44219) Non-reified type parameters with recursive bounds are not supported yet
* [KT-43599](https://youtrack.jetbrains.com/issue/KT-43599) K/N: Unbound symbols not allowed
* [KT-42172](https://youtrack.jetbrains.com/issue/KT-42172) Kotlin/Native: StableRef.dispose race condition on Kotlin deinitRuntime
* [KT-42482](https://youtrack.jetbrains.com/issue/KT-42482) Kotlin subclasses of Obj-C classes are incompatible with ISA swizzling (it causes crashes)
# 1.4.30-M1 (Dec 2020)
* [KT-43597](https://youtrack.jetbrains.com/issue/KT-43597) Xcode 12.2 support
* [KT-43276](https://youtrack.jetbrains.com/issue/KT-43276) Add watchos_x64 target
+10 -2
View File
@@ -330,7 +330,7 @@ You can add dependencies on a Pod library from `zip`, `tar`, or `jar` archive wi
1. Specify the name of a Pod library in the `pod()` function.
In the configuration block specify the path to the archive: use the `url()` function with an arbitrary HTTP address in the `source` parameter value.
Additionally, you can specify the boolean `flatten` parameter as a second argument for the `url()` function
Additionally, you can specify the boolean `flatten` parameter as a second argument for the `url()` function.
This parameter indicates that all the Pod files are located in the root directory of the archive.
2. Specify the minimum deployment target version for the Pod library.
@@ -430,7 +430,15 @@ You can add dependencies on a Pod library from a custom Podspec repository with
4. Re-import the project.
> To work correctly with Xcode, you should specify the location of specs at the beginning of your Podfile.
> For example, `source 'https://github.com/Kotlin/kotlin-cocoapods-spec.git'`
> For example:
>
> <div class="sample" markdown="1" theme="idea" data-highlight-only>
>
> ```ruby
> source 'https://github.com/Kotlin/kotlin-cocoapods-spec.git'
> ```
>
> </div>
>
> You should also specify the path to the Podspec in your Podfile.
> For example:
+2 -2
View File
@@ -170,7 +170,7 @@ kotlin.sourceSets {
// Configure all native platform sources sets to use it as a common one.
linuxX64Main.dependsOn(nativeMain)
macosX64Main.dependsOn(nativeMain)
//...
// ...
}
```
@@ -459,7 +459,7 @@ dependencies {
</div>
It's possible to depend on a Kotlin/Native library published earlier in a maven repo. The plugin relies on Gradle's
[metadata](https://github.com/gradle/gradle/blob/master/subprojects/docs/src/docs/design/gradle-module-metadata-specification.md)
[metadata](https://github.com/gradle/gradle/blob/master/subprojects/docs/src/docs/design/gradle-module-metadata-latest-specification.md)
support so the corresponding feature must be enabled. Add the following line in your `settings.gradle`:
<div class="sample" markdown="1" theme="idea" mode="groovy">
+1 -1
View File
@@ -225,7 +225,7 @@ directory structure, with the following layout:
- foo/
- $component_name/
- ir/
- Seriaized Kotlin IR.
- Serialized Kotlin IR.
- targets/
- $platform/
- kotlin/
@@ -379,4 +379,10 @@ internal val foldConstantLoweringPhase = makeKonanFileOpPhase(
name = "FoldConstantLowering",
description = "Constant Folding",
prerequisite = setOf(flattenStringConcatenationPhase)
)
internal val computeStringTrimPhase = makeKonanFileLoweringPhase(
::StringTrimLowering,
name = "StringTrimLowering",
description = "Compute trimIndent and trimMargin operations on constant strings"
)
@@ -3,11 +3,8 @@ package org.jetbrains.kotlin.backend.konan
import org.jetbrains.kotlin.konan.KonanExternalToolFailure
import org.jetbrains.kotlin.konan.exec.Command
import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.LinkerOutputKind
import org.jetbrains.kotlin.konan.library.KonanLibrary
import org.jetbrains.kotlin.konan.target.supportsMimallocAllocator
import org.jetbrains.kotlin.konan.target.*
import org.jetbrains.kotlin.library.resolver.TopologicalLibraryOrder
import org.jetbrains.kotlin.library.uniqueName
import org.jetbrains.kotlin.utils.addToStdlib.cast
@@ -169,7 +166,7 @@ internal class Linker(val context: Context) {
"""
Please try to disable compiler caches and rerun the build. To disable compiler caches, add the following line to the gradle.properties file in the project's root directory:
kotlin.native.cacheKind=none
kotlin.native.cacheKind.${target.presetName}=none
Also, consider filing an issue with full Gradle log here: https://kotl.in/issue
""".trimIndent()
@@ -221,6 +221,7 @@ internal val allLoweringsPhase = NamedCompilerPhase(
forLoopsPhase,
flattenStringConcatenationPhase,
foldConstantLoweringPhase,
computeStringTrimPhase,
stringConcatenationPhase,
enumConstructorsPhase,
initializersPhase,
@@ -1251,6 +1251,8 @@ internal class FunctionGenerationContext(val function: LLVMValueRef,
returnType == voidType -> {
releaseVars()
assert(returnSlot == null)
if (context.memoryModel == MemoryModel.EXPERIMENTAL)
call(context.llvm.Kotlin_mm_safePointFunctionEpilogue, emptyList())
LLVMBuildRetVoid(builder)
}
returns.isNotEmpty() -> {
@@ -1260,6 +1262,8 @@ internal class FunctionGenerationContext(val function: LLVMValueRef,
updateReturnRef(returnPhi, returnSlot!!)
}
releaseVars()
if (context.memoryModel == MemoryModel.EXPERIMENTAL)
call(context.llvm.Kotlin_mm_safePointFunctionEpilogue, emptyList())
LLVMBuildRet(builder, returnPhi)
}
// Do nothing, all paths throw.
@@ -1300,6 +1304,8 @@ internal class FunctionGenerationContext(val function: LLVMValueRef,
}
releaseVars()
if (context.memoryModel == MemoryModel.EXPERIMENTAL)
call(context.llvm.Kotlin_mm_safePointExceptionUnwind, emptyList())
LLVMBuildResume(builder, landingpad)
}
@@ -546,6 +546,10 @@ internal class Llvm(val context: Context, val llvmModule: LLVMModuleRef) {
val Kotlin_ObjCExport_createContinuationArgument by lazyRtFunction
val Kotlin_ObjCExport_resumeContinuation by lazyRtFunction
val Kotlin_mm_safePointFunctionEpilogue by lazyRtFunction
val Kotlin_mm_safePointWhileLoopBody by lazyRtFunction
val Kotlin_mm_safePointExceptionUnwind by lazyRtFunction
val tlsMode by lazy {
when (target) {
KonanTarget.WASM32,
@@ -534,9 +534,9 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
}
override fun genContinue(destination: IrContinue) {
if (destination.loop == loop)
if (destination.loop == loop) {
functionGenerationContext.br(loopCheck)
else
} else
super.genContinue(destination)
}
@@ -1240,6 +1240,8 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
functionGenerationContext.condBr(condition, loopBody, loopScope.loopExit)
functionGenerationContext.positionAtEnd(loopBody)
if (context.memoryModel == MemoryModel.EXPERIMENTAL)
call(context.llvm.Kotlin_mm_safePointWhileLoopBody, emptyList())
loop.body?.generate()
functionGenerationContext.br(loopScope.loopCheck)
@@ -1260,6 +1262,8 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
functionGenerationContext.br(loopBody)
functionGenerationContext.positionAtEnd(loopBody)
if (context.memoryModel == MemoryModel.EXPERIMENTAL)
call(context.llvm.Kotlin_mm_safePointWhileLoopBody, emptyList())
loop.body?.generate()
functionGenerationContext.br(loopScope.loopCheck)
@@ -1586,34 +1586,22 @@ internal fun ObjCExportCodeGenerator.getEncoding(methodBridge: MethodBridge): St
}
}
val targetFamily = context.config.target.family
val returnTypeEncoding = methodBridge.returnBridge.getObjCEncoding(targetFamily)
val returnTypeEncoding = methodBridge.returnBridge.getObjCEncoding(context)
val paramSize = paramOffset
return "$returnTypeEncoding$paramSize$params"
}
// https://developer.apple.com/documentation/objectivec/nsuinteger?language=objc
// `typedef unsigned long NSUInteger` on iOS, macOS, tvOS.
// `typedef unsigned int NSInteger` on watchOS.
private val Family.nsUIntegerEncoding: String get() = when (this) {
Family.OSX,
Family.IOS,
Family.TVOS -> "L"
Family.WATCHOS -> "I"
else -> error("Unexpected target platform: $this")
}
private fun MethodBridge.ReturnValue.getObjCEncoding(targetFamily: Family): String = when (this) {
private fun MethodBridge.ReturnValue.getObjCEncoding(context: Context): String = when (this) {
MethodBridge.ReturnValue.Suspend,
MethodBridge.ReturnValue.Void -> "v"
MethodBridge.ReturnValue.HashCode -> targetFamily.nsUIntegerEncoding
MethodBridge.ReturnValue.HashCode -> if (context.is64BitNSInteger()) "Q" else "I"
is MethodBridge.ReturnValue.Mapped -> this.bridge.objCEncoding
MethodBridge.ReturnValue.WithError.Success -> ObjCValueType.BOOL.encoding
MethodBridge.ReturnValue.Instance.InitResult,
MethodBridge.ReturnValue.Instance.FactoryResult -> ReferenceBridge.objCEncoding
is MethodBridge.ReturnValue.WithError.ZeroForError -> this.successBridge.getObjCEncoding(targetFamily)
is MethodBridge.ReturnValue.WithError.ZeroForError -> this.successBridge.getObjCEncoding(context)
}
private val MethodBridgeParameter.objCEncoding: String get() = when (this) {
+17 -36
View File
@@ -783,9 +783,7 @@ task array_to_any(type: KonanLocalTest) {
}
standaloneTest("runtime_basic_init") {
disabled = (project.testTarget == 'wasm32') // -g not yet properly works for WASM.
source = "runtime/basic/init.kt"
flags = ['-g']
expectedExitStatus = 0
}
@@ -813,6 +811,10 @@ task hello0(type: KonanLocalTest) {
source = "runtime/basic/hello0.kt"
}
task stringTrim(type: KonanLocalTest) {
source = "codegen/stringTrim/stringTrim.kt"
}
standaloneTest("hello1") {
goldValue = "Hello World"
testData = "Hello World\n"
@@ -937,20 +939,6 @@ standaloneTest("cleaner_in_main_without_checker") {
goldValue = ""
}
standaloneTest("cleaner_leak_release") {
enabled = !project.globalTestArgs.contains('-g') && (project.testTarget != 'wasm32') // Cleaners need workers
source = "runtime/basic/cleaner_leak.kt"
goldValue = ""
}
standaloneTest("cleaner_leak_debug") {
enabled = !project.globalTestArgs.contains('-opt') && (project.testTarget != 'wasm32') // Cleaners need workers
source = "runtime/basic/cleaner_leak.kt"
flags = ['-g']
expectedExitStatusChecker = { it != 0 }
outputChecker = { s -> (s =~ /Cleaner (0x)?[0-9a-fA-F]+ was disposed during program exit/).find() }
}
standaloneTest("cleaner_leak_without_checker") {
enabled = (project.testTarget != 'wasm32') // Cleaners need workers
source = "runtime/basic/cleaner_leak_without_checker.kt"
@@ -1062,9 +1050,8 @@ task worker11(type: KonanLocalTest) {
}
standaloneTest("worker_threadlocal_no_leak") {
disabled = project.globalTestArgs.contains('-opt') || (project.testTarget == 'wasm32') // Needs debug build and pthreads.
disabled = (project.testTarget == 'wasm32') // Needs pthreads.
source = "runtime/workers/worker_threadlocal_no_leak.kt"
flags = ['-g']
}
task freeze0(type: KonanLocalTest) {
@@ -1164,17 +1151,15 @@ task enumIdentity(type: KonanLocalTest) {
}
standaloneTest("leakWorker") {
disabled = project.globalTestArgs.contains('-opt') || (project.testTarget == 'wasm32') // Needs debug build and pthreads.
disabled = (project.testTarget == 'wasm32') // Needs pthreads.
source = "runtime/workers/leak_worker.kt"
flags = ['-g']
expectedExitStatusChecker = { it != 0 }
outputChecker = { s -> s.contains("Unfinished workers detected, 1 workers leaked!") }
}
standaloneTest("leakMemoryWithWorkerTermination") {
disabled = project.globalTestArgs.contains('-opt') || (project.testTarget == 'wasm32') // Needs debug build and pthreads.
disabled = (project.testTarget == 'wasm32') // Needs pthreads.
source = "runtime/workers/leak_memory_with_worker_termination.kt"
flags = ['-g']
expectedExitStatusChecker = { it != 0 }
outputChecker = { s -> s.contains("Memory leaks detected, 1 objects leaked!") }
}
@@ -2961,7 +2946,6 @@ standaloneTest("cycle_detector") {
standaloneTest("cycle_collector") {
disabled = true // Needs USE_CYCLIC_GC, which is disabled.
flags = ['-g']
source = "runtime/memory/cycle_collector.kt"
}
@@ -2971,9 +2955,14 @@ standaloneTest("cycle_collector_deadlock1") {
}
standaloneTest("leakMemory") {
disabled = project.globalTestArgs.contains('-opt') || (project.testTarget == 'wasm32') // Needs debug build.
source = "runtime/memory/leak_memory.kt"
flags = ['-g']
expectedExitStatusChecker = { it != 0 }
outputChecker = { s -> s.contains("Memory leaks detected, 1 objects leaked!") }
}
standaloneTest("leakMemoryWithTestRunner") {
source = "runtime/memory/leak_memory_test_runner.kt"
flags = ['-tr']
expectedExitStatusChecker = { it != 0 }
outputChecker = { s -> s.contains("Memory leaks detected, 1 objects leaked!") }
}
@@ -4080,17 +4069,13 @@ interopTest("interop_kt43265") {
}
interopTest("interop_leakMemoryWithRunningThreadUnchecked") {
disabled = project.globalTestArgs.contains('-opt') || (project.testTarget == 'wasm32') // Needs debug build.
interop = 'leakMemoryWithRunningThread'
source = "interop/leakMemoryWithRunningThread/unchecked.kt"
flags = ['-g']
}
interopTest("interop_leakMemoryWithRunningThreadChecked") {
disabled = project.globalTestArgs.contains('-opt') || (project.testTarget == 'wasm32') // Needs debug build.
interop = 'leakMemoryWithRunningThread'
source = "interop/leakMemoryWithRunningThread/checked.kt"
flags = ['-g']
expectedExitStatusChecker = { it != 0 }
outputChecker = { s -> s.contains("Cannot run checkers when there are 1 alive runtimes at the shutdown") }
}
@@ -4188,7 +4173,6 @@ if (PlatformInfo.isAppleTarget(project)) {
goldValue = "OK\n"
source = "interop/objc_with_initializer/objc_test.kt"
interop = 'objcMisc'
flags = ['-g']
doBeforeBuild {
mkdir(buildDir)
@@ -4391,11 +4375,10 @@ dynamicTest("interop_concurrentRuntime") {
}
dynamicTest("interop_kt42397") {
disabled = project.target.name != project.hostName || project.globalTestArgs.contains('-opt')
disabled = project.target.name != project.hostName
source = "interop/kt42397/knlibrary.kt"
cSource = "$projectDir/interop/kt42397/test.cpp"
clangTool = "clang++"
flags = ['-g']
}
dynamicTest("interop_cleaners_main_thread") {
@@ -4443,12 +4426,11 @@ dynamicTest("interop_migrating_main_thread") {
}
dynamicTest("interop_memory_leaks") {
disabled = (project.target.name != project.hostName) ||
project.globalTestArgs.contains('-opt') || (project.testTarget == 'wasm32') // Needs debug build.
disabled = (project.target.name != project.hostName)
source = "interop/memory_leaks/lib.kt"
cSource = "$projectDir/interop/memory_leaks/main.cpp"
clangTool = "clang++"
flags = ['-g', '-Xdestroy-runtime-mode=legacy'] // Runtime cannot be destroyed with interop with on-shutdown.
flags = ['-Xdestroy-runtime-mode=legacy'] // Runtime cannot be destroyed with interop with on-shutdown.
expectedExitStatusChecker = { it != 0 }
outputChecker = { s -> s.contains("Memory leaks detected, 1 objects leaked!") }
}
@@ -4728,7 +4710,6 @@ if (isAppleTarget(project)) {
enabled = !project.globalTestArgs.contains('-opt')
framework("Kt42397") {
sources = ['framework/kt42397']
opts = ['-g']
}
swiftSources = ['framework/kt42397']
}
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package codegen.stringTrim.stringTrim
import kotlin.test.*
// TODO: check IR
fun constantIndent(): String {
return """
Hello,
World
""".trimIndent()
}
fun constantMargin(): String {
return """
|Hello,
|World
""".trimMargin()
}
@Test
fun runTest() {
assertTrue(constantIndent() === constantIndent())
assertTrue(constantMargin() === constantMargin())
}
@@ -1,3 +1,5 @@
import kotlin.native.Platform
// The following 2 singletons are unused. However, since we are generating ObjC bindings for them,
// they should be marked as used, so that the code generator emits their deinitialization.
@@ -10,3 +12,7 @@ class B {
fun foo() = 2
}
}
fun enableMemoryChecker() {
Platform.isMemoryLeakCheckerActive = true
}
@@ -7,6 +7,7 @@ class Results {
func runTestKt42397(pointer: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? {
autoreleasepool {
KnlibraryKt.enableMemoryChecker()
let results = pointer.bindMemory(to: Results.self, capacity: 1).pointee
results.aFoo = A().foo()
results.bFoo = B.Companion().foo()
@@ -1,5 +1,7 @@
package knlibrary
import kotlin.native.Platform
// The following 2 singletons are unused. However, since we are generating C bindings for them,
// they should be marked as used, so that the code generator emits their deinitialization.
@@ -8,3 +10,7 @@ object A {}
class B {
companion object {}
}
fun enableMemoryChecker() {
Platform.isMemoryLeakCheckerActive = true
}
@@ -6,6 +6,8 @@ int main() {
auto t = std::thread([] {
auto lib = testlib_symbols();
lib->kotlin.root.knlibrary.enableMemoryChecker();
// Initialize A and B.Companion and get their stable pointers.
auto a = lib->kotlin.root.knlibrary.A._instance();
auto bCompanion = lib->kotlin.root.knlibrary.B.Companion._instance();
@@ -1,5 +1,6 @@
import leakMemory.*
import kotlin.native.concurrent.*
import kotlin.native.Platform
import kotlin.test.*
import kotlinx.cinterop.*
@@ -13,6 +14,7 @@ fun ensureInititalized() {
}
fun main() {
Platform.isMemoryLeakCheckerActive = true
kotlin.native.internal.Debugging.forceCheckedShutdown = true
assertTrue(global.value == 0)
// Created a thread, made sure Kotlin is initialized there.
@@ -1,5 +1,6 @@
import leakMemory.*
import kotlin.native.concurrent.*
import kotlin.native.Platform
import kotlin.test.*
import kotlinx.cinterop.*
@@ -13,6 +14,7 @@ fun ensureInititalized() {
}
fun main() {
Platform.isMemoryLeakCheckerActive = true
kotlin.native.internal.Debugging.forceCheckedShutdown = false
assertTrue(global.value == 0)
// Created a thread, made sure Kotlin is initialized there.
@@ -4,6 +4,11 @@
*/
import kotlinx.cinterop.*
import kotlin.native.Platform
fun enableMemoryChecker() {
Platform.isMemoryLeakCheckerActive = true
}
fun leakMemory() {
StableRef.create(Any())
@@ -9,6 +9,7 @@
int main() {
std::thread t([]() {
testlib_symbols()->kotlin.root.enableMemoryChecker();
testlib_symbols()->kotlin.root.leakMemory();
});
t.join();
@@ -67,11 +67,10 @@ fun run() {
// hashCode (directly):
// hash() returns value of NSUInteger type.
val hash = when (Platform.osFamily) {
// `typedef unsigned int NSInteger` on watchOS.
OsFamily.WATCHOS -> foo.hash().toInt()
// `typedef unsigned long NSUInteger` on iOS, macOS, tvOS.
else -> foo.hash().let { it.toInt() xor (it shr 32).toInt() }
val hash = if (sizeOf<NSUIntegerVar>() == 4L) {
foo.hash().toInt()
} else {
foo.hash().let { it.toInt() xor (it shr 32).toInt() }
}
if (foo.hashCode() == hash) {
// toString (virtually):
@@ -1,7 +1,9 @@
import objc_misc.*
import kotlin.native.Platform
val a = B.giveC()!! as C
fun main() {
Platform.isMemoryLeakCheckerActive = true
println("OK")
}
}
@@ -1,24 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
@file:OptIn(ExperimentalStdlibApi::class)
import kotlin.test.*
import kotlin.native.internal.*
// This cleaner won't be run, because it's deinitialized with globals after
// cleaners are disabled.
val globalCleaner = createCleaner(42) {
println(it)
}
fun main() {
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
// the memory will leak. Suppress memory leak checker to check for cleaners
// leak only.
Platform.isMemoryLeakCheckerActive = false
// Make sure cleaner is initialized.
assertNotNull(globalCleaner)
}
@@ -16,10 +16,6 @@ val globalCleaner = createCleaner(42) {
}
fun main() {
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
// the memory will leak. Suppress memory leak checker to check for cleaners
// leak only.
Platform.isMemoryLeakCheckerActive = false
Platform.isCleanersLeakCheckerActive = true
// Make sure cleaner is initialized.
assertNotNull(globalCleaner)
@@ -16,11 +16,6 @@ val globalCleaner = createCleaner(42) {
}
fun main() {
// Cleaner holds onto a finalization lambda. If it doesn't get executed,
// the memory will leak. Suppress memory leak checker to check for cleaners
// leak only.
Platform.isMemoryLeakCheckerActive = false
Platform.isCleanersLeakCheckerActive = false
// Make sure cleaner is initialized.
assertNotNull(globalCleaner)
}
@@ -1,5 +1,6 @@
import kotlin.native.concurrent.*
import kotlin.native.internal.GC
import kotlin.native.Platform
import kotlin.test.*
fun test1() {
@@ -175,6 +176,7 @@ fun test9() {
}
fun main() {
Platform.isMemoryLeakCheckerActive = true
kotlin.native.internal.GC.cyclicCollectorEnabled = true
test1()
test2()
@@ -187,4 +189,4 @@ fun main() {
test7()
test8()
test9()
}
}
@@ -1,5 +1,6 @@
import kotlin.native.concurrent.*
import kotlin.native.internal.GC
import kotlin.native.Platform
import kotlin.test.*
class Holder(var other: Any?)
@@ -32,6 +33,11 @@ fun assertArrayEquals(
}
}
@BeforeTest
fun enableMemoryChecker() {
Platform.isMemoryLeakCheckerActive = true
}
@Test
fun noCycles() {
val atomic1 = AtomicReference<Any?>(null)
@@ -1,5 +1,7 @@
import kotlinx.cinterop.*
import kotlin.native.Platform
fun main() {
Platform.isMemoryLeakCheckerActive = true
StableRef.create(Any())
}
@@ -0,0 +1,14 @@
import kotlin.test.*
import kotlinx.cinterop.*
import kotlin.native.Platform
@BeforeTest
fun enableMemoryChecker() {
Platform.isMemoryLeakCheckerActive = true
}
@Test
fun test() {
StableRef.create(Any())
}
@@ -1,7 +1,9 @@
import kotlin.native.concurrent.*
import kotlin.native.Platform
import kotlinx.cinterop.*
fun main() {
Platform.isMemoryLeakCheckerActive = true
val worker = Worker.start()
// Make sure worker is initialized.
worker.execute(TransferMode.SAFE, {}, {}).result;
@@ -1,7 +1,9 @@
import kotlin.native.concurrent.*
import kotlin.native.Platform
import kotlinx.cinterop.*
fun main() {
Platform.isMemoryLeakCheckerActive = true
val worker = Worker.start()
// Make sure worker is initialized.
worker.execute(TransferMode.SAFE, {}, {}).result;
@@ -4,11 +4,13 @@
*/
import kotlin.native.concurrent.*
import kotlin.native.Platform
@ThreadLocal
var x = Any()
fun main() {
Platform.isMemoryLeakCheckerActive = true
val worker = Worker.start()
worker.execute(TransferMode.SAFE, {}) {
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.ExecClang
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.target.SanitizerKind
import java.io.File
import javax.inject.Inject
@@ -19,7 +20,7 @@ open class CompileToBitcode @Inject constructor(
val srcRoot: File,
val folderName: String,
val target: String,
val outputGroup: String
val outputGroup: String,
) : DefaultTask() {
enum class Language {
@@ -46,9 +47,22 @@ open class CompileToBitcode @Inject constructor(
@Input
var language = Language.CPP
private val targetDir by lazy { project.buildDir.resolve("bitcode/$outputGroup/$target") }
@Input @Optional
var sanitizer: SanitizerKind? = null
val objDir by lazy { File(targetDir, folderName) }
private val targetDir: File
get() {
val sanitizerSuffix = when (sanitizer) {
null -> ""
SanitizerKind.ADDRESS -> "-asan"
SanitizerKind.THREAD -> "-tsan"
}
return project.buildDir.resolve("bitcode/$outputGroup/$target$sanitizerSuffix")
}
@get:Input
val objDir
get() = File(targetDir, folderName)
private val KonanTarget.isMINGW
get() = this.family == Family.MINGW
@@ -63,6 +77,11 @@ open class CompileToBitcode @Inject constructor(
val compilerFlags: List<String>
get() {
val commonFlags = listOf("-c", "-emit-llvm") + headersDirs.map { "-I$it" }
val sanitizerFlags = when (sanitizer) {
null -> listOf()
SanitizerKind.ADDRESS -> listOf("-fsanitize=address")
SanitizerKind.THREAD -> listOf("-fsanitize=thread")
}
val languageFlags = when (language) {
Language.C ->
// Used flags provided by original build of allocator C code.
@@ -73,7 +92,7 @@ open class CompileToBitcode @Inject constructor(
"-Wno-unused-parameter", // False positives with polymorphic functions.
"-fPIC".takeIf { !HostManager().targetByName(target).isMINGW })
}
return commonFlags + languageFlags + compilerArgs
return commonFlags + sanitizerFlags + languageFlags + compilerArgs
}
@get:SkipWhenEmpty
@@ -127,8 +146,9 @@ open class CompileToBitcode @Inject constructor(
}
}
@OutputFile
val outFile = File(targetDir, "${folderName}.bc")
@get:OutputFile
val outFile: File
get() = File(targetDir, "${folderName}.bc")
@TaskAction
fun compile() {
@@ -9,6 +9,9 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin
import org.jetbrains.kotlin.createCompilationDatabasesFromCompileToBitcodeTasks
import org.jetbrains.kotlin.konan.target.PlatformManager
import org.jetbrains.kotlin.konan.target.SanitizerKind
import org.jetbrains.kotlin.konan.target.supportedSanitizers
import java.io.File
import javax.inject.Inject
@@ -45,20 +48,40 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
configurationBlock: CompileToBitcode.() -> Unit = {}
) {
targetList.get().forEach { targetName ->
project.tasks.register(
"${targetName}${name.snakeCaseToCamelCase().capitalize()}",
CompileToBitcode::class.java,
srcDir, name, targetName, outputGroup
).configure {
it.group = BasePlugin.BUILD_GROUP
it.description = "Compiles '$name' to bitcode for $targetName"
it.configurationBlock()
val platformManager = project.rootProject.findProperty("platformManager") as PlatformManager
val target = platformManager.targetByName(targetName)
val sanitizers: List<SanitizerKind?> = target.supportedSanitizers() + listOf(null)
sanitizers.forEach { sanitizer ->
project.tasks.register(
"${targetName}${name.snakeCaseToCamelCase().capitalize()}${suffixForSanitizer(sanitizer)}",
CompileToBitcode::class.java,
srcDir, name, targetName, outputGroup
).configure {
it.sanitizer = sanitizer
it.group = BasePlugin.BUILD_GROUP
val sanitizerDescription = when (sanitizer) {
null -> ""
SanitizerKind.ADDRESS -> " with ASAN"
SanitizerKind.THREAD -> " with TSAN"
}
it.description = "Compiles '$name' to bitcode for $targetName$sanitizerDescription"
it.configurationBlock()
}
}
}
}
companion object {
private fun String.snakeCaseToCamelCase() =
split('_').joinToString(separator = "") { it.capitalize() }
fun suffixForSanitizer(sanitizer: SanitizerKind?) =
when (sanitizer) {
null -> ""
SanitizerKind.ADDRESS -> "_ASAN"
SanitizerKind.THREAD -> "_TSAN"
}
}
}
@@ -13,6 +13,7 @@ import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.tasks.*
import org.jetbrains.kotlin.ExecClang
import org.jetbrains.kotlin.bitcode.CompileToBitcode
import org.jetbrains.kotlin.bitcode.CompileToBitcodeExtension
import org.jetbrains.kotlin.konan.target.*
open class CompileNativeTest @Inject constructor(
@@ -25,18 +26,29 @@ open class CompileNativeTest @Inject constructor(
@Input
val clangArgs = mutableListOf<String>()
@Input @Optional
var sanitizer: SanitizerKind? = null
@Input
private val sanitizerFlags = when (sanitizer) {
null -> listOf()
SanitizerKind.ADDRESS -> listOf("-fsanitize=address")
SanitizerKind.THREAD -> listOf("-fsanitize=thread")
}
@TaskAction
fun compile() {
val plugin = project.convention.getPlugin(ExecClang::class.java)
val args = clangArgs + sanitizerFlags + listOf(inputFile.absolutePath, "-o", outputFile.absolutePath)
if (target.family.isAppleFamily) {
plugin.execToolchainClang(target) {
it.executable = "clang++"
it.args = clangArgs + listOf(inputFile.absolutePath, "-o", outputFile.absolutePath)
it.args = args
}
} else {
plugin.execBareClang {
it.executable = "clang++"
it.args = clangArgs + listOf(inputFile.absolutePath, "-o", outputFile.absolutePath)
it.args = args
}
}
}
@@ -92,7 +104,7 @@ open class LinkNativeTest @Inject constructor(
@Internal val target: String,
@Internal val linkerArgs: List<String>,
private val platformManager: PlatformManager,
private val mimallocEnabled: Boolean
private val mimallocEnabled: Boolean,
) : DefaultTask () {
companion object {
fun create(
@@ -103,7 +115,7 @@ open class LinkNativeTest @Inject constructor(
target: String,
outputFile: File,
linkerArgs: List<String>,
mimallocEnabled: Boolean
mimallocEnabled: Boolean,
): LinkNativeTest = project.tasks.create(
taskName,
LinkNativeTest::class.java,
@@ -133,6 +145,9 @@ open class LinkNativeTest @Inject constructor(
linkerArgs, mimallocEnabled)
}
@Input @Optional
var sanitizer: SanitizerKind? = null
@get:Input
val commands: List<List<String>>
get() {
@@ -149,7 +164,8 @@ open class LinkNativeTest @Inject constructor(
kind = LinkerOutputKind.EXECUTABLE,
outputDsymBundle = "",
needsProfileLibrary = false,
mimallocEnabled = mimallocEnabled
mimallocEnabled = mimallocEnabled,
sanitizer = sanitizer,
).map { it.argsWithExecutable }
}
@@ -163,11 +179,11 @@ open class LinkNativeTest @Inject constructor(
}
}
fun createTestTask(
private fun createTestTask(
project: Project,
testName: String,
testTaskName: String,
testedTaskNames: List<String>,
sanitizer: SanitizerKind?,
configureCompileToBitcode: CompileToBitcode.() -> Unit = {},
): Task {
val platformManager = project.rootProject.findProperty("platformManager") as PlatformManager
@@ -188,6 +204,7 @@ fun createTestTask(
"${it.folderName}Tests",
target, "test"
).apply {
this.sanitizer = sanitizer
excludeFiles = emptyList()
includeFiles = listOf("**/*Test.cpp", "**/*Test.mm")
dependsOn(it)
@@ -201,18 +218,19 @@ fun createTestTask(
else
task
}
// TODO: Consider using sanitized versions.
val testFrameworkTasks = listOf(
project.tasks.getByName("${target}Googletest") as CompileToBitcode,
project.tasks.getByName("${target}Googlemock") as CompileToBitcode
)
val testSupportTask = project.tasks.getByName("${target}TestSupport") as CompileToBitcode
val testSupportTask = project.tasks.getByName("${target}TestSupport${CompileToBitcodeExtension.suffixForSanitizer(sanitizer)}") as CompileToBitcode
// TODO: It may make sense to merge llvm-link, compile and link to a single task.
val llvmLinkTask = project.tasks.create(
"${testTaskName}LlvmLink",
"${testName}LlvmLink",
LlvmLinkNativeTest::class.java,
testTaskName, target, testSupportTask.outFile
testName, target, testSupportTask.outFile
).apply {
val tasksToLink = (compileToBitcodeTasks + testedTasks + testFrameworkTasks)
inputFiles = project.files(tasksToLink.map { it.outFile })
@@ -222,11 +240,12 @@ fun createTestTask(
val clangFlags = platformManager.platform(konanTarget).configurables as ClangFlags
val compileTask = project.tasks.create(
"${testTaskName}Compile",
"${testName}Compile",
CompileNativeTest::class.java,
llvmLinkTask.outputFile,
konanTarget,
).apply {
this.sanitizer = sanitizer
dependsOn(llvmLinkTask)
clangArgs.addAll(clangFlags.clangFlags)
clangArgs.addAll(clangFlags.clangNooptFlags)
@@ -236,22 +255,31 @@ fun createTestTask(
val linkTask = LinkNativeTest.create(
project,
platformManager,
"${testTaskName}Link",
"${testName}Link${CompileToBitcodeExtension.suffixForSanitizer(sanitizer)}",
listOf(compileTask.outputFile),
target,
testTaskName,
mimallocEnabled
testName,
mimallocEnabled,
).apply {
this.sanitizer = sanitizer
dependsOn(compileTask)
}
return project.tasks.create(testTaskName, Exec::class.java).apply {
return project.tasks.create(testName, Exec::class.java).apply {
dependsOn(linkTask)
workingDir = project.buildDir.resolve("testReports/$testTaskName")
workingDir = project.buildDir.resolve("testReports/$testName")
val xmlReport = workingDir.resolve("report.xml")
executable(linkTask.outputFile)
args("--gtest_output=xml:${xmlReport.absoluteFile}")
when (sanitizer) {
SanitizerKind.THREAD -> {
val file = project.file("tsan_suppressions.txt")
inputs.file(file)
environment("TSAN_OPTIONS", "suppressions=${file.absolutePath}")
}
else -> {} // no action required
}
doFirst {
workingDir.mkdirs()
@@ -267,3 +295,24 @@ fun createTestTask(
}
}
}
// TODO: These tests should be created by `CompileToBitcodeExtension`
fun createTestTasks(
project: Project,
targetName: String,
testTaskName: String,
testedTaskNames: List<String>,
configureCompileToBitcode: CompileToBitcode.() -> Unit = {},
): List<Task> {
val platformManager = project.rootProject.findProperty("platformManager") as PlatformManager
val target = platformManager.targetByName(targetName)
val sanitizers: List<SanitizerKind?> = target.supportedSanitizers() + listOf(null)
return sanitizers.map { sanitizer ->
val suffix = CompileToBitcodeExtension.suffixForSanitizer(sanitizer)
val name = testTaskName + suffix
val testedNames = testedTaskNames.map {
it + suffix
}
createTestTask(project, name, testedNames, sanitizer, configureCompileToBitcode)
}
}
+6 -6
View File
@@ -18,12 +18,12 @@
buildKotlinVersion=1.4.20-dev-2167
buildKotlinCompilerRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_KotlinPublic_Compiler),number:1.4.20-dev-2167,branch:default:any,pinned:true/artifacts/content/maven
remoteRoot=konan_tests
kotlinCompilerRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_KotlinPublic_Compiler),number:1.5.0-dev-1616,branch:default:any,pinned:true/artifacts/content/maven
kotlinVersion=1.5.0-dev-1616
kotlinStdlibRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_KotlinPublic_Compiler),number:1.5.0-dev-1616,branch:default:any,pinned:true/artifacts/content/maven
kotlinStdlibVersion=1.5.0-dev-1616
kotlinStdlibTestsVersion=1.5.0-dev-1616
testKotlinCompilerVersion=1.5.0-dev-1616
kotlinCompilerRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_KotlinPublic_Compiler),number:1.5.0-dev-1963,branch:default:any,pinned:true/artifacts/content/maven
kotlinVersion=1.5.0-dev-1963
kotlinStdlibRepo=https://teamcity.jetbrains.com/guestAuth/app/rest/builds/buildType:(id:Kotlin_KotlinPublic_Compiler),number:1.5.0-dev-1963,branch:default:any,pinned:true/artifacts/content/maven
kotlinStdlibVersion=1.5.0-dev-1963
kotlinStdlibTestsVersion=1.5.0-dev-1963
testKotlinCompilerVersion=1.5.0-dev-1963
konanVersion=1.5.0
# A version of Xcode required to build the Kotlin/Native compiler.
+19 -16
View File
@@ -5,6 +5,8 @@
import org.jetbrains.kotlin.*
import org.jetbrains.kotlin.testing.native.*
import org.jetbrains.kotlin.bitcode.CompileToBitcode
import org.jetbrains.kotlin.bitcode.CompileToBitcodeExtension
import org.jetbrains.kotlin.konan.target.*
plugins {
id("compile-to-bitcode")
@@ -42,6 +44,7 @@ bitcode {
"${target}ExperimentalMemoryManager"
)
includeRuntime()
// TODO: Should depend on the sanitizer.
linkerArgs.add(project.file("../common/build/bitcode/main/$target/hash.bc").path)
}
@@ -107,9 +110,11 @@ bitcode {
}
targetList.forEach { targetName ->
createTestTask(
val allTests = mutableListOf<Task>()
allTests.addAll(createTestTasks(
project,
"StdAlloc",
targetName,
"${targetName}StdAllocRuntimeTests",
listOf(
"${targetName}Runtime",
@@ -120,11 +125,11 @@ targetList.forEach { targetName ->
)
) {
includeRuntime()
}
})
createTestTask(
allTests.addAll(createTestTasks(
project,
"Mimalloc",
targetName,
"${targetName}MimallocRuntimeTests",
listOf(
"${targetName}Runtime",
@@ -136,11 +141,11 @@ targetList.forEach { targetName ->
)
) {
includeRuntime()
}
})
createTestTask(
allTests.addAll(createTestTasks(
project,
"ExperimentalMMMimalloc",
targetName,
"${targetName}ExperimentalMMMimallocRuntimeTests",
listOf(
"${targetName}Runtime",
@@ -151,11 +156,11 @@ targetList.forEach { targetName ->
)
) {
includeRuntime()
}
})
createTestTask(
allTests.addAll(createTestTasks(
project,
"ExperimentalMMStdAlloc",
targetName,
"${targetName}ExperimentalMMStdAllocRuntimeTests",
listOf(
"${targetName}Runtime",
@@ -165,13 +170,11 @@ targetList.forEach { targetName ->
)
) {
includeRuntime()
}
})
// TODO: This "all tests" tasks should be provided by `CompileToBitcodeExtension`
tasks.register("${targetName}RuntimeTests") {
dependsOn("${targetName}StdAllocRuntimeTests")
dependsOn("${targetName}MimallocRuntimeTests")
dependsOn("${targetName}ExperimentalMMStdAllocRuntimeTests")
dependsOn("${targetName}ExperimentalMMMimallocRuntimeTests")
dependsOn(allTests)
}
}
@@ -3684,4 +3684,16 @@ ALWAYS_INLINE RUNTIME_NOTHROW void Kotlin_mm_switchThreadStateRunnable() {
// no-op, used by the new MM only.
}
ALWAYS_INLINE RUNTIME_NOTHROW void Kotlin_mm_safePointFunctionEpilogue() {
// no-op, used by the new MM only.
}
ALWAYS_INLINE RUNTIME_NOTHROW void Kotlin_mm_safePointWhileLoopBody() {
// no-op, used by the new MM only.
}
ALWAYS_INLINE RUNTIME_NOTHROW void Kotlin_mm_safePointExceptionUnwind() {
// no-op, used by the new MM only.
}
} // extern "C"
@@ -112,7 +112,9 @@ TEST_F(KonanAllocatorAwareTest, PlacementAllocated) {
TEST_F(KonanAllocatorAwareTest, PlacementConstructedArray) {
constexpr size_t kCount = 5;
std::array<uint8_t, sizeof(A) * kCount> buffer;
// TODO: Consider removing support for placement new[] altogether, since there's no
// portable way to know needed storage size ahead of time.
alignas(A) std::array<uint8_t, sizeof(A) * kCount + sizeof(size_t)> buffer;
A* as = new (buffer.data()) A[kCount];
std::vector<int> actual;
@@ -44,4 +44,7 @@ void EnsureDeclarationsEmitted() {
ensureUsed(CheckGlobalsAccessible);
ensureUsed(Kotlin_mm_switchThreadStateNative);
ensureUsed(Kotlin_mm_switchThreadStateRunnable);
ensureUsed(Kotlin_mm_safePointFunctionEpilogue);
ensureUsed(Kotlin_mm_safePointWhileLoopBody);
ensureUsed(Kotlin_mm_safePointExceptionUnwind);
}
@@ -283,6 +283,11 @@ ALWAYS_INLINE RUNTIME_NOTHROW void Kotlin_mm_switchThreadStateNative();
// Sets state of the current thread to RUNNABLE (used by the new MM).
ALWAYS_INLINE RUNTIME_NOTHROW void Kotlin_mm_switchThreadStateRunnable();
// Safe point callbacks from Kotlin code generator.
void Kotlin_mm_safePointFunctionEpilogue() RUNTIME_NOTHROW;
void Kotlin_mm_safePointWhileLoopBody() RUNTIME_NOTHROW;
void Kotlin_mm_safePointExceptionUnwind() RUNTIME_NOTHROW;
#ifdef __cplusplus
}
#endif
@@ -71,8 +71,8 @@ void InitOrDeinitGlobalVariables(int initialize, MemoryState* memory) {
}
}
KBoolean g_checkLeaks = KonanNeedDebugInfo;
KBoolean g_checkLeakedCleaners = KonanNeedDebugInfo;
KBoolean g_checkLeaks = false;
KBoolean g_checkLeakedCleaners = false;
KBoolean g_forceCheckedShutdown = false;
constexpr RuntimeState* kInvalidRuntime = nullptr;
@@ -8,6 +8,9 @@ namespace kotlin {
#if KONAN_WINDOWS
// TODO: Figure out why creating many threads on windows is so slow.
constexpr int kDefaultThreadCount = 10;
#elif __has_feature(thread_sanitizer)
// TSAN has a huge overhead.
constexpr int kDefaultThreadCount = 10;
#else
constexpr int kDefaultThreadCount = 100;
#endif
@@ -23,16 +23,21 @@ fun testLauncherEntryPoint(args: Array<String>): Int {
}
fun main(args: Array<String>) {
exitProcess(testLauncherEntryPoint(args))
val exitCode = testLauncherEntryPoint(args)
if (exitCode != 0) {
exitProcess(exitCode)
}
}
fun worker(args: Array<String>) {
val worker = Worker.start()
val result = worker.execute(TransferMode.SAFE, { args.freeze() }) {
val exitCode = worker.execute(TransferMode.SAFE, { args.freeze() }) {
it -> testLauncherEntryPoint(it)
}.result
worker.requestTermination().result
exitProcess(result)
if (exitCode != 0) {
exitProcess(exitCode)
}
}
fun mainNoExit(args: Array<String>) {
@@ -14,9 +14,27 @@
using namespace kotlin;
namespace {
template <typename T>
ALWAYS_INLINE T UnsafeRead(T* location) noexcept {
#if __has_feature(thread_sanitizer)
// Make TSAN think that this load is fine.
return __atomic_load_n(location, __ATOMIC_ACQUIRE);
#else
return *location;
#endif
}
} // namespace
// static
mm::ExtraObjectData& mm::ExtraObjectData::Install(ObjHeader* object) noexcept {
TypeInfo* typeInfo = object->typeInfoOrMeta_;
// TODO: Consider extracting initialization scheme with speculative load.
// `object->typeInfoOrMeta_` is assigned at most once. If we read some old value (i.e. not a meta object),
// we will fail at CAS below. If we read the new value, we will immediately return it.
TypeInfo* typeInfo = UnsafeRead(&object->typeInfoOrMeta_);
if (auto* metaObject = ObjHeader::AsMetaObject(typeInfo)) {
return mm::ExtraObjectData::FromMetaObjHeader(metaObject);
}
@@ -142,6 +142,16 @@ extern "C" RUNTIME_NOTHROW void InitAndRegisterGlobal(ObjHeader** location, cons
extern "C" const MemoryModel CurrentMemoryModel = MemoryModel::kExperimental;
extern "C" RUNTIME_NOTHROW void EnterFrame(ObjHeader** start, int parameters, int count) {
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
threadData->shadowStack().EnterFrame(start, parameters, count);
}
extern "C" RUNTIME_NOTHROW void LeaveFrame(ObjHeader** start, int parameters, int count) {
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
threadData->shadowStack().LeaveFrame(start, parameters, count);
}
extern "C" RUNTIME_NOTHROW void AddTLSRecord(MemoryState* memory, void** key, int size) {
GetThreadData(memory)->tls().AddRecord(key, size);
}
@@ -0,0 +1,37 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
#include "ShadowStack.hpp"
using namespace kotlin;
mm::ShadowStack::Iterator& mm::ShadowStack::Iterator::operator++() noexcept {
++object_;
Init();
return *this;
}
void mm::ShadowStack::Iterator::Init() noexcept {
while (frame_) {
if (object_ < end_) return;
frame_ = frame_->previous;
object_ = begin();
end_ = end();
}
}
void mm::ShadowStack::EnterFrame(ObjHeader** start, int parameters, int count) noexcept {
FrameOverlay* frame = reinterpret_cast<FrameOverlay*>(start);
frame->previous = currentFrame_;
currentFrame_ = frame;
// TODO: maybe compress in single value somehow.
frame->parameters = parameters;
frame->count = count;
}
void mm::ShadowStack::LeaveFrame(ObjHeader** start, int parameters, int count) noexcept {
FrameOverlay* frame = reinterpret_cast<FrameOverlay*>(start);
currentFrame_ = frame->previous;
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
#ifndef RUNTIME_MM_SHADOW_STACK
#define RUNTIME_MM_SHADOW_STACK
#include "Memory.h"
#include "Utils.hpp"
struct FrameOverlay;
struct ObjHeader;
namespace kotlin {
namespace mm {
// Accessing current stack as provided by K/N compiler. The compiler calls `EnterFrame` when
// it has allocated and zeroed stack space in the function prologue. And it calls `LeaveFrame` in
// the function epilogue (both for regular return and for exception unwinding).
//
// Stack scanning does not lock anything and so must either be done while the mutator is stopped (or is
// running code outside Kotlin), or by the mutator itself. So, in concurrent collection case, make sure
// to do as little as possible while scanning the stack to free the mutator as soon as possible.
//
// TODO: This is currently incompatible with stack-allocated objects. Fix it.
class ShadowStack : private Pinned {
public:
class Iterator {
public:
explicit Iterator(FrameOverlay* frame) noexcept : frame_(frame), object_(begin()), end_(end()) { Init(); }
ObjHeader*& operator*() noexcept { return *object_; }
Iterator& operator++() noexcept;
bool operator==(const Iterator& rhs) const noexcept { return frame_ == rhs.frame_ && object_ == rhs.object_; }
bool operator!=(const Iterator& rhs) const noexcept { return !(*this == rhs); }
private:
void Init() noexcept;
// TODO: This copies the approach in the old MM. Do we need to also traverse function parameters in the new MM?
ObjHeader** begin() noexcept { return frame_ ? reinterpret_cast<ObjHeader**>(frame_ + 1) + frame_->parameters : nullptr; }
ObjHeader** end() noexcept {
constexpr int kFrameOverlaySlots = sizeof(FrameOverlay) / sizeof(ObjHeader**);
return frame_ ? begin() + frame_->count - kFrameOverlaySlots - frame_->parameters : nullptr;
}
FrameOverlay* frame_;
ObjHeader** object_ = nullptr;
ObjHeader** end_ = nullptr;
};
void EnterFrame(ObjHeader** start, int parameters, int count) noexcept;
void LeaveFrame(ObjHeader** start, int parameters, int count) noexcept;
Iterator begin() noexcept { return Iterator(currentFrame_); }
Iterator end() noexcept { return Iterator(nullptr); }
private:
FrameOverlay* currentFrame_ = nullptr;
};
} // namespace mm
} // namespace kotlin
#endif // RUNTIME_MM_SHADOW_STACK
@@ -0,0 +1,121 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
#include "ShadowStack.hpp"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "Memory.h"
#include "Types.h"
#include "Utils.hpp"
using namespace kotlin;
namespace {
template <size_t ParametersCount, size_t LocalsCount>
class StackEntry : private Pinned {
public:
static_assert(ParametersCount + LocalsCount > 0, "Must have at least 1 object on stack");
explicit StackEntry(mm::ShadowStack& shadowStack) : shadowStack_(shadowStack), value_(make_unique<ObjHeader>()) {
// Fill `locals_` with some values.
for (size_t i = 0; i < LocalsCount; ++i) {
(*this)[i] = value_.get() + i;
}
shadowStack_.EnterFrame(data_.data(), ParametersCount, kTotalCount);
}
~StackEntry() { shadowStack_.LeaveFrame(data_.data(), ParametersCount, kTotalCount); }
ObjHeader*& operator[](size_t index) { return data_[kFrameOverlayCount + ParametersCount + index]; }
private:
mm::ShadowStack& shadowStack_;
KStdUniquePtr<ObjHeader> value_;
// The following is what the compiler creates on the stack.
static inline constexpr int kFrameOverlayCount = sizeof(FrameOverlay) / sizeof(ObjHeader**);
static inline constexpr int kTotalCount = kFrameOverlayCount + ParametersCount + LocalsCount;
std::array<ObjHeader*, kTotalCount> data_;
};
KStdVector<ObjHeader*> Collect(mm::ShadowStack& shadowStack) {
KStdVector<ObjHeader*> result;
for (ObjHeader* local : shadowStack) {
result.push_back(local);
}
return result;
}
} // namespace
TEST(ShadowStackTest, Empty) {
mm::ShadowStack shadowStack;
auto actual = Collect(shadowStack);
EXPECT_THAT(actual, testing::IsEmpty());
}
TEST(ShadowStackTest, OneLocal) {
mm::ShadowStack shadowStack;
StackEntry<0, 1> frame1(shadowStack);
auto actual = Collect(shadowStack);
EXPECT_THAT(actual, testing::ElementsAre(frame1[0]));
}
TEST(ShadowStackTest, ThreeLocals) {
mm::ShadowStack shadowStack;
StackEntry<0, 3> frame1(shadowStack);
auto actual = Collect(shadowStack);
EXPECT_THAT(actual, testing::ElementsAre(frame1[0], frame1[1], frame1[2]));
}
TEST(ShadowStackTest, OneParameter) {
mm::ShadowStack shadowStack;
StackEntry<1, 0> frame1(shadowStack);
auto actual = Collect(shadowStack);
EXPECT_THAT(actual, testing::IsEmpty());
}
TEST(ShadowStackTest, ThreeLocalsAndOneParameter) {
mm::ShadowStack shadowStack;
StackEntry<1, 3> frame1(shadowStack);
auto actual = Collect(shadowStack);
EXPECT_THAT(actual, testing::ElementsAre(frame1[0], frame1[1], frame1[2]));
}
TEST(ShadowStackTest, TwoStackFrames) {
mm::ShadowStack shadowStack;
StackEntry<1, 3> frame1(shadowStack);
StackEntry<1, 3> frame2(shadowStack);
auto actual = Collect(shadowStack);
EXPECT_THAT(actual, testing::ElementsAre(frame2[0], frame2[1], frame2[2], frame1[0], frame1[1], frame1[2]));
}
TEST(ShadowStackTest, ManyStackFrames) {
mm::ShadowStack shadowStack;
StackEntry<0, 3> frame1(shadowStack);
StackEntry<1, 0> frame2(shadowStack);
StackEntry<3, 1> frame3(shadowStack);
StackEntry<3, 3> frame4(shadowStack);
auto actual = Collect(shadowStack);
EXPECT_THAT(actual, testing::ElementsAre(frame4[0], frame4[1], frame4[2], frame3[0], frame1[0], frame1[1], frame1[2]));
}
+12 -8
View File
@@ -70,14 +70,6 @@ RUNTIME_NOTHROW OBJ_GETTER(ReadHeapRefLocked, ObjHeader** location, int32_t* spi
TODO();
}
RUNTIME_NOTHROW void EnterFrame(ObjHeader** start, int parameters, int count) {
TODO();
}
RUNTIME_NOTHROW void LeaveFrame(ObjHeader** start, int parameters, int count) {
TODO();
}
void MutationCheck(ObjHeader* obj) {
TODO();
}
@@ -110,4 +102,16 @@ ForeignRefContext InitLocalForeignRef(ObjHeader* object) {
TODO();
}
RUNTIME_NOTHROW void Kotlin_mm_safePointFunctionEpilogue() {
TODO();
}
RUNTIME_NOTHROW void Kotlin_mm_safePointWhileLoopBody() {
TODO();
}
RUNTIME_NOTHROW void Kotlin_mm_safePointExceptionUnwind() {
TODO();
}
} // extern "C"
@@ -11,6 +11,7 @@
#include "ObjectFactory.hpp"
#include "GlobalsRegistry.hpp"
#include "ShadowStack.hpp"
#include "StableRefRegistry.hpp"
#include "ThreadLocalStorage.hpp"
#include "Utils.hpp"
@@ -46,6 +47,8 @@ public:
ObjectFactory::ThreadQueue& objectFactoryThreadQueue() noexcept { return objectFactoryThreadQueue_; }
ShadowStack& shadowStack() noexcept { return shadowStack_; }
private:
const pthread_t threadId_;
GlobalsRegistry::ThreadQueue globalsThreadQueue_;
@@ -53,6 +56,7 @@ private:
StableRefRegistry::ThreadQueue stableRefThreadQueue_;
std::atomic<ThreadState> state_;
ObjectFactory::ThreadQueue objectFactoryThreadQueue_;
ShadowStack shadowStack_;
};
} // namespace mm
@@ -0,0 +1,3 @@
# Trust mimalloc to be thread safe.
race:^mi_
race:^_mi_
@@ -30,3 +30,14 @@ fun KonanTarget.supportsThreads(): Boolean =
is KonanTarget.ZEPHYR -> false
else -> true
}
fun KonanTarget.supportedSanitizers(): List<SanitizerKind> =
when(this) {
is KonanTarget.LINUX_X64 -> listOf(SanitizerKind.ADDRESS)
is KonanTarget.MACOS_X64 -> listOf(SanitizerKind.THREAD)
// TODO: Enable ASAN on macOS. Currently there's an incompatibility between clang frontend version and clang_rt.asan version.
// TODO: Enable TSAN on linux. Currently there's a link error between clang_rt.tsan and libstdc++.
// TODO: Consider supporting mingw.
// TODO: Support macOS arm64
else -> listOf()
}
@@ -76,7 +76,8 @@ abstract class LinkerFlags(val configurables: Configurables) {
libraries: List<String>, linkerArgs: List<String>,
optimize: Boolean, debug: Boolean,
kind: LinkerOutputKind, outputDsymBundle: String,
needsProfileLibrary: Boolean, mimallocEnabled: Boolean): List<Command>
needsProfileLibrary: Boolean, mimallocEnabled: Boolean,
sanitizer: SanitizerKind? = null): List<Command>
/**
* Returns list of commands that link object files into a single one.
@@ -93,7 +94,7 @@ abstract class LinkerFlags(val configurables: Configurables) {
return libraries
}
protected open fun provideCompilerRtLibrary(libraryName: String): String? {
protected open fun provideCompilerRtLibrary(libraryName: String, isDynamic: Boolean = false): String? {
System.err.println("Can't provide $libraryName.")
return null
}
@@ -123,7 +124,11 @@ class AndroidLinker(targetProperties: AndroidConfigurables)
libraries: List<String>, linkerArgs: List<String>,
optimize: Boolean, debug: Boolean,
kind: LinkerOutputKind, outputDsymBundle: String,
needsProfileLibrary: Boolean, mimallocEnabled: Boolean): List<Command> {
needsProfileLibrary: Boolean, mimallocEnabled: Boolean,
sanitizer: SanitizerKind?): List<Command> {
require(sanitizer == null) {
"Sanitizers are unsupported"
}
if (kind == LinkerOutputKind.STATIC_LIBRARY)
return staticGnuArCommands(ar, executable, objectFiles, libraries)
@@ -170,7 +175,12 @@ class MacOSBasedLinker(targetProperties: AppleConfigurables)
get() = this == KonanTarget.TVOS_X64 || this == KonanTarget.IOS_X64 ||
this == KonanTarget.WATCHOS_X86 || this == KonanTarget.WATCHOS_X64
override fun provideCompilerRtLibrary(libraryName: String): String? {
private val compilerRtDir: String? by lazy {
val dir = File("$absoluteTargetToolchain/usr/lib/clang/").listFiles.firstOrNull()?.absolutePath
if (dir != null) "$dir/lib/darwin/" else null
}
override fun provideCompilerRtLibrary(libraryName: String, isDynamic: Boolean): String? {
val prefix = when (target.family) {
Family.IOS -> "ios"
Family.WATCHOS -> "watchos"
@@ -184,10 +194,11 @@ class MacOSBasedLinker(targetProperties: AppleConfigurables)
""
}
val dir = File("$absoluteTargetToolchain/usr/lib/clang/").listFiles.firstOrNull()?.absolutePath
val dir = compilerRtDir
val mangledLibraryName = if (libraryName.isEmpty()) "" else "${libraryName}_"
val extension = if (isDynamic) "_dynamic.dylib" else ".a"
return if (dir != null) "$dir/lib/darwin/libclang_rt.$mangledLibraryName$prefix$suffix.a" else null
return if (dir != null) "$dir/libclang_rt.$mangledLibraryName$prefix$suffix$extension" else null
}
private val osVersionMinFlags: List<String> by lazy {
@@ -210,14 +221,19 @@ class MacOSBasedLinker(targetProperties: AppleConfigurables)
libraries: List<String>, linkerArgs: List<String>,
optimize: Boolean, debug: Boolean, kind: LinkerOutputKind,
outputDsymBundle: String,
needsProfileLibrary: Boolean, mimallocEnabled: Boolean): List<Command> {
if (kind == LinkerOutputKind.STATIC_LIBRARY)
needsProfileLibrary: Boolean, mimallocEnabled: Boolean,
sanitizer: SanitizerKind?): List<Command> {
if (kind == LinkerOutputKind.STATIC_LIBRARY) {
require(sanitizer == null) {
"Sanitizers are unsupported"
}
return listOf(Command(libtool).apply {
+"-static"
+listOf("-o", executable)
+objectFiles
+libraries
})
}
val dynamic = kind == LinkerOutputKind.DYNAMIC_LIBRARY
val result = mutableListOf<Command>()
@@ -237,7 +253,12 @@ class MacOSBasedLinker(targetProperties: AppleConfigurables)
if (needsProfileLibrary) +profileLibrary!!
+libraries
+linkerArgs
+rpath(dynamic)
+rpath(dynamic, sanitizer)
when (sanitizer) {
null -> {}
SanitizerKind.ADDRESS -> +provideCompilerRtLibrary("asan", isDynamic=true)!!
SanitizerKind.THREAD -> +provideCompilerRtLibrary("tsan", isDynamic=true)!!
}
}
// TODO: revise debug information handling.
@@ -255,7 +276,7 @@ class MacOSBasedLinker(targetProperties: AppleConfigurables)
provideCompilerRtLibrary("")
}
private fun rpath(dynamic: Boolean): List<String> = listOfNotNull(
private fun rpath(dynamic: Boolean, sanitizer: SanitizerKind?): List<String> = listOfNotNull(
when (target.family) {
Family.OSX -> "@executable_path/../Frameworks"
Family.IOS,
@@ -263,7 +284,8 @@ class MacOSBasedLinker(targetProperties: AppleConfigurables)
Family.TVOS -> "@executable_path/Frameworks"
else -> error(target)
},
"@loader_path/Frameworks".takeIf { dynamic }
"@loader_path/Frameworks".takeIf { dynamic },
compilerRtDir.takeIf { sanitizer != null },
).flatMap { listOf("-rpath", it) }
fun dsymUtilCommand(executable: ExecutableFile, outputDsymBundle: String) =
@@ -319,7 +341,10 @@ class GccBasedLinker(targetProperties: GccConfigurables)
private val specificLibs = abiSpecificLibraries.map { "-L${absoluteTargetSysRoot}/$it" }
override fun provideCompilerRtLibrary(libraryName: String): String? {
override fun provideCompilerRtLibrary(libraryName: String, isDynamic: Boolean): String? {
require(!isDynamic) {
"Dynamic compiler rt librares are unsupported"
}
val targetSuffix = when (target) {
KonanTarget.LINUX_X64 -> "x86_64"
else -> error("$target is not supported.")
@@ -334,9 +359,14 @@ class GccBasedLinker(targetProperties: GccConfigurables)
libraries: List<String>, linkerArgs: List<String>,
optimize: Boolean, debug: Boolean,
kind: LinkerOutputKind, outputDsymBundle: String,
needsProfileLibrary: Boolean, mimallocEnabled: Boolean): List<Command> {
if (kind == LinkerOutputKind.STATIC_LIBRARY)
needsProfileLibrary: Boolean, mimallocEnabled: Boolean,
sanitizer: SanitizerKind?): List<Command> {
if (kind == LinkerOutputKind.STATIC_LIBRARY) {
require(sanitizer == null) {
"Sanitizers are unsupported"
}
return staticGnuArCommands(ar, executable, objectFiles, libraries)
}
val isMips = target == KonanTarget.LINUX_MIPS32 || target == KonanTarget.LINUX_MIPSEL32
val dynamic = kind == LinkerOutputKind.DYNAMIC_LIBRARY
val crtPrefix = "$absoluteTargetSysRoot/$crtFilesLocation"
@@ -373,6 +403,19 @@ class GccBasedLinker(targetProperties: GccConfigurables)
+linkerGccFlags
+if (dynamic) "$libGcc/crtendS.o" else "$libGcc/crtend.o"
+"$crtPrefix/crtn.o"
when (sanitizer) {
null -> {}
SanitizerKind.ADDRESS -> {
+"-lrt"
+provideCompilerRtLibrary("asan")!!
+provideCompilerRtLibrary("asan_cxx")!!
}
SanitizerKind.THREAD -> {
+"-lrt"
+provideCompilerRtLibrary("tsan")!!
+provideCompilerRtLibrary("tsan_cxx")!!
}
}
})
}
}
@@ -387,7 +430,10 @@ class MingwLinker(targetProperties: MingwConfigurables)
override fun filterStaticLibraries(binaries: List<String>) = binaries.filter { it.isWindowsStaticLib || it.isUnixStaticLib }
override fun provideCompilerRtLibrary(libraryName: String): String? {
override fun provideCompilerRtLibrary(libraryName: String, isDynamic: Boolean): String? {
require(!isDynamic) {
"Dynamic compiler rt librares are unsupported"
}
val targetSuffix = when (target) {
KonanTarget.MINGW_X64 -> "x86_64"
else -> error("$target is not supported.")
@@ -400,7 +446,11 @@ class MingwLinker(targetProperties: MingwConfigurables)
libraries: List<String>, linkerArgs: List<String>,
optimize: Boolean, debug: Boolean,
kind: LinkerOutputKind, outputDsymBundle: String,
needsProfileLibrary: Boolean, mimallocEnabled: Boolean): List<Command> {
needsProfileLibrary: Boolean, mimallocEnabled: Boolean,
sanitizer: SanitizerKind?): List<Command> {
require(sanitizer == null) {
"Sanitizers are unsupported"
}
if (kind == LinkerOutputKind.STATIC_LIBRARY)
return staticGnuArCommands(ar, executable, objectFiles, libraries)
@@ -437,8 +487,12 @@ class WasmLinker(targetProperties: WasmConfigurables)
libraries: List<String>, linkerArgs: List<String>,
optimize: Boolean, debug: Boolean,
kind: LinkerOutputKind, outputDsymBundle: String,
needsProfileLibrary: Boolean, mimallocEnabled: Boolean): List<Command> {
needsProfileLibrary: Boolean, mimallocEnabled: Boolean,
sanitizer: SanitizerKind?): List<Command> {
if (kind != LinkerOutputKind.EXECUTABLE) throw Error("Unsupported linker output kind")
require(sanitizer == null) {
"Sanitizers are unsupported"
}
val linkage = Command("$llvmBin/wasm-ld").apply {
+objectFiles
@@ -489,8 +543,12 @@ open class ZephyrLinker(targetProperties: ZephyrConfigurables)
libraries: List<String>, linkerArgs: List<String>,
optimize: Boolean, debug: Boolean,
kind: LinkerOutputKind, outputDsymBundle: String,
needsProfileLibrary: Boolean, mimallocEnabled: Boolean): List<Command> {
needsProfileLibrary: Boolean, mimallocEnabled: Boolean,
sanitizer: SanitizerKind?): List<Command> {
if (kind != LinkerOutputKind.EXECUTABLE) throw Error("Unsupported linker output kind: $kind")
require(sanitizer == null) {
"Sanitizers are unsupported"
}
return listOf(Command(linker).apply {
+listOf("-r", "--gc-sections", "--entry", "main")
+listOf("-o", executable)
@@ -0,0 +1,11 @@
/*
* Copyright 2010-2020 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.
*/
package org.jetbrains.kotlin.konan.target
enum class SanitizerKind {
ADDRESS,
THREAD,
}
@@ -25,10 +25,13 @@ val MeanVarianceBenchmark.description: String
// Calculate difference in percentage compare to another.
fun MeanVarianceBenchmark.calcPercentageDiff(other: MeanVarianceBenchmark): MeanVariance {
if (score == 0.0 && variance == 0.0 && other.score == 0.0 && other.variance == 0.0)
return MeanVariance(score, variance)
assert(other.score >= 0 &&
other.variance >= 0 &&
other.score - other.variance != 0.0,
(other.score - other.variance != 0.0 || other.score == 0.0),
{ "Mean and variance should be positive and not equal!" })
// Analyze intervals. Calculate difference between border points.
val (bigValue, smallValue) = if (score > other.score) Pair(this, other) else Pair(other, this)
val bigValueIntervalStart = bigValue.score - bigValue.variance
@@ -50,11 +53,13 @@ fun MeanVarianceBenchmark.calcPercentageDiff(other: MeanVarianceBenchmark): Mean
// Calculate ratio value compare to another.
fun MeanVarianceBenchmark.calcRatio(other: MeanVarianceBenchmark): MeanVariance {
if (other.score == 0.0 && other.variance == 0.0)
return MeanVariance(1.0, 0.0)
assert(other.score >= 0 &&
other.variance >= 0 &&
other.score - other.variance != 0.0,
(other.score - other.variance != 0.0 || other.score == 0.0),
{ "Mean and variance should be positive and not equal!" })
val mean = score / other.score
val mean = if (other.score != 0.0) (score / other.score) else 0.0
val minRatio = (score - variance) / (other.score + other.variance)
val maxRatio = (score + variance) / (other.score - other.variance)
val ratioConfInt = min(abs(minRatio - mean), abs(maxRatio - mean))
@@ -63,7 +68,7 @@ fun MeanVarianceBenchmark.calcRatio(other: MeanVarianceBenchmark): MeanVariance
fun geometricMean(values: Collection<Double>, totalNumber: Int = values.size) =
with(values.asSequence().filter { it != 0.0 }) {
if (count() == 0) {
if (count() == 0 || totalNumber == 0) {
0.0
} else {
map { it.pow(1.0 / totalNumber) }.reduce { a, b -> a * b }
@@ -18,13 +18,11 @@ typealias BenchmarksTable = Map<String, MeanVarianceBenchmark>
typealias SummaryBenchmarksTable = Map<String, SummaryBenchmark>
typealias ScoreChange = Pair<MeanVariance, MeanVariance>
// Summary report with comparasion of separate benchmarks results.
class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
val previousReport: BenchmarksReport? = null,
val meaningfulChangesValue: Double = 0.5) {
class DetailedBenchmarksReport(currentBenchmarks: Map<String, List<BenchmarkResult>>,
previousBenchmarks: Map<String, List<BenchmarkResult>>? = null,
val meaningfulChangesValue: Double = 0.5) {
// Report created by joining comparing reports.
val mergedReport: Map<String, SummaryBenchmark>
private val benchmarksDurations: Map<String, Pair<Double?, Double?>>
// Lists of benchmarks in different status.
private val benchmarksWithChangedStatus = mutableListOf<FieldChange<BenchmarkResult.Status>>()
@@ -40,30 +38,6 @@ class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
var geoMeanScoreChange: ScoreChange? = null
private set
// Environment and tools.
val environments: Pair<Environment, Environment?>
val compilers: Pair<Compiler, Compiler?>
// Countable properties.
val failedBenchmarks: List<String>
get() = mergedReport.filter { it.value.first?.status == BenchmarkResult.Status.FAILED }
.map { it.key }
val addedBenchmarks: List<String>
get() = mergedReport.filter { it.value.second == null }.map { it.key }
val removedBenchmarks: List<String>
get() = mergedReport.filter { it.value.first == null }.map { it.key }
val benchmarksNumber: Int
get() = mergedReport.keys.size
val currentMeanVarianceBenchmarks: List<MeanVarianceBenchmark>
get() = mergedReport.filter { it.value.first != null }.map { it.value.first!! }
val currentBenchmarksDuration: Map<String, Double>
get() = benchmarksDurations.filter { it.value.first != null }.map { it.key to it.value.first!! }.toMap()
val maximumRegression: Double
get() = getMaximumChange(regressions)
@@ -76,90 +50,53 @@ class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
val improvementsGeometricMean: Double
get() = getGeometricMeanOfChanges(improvements)
val envChanges: List<FieldChange<String>>
get() {
val previousEnvironment = environments.second
val currentEnvironment = environments.first
return previousEnvironment?.let {
mutableListOf<FieldChange<String>>().apply {
addFieldChange("Machine CPU", previousEnvironment.machine.cpu, currentEnvironment.machine.cpu)
addFieldChange("Machine OS", previousEnvironment.machine.os, currentEnvironment.machine.os)
addFieldChange("JDK version", previousEnvironment.jdk.version, currentEnvironment.jdk.version)
addFieldChange("JDK vendor", previousEnvironment.jdk.vendor, currentEnvironment.jdk.vendor)
}
} ?: listOf<FieldChange<String>>()
}
val kotlinChanges: List<FieldChange<String>>
get() {
val previousCompiler = compilers.second
val currentCompiler = compilers.first
return previousCompiler?.let {
mutableListOf<FieldChange<String>>().apply {
addFieldChange("Backend type", previousCompiler.backend.type.type, currentCompiler.backend.type.type)
addFieldChange("Backend version", previousCompiler.backend.version, currentCompiler.backend.version)
addFieldChange("Backend flags", previousCompiler.backend.flags.toString(),
currentCompiler.backend.flags.toString())
addFieldChange("Kotlin version", previousCompiler.kotlinVersion, currentCompiler.kotlinVersion)
}
} ?: listOf<FieldChange<String>>()
}
val benchmarksNumber: Int
get() = mergedReport.keys.size
init {
// Count avarage values for each benchmark.
val currentBenchmarksTable = collectMeanResults(currentReport.benchmarks)
val previousBenchmarksTable = previousReport?.let {
collectMeanResults(previousReport.benchmarks)
val currentBenchmarksTable = collectMeanResults(currentBenchmarks)
val previousBenchmarksTable = previousBenchmarks?.let {
collectMeanResults(previousBenchmarks)
}
mergedReport = createMergedReport(currentBenchmarksTable, previousBenchmarksTable)
benchmarksDurations = calculateBenchmarksDuration(currentReport, previousReport)
geoMeanBenchmark = calculateGeoMeanBenchmark(currentBenchmarksTable, previousBenchmarksTable)
environments = Pair(currentReport.env, previousReport?.env)
compilers = Pair(currentReport.compiler, previousReport?.compiler)
if (previousReport != null) {
if (previousBenchmarks != null) {
// Check changes in environment and tools.
analyzePerformanceChanges()
}
}
// Get benchmark report.
fun getBenchmarksReport(takeMainReport: Boolean = true) =
if (takeMainReport)
BenchmarksReport(environments.first, mergedReport.map { (_, value) -> value.first!! }, compilers.first)
else
BenchmarksReport(environments.second!!, mergedReport.map { (_, value) -> value.second!! }, compilers.second!!)
fun getResultsByMetric(metric: BenchmarkResult.Metric, getGeoMean: Boolean = true, filter: List<String>? = null,
normalizeData: Map<String, Map<String, Double>>? = null): List<Double?> {
val benchmarks = filter?.let {
mergedReport.filter { entry ->
filter.find {
entry.key.startsWith(it)
} != null
}
} ?: mergedReport
val results = benchmarks.map { entry ->
val name = entry.key.removeSuffix(metric.suffix)
if (entry.value.first!!.metric == metric) {
val score = entry.value.first!!.score
val value = normalizeData?.let {
it.get(name)?.get("$metric")?.let { score / it }
?: error("No normalization data for benchmark $name and metric $metric")
} ?: score
name to value
} else name to null
}.toMap()
if (getGeoMean) {
return listOf(geometricMean(results.values.filterNotNull()))
}
return filter?.let { it.map { results[it] }.toList() } ?: results.values.toList()
}
private fun getMaximumChange(bucket: Map<String, ScoreChange>): Double =
// Maps of regressions and improvements are sorted.
if (bucket.isEmpty()) 0.0 else bucket.values.map { it.first.mean }.first()
// Analyze and collect changes in performance between same becnhmarks.
private fun analyzePerformanceChanges() {
val performanceChanges = mergedReport.asSequence().map { (name, element) ->
getBenchmarkPerfomanceChange(name, element)
}.filterNotNull().groupBy {
if (it.second.first.mean > 0) "regressions" else "improvements"
}
// Sort regressions and improvements.
regressions = performanceChanges["regressions"]
?.sortedByDescending { it.second.first.mean }?.map { it.first to it.second }
?.toMap() ?: mapOf<String, ScoreChange>()
improvements = performanceChanges["improvements"]
?.sortedBy { it.second.first.mean }?.map { it.first to it.second }
?.toMap() ?: mapOf<String, ScoreChange>()
// Calculate change for geometric mean.
val (current, previous) = geoMeanBenchmark
geoMeanScoreChange = current?.let {
previous?.let {
Pair(current.calcPercentageDiff(previous), current.calcRatio(previous))
}
}
}
private fun getGeometricMeanOfChanges(bucket: Map<String, ScoreChange>): Double {
if (bucket.isEmpty())
return 0.0
@@ -175,25 +112,6 @@ class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
fun getBenchmarksWithChangedStatus(): List<FieldChange<BenchmarkResult.Status>> = benchmarksWithChangedStatus
// Create geometric mean.
private fun createGeoMeanBenchmark(benchTable: BenchmarksTable): MeanVarianceBenchmark {
val geoMeanBenchmarkName = "Geometric mean"
val geoMean = geometricMean(benchTable.toList().map { (_, value) -> value.score })
val varianceGeoMean = geometricMean(benchTable.toList().map { (_, value) -> value.variance })
return MeanVarianceBenchmark(geoMeanBenchmarkName, geoMean, varianceGeoMean)
}
// Generate map with summary durations of each benchmark.
private fun calculateBenchmarksDuration(currentReport: BenchmarksReport, previousReport: BenchmarksReport?):
Map<String, Pair<Double?, Double?>> {
val currentDurations = collectBenchmarksDurations(currentReport.benchmarks)
val previousDurations = previousReport?.let {
collectBenchmarksDurations(previousReport.benchmarks)
} ?: mapOf<String, Double>()
return currentDurations.keys.union(previousDurations.keys)
.map { it to Pair(currentDurations[it], previousDurations[it]) }.toMap()
}
// Merge current and compare to report.
private fun createMergedReport(currentBenchmarks: BenchmarksTable, previousBenchmarks: BenchmarksTable?):
Map<String, SummaryBenchmark> {
@@ -249,29 +167,144 @@ class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
return null
}
// Analyze and collect changes in performance between same becnhmarks.
private fun analyzePerformanceChanges() {
val performanceChanges = mergedReport.asSequence().map { (name, element) ->
getBenchmarkPerfomanceChange(name, element)
}.filterNotNull().groupBy {
if (it.second.first.mean > 0) "regressions" else "improvements"
// Create geometric mean.
private fun createGeoMeanBenchmark(benchTable: BenchmarksTable): MeanVarianceBenchmark {
val geoMeanBenchmarkName = "Geometric mean"
val geoMean = geometricMean(benchTable.toList().map { (_, value) -> value.score })
val varianceGeoMean = geometricMean(benchTable.toList().map { (_, value) -> value.variance })
return MeanVarianceBenchmark(geoMeanBenchmarkName, geoMean, varianceGeoMean)
}
}
// Summary report with comparasion of separate benchmarks results.
class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
val previousReport: BenchmarksReport? = null,
val meaningfulChangesValue: Double = 0.5,
private val unstableBenchmarks: List<String> = emptyList()) {
val detailedMetricReports: Map<BenchmarkResult.Metric, DetailedBenchmarksReport>
private val benchmarksDurations: Map<String, Pair<Double?, Double?>>
// Lists of benchmarks in different status.
val benchmarksWithChangedStatus
get() = getReducedResult { report ->
report.getBenchmarksWithChangedStatus()
}
// Sort regressions and improvements.
regressions = performanceChanges["regressions"]
?.sortedByDescending { it.second.first.mean }?.map { it.first to it.second }
?.toMap() ?: mapOf<String, ScoreChange>()
improvements = performanceChanges["improvements"]
?.sortedBy { it.second.first.mean }?.map { it.first to it.second }
?.toMap() ?: mapOf<String, ScoreChange>()
// Environment and tools.
val environments: Pair<Environment, Environment?>
val compilers: Pair<Compiler, Compiler?>
// Calculate change for geometric mean.
val (current, previous) = geoMeanBenchmark
geoMeanScoreChange = current?.let {
previous?.let {
Pair(current.calcPercentageDiff(previous), current.calcRatio(previous))
}
private fun <T> getReducedResult(convertor: (DetailedBenchmarksReport) -> List<T>): List<T> {
return detailedMetricReports.values.map {
convertor(it)
}.flatten()
}
// Countable properties.
val failedBenchmarks: List<String>
get() = getReducedResult { report ->
report.mergedReport.filter { it.value.first?.status == BenchmarkResult.Status.FAILED }.map { it.key }
}
val addedBenchmarks: List<String>
get() = getReducedResult { report ->
report.mergedReport.filter { it.value.second == null }.map { it.key }
}
val removedBenchmarks: List<String>
get() = getReducedResult { report ->
report.mergedReport.filter { it.value.first == null }.map { it.key }
}
val currentMeanVarianceBenchmarks: List<MeanVarianceBenchmark>
get() = getReducedResult { report ->
report.mergedReport.filter { it.value.first != null }.map { it.value.first!! }
}
val benchmarksNumber: Int
get() = detailedMetricReports.values.fold(0) { acc, it -> acc + it.benchmarksNumber }
val currentBenchmarksDuration: Map<String, Double>
get() = benchmarksDurations.filter { it.value.first != null }.map { it.key to it.value.first!! }.toMap()
val envChanges: List<FieldChange<String>>
get() {
val previousEnvironment = environments.second
val currentEnvironment = environments.first
return previousEnvironment?.let {
mutableListOf<FieldChange<String>>().apply {
addFieldChange("Machine CPU", previousEnvironment.machine.cpu, currentEnvironment.machine.cpu)
addFieldChange("Machine OS", previousEnvironment.machine.os, currentEnvironment.machine.os)
addFieldChange("JDK version", previousEnvironment.jdk.version, currentEnvironment.jdk.version)
addFieldChange("JDK vendor", previousEnvironment.jdk.vendor, currentEnvironment.jdk.vendor)
}
} ?: listOf<FieldChange<String>>()
}
val kotlinChanges: List<FieldChange<String>>
get() {
val previousCompiler = compilers.second
val currentCompiler = compilers.first
return previousCompiler?.let {
mutableListOf<FieldChange<String>>().apply {
addFieldChange("Backend type", previousCompiler.backend.type.type, currentCompiler.backend.type.type)
addFieldChange("Backend version", previousCompiler.backend.version, currentCompiler.backend.version)
addFieldChange("Backend flags", previousCompiler.backend.flags.toString(),
currentCompiler.backend.flags.toString())
addFieldChange("Kotlin version", previousCompiler.kotlinVersion, currentCompiler.kotlinVersion)
}
} ?: listOf<FieldChange<String>>()
}
init {
// Count avarage values for each benchmark.
detailedMetricReports = BenchmarkResult.Metric.values().map { metric ->
val currentBenchmarks = currentReport.benchmarks.map { (name, benchmarks) ->
name to benchmarks.filter { it.metric == metric }
}.filter { it.second.isNotEmpty() }.toMap()
val previousBenchmarks = previousReport?.benchmarks?.map { (name, benchmarks) ->
name to benchmarks.filter { it.metric == metric }
}?.filter { it.second.isNotEmpty() }?.toMap()
metric to DetailedBenchmarksReport(
currentReport.benchmarks.map { (name, benchmarks) ->
name to benchmarks.filter { it.metric == metric }
}.filter { it.second.isNotEmpty() }.toMap(),
previousReport?.benchmarks?.map { (name, benchmarks) ->
name to benchmarks.filter { it.metric == metric }
}?.filter { it.second.isNotEmpty() }?.toMap(),
meaningfulChangesValue
)
}.toMap()
benchmarksDurations = calculateBenchmarksDuration(currentReport, previousReport)
environments = Pair(currentReport.env, previousReport?.env)
compilers = Pair(currentReport.compiler, previousReport?.compiler)
}
// Get benchmark report.
fun getBenchmarksReport(takeMainReport: Boolean = true) =
if (takeMainReport)
BenchmarksReport(environments.first, getReducedResult { report ->
report.mergedReport.map { (_, value) -> value.first!! }
}, compilers.first)
else
BenchmarksReport(environments.second!!, getReducedResult { report ->
report.mergedReport.map { (_, value) -> value.second!! }
}, compilers.second!!)
fun getUnstableBenchmarksForMetric(metric: BenchmarkResult.Metric) =
if (metric == BenchmarkResult.Metric.EXECUTION_TIME) unstableBenchmarks else emptyList()
// Generate map with summary durations of each benchmark.
private fun calculateBenchmarksDuration(currentReport: BenchmarksReport, previousReport: BenchmarksReport?):
Map<String, Pair<Double?, Double?>> {
val currentDurations = collectBenchmarksDurations(currentReport.benchmarks)
val previousDurations = previousReport?.let {
collectBenchmarksDurations(previousReport.benchmarks)
} ?: mapOf<String, Double>()
return currentDurations.keys.union(previousDurations.keys)
.map { it to Pair(currentDurations[it], previousDurations[it]) }.toMap()
}
private fun <T> MutableList<FieldChange<T>>.addFieldChange(field: String, previous: T, current: T) {
@@ -270,7 +270,6 @@ open class BenchmarkResult(val name: String, val status: Status,
val metric = if (metricElement != null && metricElement is JsonLiteral)
metricFromString(metricElement.unquoted()) ?: Metric.EXECUTION_TIME
else Metric.EXECUTION_TIME
name += metric.suffix
val statusElement = data.getRequiredField("status")
if (statusElement is JsonLiteral) {
val status = statusFromString(statusElement.unquoted())
@@ -348,4 +347,35 @@ open class MeanVarianceBenchmark(name: String, status: BenchmarkResult.Status, s
"variance": $variance
"""
}
}
// Benchmark with set results stability state.
open class BenchmarkWithStabilityState(name: String, status: BenchmarkResult.Status, score: Double, metric: BenchmarkResult.Metric,
runtimeInUs: Double, repeat: Int, warmup: Int, val unstable: Boolean) :
BenchmarkResult(name, status, score, metric, runtimeInUs, repeat, warmup) {
constructor(benchmarkResult: BenchmarkResult, unstable: Boolean) : this(benchmarkResult.name,
benchmarkResult.status, benchmarkResult.score, benchmarkResult.metric,
benchmarkResult.runtimeInUs, benchmarkResult.repeat, benchmarkResult.warmup, unstable)
override fun serializeFields(): String {
return """
${super.serializeFields()},
"unstable": $unstable
"""
}
companion object : EntityFromJsonFactory<BenchmarkResult> {
override fun create(data: JsonElement): BenchmarkWithStabilityState {
val parsedObject = BenchmarkResult.create(data)
if (data is JsonObject) {
val unstableElement = data.getOptionalField("unstable")
val unstableFlag = if (unstableElement != null && unstableElement is JsonPrimitive)
unstableElement.boolean else false
return BenchmarkWithStabilityState(parsedObject, unstableFlag)
} else {
error("Benchmark entity is expected to be an object. Please, check origin files.")
}
}
}
}
@@ -79,6 +79,21 @@ object DBServerConnector : Connector() {
val accessFileUrl = "$serverUrl/report/$target/$buildNumber"
return sendGetRequest(accessFileUrl)
}
fun getUnstableBenchmarks(): List<String>? {
try {
val unstableList = sendGetRequest("$serverUrl/unstable")
val data = JsonTreeParser.parse(unstableList)
if (data !is JsonArray) {
return null
}
return data.jsonArray.map {
(it as JsonPrimitive).content
}
} catch (e: Exception) {
return null
}
}
}
fun getFileContent(fileName: String, user: String? = null): String {
@@ -155,6 +170,11 @@ fun main(args: Array<String>) {
val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization")
argParser.parse(args)
// Get unstable benchmarks.
val unstableBenchmarks = DBServerConnector.getUnstableBenchmarks()
unstableBenchmarks ?: println("Failed to get access to server and get unstable benchmarks list!")
// Read contents of file.
val mainBenchsReport = mergeReportsWithDetailedFlags(getBenchmarkReport(mainReport, user))
@@ -164,8 +184,8 @@ fun main(args: Array<String>) {
// Generate comparasion report.
val summaryReport = SummaryBenchmarksReport(mainBenchsReport,
compareToBenchsReport,
epsValue)
compareToBenchsReport, epsValue,
unstableBenchmarks ?: emptyList())
var outputFile = output
renders.forEach {
@@ -405,7 +405,7 @@ class HTMLRender: Render() {
}
}
val benchmarksWithChangedStatus = report.getBenchmarksWithChangedStatus()
val benchmarksWithChangedStatus = report.benchmarksWithChangedStatus
val newFailures = benchmarksWithChangedStatus
.filter { it.current == BenchmarkResult.Status.FAILED }
val newPasses = benchmarksWithChangedStatus
@@ -479,59 +479,86 @@ class HTMLRender: Render() {
}
private fun BodyTag.renderPerformanceSummary(report: SummaryBenchmarksReport) {
if (!report.improvements.isEmpty() || !report.regressions.isEmpty()) {
if (report.detailedMetricReports.values.any { it.improvements.isNotEmpty() } ||
report.detailedMetricReports.values.any { it.regressions.isNotEmpty() }) {
h4 { +"Performance Summary" }
table {
attributes["class"] = "table table-sm table-striped table-hover"
attributes["style"] = "width:initial;"
thead {
tr {
th { +"Change" }
th { +"#" }
th { +"Maximum" }
th { +"Geometric mean" }
}
}
val maximumRegression = report.maximumRegression
val maximumImprovement = report.maximumImprovement
val regressionsGeometricMean = report.regressionsGeometricMean
val improvementsGeometricMean = report.improvementsGeometricMean
tbody {
if (!report.regressions.isEmpty()) {
tr {
th { +"Regressions" }
td { +"${report.regressions.size}" }
td {
attributes["bgcolor"] = ColoredCell(
maximumRegression/maxOf(maximumRegression, abs(maximumImprovement)))
.backgroundStyle
+formatValue(maximumRegression, true)
}
td {
attributes["bgcolor"] = ColoredCell(
regressionsGeometricMean/maxOf(regressionsGeometricMean,
abs(improvementsGeometricMean)))
.backgroundStyle
+formatValue(report.regressionsGeometricMean, true)
}
th(rowspan = Natural(2)) { +"Change" }
report.detailedMetricReports.forEach { (metric, _) ->
th(colspan = Natural(3)) { +metric.value }
}
}
if (!report.improvements.isEmpty()) {
tr {
report.detailedMetricReports.forEach { _ ->
th { +"#" }
th { +"Maximum" }
th { +"Geometric mean" }
}
}
}
tbody {
tr {
th { +"Regressions" }
report.detailedMetricReports.values.forEach { report ->
val maximumRegression = report.maximumRegression
val regressionsGeometricMean = report.regressionsGeometricMean
val maximumImprovement = report.maximumImprovement
val improvementsGeometricMean = report.improvementsGeometricMean
val maximumChange = maxOf(maximumRegression, abs(maximumImprovement))
val maximumChangeGeoMean = maxOf(regressionsGeometricMean,
abs(improvementsGeometricMean))
if (!report.regressions.isEmpty()) {
td { +"${report.regressions.size}" }
td {
attributes["bgcolor"] = ColoredCell(
(maximumRegression/maximumChange).takeIf{ maximumChange > 0.0 }
).backgroundStyle
+formatValue(maximumRegression, true)
}
td {
attributes["bgcolor"] = ColoredCell(
(regressionsGeometricMean/maximumChangeGeoMean).takeIf{ maximumChangeGeoMean > 0.0 }
).backgroundStyle
+formatValue(report.regressionsGeometricMean, true)
}
} else {
repeat(3) { td { +"-" } }
}
}
tr {
th { +"Improvements" }
td { +"${report.improvements.size}" }
td {
attributes["bgcolor"] = ColoredCell(
maximumImprovement/maxOf(maximumRegression, abs(maximumImprovement)))
.backgroundStyle
+formatValue(report.maximumImprovement, true)
}
td {
attributes["bgcolor"] = ColoredCell(
improvementsGeometricMean/maxOf(regressionsGeometricMean,
abs(improvementsGeometricMean)))
.backgroundStyle
+formatValue(report.improvementsGeometricMean, true)
report.detailedMetricReports.values.forEach { report ->
val maximumRegression = report.maximumRegression
val regressionsGeometricMean = report.regressionsGeometricMean
val maximumImprovement = report.maximumImprovement
val improvementsGeometricMean = report.improvementsGeometricMean
val maximumChange = maxOf(maximumRegression, abs(maximumImprovement))
val maximumChangeGeoMean = maxOf(regressionsGeometricMean,
abs(improvementsGeometricMean))
if (!report.improvements.isEmpty()) {
td { +"${report.improvements.size}" }
td {
attributes["bgcolor"] = ColoredCell(
(maximumImprovement / maximumChange).takeIf{ maximumChange > 0.0 }
).backgroundStyle
+formatValue(report.maximumImprovement, true)
}
td {
attributes["bgcolor"] = ColoredCell(
(improvementsGeometricMean / maximumChangeGeoMean)
.takeIf{ maximumChangeGeoMean > 0.0 }
).backgroundStyle
+formatValue(report.improvementsGeometricMean, true)
}
} else {
repeat(3) { td { +"-" } }
}
}
}
}
@@ -584,43 +611,84 @@ class HTMLRender: Render() {
}
}
private fun TableBlock.renderFilteredBenchmarks(detailedReport: DetailedBenchmarksReport,
onlyChanges: Boolean, unstableBenchmarks: List<String>,
filterUnstable: Boolean) {
fun <T> filterBenchmarks(bucket: Map<String, T>) =
bucket.filter { (name, _) ->
if (filterUnstable) name in unstableBenchmarks else name !in unstableBenchmarks
}
val filteredRegressions = filterBenchmarks(detailedReport.regressions)
val filteredImprovements = filterBenchmarks(detailedReport.improvements)
renderBenchmarksDetails(detailedReport.mergedReport, filteredRegressions)
renderBenchmarksDetails(detailedReport.mergedReport, filteredImprovements)
if (!onlyChanges) {
// Print all remaining results.
renderBenchmarksDetails(filterBenchmarks(detailedReport.mergedReport).filter {
it.key !in detailedReport.regressions.keys &&
it.key !in detailedReport.improvements.keys
})
}
}
private fun BodyTag.renderPerformanceDetails(report: SummaryBenchmarksReport, onlyChanges: Boolean) {
if (onlyChanges) {
if (report.regressions.isEmpty() && report.improvements.isEmpty()) {
if (report.detailedMetricReports.values.all { it.improvements.isEmpty() } &&
report.detailedMetricReports.values.all { it.regressions.isEmpty() }) {
div("alert alert-success") {
attributes["role"] = "alert"
+"All becnhmarks are stable!"
}
}
}
report.detailedMetricReports.forEach { (metric, detailedReport) ->
renderCollapsedData(metric.value, false) {
table {
attributes["id"] = "result"
attributes["class"] = "table table-striped table-bordered"
thead {
tr {
th { +"Benchmark" }
th { +"First score" }
th { +"Second score" }
th { +"Percent" }
th { +"Ratio" }
}
}
val geoMeanChangeMap = detailedReport.geoMeanScoreChange?.let {
mapOf(detailedReport.geoMeanBenchmark.first!!.name to detailedReport.geoMeanScoreChange!!)
}
table {
attributes["id"] = "result"
attributes["class"] = "table table-striped table-bordered"
thead {
tr {
th { +"Benchmark" }
th { +"First score" }
th { +"Second score" }
th { +"Percent" }
th { +"Ratio" }
}
}
val geoMeanChangeMap = report.geoMeanScoreChange?.
let { mapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanScoreChange!!) }
tbody {
renderBenchmarksDetails(
mutableMapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanBenchmark),
geoMeanChangeMap, "border-bottom: 2.3pt solid black; border-top: 2.3pt solid black")
renderBenchmarksDetails(report.mergedReport, report.regressions)
renderBenchmarksDetails(report.mergedReport, report.improvements)
if (!onlyChanges) {
// Print all remaining results.
renderBenchmarksDetails(report.mergedReport.filter { it.key !in report.regressions.keys &&
it.key !in report.improvements.keys })
tbody {
val boldRowStyle = "border-bottom: 2.3pt solid black; border-top: 2.3pt solid black"
renderBenchmarksDetails(
mutableMapOf(detailedReport.geoMeanBenchmark.first!!.name to detailedReport.geoMeanBenchmark),
geoMeanChangeMap, boldRowStyle)
val unstableBenchmarks = report.getUnstableBenchmarksForMetric(metric)
if (unstableBenchmarks.isNotEmpty()) {
tr {
attributes["style"] = boldRowStyle
th(colspan = Natural(5)) { +"Stable" }
}
}
renderFilteredBenchmarks(detailedReport, onlyChanges, unstableBenchmarks, false)
if (unstableBenchmarks.isNotEmpty()) {
tr {
attributes["style"] = boldRowStyle
th(colspan = Natural(5)) { +"Unstable" }
}
}
renderFilteredBenchmarks(detailedReport, onlyChanges, unstableBenchmarks, true)
}
}
}
hr {}
}
}
@@ -14,14 +14,16 @@ class MetricResultsRender: Render() {
get() = "metrics"
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
val results = report.mergedReport.map { entry ->
buildString {
val metric = entry.value.first!!.metric
append("{ \"benchmarkName\": \"${entry.key.removeSuffix(metric.suffix)}\",")
append("\"metric\": \"${metric}\",")
append("\"value\": \"${entry.value.first!!.score}\" }")
val results = report.detailedMetricReports.values.map { it.mergedReport }.map { report ->
report.map { entry ->
buildString {
val metric = entry.value.first!!.metric
append("{ \"benchmarkName\": \"${entry.key.removeSuffix(metric.suffix)}\",")
append("\"metric\": \"${metric}\",")
append("\"value\": \"${entry.value.first!!.score}\" }")
}
}
}.joinToString(", ")
}.flatten().joinToString(", ")
return "[ $results ]"
}
@@ -54,7 +54,7 @@ class TextRender: Render() {
renderEnvChanges(report.envChanges, "Environment")
renderEnvChanges(report.kotlinChanges, "Compiler")
renderStatusSummary(report)
renderStatusChangesDetails(report.getBenchmarksWithChangedStatus())
renderStatusChangesDetails(report.benchmarksWithChangedStatus)
renderPerformanceSummary(report)
renderPerformanceDetails(report, onlyChanges)
return content.toString()
@@ -113,18 +113,27 @@ class TextRender: Render() {
}
fun renderPerformanceSummary(report: SummaryBenchmarksReport) {
if (!report.regressions.isEmpty() || !report.improvements.isEmpty()) {
if (report.detailedMetricReports.values.any { it.improvements.isNotEmpty() } ||
report.detailedMetricReports.values.any { it.regressions.isNotEmpty() }) {
append("Performance summary")
append(headerSeparator)
if (!report.regressions.isEmpty()) {
append("Regressions: Maximum = ${formatValue(report.maximumRegression, true)}," +
" Geometric mean = ${formatValue(report.regressionsGeometricMean, true)}")
}
if (!report.improvements.isEmpty()) {
append("Improvements: Maximum = ${formatValue(report.maximumImprovement, true)}," +
" Geometric mean = ${formatValue(report.improvementsGeometricMean, true)}")
}
append()
report.detailedMetricReports.forEach { (metric, detailedReport) ->
if (detailedReport.regressions.isNotEmpty() || detailedReport.improvements.isNotEmpty()) {
append(metric.value)
append(headerSeparator)
if (!detailedReport.regressions.isEmpty()) {
append("Regressions: Maximum = ${formatValue(detailedReport.maximumRegression, true)}," +
" Geometric mean = ${formatValue(detailedReport.regressionsGeometricMean, true)}")
}
if (!detailedReport.improvements.isEmpty()) {
append("Improvements: Maximum = ${formatValue(detailedReport.maximumImprovement, true)}," +
" Geometric mean = ${formatValue(detailedReport.improvementsGeometricMean, true)}")
}
append()
}
}
}
}
@@ -176,25 +185,58 @@ class TextRender: Render() {
append(headerSeparator)
if (onlyChanges) {
if (report.regressions.isEmpty() && report.improvements.isEmpty()) {
if (report.detailedMetricReports.values.all { it.improvements.isEmpty() } &&
report.detailedMetricReports.values.all { it.regressions.isEmpty() }) {
append("All becnhmarks are stable.")
}
}
val tableWidth = printPerformanceTableHeader()
// Print geometric mean.
val geoMeanChangeMap = report.geoMeanScoreChange?.
let { mapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanScoreChange!!) }
printBenchmarksDetails(
mutableMapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanBenchmark),
geoMeanChangeMap)
printTableLineSeparator(tableWidth)
printBenchmarksDetails(report.mergedReport, report.regressions)
printBenchmarksDetails(report.mergedReport, report.improvements)
report.detailedMetricReports.forEach { (metric, detailedReport) ->
append()
append(metric.value)
append(headerSeparator)
val tableWidth = printPerformanceTableHeader()
// Print geometric mean.
val geoMeanChangeMap = detailedReport.geoMeanScoreChange?.let {
mapOf(detailedReport.geoMeanBenchmark.first!!.name to detailedReport.geoMeanScoreChange!!)
}
printBenchmarksDetails(
mutableMapOf(detailedReport.geoMeanBenchmark.first!!.name to detailedReport.geoMeanBenchmark),
geoMeanChangeMap)
printTableLineSeparator(tableWidth)
val unstableBenchmarks = report.getUnstableBenchmarksForMetric(metric)
if (unstableBenchmarks.isNotEmpty()) {
append("Stable")
printTableLineSeparator(tableWidth)
}
renderFilteredPerformanceDetails(detailedReport, onlyChanges, unstableBenchmarks, false)
if (unstableBenchmarks.isNotEmpty()) {
printTableLineSeparator(tableWidth)
append("Unstable")
printTableLineSeparator(tableWidth)
}
renderFilteredPerformanceDetails(detailedReport, onlyChanges, unstableBenchmarks,true)
}
}
fun renderFilteredPerformanceDetails(detailedReport: DetailedBenchmarksReport,
onlyChanges: Boolean, unstableBenchmarks: List<String>,
filterUnstable: Boolean) {
fun <T> filterBenchmarks(bucket: Map<String, T>) =
bucket.filter { (name, _) ->
if (filterUnstable) name in unstableBenchmarks else name !in unstableBenchmarks
}
val filteredRegressions = filterBenchmarks(detailedReport.regressions)
val filteredImprovements = filterBenchmarks(detailedReport.improvements)
printBenchmarksDetails(detailedReport.mergedReport, filteredRegressions)
printBenchmarksDetails(detailedReport.mergedReport, filteredImprovements)
if (!onlyChanges) {
// Print all remaining results.
printBenchmarksDetails(report.mergedReport.filter { it.key !in report.regressions.keys &&
it.key !in report.improvements.keys })
printBenchmarksDetails(filterBenchmarks(detailedReport.mergedReport).filter {
it.key !in detailedReport.regressions.keys &&
it.key !in detailedReport.improvements.keys
})
}
}
}
@@ -20,7 +20,7 @@ class StatisticsRender: Render() {
private var content = StringBuilder()
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
val benchmarksWithChangedStatus = report.getBenchmarksWithChangedStatus()
val benchmarksWithChangedStatus = report.benchmarksWithChangedStatus
val newPasses = benchmarksWithChangedStatus
.filter { it.current == BenchmarkResult.Status.PASSED }
val newFailures = benchmarksWithChangedStatus
@@ -28,6 +28,12 @@ class StatisticsRender: Render() {
if (report.failedBenchmarks.isNotEmpty()) {
content.append("failed: ${report.failedBenchmarks.size}\n")
}
val regressionsSize = report.detailedMetricReports.values.fold(0) { acc, it ->
acc + it.regressions.size
}
val improvementsSize = report.detailedMetricReports.values.fold(0) { acc, it ->
acc + it.improvements.size
}
val status = when {
newFailures.isNotEmpty() -> {
content.append("new failures: ${newFailures.size}\n")
@@ -37,16 +43,16 @@ class StatisticsRender: Render() {
content.append("new passes: ${newPasses.size}\n")
Status.FIXED
}
report.improvements.isNotEmpty() && report.regressions.isNotEmpty() -> {
content.append("regressions: ${report.regressions.size}\nimprovements: ${report.improvements.size}")
regressionsSize != 0 && improvementsSize != 0 -> {
content.append("regressions: $regressionsSize\nimprovements: $improvementsSize")
Status.UNSTABLE
}
report.improvements.isNotEmpty() && report.regressions.isEmpty() -> {
content.append("improvements: ${report.improvements.size}")
improvementsSize != 0 && regressionsSize == 0 -> {
content.append("improvements: $improvementsSize")
Status.IMPROVED
}
report.improvements.isEmpty() && report.regressions.isNotEmpty() -> {
content.append("regressions: ${report.regressions.size}")
improvementsSize == 0 && regressionsSize != 0 -> {
content.append("regressions: $regressionsSize")
Status.REGRESSED
}
else -> Status.STABLE
@@ -29,7 +29,9 @@ class TeamCityStatisticsRender: Render() {
content.append("##teamcity[testSuiteFinished name='Benchmarks']\n")
// Report geometric mean as build statistic value
renderGeometricMean(report.geoMeanBenchmark.first!!)
report.detailedMetricReports.forEach { (metric, detailedReport) ->
renderGeometricMean(metric.value, detailedReport.geoMeanBenchmark.first!!)
}
return content.toString()
}
@@ -51,8 +53,8 @@ class TeamCityStatisticsRender: Render() {
content.append("##teamcity[testFinished name='${benchmark.name}' duration='${(duration / 1000).toInt()}']\n")
}
private fun renderGeometricMean(geoMeanBenchmark: MeanVarianceBenchmark) {
content.append("##teamcity[buildStatisticValue key='Geometric mean' value='${geoMeanBenchmark.score}']\n")
content.append("##teamcity[buildStatisticValue key='Geometric mean variance' value='${geoMeanBenchmark.variance}']\n")
private fun renderGeometricMean(metricName: String, geoMeanBenchmark: MeanVarianceBenchmark) {
content.append("##teamcity[buildStatisticValue key='$metricName Geometric mean' value='${geoMeanBenchmark.score}']\n")
content.append("##teamcity[buildStatisticValue key='$metricName Geometric mean variance' value='${geoMeanBenchmark.variance}']\n")
}
}
@@ -256,6 +256,7 @@ class BenchmarksIndexesDispatcher(connector: ElasticSearchConnector, val feature
fun getGeometricMean(metricName: String, featureValue: String = "",
buildNumbers: Iterable<String>? = null, normalize: Boolean = false,
excludeNames: List<String> = emptyList()): Promise<List<Pair<String, List<Double?>>>> {
// Filter only with metric or also with names.
val filterBenchmarks = if (excludeNames.isEmpty())
"""
@@ -264,7 +265,7 @@ class BenchmarksIndexesDispatcher(connector: ElasticSearchConnector, val feature
else """
"bool": {
"must": { "match": { "benchmarks.metric": "$metricName" } },
"must_not": { "terms" : { "benchmarks.name" : [${excludeNames.map { "\"$it\"" }.joinToString()}] } }
"must_not": [ ${excludeNames.map { """{ "match_phrase" : { "benchmarks.name" : "$it" } }"""}.joinToString() } ]
}
""".trimIndent()
val queryDescription = """
@@ -131,6 +131,42 @@ fun getGoldenResults(goldenResultsIndex: GoldenResultsIndex): Promise<Map<String
}
}
// Get list of unstable benchmarks from database.
fun getUnstableResults(goldenResultsIndex: GoldenResultsIndex): Promise<List<String>> {
val queryDescription = """
{
"_source": ["env"],
"query": {
"nested" : {
"path" : "benchmarks",
"query" : {
"match": { "benchmarks.unstable": true }
},
"inner_hits": {
"size": 100,
"_source": ["benchmarks.name"]
}
}
}
}
""".trimIndent()
return goldenResultsIndex.search(queryDescription, listOf("hits.hits.inner_hits")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
val results = dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits")
?: error("Wrong response:\n$responseString")
results.getObjectOrNull(0)?.let {
it
.getObject("inner_hits")
.getObject("benchmarks")
.getObject("hits")
.getArray("hits").map {
(it as JsonObject).getObject("_source").getPrimitive("name").content
}
} ?: listOf<String>()
}
}
// Get distinct values for needed field from database.
fun distinctValues(field: String, index: ElasticSearchIndex): Promise<List<String>> {
val queryDescription = """
@@ -588,6 +588,18 @@ fun router() {
}
})
// Get builds description with additional information.
router.get("/unstable", { request, response ->
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
getUnstableResults(goldenIndex).then { unstableBenchmarks ->
success(unstableBenchmarks)
}.catch {
println("Error during getting unstable benchmarks")
reject()
}
}
})
router.get("/report/:target/:buildNumber", { request, response ->
val target = urlParameterToBaseFormat(request.params.target)
val buildNumber = request.params.buildNumber.toString()
@@ -206,8 +206,8 @@ fun main(args: Array<String>) {
}
buildsNumberToShow = parameters["count"]?.toInt() ?: buildsNumberToShow
beforeDate = parameters["before"]?.let{ decodeURIComponent(it)}
afterDate = parameters["after"]?.let{ decodeURIComponent(it)}
beforeDate = parameters["before"]?.let { decodeURIComponent(it) }
afterDate = parameters["after"]?.let { decodeURIComponent(it) }
// Get branches.
val branchesUrl = "$serverUrl/branches"
@@ -285,30 +285,6 @@ fun main(args: Array<String>) {
val platformSpecificBenchs = if (parameters["target"] == "Mac_OS_X") ",FrameworkBenchmarksAnalyzer,SpaceFramework_iosX64" else
if (parameters["target"] == "Linux") ",kotlinx.coroutines" else ""
// Collect information for charts library.
val valuesToShow = mapOf("EXECUTION_TIME" to listOf(mapOf(
"normalize" to "true"
)),
"COMPILE_TIME" to listOf(mapOf(
"samples" to "HelloWorld,Videoplayer$platformSpecificBenchs",
"agr" to "samples"
)),
"CODE_SIZE" to listOf(mapOf(
"normalize" to "true",
"exclude" to if (parameters["target"] == "Linux")
"kotlinx.coroutines"
else if (parameters["target"] == "Mac_OS_X")
"SpaceFramework_iosX64"
else ""
), if (platformSpecificBenchs.isNotEmpty()) mapOf(
"normalize" to "true",
"agr" to "samples",
"samples" to platformSpecificBenchs.removePrefix(",")
) else null).filterNotNull(),
"BUNDLE_SIZE" to listOf(mapOf("samples" to "KotlinNative",
"agr" to "samples"))
)
var execData = listOf<String>() to listOf<List<Double?>>()
var compileData = listOf<String>() to listOf<List<Double?>>()
var codeSizeData = listOf<String>() to listOf<List<Double?>>()
@@ -328,6 +304,17 @@ fun main(args: Array<String>) {
val metricUrl = "$serverUrl/metricValue/${parameters["target"]}/"
val unstableBenchmarksPromise = sendGetRequest("$serverUrl/unstable").then { response ->
val unstableList = response as String
val data = JsonTreeParser.parse(unstableList)
if (data !is JsonArray) {
error("Response is expected to be an array.")
}
data.jsonArray.map {
(it as JsonPrimitive).content
}
}
// Get builds description.
val buildsInfoPromise = sendGetRequest(descriptionUrl).then { response ->
val buildsInfo = response as String
@@ -341,100 +328,131 @@ fun main(args: Array<String>) {
}
}
// Send requests to get all needed metric values.
valuesToShow.map { (metric, listOfSettings) ->
val resultValues = listOfSettings.map { settings ->
val getParameters = with(StringBuilder()) {
if (settings.isNotEmpty()) {
append("?")
}
var prefix = ""
settings.forEach { (key, value) ->
if (value.isNotEmpty()) {
append("$prefix$key=$value")
prefix = "&"
unstableBenchmarksPromise.then { unstableBenchmarks ->
// Collect information for charts library.
val valuesToShow = mapOf("EXECUTION_TIME" to listOf(mapOf(
"normalize" to "true"
),
mapOf(
"normalize" to "true",
"exclude" to unstableBenchmarks.joinToString(",")
)),
"COMPILE_TIME" to listOf(mapOf(
"samples" to "HelloWorld,Videoplayer$platformSpecificBenchs",
"agr" to "samples"
)),
"CODE_SIZE" to listOf(mapOf(
"normalize" to "true",
"exclude" to if (parameters["target"] == "Linux")
"kotlinx.coroutines"
else if (parameters["target"] == "Mac_OS_X")
"SpaceFramework_iosX64"
else ""
), if (platformSpecificBenchs.isNotEmpty()) mapOf(
"normalize" to "true",
"agr" to "samples",
"samples" to platformSpecificBenchs.removePrefix(",")
) else null).filterNotNull(),
"BUNDLE_SIZE" to listOf(mapOf("samples" to "KotlinNative",
"agr" to "samples"))
)
// Send requests to get all needed metric values.
valuesToShow.map { (metric, listOfSettings) ->
val resultValues = listOfSettings.map { settings ->
val getParameters = with(StringBuilder()) {
if (settings.isNotEmpty()) {
append("?")
}
var prefix = ""
settings.forEach { (key, value) ->
if (value.isNotEmpty()) {
append("$prefix$key=$value")
prefix = "&"
}
}
toString()
}
toString()
val branchParameter = if (parameters["branch"] != "all")
(if (getParameters.isEmpty()) "?" else "&") + "branch=${parameters["branch"]}"
else ""
val url = "$metricUrl$metric$getParameters$branchParameter${
if (parameters["type"] != "all")
(if (getParameters.isEmpty() && branchParameter.isEmpty()) "?" else "&") + "type=${parameters["type"]}"
else ""
}&count=$buildsNumberToShow${getDatesComponents()}"
sendGetRequest(url)
}.toTypedArray()
// Get metrics values for charts.
Promise.all(resultValues).then { responses ->
val valuesList = responses.map { response ->
val results = (JsonTreeParser.parse(response) as JsonArray).map {
(it as JsonObject).getPrimitive("first").content to
it.getArray("second").map { (it as JsonPrimitive).doubleOrNull }
}
val labels = results.map { it.first }
val values = results[0]?.second?.size?.let { (0..it - 1).map { i -> results.map { it.second[i] } } }
?: emptyList()
labels to values
}
val labels = valuesList[0].first
val values = valuesList.map { it.second }.reduce { acc, valuesPart -> acc + valuesPart }
when (metric) {
// Update chart with gotten data.
"COMPILE_TIME" -> {
compileData = labels to values.map { it.map { it?.let { it / 1000 } } }
compileChart = Chartist.Line("#compile_chart",
getChartData(labels, compileData.second),
getChartOptions(valuesToShow["COMPILE_TIME"]!![0]!!["samples"]!!.split(',').toTypedArray(),
"Time, milliseconds"))
buildsInfoPromise.then { builds ->
customizeChart(compileChart, "compile_chart", js("$(\"#compile_chart\")"), builds, parameters)
compileChart.update(getChartData(compileData.first, compileData.second))
}
}
"EXECUTION_TIME" -> {
execData = labels to values
execChart = Chartist.Line("#exec_chart",
getChartData(labels, execData.second),
getChartOptions(arrayOf("Geometric Mean (All)", "Geometric mean (Stable)"),
"Normalized time"))
buildsInfoPromise.then { builds ->
customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters)
execChart.update(getChartData(execData.first, execData.second))
}
}
"CODE_SIZE" -> {
codeSizeData = labels to values
codeSizeChart = Chartist.Line("#codesize_chart",
getChartData(labels, codeSizeData.second),
getChartOptions(arrayOf("Geometric Mean") + platformSpecificBenchs.split(',')
.filter { it.isNotEmpty() },
"Normalized size",
arrayOf("ct-series-4", "ct-series-5", "ct-series-6")))
buildsInfoPromise.then { builds ->
customizeChart(codeSizeChart, "codesize_chart", js("$(\"#codesize_chart\")"), builds, parameters)
codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, sizeClassNames))
}
}
"BUNDLE_SIZE" -> {
bundleSizeData = labels to values.map { it.map { it?.let { it.toInt() / 1024 / 1024 } } }
bundleSizeChart = Chartist.Line("#bundlesize_chart",
getChartData(labels,
bundleSizeData.second, sizeClassNames),
getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-4")))
buildsInfoPromise.then { builds ->
customizeChart(bundleSizeChart, "bundlesize_chart", js("$(\"#bundlesize_chart\")"), builds, parameters)
bundleSizeChart.update(getChartData(bundleSizeData.first, bundleSizeData.second, sizeClassNames))
}
}
else -> error("No chart for metric $metric")
}
true
}
val branchParameter = if (parameters["branch"] != "all")
(if (getParameters.isEmpty()) "?" else "&") + "branch=${parameters["branch"]}"
else ""
val url = "$metricUrl$metric$getParameters$branchParameter${
if (parameters["type"] != "all")
(if (getParameters.isEmpty() && branchParameter.isEmpty()) "?" else "&") + "type=${parameters["type"]}"
else ""
}&count=$buildsNumberToShow${getDatesComponents()}"
sendGetRequest(url)
}.toTypedArray()
// Get metrics values for charts.
Promise.all(resultValues).then { responses ->
val valuesList = responses.map { response ->
val results = (JsonTreeParser.parse(response) as JsonArray).map {
(it as JsonObject).getPrimitive("first").content to
it.getArray("second").map { (it as JsonPrimitive).doubleOrNull }
}
val labels = results.map { it.first }
val values = results[0]?.second?.size?.let { (0..it - 1).map { i -> results.map { it.second[i] } } }
?: emptyList()
labels to values
}
val labels = valuesList[0].first
val values = valuesList.map { it.second }.reduce { acc, valuesPart -> acc + valuesPart }
when (metric) {
// Update chart with gotten data.
"COMPILE_TIME" -> {
compileData = labels to values.map { it.map { it?.let { it / 1000 } } }
compileChart = Chartist.Line("#compile_chart",
getChartData(labels, compileData.second),
getChartOptions(valuesToShow["COMPILE_TIME"]!![0]!!["samples"]!!.split(',').toTypedArray(),
"Time, milliseconds"))
buildsInfoPromise.then { builds ->
customizeChart(compileChart, "compile_chart", js("$(\"#compile_chart\")"), builds, parameters)
compileChart.update(getChartData(compileData.first, compileData.second))
}
}
"EXECUTION_TIME" -> {
execData = labels to values
execChart = Chartist.Line("#exec_chart",
getChartData(labels, execData.second),
getChartOptions(arrayOf("Geometric Mean"), "Normalized time"))
buildsInfoPromise.then { builds ->
customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters)
execChart.update(getChartData(execData.first, execData.second))
}
}
"CODE_SIZE" -> {
codeSizeData = labels to values
codeSizeChart = Chartist.Line("#codesize_chart",
getChartData(labels, codeSizeData.second),
getChartOptions(arrayOf("Geometric Mean") + platformSpecificBenchs.split(',')
.filter { it.isNotEmpty() },
"Normalized size",
arrayOf("ct-series-4", "ct-series-5", "ct-series-6")))
buildsInfoPromise.then { builds ->
customizeChart(codeSizeChart, "codesize_chart", js("$(\"#codesize_chart\")"), builds, parameters)
codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, sizeClassNames))
}
}
"BUNDLE_SIZE" -> {
bundleSizeData = labels to values.map { it.map { it?.let { it.toInt() / 1024 / 1024 } } }
bundleSizeChart = Chartist.Line("#bundlesize_chart",
getChartData(labels,
bundleSizeData.second, sizeClassNames),
getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-4")))
buildsInfoPromise.then { builds ->
customizeChart(bundleSizeChart, "bundlesize_chart", js("$(\"#bundlesize_chart\")"), builds, parameters)
bundleSizeChart.update(getChartData(bundleSizeData.first, bundleSizeData.second, sizeClassNames))
}
}
else -> error("No chart for metric $metric")
}
true
}
}