Skip to content

Commit

Permalink
Improve trace representation: show non-this object instances on metho…
Browse files Browse the repository at this point in the history
…d calls and show field names when using VarHandle, AtomicReference, andUnsafe #325
  • Loading branch information
avpotapov00 authored May 8, 2024
1 parent 1541474 commit f274c6d
Show file tree
Hide file tree
Showing 53 changed files with 3,753 additions and 1,457 deletions.
2 changes: 1 addition & 1 deletion bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface EventTracker {
fun afterWrite()

fun beforeMethodCall(owner: Any?, className: String, methodName: String, codeLocation: Int, params: Array<Any?>)
fun beforeAtomicMethodCall(owner: Any?, methodName: String, codeLocation: Int, params: Array<Any?>)
fun beforeAtomicMethodCall(owner: Any?, className: String, methodName: String, codeLocation: Int, params: Array<Any?>)
fun onMethodCallFinishedSuccessfully(result: Any?)
fun onMethodCallThrewException(t: Throwable)

Expand Down
4 changes: 2 additions & 2 deletions bootstrap/src/sun/nio/ch/lincheck/Injections.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ public static void beforeMethodCall(Object owner, String className, String metho
* This is just an optimization of [beforeMethodCall] for trusted
* atomic constructs to avoid wrapping the invocations into try-finally blocks.
*/
public static void beforeAtomicMethodCall(Object owner, String methodName, int codeLocation, Object[] params) {
getEventTracker().beforeAtomicMethodCall(owner, methodName, codeLocation, params);
public static void beforeAtomicMethodCall(Object owner, String className, String methodName, int codeLocation, Object[] params) {
getEventTracker().beforeAtomicMethodCall(owner, className, methodName, codeLocation, params);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

package org.jetbrains.kotlinx.lincheck

import org.jetbrains.kotlinx.lincheck.strategy.managed.getObjectNumber
import org.jetbrains.kotlinx.lincheck.strategy.managed.ObjectLabelFactory.getObjectNumber
import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe
import sun.misc.Unsafe
import java.lang.reflect.Field
Expand Down
33 changes: 33 additions & 0 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import sun.nio.ch.lincheck.TestThread
import org.jetbrains.kotlinx.lincheck.runner.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.*
import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer
import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder
import org.jetbrains.kotlinx.lincheck.verifier.*
import java.io.PrintWriter
import java.io.StringWriter
Expand Down Expand Up @@ -194,6 +195,38 @@ internal val Throwable.text: String get() {
return writer.buffer.toString()
}

/**
* Returns all found fields in the hierarchy.
* Multiple fields with the same name and the same type may be returned
* if they appear in the subclass and a parent class.
*/
internal val Class<*>.allDeclaredFieldWithSuperclasses get(): List<Field> {
val fields: MutableList<Field> = ArrayList<Field>()
var currentClass: Class<*>? = this
while (currentClass != null) {
val declaredFields: Array<Field> = currentClass.declaredFields
fields.addAll(declaredFields)
currentClass = currentClass.superclass
}
return fields
}

internal fun findFieldNameByOffset(targetType: Class<*>, offset: Long): String? {
// Extract the private offset value and find the matching field.
for (field in targetType.declaredFields) {
try {
if (Modifier.isNative(field.modifiers)) continue
val fieldOffset = if (Modifier.isStatic(field.modifiers)) UnsafeHolder.UNSAFE.staticFieldOffset(field)
else UnsafeHolder.UNSAFE.objectFieldOffset(field)
if (fieldOffset == offset) return field.name
} catch (t: Throwable) {
t.printStackTrace()
}
}

return null // Field not found
}

/**
* Utility exception for test purposes.
* When this exception is thrown by an operation, it will halt testing with [UnexpectedExceptionInvocationResult].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,18 @@ internal open class ParallelThreadsRunner(

private fun createTestInstance() {
testInstance = testClass.newInstance()
// In the model checking mode, we need to ensure
// that all the necessary classes and instrumented
// after creating a test instance.
if (strategy is ModelCheckingStrategy && !ensuredTestInstanceIsTransformed) {
LincheckJavaAgent.ensureObjectIsTransformed(testInstance)
ensuredTestInstanceIsTransformed = true
if (strategy is ModelCheckingStrategy) {
// We pass the test instance to the strategy to initialize the call stack.
// It should be done here as we create the test instance in the `run` method in the runner, after
// `initializeInvocation` method call of ManagedStrategy.
strategy.initializeCallStack(testInstance)
// In the model checking mode, we need to ensure
// that all the necessary classes and instrumented
// after creating a test instance.
if (!ensuredTestInstanceIsTransformed) {
LincheckJavaAgent.ensureObjectIsTransformed(testInstance)
ensuredTestInstanceIsTransformed = true
}
}
testThreadExecutions.forEach { it.testInstance = testInstance }
validationPartExecution?.let { it.testInstance = testInstance }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

package org.jetbrains.kotlinx.lincheck.strategy.managed

import org.jetbrains.kotlinx.lincheck.findFieldNameByOffset
import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder.UNSAFE
import java.lang.reflect.Modifier
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater
import java.util.concurrent.atomic.AtomicLongFieldUpdater
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
Expand All @@ -22,7 +22,8 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
* equality and does not prevent them from being garbage collected.
*/
internal object AtomicFieldUpdaterNames {
fun getAtomicFieldUpdaterName(updater: Any): String? {

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

for (field in targetType.declaredFields) {
try {
if (Modifier.isNative(field.modifiers)) continue
val fieldOffset = if (Modifier.isStatic(field.modifiers)) UNSAFE.staticFieldOffset(field)
else UNSAFE.objectFieldOffset(field)
if (fieldOffset == offset) return field.name
} catch (t: Throwable) {
t.printStackTrace()
}
}
return findFieldNameByOffset(targetType, offset)
} catch (t: Throwable) {
t.printStackTrace()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Lincheck
*
* Copyright (C) 2019 - 2024 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.strategy.managed

import kotlinx.atomicfu.AtomicArray
import kotlinx.atomicfu.AtomicBooleanArray
import kotlinx.atomicfu.AtomicIntArray
import org.jetbrains.kotlinx.lincheck.allDeclaredFieldWithSuperclasses
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceMethodType.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceNames.AtomicReferenceOwnerWithName.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceNames.TraverseResult.*
import java.lang.reflect.Modifier
import java.util.*
import java.util.concurrent.atomic.AtomicIntegerArray
import java.util.concurrent.atomic.AtomicLongArray
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.
* 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.
*/
internal object AtomicReferenceNames {

internal fun getMethodCallType(
testObject: Any,
atomicReference: Any,
parameters: Array<Any?>
): AtomicReferenceMethodType {
val receiverAndName = getAtomicReferenceReceiverAndName(testObject, atomicReference)
return if (receiverAndName != null) {
if (isAtomicArrayIndexMethodCall(atomicReference, parameters)) {
when (receiverAndName) {
is InstanceOwnerWithName -> InstanceFieldAtomicArrayMethod(receiverAndName.receiver, receiverAndName.fieldName, parameters[0] as Int)
is StaticOwnerWithName -> StaticFieldAtomicArrayMethod(receiverAndName.clazz, receiverAndName.fieldName, parameters[0] as Int)
}
} else {
when (receiverAndName) {
is InstanceOwnerWithName -> AtomicReferenceInstanceMethod(receiverAndName.receiver, receiverAndName.fieldName)
is StaticOwnerWithName -> AtomicReferenceStaticMethod(receiverAndName.clazz, receiverAndName.fieldName)
}
}
} else {
if (isAtomicArrayIndexMethodCall(atomicReference, parameters)) {
AtomicArrayMethod(atomicReference, parameters[0] as Int)
} else {
TreatAsDefaultMethod
}
}
}

private fun isAtomicArrayIndexMethodCall(atomicReference: Any, parameters: Array<Any?>): Boolean {
if (parameters.firstOrNull() !is Int) return false
return atomicReference is AtomicReferenceArray<*> ||
atomicReference is AtomicLongArray ||
atomicReference is AtomicIntegerArray ||
atomicReference is AtomicIntArray ||
atomicReference is AtomicArray<*> ||
atomicReference is AtomicBooleanArray
}

private fun getAtomicReferenceReceiverAndName(testObject: Any, reference: Any): AtomicReferenceOwnerWithName? =
runCatching {
val visitedObjects: MutableSet<Any> = Collections.newSetFromMap(IdentityHashMap())
return when (val result = findObjectField(testObject, reference, visitedObjects)) {
is FieldName -> result.fieldName
MultipleFieldsMatching, NotFound -> null
}
}.getOrElse { exception ->
exception.printStackTrace()
null
}

private sealed interface TraverseResult {
data object NotFound : TraverseResult
data class FieldName(val fieldName: AtomicReferenceOwnerWithName) : TraverseResult
data object MultipleFieldsMatching : TraverseResult
}

private fun findObjectField(testObject: Any?, value: Any, visitedObjects: MutableSet<Any>): TraverseResult {
if (testObject == null) return NotFound
var fieldName: AtomicReferenceOwnerWithName? = null
// We take all the fields from the hierarchy.
// If two or more fields match (===) the AtomicReference object, we fall back to the default behavior,
// so there is no problem that we can receive some fields of the same name and the same type.
for (field in testObject::class.java.allDeclaredFieldWithSuperclasses) {
if (field.type.isPrimitive || !field.trySetAccessible()) continue
val fieldValue = field.get(testObject)

if (fieldValue in visitedObjects) continue
visitedObjects += testObject

if (fieldValue === value) {
if (fieldName != null) return MultipleFieldsMatching

fieldName = if (Modifier.isStatic(field.modifiers)) {
StaticOwnerWithName(field.name, testObject::class.java)
} else {
InstanceOwnerWithName(field.name, testObject)
}
continue
}
when (val result = findObjectField(fieldValue, value, visitedObjects)) {
is FieldName -> {
if (fieldName != null) {
return MultipleFieldsMatching
} else {
fieldName = result.fieldName
}
}

MultipleFieldsMatching -> return result
NotFound -> {}
}
}
return if (fieldName != null) FieldName(fieldName) else NotFound
}

private sealed class AtomicReferenceOwnerWithName(val fieldName: String) {
class StaticOwnerWithName(fieldName: String, val clazz: Class<*>) :
AtomicReferenceOwnerWithName(fieldName)

class InstanceOwnerWithName(fieldName: String, val receiver: Any) :
AtomicReferenceOwnerWithName(fieldName)
}
}

/**
* The type of the AtomicReference method call.
*/
internal sealed interface AtomicReferenceMethodType {
/**
* AtomicArray method call. In this case, we cannot find the owner of this atomic array.
*/
data class AtomicArrayMethod(val atomicArray: Any, val index: Int) : AtomicReferenceMethodType

/**
* AtomicArray method call. Returned if we found the [owner] and the [field], containing this AtomicArray.
*/
data class InstanceFieldAtomicArrayMethod(val owner: Any, val fieldName: String, val index: Int) :
AtomicReferenceMethodType

/**
* Static AtomicArray method call.
*/
data class StaticFieldAtomicArrayMethod(val ownerClass: Class<*>, val fieldName: String, val index: Int) :
AtomicReferenceMethodType

/**
* AtomicReference method call. Returned if we cannot find the owner of this atomic reference.
*/
data object TreatAsDefaultMethod : AtomicReferenceMethodType

/**
* Instance AtomicReference method call. Returned if we found the [owner] and the [fieldName], containing this AtomicArray
*/
data class AtomicReferenceInstanceMethod(val owner: Any, val fieldName: String) : AtomicReferenceMethodType

/**
* Static AtomicReference method call. Returned if we found the [ownerClass] and the [fieldName], containing this AtomicArray
*/
data class AtomicReferenceStaticMethod(val ownerClass: Class<*>, val fieldName: String) : AtomicReferenceMethodType
}
Loading

0 comments on commit f274c6d

Please sign in to comment.