Skip to content

Commit 1de201b

Browse files
authored
Improve thread creation representation (#534)
* Improve thread creation representation #512
1 parent b7b2588 commit 1de201b

File tree

12 files changed

+13826
-13585
lines changed

12 files changed

+13826
-13585
lines changed

src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import org.jetbrains.kotlinx.lincheck.execution.*
1616
import org.jetbrains.kotlinx.lincheck.runner.*
1717
import org.jetbrains.kotlinx.lincheck.runner.ExecutionPart.*
1818
import org.jetbrains.kotlinx.lincheck.strategy.*
19-
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.*
2019
import org.jetbrains.kotlinx.lincheck.transformation.*
2120
import org.jetbrains.kotlinx.lincheck.util.*
2221
import sun.nio.ch.lincheck.*
@@ -31,7 +30,6 @@ import org.jetbrains.kotlinx.lincheck.strategy.managed.VarHandleMethodType.*
3130
import org.objectweb.asm.ConstantDynamic
3231
import org.objectweb.asm.Handle
3332
import java.lang.invoke.CallSite
34-
import java.lang.invoke.MethodHandle
3533
import java.lang.reflect.*
3634
import java.util.concurrent.TimeoutException
3735
import java.util.*
@@ -1520,7 +1518,7 @@ abstract class ManagedStrategy(
15201518
className = className,
15211519
methodName = methodName,
15221520
callStackTrace = callStackTrace,
1523-
stackTraceElement = CodeLocations.stackTrace(codeLocation)
1521+
stackTraceElement = CodeLocations.stackTrace(codeLocation),
15241522
)
15251523
// handle non-atomic methods
15261524
if (atomicMethodDescriptor == null) {

src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TracePoint.kt

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.jetbrains.kotlinx.lincheck.*
1313
import org.jetbrains.kotlinx.lincheck.CancellationResult.*
1414
import org.jetbrains.kotlinx.lincheck.runner.ExecutionPart
1515
import org.jetbrains.kotlinx.lincheck.util.ThreadId
16+
import kotlin.collections.map
1617

1718
data class Trace(
1819
val trace: List<TracePoint>,
@@ -24,6 +25,23 @@ data class Trace(
2425
}
2526
}
2627

28+
private data class FunctionInfo(
29+
val className: String,
30+
val functionName: String,
31+
val parameterNames: List<String>,
32+
val defaultParameterValues: List<String>,
33+
) {
34+
init { check(parameterNames.size == defaultParameterValues.size) }
35+
}
36+
37+
private val threadFunctionInfo = FunctionInfo(
38+
className = "kotlin.concurrent.ThreadsKt",
39+
functionName = "thread",
40+
parameterNames = listOf("start", "isDaemon", "contextClassLoader", "name", "priority", "block"),
41+
defaultParameterValues = listOf("true", "false", "null", "null", "-1", "")
42+
)
43+
44+
2745
/**
2846
* Essentially, a trace is a list of trace points, which represent
2947
* interleaving events, such as code location passing or thread switches,
@@ -184,14 +202,28 @@ internal class MethodCallTracePoint(
184202
val wasSuspended get() = (returnedValue == ReturnedValueResult.CoroutineSuspended)
185203

186204
override fun toStringCompact(): String = StringBuilder().apply {
187-
if (ownerName != null)
188-
append("$ownerName.")
189-
append("$methodName(")
190-
val parameters = parameters
191-
if (parameters != null) {
192-
append(parameters.joinToString(", "))
205+
when {
206+
isThreadCreation() -> appendThreadCreation()
207+
else -> appendDefaultMethodCall()
193208
}
194-
append(")")
209+
appendReturnedValue()
210+
}.toString()
211+
212+
private fun StringBuilder.appendThreadCreation() {
213+
append("thread")
214+
val params = parameters?.let { getNonDefaultParametersWithName(threadFunctionInfo, it) }
215+
?: emptyList()
216+
if (!params.isEmpty()) {
217+
append("(${params.joinToString(", ")})")
218+
}
219+
}
220+
221+
private fun StringBuilder.appendDefaultMethodCall() {
222+
if (ownerName != null) append("$ownerName.")
223+
append("$methodName(${ parameters?.joinToString(", ") ?: "" })")
224+
}
225+
226+
private fun StringBuilder.appendReturnedValue() {
195227
val returnedValue = returnedValue
196228
if (returnedValue is ReturnedValueResult.ValueResult) {
197229
append(": ${returnedValue.valueRepresentation}")
@@ -200,8 +232,8 @@ internal class MethodCallTracePoint(
200232
} else if (thrownException != null && thrownException != ThreadAbortedError) {
201233
append(": threw ${thrownException!!.javaClass.simpleName}")
202234
}
203-
}.toString()
204-
235+
}
236+
205237
override fun deepCopy(copiedCallStackTraceElements: HashMap<CallStackTraceElement, CallStackTraceElement>): MethodCallTracePoint =
206238
MethodCallTracePoint(iThread, actorId, className, methodName, callStackTrace.deepCopy(copiedCallStackTraceElements), stackTraceElement)
207239
.also {
@@ -235,8 +267,29 @@ internal class MethodCallTracePoint(
235267
fun initializeOwnerName(ownerName: String) {
236268
this.ownerName = ownerName
237269
}
270+
271+
fun isThreadCreation() =
272+
methodName == threadFunctionInfo.functionName && className.replace('/', '.') == threadFunctionInfo.className
273+
274+
/**
275+
* Checks if [FunctionInfo.defaultParameterValues] differ from the provided [actualValues].
276+
* If so, the value is added as `name = value` with a name provided by [FunctionInfo.parameterNames].
277+
* Expects all lists to be of equal size.
278+
*/
279+
private fun getNonDefaultParametersWithName(
280+
functionInfo: FunctionInfo,
281+
actualValues: List<String>
282+
): List<String> {
283+
check(actualValues.size == functionInfo.parameterNames.size)
284+
val result = mutableListOf<String>()
285+
actualValues.forEachIndexed { index, currentValue ->
286+
if (currentValue != functionInfo.defaultParameterValues[index]) result.add("${functionInfo.parameterNames[index]} = $currentValue")
287+
}
288+
return result
289+
}
238290
}
239291

292+
240293
internal sealed interface ReturnedValueResult {
241294
data object NoValue: ReturnedValueResult
242295
data object VoidResult: ReturnedValueResult

src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/TraceReporter.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ internal fun constructTraceGraph(
168168
): List<TraceNode> {
169169
val tracePoints = trace.deepCopy().trace
170170
compressTrace(tracePoints)
171+
removeNestedThreadStartPoints(tracePoints)
171172
val scenario = failure.scenario
172173
val prefixFactory = TraceNodePrefixFactory(nThreads)
173174
val resultProvider = ExecutionResultsProvider(results, failure)
@@ -380,6 +381,24 @@ internal fun constructTraceGraph(
380381
return traceGraphNodesSections.map { it.first() }
381382
}
382383

384+
/**
385+
* When `thread() { ... }` is called it is represented as
386+
* ```
387+
* thread creation line: Thread#2 at A.fun(location)
388+
* Thread#2.start()
389+
* ```
390+
* this function gets rid of the second line.
391+
* But only if it has been created with `thread(start = true)`
392+
*/
393+
private fun removeNestedThreadStartPoints(trace: List<TracePoint>) = trace
394+
.filter { it is ThreadStartTracePoint }
395+
.forEach { tracePoint ->
396+
val threadCreationCall = tracePoint.callStackTrace.dropLast(1).lastOrNull()
397+
if(threadCreationCall?.tracePoint?.isThreadCreation() == true) {
398+
tracePoint.callStackTrace = tracePoint.callStackTrace.dropLast(1)
399+
}
400+
}
401+
383402
private fun compressTrace(trace: List<TracePoint>) =
384403
HashSet<Int>().let { removed ->
385404
trace.apply { forEach { it.callStackTrace = compressCallStackTrace(it.callStackTrace, removed) } }
@@ -454,6 +473,7 @@ private fun compressCallStackTrace(
454473
return compressedStackTrace
455474
}
456475

476+
457477
private fun actorNodeResultRepresentation(result: Result?, failure: LincheckFailure, exceptionStackTraces: Map<Throwable, ExceptionNumberAndStacktrace>): String? {
458478
// We don't mark actors that violated obstruction freedom as hung.
459479
if (result == null && failure is ObstructionFreedomViolationFailure) return null
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Lincheck
3+
*
4+
* Copyright (C) 2019 - 2025 JetBrains s.r.o.
5+
*
6+
* This Source Code Form is subject to the terms of the
7+
* Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
8+
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
*/
10+
11+
package org.jetbrains.kotlinx.lincheck_test.representation
12+
13+
import org.jetbrains.kotlinx.lincheck_test.util.TestJdkVersion
14+
import org.jetbrains.kotlinx.lincheck_test.util.testJdkVersion
15+
import kotlin.concurrent.thread
16+
17+
class ThreadCreationRepresentationTest: BaseRunConcurrentRepresentationTest<Unit>(
18+
when(testJdkVersion) {
19+
TestJdkVersion.JDK_8 -> "thread_creation_representation_test_jdk_8.txt"
20+
else -> "thread_creation_representation_test.txt"
21+
}
22+
) {
23+
24+
@Volatile
25+
private var a = 0
26+
27+
override fun block() {
28+
val t1 = thread(false, name = "thread1") { callMe() }
29+
t1.start()
30+
31+
val t2 = thread(true, name = "thread2") { callMe() }
32+
33+
val t3 = thread(name = "thread3", priority = 8) { callMe() }
34+
35+
t1.join()
36+
t2.join()
37+
t3.join()
38+
check(false)
39+
}
40+
41+
private fun callMe() {
42+
a += 1
43+
}
44+
}

src/jvm/test/resources/expected_logs/function_with_default_fields_repr_test.txt

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,25 @@ Detailed trace:
1717
| Thread 1 |
1818
| ---------------------------------------------------------------------------------------------------------------------------------------------- |
1919
| operation() |
20-
| FunctionWithDefaultFieldsReprTest.callMe(3, "Hey") at FunctionWithDefaultFieldsReprTest.operation(FunctionWithDefaultFieldsReprTest.kt:23) |
21-
| a ➜ 0 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:28) |
22-
| a = 3 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:28) |
23-
| a ➜ 3 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:29) |
24-
| a = 6 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:29) |
25-
| FunctionWithDefaultFieldsReprTest.callOther(5, "Hey") at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:30) |
26-
| a ➜ 6 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:35) |
27-
| a = 9 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:35) |
28-
| a ➜ 9 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:36) |
29-
| a = 14 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:36) |
30-
| FunctionWithDefaultFieldsReprTest.callMe(1, "Hey") at FunctionWithDefaultFieldsReprTest.operation(FunctionWithDefaultFieldsReprTest.kt:24) |
31-
| a ➜ 14 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:28) |
32-
| a = 17 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:28) |
33-
| a ➜ 17 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:29) |
34-
| a = 18 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:29) |
35-
| FunctionWithDefaultFieldsReprTest.callOther(5, "Hey") at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:30) |
36-
| a ➜ 18 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:35) |
37-
| a = 21 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:35) |
38-
| a ➜ 21 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:36) |
39-
| a = 26 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:36) |
20+
| FunctionWithDefaultFieldsReprTest.callMe(3, "Hey") at FunctionWithDefaultFieldsReprTest.operation(FunctionWithDefaultFieldsReprTest.kt:20) |
21+
| a ➜ 0 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:25) |
22+
| a = 3 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:25) |
23+
| a ➜ 3 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:26) |
24+
| a = 6 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:26) |
25+
| FunctionWithDefaultFieldsReprTest.callOther(5, "Hey") at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:27) |
26+
| a ➜ 6 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:31) |
27+
| a = 9 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:31) |
28+
| a ➜ 9 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:32) |
29+
| a = 14 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:32) |
30+
| FunctionWithDefaultFieldsReprTest.callMe(1, "Hey") at FunctionWithDefaultFieldsReprTest.operation(FunctionWithDefaultFieldsReprTest.kt:21) |
31+
| a ➜ 14 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:25) |
32+
| a = 17 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:25) |
33+
| a ➜ 17 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:26) |
34+
| a = 18 at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:26) |
35+
| FunctionWithDefaultFieldsReprTest.callOther(5, "Hey") at FunctionWithDefaultFieldsReprTest.callMe(FunctionWithDefaultFieldsReprTest.kt:27) |
36+
| a ➜ 18 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:31) |
37+
| a = 21 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:31) |
38+
| a ➜ 21 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:32) |
39+
| a = 26 at FunctionWithDefaultFieldsReprTest.callOther(FunctionWithDefaultFieldsReprTest.kt:32) |
4040
| result: void |
4141
| ---------------------------------------------------------------------------------------------------------------------------------------------- |

0 commit comments

Comments
 (0)