Skip to content

Remove access calls from trace #538

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 11 commits into from
Mar 4, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ internal class SwitchEventTracePoint(
internal abstract class CodeLocationTracePoint(
iThread: Int, actorId: Int,
callStackTrace: CallStackTrace,
val stackTraceElement: StackTraceElement
var stackTraceElement: StackTraceElement
) : TracePoint(iThread, actorId, callStackTrace) {

protected abstract fun toStringCompact(): String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,27 +399,38 @@ private fun removeNestedThreadStartPoints(trace: List<TracePoint>) = trace
}
}

private fun compressTrace(trace: List<TracePoint>) =
private fun compressTrace(trace: List<TracePoint>) {
removeSyntheticFieldAccessTracePoints(trace)
HashSet<Int>().let { removed ->
trace.apply { forEach { it.callStackTrace = compressCallStackTrace(it.callStackTrace, removed) } }
trace.apply { forEach { it.callStackTrace = compressCallStackTrace(it.callStackTrace, removed) } }
}
}

/**
* Merges fun$default(...) calls.
* Kotlin functions with default values are represented as two nested calls in the stack trace.
* For example:
* ```
* A.calLMe$default(A#1, 3, null, 2, null) at A.operation(A.kt:23)
* A.callMe(3, "Hey") at A.callMe$default(A.kt:27)
* ```
*
* This function collapses such pairs in the provided [callStackTrace]:
* Remove access$get and access$set, which is used when a lambda argument accesses a private field for example.
* This is different from fun$access, which is addressed in [compressCallStackTrace].
*/
private fun removeSyntheticFieldAccessTracePoints(trace: List<TracePoint>) {
trace
.filter { it is ReadTracePoint || it is WriteTracePoint }
.forEach { point ->
val lastCall = point.callStackTrace.lastOrNull() ?: return@forEach
if (isSyntheticFieldAccess(lastCall.tracePoint.methodName)) {
if (point is ReadTracePoint) point.stackTraceElement = lastCall.tracePoint.stackTraceElement
if (point is WriteTracePoint) point.stackTraceElement = lastCall.tracePoint.stackTraceElement
point.callStackTrace = point.callStackTrace.dropLast(1)
}
}
}

private fun isSyntheticFieldAccess(methodName: String): Boolean =
methodName.contains("access\$get") || methodName.contains("access\$set")

