Skip to content

Improve thread creation representation #534

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 21, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import org.jetbrains.kotlinx.lincheck.execution.*
import org.jetbrains.kotlinx.lincheck.runner.*
import org.jetbrains.kotlinx.lincheck.runner.ExecutionPart.*
import org.jetbrains.kotlinx.lincheck.strategy.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.*
import org.jetbrains.kotlinx.lincheck.transformation.*
import org.jetbrains.kotlinx.lincheck.util.*
import sun.nio.ch.lincheck.*
Expand All @@ -31,7 +30,6 @@ import org.jetbrains.kotlinx.lincheck.strategy.managed.VarHandleMethodType.*
import org.objectweb.asm.ConstantDynamic
import org.objectweb.asm.Handle
import java.lang.invoke.CallSite
import java.lang.invoke.MethodHandle
import java.lang.reflect.*
import java.util.concurrent.TimeoutException
import java.util.*
Expand Down Expand Up @@ -1520,7 +1518,7 @@ abstract class ManagedStrategy(
className = className,
methodName = methodName,
callStackTrace = callStackTrace,
stackTraceElement = CodeLocations.stackTrace(codeLocation)
stackTraceElement = CodeLocations.stackTrace(codeLocation),
)
// handle non-atomic methods
if (atomicMethodDescriptor == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.jetbrains.kotlinx.lincheck.*
import org.jetbrains.kotlinx.lincheck.CancellationResult.*
import org.jetbrains.kotlinx.lincheck.runner.ExecutionPart
import org.jetbrains.kotlinx.lincheck.util.ThreadId
import kotlin.collections.map

data class Trace(
val trace: List<TracePoint>,
Expand All @@ -24,6 +25,23 @@ data class Trace(
}
}

private data class FunctionInfo(
val className: String,
val functionName: String,
val parameterNames: List<String>,
val defaultParameterValues: List<String>,
) {
init { check(parameterNames.size == defaultParameterValues.size) }
}

private val threadFunctionInfo = FunctionInfo(
className = "kotlin.concurrent.ThreadsKt",
functionName = "thread",
parameterNames = listOf("start", "isDaemon", "contextClassLoader", "name", "priority", "block"),
defaultParameterValues = listOf("true", "false", "null", "null", "-1", "")
)


/**
* Essentially, a trace is a list of trace points, which represent
* interleaving events, such as code location passing or thread switches,
Expand Down Expand Up @@ -184,14 +202,28 @@ internal class MethodCallTracePoint(
val wasSuspended get() = (returnedValue == ReturnedValueResult.CoroutineSuspended)

override fun toStringCompact(): String = StringBuilder().apply {
if (ownerName != null)
append("$ownerName.")
append("$methodName(")
val parameters = parameters
if (parameters != null) {
append(parameters.joinToString(", "))
when {
isThreadCreation() -> appendThreadCreation()
else -> appendDefaultMethodCall()
}
append(")")
appendReturnedValue()
}.toString()

private fun StringBuilder.appendThreadCreation() {
append("thread")
val params = parameters?.let { getNonDefaultParametersWithName(threadFunctionInfo, it) }
?: emptyList()
if (!params.isEmpty()) {
append("(${params.joinToString(", ")})")
}
}

private fun StringBuilder.appendDefaultMethodCall() {
if (ownerName != null) append("$ownerName.")
append("$methodName(${ parameters?.joinToString(", ") ?: "" })")
}

private fun StringBuilder.appendReturnedValue() {
val returnedValue = returnedValue
if (returnedValue is ReturnedValueResult.ValueResult) {
append(": ${returnedValue.valueRepresentation}")
Expand All @@ -200,8 +232,8 @@ internal class MethodCallTracePoint(
} else if (thrownException != null && thrownException != ThreadAbortedError) {
append(": threw ${thrownException!!.javaClass.simpleName}")
}
}.toString()
}

override fun deepCopy(copiedCallStackTraceElements: HashMap<CallStackTraceElement, CallStackTraceElement>): MethodCallTracePoint =
MethodCallTracePoint(iThread, actorId, className, methodName, callStackTrace.deepCopy(copiedCallStackTraceElements), stackTraceElement)
.also {
Expand Down Expand Up @@ -235,8 +267,29 @@ internal class MethodCallTracePoint(
fun initializeOwnerName(ownerName: String) {
this.ownerName = ownerName
}

fun isThreadCreation() =
methodName == threadFunctionInfo.functionName && className.replace('/', '.') == threadFunctionInfo.className

/**
* Checks if [FunctionInfo.defaultParameterValues] differ from the provided [actualValues].
* If so, the value is added as `name = value` with a name provided by [FunctionInfo.parameterNames].
* Expects all lists to be of equal size.
*/
private fun getNonDefaultParametersWithName(
functionInfo: FunctionInfo,
actualValues: List<String>
): List<String> {
check(actualValues.size == functionInfo.parameterNames.size)
val result = mutableListOf<String>()
actualValues.forEachIndexed { index, currentValue ->
if (currentValue != functionInfo.defaultParameterValues[index]) result.add("${functionInfo.parameterNames[index]} = $currentValue")
}
return result
}
}


