Skip to content
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

Static memory snapshot: add support for Unsafe, VarHandle, AFU, kotlinx.atomicfu #429

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1ac184f
Add static memory snapshot collecting and restoring
dmitrii-artuhov Oct 17, 2024
03236cf
Add tests and fixes to snapshot algorithm
dmitrii-artuhov Oct 21, 2024
cb3b56f
Make snapshot algorithm lazy
dmitrii-artuhov Oct 30, 2024
1f71651
Add energetic traverse for non-transformed classes (such as 'AtomicIn…
dmitrii-artuhov Nov 1, 2024
609acf5
Refactor code
dmitrii-artuhov Nov 1, 2024
26ffcd7
Add energetic snapshot recording for non-transformed classes
dmitrii-artuhov Nov 7, 2024
f6f8a43
Enable snapshot restoring for all tests, fix representation tests
dmitrii-artuhov Nov 8, 2024
333a850
Add minor refactoring
dmitrii-artuhov Nov 8, 2024
ae3e92d
Add tests for java atomics and atomicfu classes for snapshot restoring
dmitrii-artuhov Nov 12, 2024
4650ce0
Add tracking for fields of objects the same type as constructor owner
dmitrii-artuhov Nov 15, 2024
8b68f53
Fix todo with subclass check in SnapshotTrackerTransformer
dmitrii-artuhov Nov 27, 2024
bd6db91
Add some PR fixes
dmitrii-artuhov Dec 2, 2024
ce3d410
Add fixes to the PR
dmitrii-artuhov Dec 2, 2024
43d404c
Track calls to System.arraycopy, Unsafe, VarHandles, and Java AFU
dmitrii-artuhov Nov 29, 2024
ddc4cb9
Remove useless comments
dmitrii-artuhov Nov 29, 2024
7b76402
Add tests for unsafe, varhandle, java afu, kotlinx.atomicfu, and util…
dmitrii-artuhov Dec 2, 2024
cb63b27
Fix compilation
dmitrii-artuhov Dec 2, 2024
e36dbeb
Suppress deprecation in test
dmitrii-artuhov Dec 2, 2024
154e98b
Remove 'System.arraycopy' support
dmitrii-artuhov Dec 6, 2024
67dffec
Refactor VarHandle methods processing
dmitrii-artuhov Dec 9, 2024
b0d83e0
Unite ignored section in 'ManagedStrategy::beforeMethodCall(...)' method
dmitrii-artuhov Dec 9, 2024
2fd1a05
Add minor performance improvements
dmitrii-artuhov Dec 12, 2024
765d728
Minor code style fixes
dmitrii-artuhov Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,57 @@ import java.util.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*

fun shouldTransformClass(className: String): Boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the code was moved from somewhere, but I do not see it removed in the original location.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I would suggest keeping the transformation-related logic in the corresponding package.

// We do not need to instrument most standard Java classes.
// It is fine to inject the Lincheck analysis only into the
// `java.util.*` ones, ignored the known atomic constructs.
if (className.startsWith("java.")) {
if (className.startsWith("java.util.concurrent.") && className.contains("Atomic")) return false
if (className.startsWith("java.util.")) return true
if (className.startsWith("com.sun.")) return false
return false
}
if (className.startsWith("sun.")) return false
if (className.startsWith("javax.")) return false
if (className.startsWith("jdk.")) return false
// We do not need to instrument most standard Kotlin classes.
// However, we need to inject the Lincheck analysis into the classes
// related to collections, iterators, random and coroutines.
if (className.startsWith("kotlin.")) {
if (className.startsWith("kotlin.collections.")) return true
if (className.startsWith("kotlin.jvm.internal.Array") && className.contains("Iterator")) return true
if (className.startsWith("kotlin.ranges.")) return true
if (className.startsWith("kotlin.random.")) return true
if (className.startsWith("kotlin.coroutines.jvm.internal.")) return false
if (className.startsWith("kotlin.coroutines.")) return true
return false
}
if (className.startsWith("kotlinx.atomicfu.")) return false
// We need to skip the classes related to the debugger support in Kotlin coroutines.
if (className.startsWith("kotlinx.coroutines.debug.")) return false
if (className == "kotlinx.coroutines.DebugKt") return false
// We should never transform the coverage-related classes.
if (className.startsWith("com.intellij.rt.coverage.")) return false
// We can also safely do not instrument some libraries for performance reasons.
if (className.startsWith("com.esotericsoftware.kryo.")) return false
if (className.startsWith("net.bytebuddy.")) return false
if (className.startsWith("net.rubygrapefruit.platform.")) return false
if (className.startsWith("io.mockk.")) return false
if (className.startsWith("it.unimi.dsi.fastutil.")) return false
if (className.startsWith("worker.org.gradle.")) return false
if (className.startsWith("org.objectweb.asm.")) return false
if (className.startsWith("org.gradle.")) return false
if (className.startsWith("org.slf4j.")) return false
if (className.startsWith("org.apache.commons.lang.")) return false
if (className.startsWith("org.junit.")) return false
if (className.startsWith("junit.framework.")) return false
// Finally, we should never instrument the Lincheck classes.
if (className.startsWith("org.jetbrains.kotlinx.lincheck.")) return false
if (className.startsWith("sun.nio.ch.lincheck.")) return false
// All the classes that were not filtered out are eligible for transformation.
return true
}

