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

Improve trace representation: show non-this object instances on method calls and show field names when using VarHandle, AtomicReference, andUnsafe #325

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
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.ObjectNumerator.getObjectNumber
import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe
import sun.misc.Unsafe
import java.lang.reflect.Field
Expand Down
11 changes: 11 additions & 0 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ internal val Throwable.text: String get() {
return writer.buffer.toString()
}

internal val Class<*>.allDeclaredFieldWithSuperclasses get(): List<Field> {
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
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
}

/**
* 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 @@ -186,9 +186,12 @@ internal open class ParallelThreadsRunner(
// 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) {
strategy.initializeCallStack(testInstance)
Copy link
Collaborator

Choose a reason for hiding this comment

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

With this change, the comment above is incorrect.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Besides, it's unclear what this call does and why you need it in the runner.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this logic added to Runner instead of ManagedStrategy.initializeInvocation?

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 @@ -11,7 +11,7 @@
package org.jetbrains.kotlinx.lincheck.strategy.managed

import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder.UNSAFE
import java.lang.reflect.Modifier
import org.jetbrains.kotlinx.lincheck.util.findFieldNameByOffset
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater
import java.util.concurrent.atomic.AtomicLongFieldUpdater
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
Expand All @@ -22,10 +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? {
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
if (updater !is AtomicIntegerFieldUpdater<*> && updater !is AtomicLongFieldUpdater<*> && updater !is AtomicReferenceFieldUpdater<*, *>) {
throw IllegalArgumentException("Provided object is not a recognized Atomic*FieldUpdater type.")
}

internal fun getAtomicFieldUpdaterName(updater: Any): String? {
// Extract the private offset value and find the matching field.
try {
// Cannot use neither reflection not MethodHandles.Lookup, as they lead to a warning.
Expand All @@ -35,16 +33,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,170 @@
/*
* 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
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