/**
* Merges two consecutive calls in the stack trace into one call if they form a compressible pair,
* see [isCompressiblePair] for details.
*
* ```
* A.callMe(3, "Hey") at A.operation(A.kt:23)
* ```
*
* Since each tracePoint itself contains a [callStackTrace] of it's own,
* Since each tracePoint itself contains a [callStackTrace] of its own,
* we need to recursively traverse each point.
*/
private fun compressCallStackTrace(
Expand Down Expand Up @@ -449,9 +460,8 @@ private fun compressCallStackTrace(
break
}

// Check if current and next are a "default" combo
if (currentElement.tracePoint.methodName == "${nextElement.tracePoint.methodName}\$default") {

// Check if current and next are compressible
if (isCompressiblePair(currentElement.tracePoint.methodName, nextElement.tracePoint.methodName)) {
// Combine fields of next and current, and store in current
currentElement.tracePoint.methodName = nextElement.tracePoint.methodName
currentElement.tracePoint.parameters = nextElement.tracePoint.parameters
Expand All @@ -473,7 +483,6 @@ 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 All @@ -488,6 +497,54 @@ private fun actorNodeResultRepresentation(result: Result?, failure: LincheckFail
}
}

private fun isCompressiblePair(currentName: String, nextName: String): Boolean =
isDefaultPair(currentName, nextName) || isAccessPair(currentName, nextName)

/**
* Used by [compressCallStackTrace] to merge `fun$default(...)` calls.
*
* Kotlin functions with default values are represented as two nested calls in the stack trace.
*
* For example:
*
* ```
* A.calLMe$default(A#1, 3, null, 2, null) at A.operation(A.kt:23)
* A.callMe(3, "Hey") at A.callMe$default(A.kt:27)
* ```
*
* will be collapsed into:
*
* ```
* A.callMe(3, "Hey") at A.operation(A.kt:23)
* ```
*
*/
private fun isDefaultPair(currentName: String, nextName: String): Boolean =
currentName == "${nextName}\$default"

/**
* Used by [compressCallStackTrace] to merge `.access$` calls.
*
* The `.access$` methods are generated by the Kotlin compiler to access otherwise inaccessible members
* (e.g., private) from lambdas, inner classes, etc.
*
* For example:
*
* ```
* A.access$callMe() at A.operation(A.kt:N)
* A.callMe() at A.access$callMe(A.kt:N)
* ```
*
* will be collapsed into:
*
* ```
* A.callMe() at A.operation(A.kt:N)
* ```
*
*/
private fun isAccessPair(currentName: String, nextName: String): Boolean =
currentName == "access$${nextName}"

/**
* Helper class to provider execution results, including a validation function result
*/
Expand Down Expand Up @@ -868,7 +925,6 @@ private class TraceNodePrefixFactory(nThreads: Int) {

private const val TRACE_INDENTATION = " "


internal class TraceEventRepresentation(val iThread: Int, val representation: String)

const val TRACE_TITLE = "The following interleaving leads to the error:"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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

class AccessFunctionRepresentationTest: BaseRunConcurrentRepresentationTest<Unit>("access_function_representation_test") {

@Volatile
private var a = 0

override fun block() {
runLambda { inc1() }
check(false)
}

private fun inc1() {
Nested().inc2()
}

private fun inc3() {
a++
}

private inner class Nested {
fun inc2() {
inc3()
}

}
}

private fun runLambda(r: () -> Unit) {
r()
}

class AccessFieldRepresentationTest: BaseRunConcurrentRepresentationTest<Unit>("access_field_representation_test") {

@Volatile
private var a = 0

override fun block() {
runLambda { a++ }
check(false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
= Concurrent test failed =

java.lang.IllegalStateException: Check failed.
at org.jetbrains.kotlinx.lincheck_test.representation.AccessFieldRepresentationTest.block(AccessFunctionRepresentationTest.kt:50)
at org.jetbrains.kotlinx.lincheck_test.representation.AccessFieldRepresentationTest.block(AccessFunctionRepresentationTest.kt:43)
at org.jetbrains.kotlinx.lincheck_test.representation.BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40)
at java.lang.Thread.run(Thread.java:750)

The following interleaving leads to the error:
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Thread 1 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AccessFieldRepresentationTest#1.block(): threw IllegalStateException at BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40) |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

Detailed trace:
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Thread 1 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AccessFieldRepresentationTest#1.block(): threw IllegalStateException at BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40) |
| block(): threw IllegalStateException at AccessFieldRepresentationTest.block(AccessFunctionRepresentationTest.kt:43) |
| AccessFunctionRepresentationTestKt.runLambda(block$1#1) at AccessFieldRepresentationTest.block(AccessFunctionRepresentationTest.kt:49) |
| block$1#1.invoke() at AccessFunctionRepresentationTestKt.runLambda(AccessFunctionRepresentationTest.kt:40) |
| invoke() at AccessFieldRepresentationTest$block$1.invoke(AccessFunctionRepresentationTest.kt:49) |
| AccessFieldRepresentationTest#1.a ➜ 1 at AccessFieldRepresentationTest$block$1.invoke(AccessFunctionRepresentationTest.kt:49) |
| AccessFieldRepresentationTest#1.a = 2 at AccessFieldRepresentationTest$block$1.invoke(AccessFunctionRepresentationTest.kt:49) |
| result: IllegalStateException #1 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
= Concurrent test failed =

java.lang.IllegalStateException: Check failed.
at org.jetbrains.kotlinx.lincheck_test.representation.AccessFieldRepresentationTest.block(AccessFunctionRepresentationTest.kt:50)
at org.jetbrains.kotlinx.lincheck_test.representation.AccessFieldRepresentationTest.block(AccessFunctionRepresentationTest.kt:43)
at org.jetbrains.kotlinx.lincheck_test.representation.BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40)
at java.base/java.lang.Thread.run(Thread.java:829)

The following interleaving leads to the error:
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Thread 1 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AccessFieldRepresentationTest#1.block(): threw IllegalStateException at BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40) |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

Detailed trace:
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Thread 1 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AccessFieldRepresentationTest#1.block(): threw IllegalStateException at BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40) |
| block(): threw IllegalStateException at AccessFieldRepresentationTest.block(AccessFunctionRepresentationTest.kt:43) |
| AccessFunctionRepresentationTestKt.runLambda(block$1) at AccessFieldRepresentationTest.block(AccessFunctionRepresentationTest.kt:49) |
| block$1.invoke() at AccessFunctionRepresentationTestKt.runLambda(AccessFunctionRepresentationTest.kt:40) |
| invoke() at AccessFieldRepresentationTest$block$1.invoke(AccessFunctionRepresentationTest.kt:49) |
| AccessFieldRepresentationTest#1.a ➜ 1 at AccessFieldRepresentationTest$block$1.invoke(AccessFunctionRepresentationTest.kt:49) |
| AccessFieldRepresentationTest#1.a = 2 at AccessFieldRepresentationTest$block$1.invoke(AccessFunctionRepresentationTest.kt:49) |
| result: IllegalStateException #1 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
= Concurrent test failed =

java.lang.IllegalStateException: Check failed.
at org.jetbrains.kotlinx.lincheck_test.representation.AccessFunctionRepresentationTest.block(AccessFunctionRepresentationTest.kt:20)
at org.jetbrains.kotlinx.lincheck_test.representation.AccessFunctionRepresentationTest.block(AccessFunctionRepresentationTest.kt:13)
at org.jetbrains.kotlinx.lincheck_test.representation.BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40)
at java.lang.Thread.run(Thread.java:750)

The following interleaving leads to the error:
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Thread 1 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| AccessFunctionRepresentationTest#1.block(): threw IllegalStateException at BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40) |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

Detailed trace:
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Thread 1 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| AccessFunctionRepresentationTest#1.block(): threw IllegalStateException at BaseRunConcurrentRepresentationTest$testRunWithModelChecker$result$1$1.invoke(RunConcurrentRepresentationTests.kt:40) |
| block(): threw IllegalStateException at AccessFunctionRepresentationTest.block(AccessFunctionRepresentationTest.kt:13) |
| AccessFunctionRepresentationTestKt.runLambda(block$1#1) at AccessFunctionRepresentationTest.block(AccessFunctionRepresentationTest.kt:19) |
| block$1#1.invoke() at AccessFunctionRepresentationTestKt.runLambda(AccessFunctionRepresentationTest.kt:40) |
| invoke() at AccessFunctionRepresentationTest$block$1.invoke(AccessFunctionRepresentationTest.kt:19) |
| AccessFunctionRepresentationTest.inc1() at AccessFunctionRepresentationTest$block$1.invoke(AccessFunctionRepresentationTest.kt:19) |
| Nested#1.inc2() at AccessFunctionRepresentationTest.inc1(AccessFunctionRepresentationTest.kt:24) |
| AccessFunctionRepresentationTest.inc3() at AccessFunctionRepresentationTest$Nested.inc2(AccessFunctionRepresentationTest.kt:33) |
| a ➜ 1 at AccessFunctionRepresentationTest.inc3(AccessFunctionRepresentationTest.kt:28) |
| a = 2 at AccessFunctionRepresentationTest.inc3(AccessFunctionRepresentationTest.kt:28) |
| result: IllegalStateException #1 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
Loading