fun <T> List<T>.isSuffixOf(list: List<T>): Boolean {
if (size > list.size) return false
for (i in indices) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
internal object AtomicFieldUpdaterNames {

@Suppress("DEPRECATION")
internal fun getAtomicFieldUpdaterName(updater: Any): String? {
internal fun getAtomicFieldUpdaterName(updater: Any): AtomicFieldUpdaterDescriptor? {
if (updater !is AtomicIntegerFieldUpdater<*> && updater !is AtomicLongFieldUpdater<*> && updater !is AtomicReferenceFieldUpdater<*, *>) {
throw IllegalArgumentException("Provided object is not a recognized Atomic*FieldUpdater type.")
}
Expand All @@ -37,11 +37,22 @@ internal object AtomicFieldUpdaterNames {
val offsetField = updater.javaClass.getDeclaredField("offset")
val offset = UNSAFE.getLong(updater, UNSAFE.objectFieldOffset(offsetField))

return findFieldNameByOffsetViaUnsafe(targetType, offset)
return findFieldNameByOffsetViaUnsafe(targetType, offset).let { fieldName ->
if (fieldName != null) AtomicFieldUpdaterDescriptor(targetType, fieldName)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please always add { .. } when the else branch is present.

else null
}
} catch (t: Throwable) {
t.printStackTrace()
}

return null // Field not found
}
}
}

/**
* Descriptor of the Java AFU instance.
*/
internal data class AtomicFieldUpdaterDescriptor(
val targetType: Class<*>,
val fieldName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicReferenceArray

/**
* Provides method call type to create a more convenient trace point
* with a owner of this AtomicReference field and a name if it can be found.
* with an owner of this AtomicReference field and a name if it can be found.
* Recursively scans the test object, trying to find the provided AtomicReference
* instance as a field. If two or more fields contain this AtomicReference field, then we
* fall back to the default behavior.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,63 @@ abstract class ManagedStrategy(
staticMemorySnapshot.trackObjects(objs)
}



/**
* Tracks fields that are accessed via System.arraycopy, Unsafe API, VarHandle API, Java AFU API, and kotlinx.atomicfu.
*/
private fun processMethodEffectOnStaticSnapshot(owner: Any?, params: Array<Any?>) {
when {
// Unsafe API
isUnsafe(owner) -> {
val methodType: UnsafeName = UnsafeNames.getMethodCallType(params)
when (methodType) {
is UnsafeInstanceMethod -> {
staticMemorySnapshot.trackField(methodType.owner, methodType.owner.javaClass, methodType.fieldName)
}
is UnsafeStaticMethod -> {
staticMemorySnapshot.trackField(null, methodType.clazz, methodType.fieldName)
}
is UnsafeArrayMethod -> {
staticMemorySnapshot.trackArrayCell(methodType.array, methodType.index)
}
else -> {}
}
}
// VarHandle API
isVarHandle(owner) &&
(
VarHandleNames.isInstanceVarHandle(owner) && staticMemorySnapshot.isTracked(params.firstOrNull()) ||
VarHandleNames.isArrayVarHandle(owner) && staticMemorySnapshot.isTracked(params.firstOrNull()) ||
VarHandleNames.isStaticVarHandle(owner)
) -> {
val methodType: VarHandleMethodType = VarHandleNames.varHandleMethodType(owner, params)
when (methodType) {
is InstanceVarHandleMethod -> {
staticMemorySnapshot.trackField(methodType.owner, methodType.owner.javaClass, methodType.fieldName)
}
is StaticVarHandleMethod -> {
staticMemorySnapshot.trackField(null, methodType.ownerClass, methodType.fieldName)
}
is ArrayVarHandleMethod -> {
staticMemorySnapshot.trackArrayCell(methodType.array, methodType.index)
}
else -> {}
}
}
// Java AFU (this also automatically handles the `kotlinx.atomicfu`, since they are compiled to Java AFU + Java atomic arrays)
isAtomicFieldUpdater(owner) -> {
val obj = params[0]
val afuDesc: AtomicFieldUpdaterDescriptor? = AtomicFieldUpdaterNames.getAtomicFieldUpdaterName(owner!!)
check(afuDesc != null) { "Cannot extract field name referenced by Java AFU object $owner" }

staticMemorySnapshot.trackField(obj, afuDesc.targetType, afuDesc.fieldName)
}
// TODO: System.arraycopy
// TODO: reflexivity
}
}

private fun methodGuaranteeType(owner: Any?, className: String, methodName: String): ManagedGuaranteeType? = runInIgnoredSection {
userDefinedGuarantees?.forEach { guarantee ->
val ownerName = owner?.javaClass?.canonicalName ?: className
Expand All @@ -969,9 +1026,15 @@ abstract class ManagedStrategy(
methodId: Int,
params: Array<Any?>
) {
val guarantee = runInIgnoredSection {
var guarantee: ManagedGuaranteeType? = null

runInIgnoredSection {
// process method effect on the static memory snapshot
processMethodEffectOnStaticSnapshot(owner, params)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the name of the method or what it does?
This method checks what kind of function was called (Unsafe API, VarHandle API, Java AFU) and updates static snapshot accordingly


// process known method concurrency guarantee
val atomicMethodDescriptor = getAtomicMethodDescriptor(owner, methodName)
val guarantee = when {
guarantee = when {
(atomicMethodDescriptor != null) -> ManagedGuaranteeType.TREAT_AS_ATOMIC
else -> methodGuaranteeType(owner, className, methodName)
}
Expand All @@ -988,8 +1051,8 @@ abstract class ManagedStrategy(
if (guarantee == null) {
loopDetector.beforeMethodCall(codeLocation, params)
}
guarantee
}

if (guarantee == ManagedGuaranteeType.IGNORE ||
guarantee == ManagedGuaranteeType.TREAT_AS_ATOMIC) {
// It's important that this method can't be called inside runInIgnoredSection, as the ignored section
Expand Down Expand Up @@ -1346,7 +1409,7 @@ abstract class ManagedStrategy(
atomicUpdater: Any,
parameters: Array<Any?>,
): MethodCallTracePoint {
getAtomicFieldUpdaterName(atomicUpdater)?.let { tracePoint.initializeOwnerName(it) }
getAtomicFieldUpdaterName(atomicUpdater)?.let { tracePoint.initializeOwnerName(it.fieldName) }
tracePoint.initializeParameters(parameters.drop(1).map { adornedStringRepresentation(it) })
return tracePoint
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,27 @@ class SnapshotTracker {
class ArrayCellNode(val index: Int, initialValue: Any?) : MemoryNode(initialValue)
}

fun trackField(obj: Any?, className: String, fieldName: String) {
fun isTracked(obj: Any?): Boolean = obj != null && obj in trackedObjects

fun trackField(obj: Any?, accessClass: Class<*>, fieldName: String) {
if (obj != null && obj !in trackedObjects) return
trackFieldImpl(
obj = obj,
clazz = getDeclaringClass(obj, accessClass, fieldName),
fieldName = fieldName
)
}

fun trackField(obj: Any?, accessClassName: String, fieldName: String) {
if (obj != null && obj !in trackedObjects) return
trackFieldImpl(
obj = obj,
clazz = getDeclaringClass(obj, Class.forName(accessClassName), fieldName),
fieldName = fieldName
)
}

val clazz = getDeclaringClass(obj, className, fieldName)
private fun trackFieldImpl(obj: Any?, clazz: Class<*>, fieldName: String) {
val field = clazz.findField(fieldName)
val readResult = readFieldSafely(obj, field)

Expand Down Expand Up @@ -179,9 +196,8 @@ class SnapshotTracker {
private fun shouldTrackEagerly(obj: Any?): Boolean {
if (obj == null) return false
return (
// TODO: in further development of snapshot restoring feature this check should be removed
// (and only check for java atomic classes should be inserted), see https://github.com/JetBrains/lincheck/pull/418#issue-2595977113
// right now it is needed for collections to be restored properly (because of missing support for `System.arrayCopy()` and other similar methods)
// TODO: after support for System.arraycopy,
// rewrite to `obj.javaClass.name.startsWith("java.util.concurrent.") && obj.javaClass.name.contains("Atomic")`
obj.javaClass.name.startsWith("java.util.")
)
}
Expand Down Expand Up @@ -237,8 +253,8 @@ class SnapshotTracker {
)
}

private fun getDeclaringClass(obj: Any?, className: String, fieldName: String): Class<*> {
return Class.forName(className).let {
private fun getDeclaringClass(obj: Any?, clazz: Class<*>, fieldName: String): Class<*> {
return clazz.let {
if (obj != null) it
else it.findField(fieldName).declaringClass
}
Expand Down
Loading