Skip to content

Commit

Permalink
Optimize tracking of constructors' arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitrii-artuhov committed Nov 26, 2024
1 parent d143878 commit 22939e7
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 9 deletions.
13 changes: 7 additions & 6 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ internal fun primitiveOrIdentityHashCode(value: Any?): Int {
return if (value.isPrimitiveWrapper) value.hashCode() else System.identityHashCode(value)
}

private val Any?.isPrimitiveWrapper get() = when (this) {
internal val Any?.isPrimitiveWrapper get() = when (this) {
is Boolean, is Int, is Short, is Long, is Double, is Float, is Char, is Byte -> true
else -> false
}
Expand Down Expand Up @@ -247,10 +247,10 @@ internal val Throwable.text: String get() {
* @param onArrayElement callback for array elements traversal, accepts `(array, index, elementValue)`.
* Returns an object to be traversed next.
*/
internal inline fun traverseObjectGraph(
internal fun traverseObjectGraph(
root: Any,
onField: (obj: Any, field: Field, value: Any?) -> Any?,
onArrayElement: (array: Any, index: Int, element: Any?) -> Any?,
onField: ((obj: Any, field: Field, value: Any?) -> Any?)? = null,
onArrayElement: ((array: Any, index: Int, element: Any?) -> Any?)? = null,
) {
val queue = ArrayDeque<Any>()
val visitedObjects = Collections.newSetFromMap<Any>(IdentityHashMap())
Expand Down Expand Up @@ -280,12 +280,13 @@ internal inline fun traverseObjectGraph(
val currentObj = queue.removeFirst()

when {
currentObj.javaClass.isArray || isAtomicArray(currentObj) -> {
onArrayElement != null &&
(currentObj.javaClass.isArray || isAtomicArray(currentObj)) -> {
traverseArrayElements(currentObj) { _ /* currentObj */, index, elementValue ->
processNextObject(onArrayElement(currentObj, index, elementValue))
}
}
else -> {
onField != null -> {
traverseObjectFields(currentObj) { _ /* currentObj */, field, fieldValue ->
processNextObject(onField(currentObj, field, fieldValue))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed

import org.jetbrains.kotlinx.lincheck.findField
import org.jetbrains.kotlinx.lincheck.getFieldOffset
import org.jetbrains.kotlinx.lincheck.isPrimitiveWrapper
import org.jetbrains.kotlinx.lincheck.strategy.managed.SnapshotTracker.Descriptor.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.SnapshotTracker.MemoryNode.*
import org.jetbrains.kotlinx.lincheck.traverseObjectGraph
Expand Down Expand Up @@ -55,6 +56,54 @@ class SnapshotTracker {

fun size(): Int = (trackedObjects.keys.filter { it !is Class<*> } + trackedObjects.values.flatMap { it }.mapNotNull { it.initialValue }).toSet().size

// fun printRoots() {
// val rootSizes = mutableMapOf<Any, Int>()
// val mappedFromRoots = mutableMapOf<Any, MutableList<Any>>()
// trackedObjects.keys.filterIsInstance<Class<*>>().forEach { root ->
// var size = 0
// val countedObjs = mutableSetOf<Any>()
// mappedFromRoots[root] = mutableListOf()
//
// trackedObjects[root]!!.forEach nodesTraverse@{ node ->
// if (node.initialValue == null) return@nodesTraverse
//
// mappedFromRoots[root]!!.add(node.initialValue)
// size++
//
// traverseObjectGraph(
// node.initialValue,
// onArrayElement = { _, _, value ->
// if (value in trackedObjects && value !in countedObjs) {
// countedObjs.add(value!!)
// mappedFromRoots[root]!!.add(value)
// size++
// }
// value
// },
// onField = { _, _, value ->
// if (value in trackedObjects && value !in countedObjs) {
// countedObjs.add(value!!)
// mappedFromRoots[root]!!.add(value)
// size++
// }
// value
// }
// )
// }
//
// rootSizes[root] = size
// }
//
// val sizesSorted = rootSizes.entries
// .sortedBy { -it.value } // Sort by values
// .associate { it.key to it.value } // Convert back to a map
// mappedFromRoots.entries
// .sortedBy { -it.value.size }
// .toList()
//
//// println("Snapshot roots (${sizesSorted.size}): $sizesSorted")
// }

@OptIn(ExperimentalStdlibApi::class)
fun trackField(obj: Any?, className: String, fieldName: String, @Suppress("UNUSED_PARAMETER") location: String = "") {
//println("Consider ($location): obj=${obj?.javaClass?.simpleName}${if (obj != null) "@" + System.identityHashCode(obj).toHexString() else ""}, className=$className, fieldName=$fieldName")
Expand Down Expand Up @@ -100,7 +149,60 @@ class SnapshotTracker {
}

fun trackObjects(objs: Array<Any?>) {
objs.filterNotNull().forEach { trackHierarchy(it) }
val processField = { owner: Any, field: Field, fieldValue: Any? ->
trackSingleField(owner, owner.javaClass, field, fieldValue) {
if (shouldTrackEnergetically(fieldValue) /* also checks for `fieldValue != null` */) {
trackHierarchy(fieldValue!!)
}
}
}

objs
// leave only those objects from constructor arguments that were tracked before the call to constructors itself
.filter { it != null && it in trackedObjects }
.forEach { obj ->
// We want to track the following values:
// 1. objects themselves (already tracked because of the filtering)
// 2. 1st layer of fields of these objects (tracking the whole hierarchy is too expensive, and full laziness does not work,
// because of the JVM class verificator limitations, see https://github.com/JetBrains/lincheck/issues/424, thus, we collect
// fields which afterward can be used for further lazy tracking)
// 3. values that are subclasses of the objects' class and their 1st layer of fields
// (we are not sure if they are going to require restoring, but we still add them preventively,
// again, because there is verificator limitation on tracking such values lazily)
traverseObjectGraph(
obj!!,
// `obj` cannot be an array because it is the same type as some class, which constructor was called, and arrays have not constructors
onArrayElement = null,
onField = { owner, field, fieldValue ->
when {
// add 1st layer of fields of `obj` (2)
obj == owner -> {
processField(owner, field, fieldValue)
// track subclasses of `obj` and their 1st layer of fields (bullet-point 3) recursively
if (fieldValue?.javaClass?.isInstance(obj) == true) {
// allow traversing further, because `obj`'s 1st layer contains an object which is a subclass of `obj`
// the `owner` of `fieldValue` is already added to `trackedObjects` (because `owner == obj`)
fieldValue
}
else {
null
}
}

// track subclasses of `obj` and their 1st layer of fields (3)
fieldValue != null && fieldValue.javaClass.isInstance(obj) -> {
// track the `owner` of `fieldValue`, because otherwise we will not be able to
// restore the initial value of `fieldValue` object
trackedObjects.putIfAbsent(owner, mutableListOf<MemoryNode>())
processField(owner, field, fieldValue)
fieldValue
}

else -> null
}
}
)
}
}

fun restoreValues() {
Expand Down Expand Up @@ -231,9 +333,10 @@ class SnapshotTracker {

private fun isTrackableObject(value: Any?): Boolean {
return (
value != null &&
value != null &&
!value.javaClass.isPrimitive &&
!value.javaClass.isEnum
!value.javaClass.isEnum &&
!value.isPrimitiveWrapper
)
}

Expand Down

0 comments on commit 22939e7

Please sign in to comment.