internal sealed interface ReturnedValueResult {
data object NoValue: ReturnedValueResult
data object VoidResult: ReturnedValueResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ internal fun constructTraceGraph(
): List<TraceNode> {
val tracePoints = trace.deepCopy().trace
compressTrace(tracePoints)
removeNestedThreadStartPoints(tracePoints)
val scenario = failure.scenario
val prefixFactory = TraceNodePrefixFactory(nThreads)
val resultProvider = ExecutionResultsProvider(results, failure)
Expand Down Expand Up @@ -380,6 +381,24 @@ internal fun constructTraceGraph(
return traceGraphNodesSections.map { it.first() }
}

/**
* When `thread() { ... }` is called it is represented as
* ```
* thread creation line: Thread#2 at A.fun(location)
* Thread#2.start()
* ```
* this function gets rid of the second line.
* But only if it has been created with `thread(start = true)`
*/
private fun removeNestedThreadStartPoints(trace: List<TracePoint>) = trace
.filter { it is ThreadStartTracePoint }
.forEach { tracePoint ->
val threadCreationCall = tracePoint.callStackTrace.dropLast(1).lastOrNull()
if(threadCreationCall?.tracePoint?.isThreadCreation() == true) {
tracePoint.callStackTrace = tracePoint.callStackTrace.dropLast(1)
}
}

private fun compressTrace(trace: List<TracePoint>) =
HashSet<Int>().let { removed ->
trace.apply { forEach { it.callStackTrace = compressCallStackTrace(it.callStackTrace, removed) } }
Expand Down Expand Up @@ -454,6 +473,7 @@ private fun compressCallStackTrace(
return compressedStackTrace
}


private fun actorNodeResultRepresentation(result: Result?, failure: LincheckFailure, exceptionStackTraces: Map<Throwable, ExceptionNumberAndStacktrace>): String? {
// We don't mark actors that violated obstruction freedom as hung.
if (result == null && failure is ObstructionFreedomViolationFailure) return null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Lincheck
*
* Copyright (C) 2019 - 2025 JetBrains s.r.o.
*
* This Source Code Form is subject to the terms of the
* Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.jetbrains.kotlinx.lincheck_test.representation

import org.jetbrains.kotlinx.lincheck_test.util.TestJdkVersion
import org.jetbrains.kotlinx.lincheck_test.util.testJdkVersion
import kotlin.concurrent.thread

class ThreadCreationRepresentationTest: BaseRunConcurrentRepresentationTest<Unit>(
when(testJdkVersion) {
TestJdkVersion.JDK_8 -> "thread_creation_representation_test_jdk_8.txt"
else -> "thread_creation_representation_test.txt"
}
) {

@Volatile
private var a = 0

override fun block() {
val t1 = thread(false, name = "thread1") { callMe() }
t1.start()

val t2 = thread(true, name = "thread2") { callMe() }

val t3 = thread(name = "thread3", priority = 8) { callMe() }

t1.join()
t2.join()
t3.join()
check(false)
}

private fun callMe() {
a += 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,25 @@ Detailed trace:
| Thread 1 |
| ---------------------------------------------------------------------------------------------------------------------------------------------- |
| operation() |
| FunctionWithDefaultFieldsReprTest.callMe(3, "Hey") at FunctionWithDefaultFieldsReprTest.operation(FunctionWithDefaultFieldsReprTest.kt:23) |
| a ➜ 0 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:28) |
| a = 3 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:28) |
| a ➜ 3 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:29) |
| a = 6 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:29) |
| FunctionWithDefaultFieldsReprTest.callOther(5, "Hey") at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:30) |
| a ➜ 6 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:35) |
| a = 9 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:35) |
| a ➜ 9 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:36) |
| a = 14 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:36) |
| FunctionWithDefaultFieldsReprTest.callMe(1, "Hey") at FunctionWithDefaultFieldsReprTest.operation(FunctionWithDefaultFieldsReprTest.kt:24) |
| a ➜ 14 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:28) |
| a = 17 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:28) |
| a ➜ 17 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:29) |
| a = 18 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:29) |
| FunctionWithDefaultFieldsReprTest.callOther(5, "Hey") at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:30) |
| a ➜ 18 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:35) |
| a = 21 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:35) |
| a ➜ 21 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:36) |
| a = 26 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:36) |
| FunctionWithDefaultFieldsReprTest.callMe(3, "Hey") at FunctionWithDefaultFieldsReprTest.operation(FunctionWithDefaultFieldsReprTest.kt:20) |
| a ➜ 0 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:25) |
| a = 3 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:25) |
| a ➜ 3 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:26) |
| a = 6 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:26) |
| FunctionWithDefaultFieldsReprTest.callOther(5, "Hey") at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:27) |
| a ➜ 6 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:31) |
| a = 9 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:31) |
| a ➜ 9 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:32) |
| a = 14 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:32) |
| FunctionWithDefaultFieldsReprTest.callMe(1, "Hey") at FunctionWithDefaultFieldsReprTest.operation(FunctionWithDefaultFieldsReprTest.kt:21) |
| a ➜ 14 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:25) |
| a = 17 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:25) |
| a ➜ 17 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:26) |
| a = 18 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:26) |
| FunctionWithDefaultFieldsReprTest.callOther(5, "Hey") at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:27) |
| a ➜ 18 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:31) |
| a = 21 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:31) |
| a ➜ 21 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:32) |
| a = 26 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:32) |
| result: void |
| ---------------------------------------------------------------------------------------------------------------------------------------------- |
Loading