diff --git a/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java index 6d7b4b5d5..e55b1454b 100644 --- a/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java +++ b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java @@ -36,6 +36,8 @@ public interface EventTracker { void beforeNewObjectCreation(String className); void afterNewObjectCreation(Object obj); + long getNextObjectId(); + void advanceCurrentObjectId(long oldId); void updateSnapshotBeforeConstructorCall(Object[] objs); diff --git a/bootstrap/src/sun/nio/ch/lincheck/Injections.java b/bootstrap/src/sun/nio/ch/lincheck/Injections.java index 9e3bd4d0e..bb47ffd29 100644 --- a/bootstrap/src/sun/nio/ch/lincheck/Injections.java +++ b/bootstrap/src/sun/nio/ch/lincheck/Injections.java @@ -421,6 +421,40 @@ public static void updateSnapshotBeforeConstructorCall(Object[] objs) { getEventTracker().updateSnapshotBeforeConstructorCall(objs); } + /** + * Retrieves the next object id, used for identity hash code substitution, and then advances it by one. + */ + public static long getNextObjectId() { + return getEventTracker().getNextObjectId(); + } + + /** + * Advances the current object id with the delta, associated with the old id {@code oldId}, + * previously received with {@code getNextObjectId}. + *

+ * If for the given {@code oldId} there is no saved {@code newId}, + * the function saves the current object id and associates it with the {@code oldId}. + * On subsequent re-runs, when for the given {@code oldId} there exists a saved {@code newId}, + * the function sets the counter to the {@code newId}. + *

+ * This function is typically used to account for some cached computations: + * on the first run the actual computation is performed and its result is cached, + * and on subsequent runs the cached value is re-used. + * One example of such a situation is the {@code invokedynamic} instruction. + *

+ * In such cases, on the first run, the performed computation may allocate more objects, + * assigning more object ids to them. + * On subsequent runs, however, these objects will not be allocated, and thus the object ids numbering may vary. + * To account for this, before the first invocation of the cached computation, + * the last allocated object id {@code oldId} can be saved, and after the computation, + * the new last object id can be associated with it via a call {@code advanceCurrentObjectId(oldId)}. + * On subsequent re-runs, the cached computation will be skipped, but the + * current object id will still be advanced by the required delta via a call to {@code advanceCurrentObjectId(oldId)}. + */ + public static void advanceCurrentObjectId(long oldId) { + getEventTracker().advanceCurrentObjectId(oldId); + } + /** * Called from the instrumented code to replace [java.lang.Object.hashCode] method call with some * deterministic value. diff --git a/build.gradle.kts b/build.gradle.kts index cfcd93914..67b1308b6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,22 @@ repositories { mavenCentral() } +fun SourceDirectorySet.configureTestSources() { + srcDir("src/jvm/test-common") + val testInTraceDebuggerMode: String by project + if (testInTraceDebuggerMode.toBoolean().also(::println)) { + srcDir("src/jvm/test-trace-debugger") + } else { + srcDir("src/jvm/test") + val jdkToolchainVersion: String by project + if (jdkToolchainVersion.toInt() >= 11) { + srcDir("src/jvm/test-jdk11") + } else { + srcDir("src/jvm/test-jdk8") + } + } +} + kotlin { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { @@ -60,13 +76,7 @@ kotlin { } val jvmTest by getting { - kotlin.srcDir("src/jvm/test") - val jdkToolchainVersion: String by project - if (jdkToolchainVersion.toInt() >= 11) { - kotlin.srcDir("src/jvm/test-jdk11") - } else { - kotlin.srcDir("src/jvm/test-jdk8") - } + kotlin.configureTestSources() val junitVersion: String by project val jctoolsVersion: String by project @@ -105,7 +115,7 @@ sourceSets.main { } sourceSets.test { - java.srcDirs("src/jvm/test") + java.configureTestSources() resources { srcDir("src/jvm/test/resources") } @@ -149,6 +159,10 @@ tasks { if (withEventIdSequentialCheck.toBoolean()) { extraArgs.add("-Dlincheck.debug.withEventIdSequentialCheck=true") } + val testInTraceDebuggerMode: String by project + if (testInTraceDebuggerMode.toBoolean()) { + extraArgs.add("-Dlincheck.traceDebuggerMode=true") + } extraArgs.add("-Dlincheck.version=$version") jvmArgs(extraArgs) } diff --git a/gradle.properties b/gradle.properties index a47475d24..3f34c76cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,6 +19,7 @@ jdkToolchainVersion=17 runAllTestsInSeparateJVMs=false instrumentAllClasses=false withEventIdSequentialCheck=false +testInTraceDebuggerMode=false kotlinVersion=1.9.25 kotlinxCoroutinesVersion=1.7.3 diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/TraceDebugger.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/TraceDebugger.kt new file mode 100644 index 000000000..a76177830 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/TraceDebugger.kt @@ -0,0 +1,14 @@ +/* + * 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 + +val TRACE_DEBUGGER_MODE = System.getProperty("lincheck.traceDebuggerMode", "false").toBoolean() +val isLoopDetectorEnabled get() = System.getProperty("lincheck.loopDetectorEnabled", "true").toBoolean() diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt index f05a17960..f83f0fa13 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt @@ -66,10 +66,13 @@ abstract class ManagedStrategy( protected val currentActorId = mutableThreadMapOf() // Detector of loops or hangs (i.e. active locks). - internal val loopDetector: LoopDetector = LoopDetector(testCfg.hangingDetectionThreshold) + internal val loopDetector: LoopDetector? = + if (isLoopDetectorEnabled) LoopDetector(testCfg.hangingDetectionThreshold) else null // Tracker of objects' allocations and object graph topology. protected abstract val objectTracker: ObjectTracker + // Tracker of objects' identity hash codes. + internal abstract val identityHashCodeTracker: ObjectIdentityHashCodeTracker // Tracker of the monitors' operations. protected abstract val monitorTracker: MonitorTracker // Tracker of the thread parking. @@ -201,7 +204,7 @@ abstract class ManagedStrategy( protected open fun initializeInvocation() { traceCollector = if (collectTrace) TraceCollector() else null suddenInvocationResult = null - loopDetector.initialize() + loopDetector?.initialize() objectTracker.reset() monitorTracker.reset() parkingTracker.reset() @@ -260,7 +263,7 @@ abstract class ManagedStrategy( POST -> 0 VALIDATION -> 0 } - loopDetector.beforePart(nextThread) + loopDetector?.beforePart(nextThread) threadScheduler.scheduleThread(nextThread) } @@ -282,12 +285,13 @@ abstract class ManagedStrategy( } collectTrace = true - loopDetector.enableReplayMode( + loopDetector?.enableReplayMode( failDueToDeadlockInTheEnd = result is ManagedDeadlockInvocationResult || result is ObstructionFreedomViolationInvocationResult ) cleanObjectNumeration() + identityHashCodeTracker.resetObjectIds() runner.close() runner = createRunner() @@ -395,7 +399,7 @@ abstract class ManagedStrategy( * In the considered example, we will retain that we will switch soon after * the spin cycle in thread 1, so no bug will appear. */ - loopDetector.replayModeEnabled -> + loopDetector?.replayModeEnabled == true -> loopDetector.shouldSwitchInReplayMode() /* * In the regular mode, we use loop detector only to determine should we @@ -406,13 +410,13 @@ abstract class ManagedStrategy( (runner.currentExecutionPart == PARALLEL) && shouldSwitch(iThread) } // check if live-lock is detected - val decision = loopDetector.visitCodeLocation(iThread, codeLocation) + val decision = loopDetector?.visitCodeLocation(iThread, codeLocation) // if we reached maximum number of events threshold, then fail immediately if (decision == LoopDetector.Decision.EventsThresholdReached) { failDueToDeadlock() } // if any kind of live-lock was detected, check for obstruction-freedom violation - if (decision.isLivelockDetected) { + if (decision?.isLivelockDetected == true) { failIfObstructionFreedomIsRequired { if (decision is LoopDetector.Decision.LivelockFailureDetected) { // if failure is detected, add a special obstruction-freedom violation @@ -432,14 +436,14 @@ abstract class ManagedStrategy( } // if live-lock was detected, and replay was requested, // then abort current execution and start the replay - if (decision.isReplayRequired) { + if (decision?.isReplayRequired == true) { abortWithSuddenInvocationResult(SpinCycleFoundAndReplayRequired) } // if the current thread in a live-lock, then try to switch to another thread if (decision is LoopDetector.Decision.LivelockThreadSwitch) { val switchHappened = switchCurrentThread(iThread, BlockingReason.LiveLocked, tracePoint) if (switchHappened) { - loopDetector.initializeFirstCodeLocationAfterSwitch(codeLocation) + loopDetector?.initializeFirstCodeLocationAfterSwitch(codeLocation) } traceCollector?.passCodeLocation(tracePoint) return @@ -448,12 +452,12 @@ abstract class ManagedStrategy( if (shouldSwitch) { val switchHappened = switchCurrentThread(iThread, tracePoint = tracePoint) if (switchHappened) { - loopDetector.initializeFirstCodeLocationAfterSwitch(codeLocation) + loopDetector?.initializeFirstCodeLocationAfterSwitch(codeLocation) } traceCollector?.passCodeLocation(tracePoint) return } - if (!loopDetector.replayModeEnabled) { + if (loopDetector != null && !loopDetector.replayModeEnabled) { loopDetector.onNextExecutionPoint(codeLocation) } traceCollector?.passCodeLocation(tracePoint) @@ -581,7 +585,7 @@ abstract class ManagedStrategy( open fun onThreadFinish(iThread: Int) { threadScheduler.awaitTurn(iThread) threadScheduler.finishThread(iThread) - loopDetector.onThreadFinish(iThread) + loopDetector?.onThreadFinish(iThread) traceCollector?.onThreadFinish() unblockJoiningThreads(iThread) val nextThread = chooseThreadSwitch(iThread, true) @@ -614,7 +618,7 @@ abstract class ManagedStrategy( currentActorId[iThread] = actorId callStackTrace[iThread]!!.clear() suspendedFunctionsStack[iThread]!!.clear() - loopDetector.onActorStart(iThread) + loopDetector?.onActorStart(iThread) enterTestingCode() } @@ -708,7 +712,7 @@ abstract class ManagedStrategy( @JvmName("setNextThread") private fun setCurrentThread(nextThread: Int) { - loopDetector.onThreadSwitch(nextThread) + loopDetector?.onThreadSwitch(nextThread) threadScheduler.scheduleThread(nextThread) } @@ -943,7 +947,7 @@ abstract class ManagedStrategy( lastReadTracePoint[iThread] = tracePoint } newSwitchPoint(iThread, codeLocation, tracePoint) - loopDetector.beforeReadField(obj) + loopDetector?.beforeReadField(obj) return@runInIgnoredSection true } @@ -970,7 +974,7 @@ abstract class ManagedStrategy( lastReadTracePoint[iThread] = tracePoint } newSwitchPoint(iThread, codeLocation, tracePoint) - loopDetector.beforeReadArrayElement(array, index) + loopDetector?.beforeReadArrayElement(array, index) true } @@ -980,7 +984,8 @@ abstract class ManagedStrategy( lastReadTracePoint[iThread]?.initializeReadValue(adornedStringRepresentation(value)) lastReadTracePoint[iThread] = null } - loopDetector.afterRead(value) + loopDetector?.afterRead(value) + Unit } override fun beforeWriteField(obj: Any?, className: String, fieldName: String, value: Any?, codeLocation: Int, @@ -1010,7 +1015,7 @@ abstract class ManagedStrategy( null } newSwitchPoint(iThread, codeLocation, tracePoint) - loopDetector.beforeWriteField(obj, value) + loopDetector?.beforeWriteField(obj, value) return@runInIgnoredSection true } @@ -1036,7 +1041,7 @@ abstract class ManagedStrategy( null } newSwitchPoint(iThread, codeLocation, tracePoint) - loopDetector.beforeWriteArrayElement(array, index, value) + loopDetector?.beforeWriteArrayElement(array, index, value) true } @@ -1086,9 +1091,16 @@ abstract class ManagedStrategy( LincheckJavaAgent.ensureClassHierarchyIsTransformed(className) } + override fun advanceCurrentObjectId(oldId: Long) { + identityHashCodeTracker.advanceCurrentObjectId(oldId) + } + + override fun getNextObjectId(): Long = identityHashCodeTracker.getNextObjectId() + override fun afterNewObjectCreation(obj: Any) { if (obj is String || obj is Int || obj is Long || obj is Byte || obj is Char || obj is Float || obj is Double) return runInIgnoredSection { + identityHashCodeTracker.afterNewTrackedObjectCreation(obj) objectTracker.registerNewObject(obj) } } @@ -1230,11 +1242,11 @@ abstract class ManagedStrategy( if (guarantee == ManagedGuaranteeType.TREAT_AS_ATOMIC) { // re-use last call trace point newSwitchPoint(iThread, codeLocation, callStackTrace[iThread]!!.lastOrNull()?.tracePoint) - loopDetector.passParameters(params) + loopDetector?.passParameters(params) } // notify loop detector about the method call if (guarantee == null) { - loopDetector.beforeMethodCall(codeLocation, params) + loopDetector?.beforeMethodCall(codeLocation, params) } // method's guarantee guarantee @@ -1251,7 +1263,7 @@ abstract class ManagedStrategy( override fun onMethodCallReturn(result: Any?) { runInIgnoredSection { - loopDetector.afterMethodCall() + loopDetector?.afterMethodCall() if (collectTrace) { val iThread = threadScheduler.getCurrentThreadId() // this case is possible and can occur when we resume the coroutine, @@ -1278,7 +1290,7 @@ abstract class ManagedStrategy( override fun onMethodCallException(t: Throwable) { runInIgnoredSection { - loopDetector.afterMethodCall() + loopDetector?.afterMethodCall() } if (collectTrace) { runInIgnoredSection { @@ -1788,6 +1800,7 @@ abstract class ManagedStrategy( } fun checkActiveLockDetected() { + if (loopDetector == null) return val currentThreadId = threadScheduler.getCurrentThreadId() if (!loopDetector.replayModeCurrentlyInSpinCycle) return if (spinCycleStartAdded) { diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectIdentityHashCodeTracker.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectIdentityHashCodeTracker.kt new file mode 100644 index 000000000..a631d4366 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectIdentityHashCodeTracker.kt @@ -0,0 +1,110 @@ +/* + * 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 org.jetbrains.kotlinx.lincheck.TRACE_DEBUGGER_MODE +import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + + +private typealias Id = Long + +/** + * This class stores initial identity hashcodes of created objects. + * When the program is rerun the firstly calculated hashcodes are left as is and used to + * substitute new hashcodes via [sun.misc.Unsafe] in the object headers. + * + * To guarantee correct work, ensure that replays are deterministic. + */ +internal class ObjectIdentityHashCodeTracker { + companion object { + /** + * Offset (in bytes) of identity hashcode in an object header. + * + * @see JVM Anatomy Quark #26: Identity Hash Code + */ + private const val IDENTITY_HASHCODE_OFFSET: Long = 1L + } + private val initialHashCodes = ConcurrentHashMap() + private val nextObjectId = AtomicLong(0) + private val objectIdAdvances = ConcurrentHashMap() + + /** + * This method substitutes identity hash code of the object in its header with the initial (from the first test execution) identity hashcode. + * @return id of the created object. + */ + fun afterNewTrackedObjectCreation(obj: Any): Id { + val currentObjectId = if (TRACE_DEBUGGER_MODE) getNextObjectId() else 0 + val initialIdentityHashCode = getInitialIdentityHashCode( + objectId = currentObjectId, + identityHashCode = if (TRACE_DEBUGGER_MODE) System.identityHashCode(obj) else 0 + ) + // ATTENTION: bizarre and crazy code below (might not work for all JVM implementations) + UnsafeHolder.UNSAFE.putInt(obj, IDENTITY_HASHCODE_OFFSET, initialIdentityHashCode) + return currentObjectId + } + + /** + * Resets ids numeration and starts them from 0. + */ + fun resetObjectIds() { + if (!TRACE_DEBUGGER_MODE) return + nextObjectId.set(0) + } + + /** + * Advances the current object id with the delta, associated with the old id {@code oldId}, + * previously received with {@code getNextObjectId}. + *

+ * If for the given {@code oldId} there is no saved {@code newId}, + * the function saves the current object id and associates it with the {@code oldId}. + * On subsequent re-runs, when for the given {@code oldId} there exists a saved {@code newId}, + * the function sets the counter to the {@code newId}. + *

+ * This function is typically used to account for some cached computations: + * on the first run the actual computation is performed and its result is cached, + * and on subsequent runs the cached value is re-used. + * One example of such a situation is the {@code invokedynamic} instruction. + *

+ * In such cases, on the first run, the performed computation may allocate more objects, + * assigning more object ids to them. + * On subsequent runs, however, these objects will not be allocated, and thus the object ids numbering may vary. + * To account for this, before the first invocation of the cached computation, + * the last allocated object id {@code oldId} can be saved, and after the computation, + * the new last object id can be associated with it via a call {@code advanceCurrentObjectId(oldId)}. + * On subsequent re-runs, the cached computation will be skipped, but the + * current object id will still be advanced by the required delta via a call to {@code advanceCurrentObjectId(oldId)}. + */ + fun advanceCurrentObjectId(oldObjectId: Id) { + if (!TRACE_DEBUGGER_MODE) return + val newObjectId = nextObjectId.get() + val existingAdvance = objectIdAdvances.putIfAbsent(oldObjectId, newObjectId) + if (existingAdvance != null) { + nextObjectId.set(existingAdvance) + } + } + + /** + * @return id of current object and increments the global counter. + */ + fun getNextObjectId(): Id { + if (!TRACE_DEBUGGER_MODE) return 0 + return nextObjectId.getAndIncrement() + } + + /** + * @return initial identity hashcode for object with specified [objectId]. + * If this is first time function is called for this object, then provided [identityHashCode] is treated as initial. + */ + private fun getInitialIdentityHashCode(objectId: Id, identityHashCode: Int): Int = + initialHashCodes.getOrPut(objectId) { identityHashCode } +} diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt index 58397fa80..e26f0c2d3 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt @@ -55,6 +55,8 @@ internal class ModelCheckingStrategy( // Tracker of objects' allocations and object graph topology. override val objectTracker: ObjectTracker = LocalObjectManager() + // Tracker of objects' identity hash codes. + override val identityHashCodeTracker: ObjectIdentityHashCodeTracker = ObjectIdentityHashCodeTracker() // Tracker of the monitors' operations. override val monitorTracker: MonitorTracker = ModelCheckingMonitorTracker() // Tracker of the thread parking. @@ -63,6 +65,7 @@ internal class ModelCheckingStrategy( override fun nextInvocation(): Boolean { currentInterleaving = root.nextInterleaving() ?: return false + identityHashCodeTracker.resetObjectIds() return true } @@ -133,6 +136,7 @@ internal class ModelCheckingStrategy( private fun doReplay(): InvocationResult { cleanObjectNumeration() + identityHashCodeTracker.resetObjectIds() currentInterleaving = currentInterleaving.copy() resetEventIdProvider() return runInvocation() diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt index a7fe7d17a..3a4269d4c 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt @@ -80,17 +80,19 @@ internal class LincheckClassVisitor( } } fun MethodVisitor.newAdapter() = GeneratorAdapter(this, access, methodName, desc) - if (methodName == "" || - // Debugger implicitly evaluates toString for variables rendering - // We need to disable breakpoints in such a case, as the numeration will break. - // Breakpoints are disabled as we do not instrument toString and enter an ignored section, - // so there are no beforeEvents inside. - ideaPluginEnabled && methodName == "toString" && desc == "()Ljava/lang/String;") { + if (methodName == "") { mv = WrapMethodInIgnoredSectionTransformer(fileName, className, methodName, mv.newAdapter()) return mv } - if (methodName == "") { + // Debugger implicitly evaluates toString for variables rendering + // We need to disable breakpoints in such a case, as the numeration will break. + // Breakpoints are disabled as we do not instrument toString and enter an ignored section, + // so there are no beforeEvents inside. + if (methodName == "" || ideaPluginEnabled && methodName == "toString" && desc == "()Ljava/lang/String;") { mv = ObjectCreationTransformer(fileName, className, methodName, mv.newAdapter()) + if (TRACE_DEBUGGER_MODE) { + mv = DeterministicInvokeDynamicTransformer(fileName, className, methodName, mv.newAdapter()) + } mv = run { val st = ConstructorArgumentsSnapshotTrackerTransformer(fileName, className, methodName, mv.newAdapter(), classVisitor::isInstanceOf) val sv = SharedMemoryAccessTransformer(fileName, className, methodName, st.newAdapter()) @@ -148,7 +150,9 @@ internal class LincheckClassVisitor( mv = WaitNotifyTransformer(fileName, className, methodName, mv.newAdapter()) mv = ParkingTransformer(fileName, className, methodName, mv.newAdapter()) mv = ObjectCreationTransformer(fileName, className, methodName, mv.newAdapter()) - mv = DeterministicHashCodeTransformer(fileName, className, methodName, mv.newAdapter()) + if (TRACE_DEBUGGER_MODE) { + mv = DeterministicInvokeDynamicTransformer(fileName, className, methodName, mv.newAdapter()) + } mv = DeterministicTimeTransformer(mv.newAdapter()) mv = DeterministicRandomTransformer(fileName, className, methodName, mv.newAdapter()) // `SharedMemoryAccessTransformer` goes first because it relies on `AnalyzerAdapter`, diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt index a2e998105..5659dc3bc 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt @@ -158,6 +158,50 @@ internal fun GeneratorAdapter.storeArguments(methodDescriptor: String): IntArray return storeLocals(argumentTypes) } +/** + * Executes a try-catch-finally block within the context of the GeneratorAdapter. + * + * Attention: this method does not insert `finally` blocks before return statements. + * + * @param tryBlock The code block to be executed in the try section. + * @param exceptionType The type of exception to be caught in the catch section, or null to catch all exceptions. + * @param catchBlock The code block to be executed in the catch section if an exception is thrown. By default, it re-throws the exception. + * @param finallyBlock The code block to be executed in the finally section. This is optional. + */ +internal fun GeneratorAdapter.tryCatchFinally( + tryBlock: GeneratorAdapter.() -> Unit, + exceptionType: Type? = null, + catchBlock: GeneratorAdapter.() -> Unit = { throwException() }, + finallyBlock: (GeneratorAdapter.() -> Unit)? = null, +) { + val startTryBlockLabel = newLabel() + val endTryBlockLabel = newLabel() + val exceptionHandlerLabel = newLabel() + val endLabel = newLabel() + visitTryCatchBlock( + startTryBlockLabel, + endTryBlockLabel, + exceptionHandlerLabel, + exceptionType?.internalName + ) + visitLabel(startTryBlockLabel) + tryBlock() + visitLabel(endTryBlockLabel) + if (finallyBlock != null) finallyBlock() + goTo(endLabel) + visitLabel(exceptionHandlerLabel) + if (finallyBlock != null) { + val exception = newLocal(exceptionType ?: getType(Throwable::class.java)) + storeLocal(exception) + finallyBlock() + loadLocal(exception) + catchBlock() + } else { + catchBlock() + } + visitLabel(endLabel) +} + /** * Copies arguments of the method in the local variables. * diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterminismTransformers.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterminismTransformers.kt index f2f958e64..fceed5ee5 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterminismTransformers.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterminismTransformers.kt @@ -18,53 +18,6 @@ import org.jetbrains.kotlinx.lincheck.transformation.* import sun.nio.ch.lincheck.* import java.util.* -/** - * [DeterministicHashCodeTransformer] tracks invocations of [Object.hashCode] and [System.identityHashCode] methods, - * and replaces them with the [Injections.hashCodeDeterministic] and [Injections.identityHashCodeDeterministic] calls. - * - * This transformation aims to prevent non-determinism due to the native [hashCode] implementation, - * which typically returns memory address of the object. - * There is no guarantee that memory addresses will be the same in different runs. - */ -internal class DeterministicHashCodeTransformer( - fileName: String, - className: String, - methodName: String, - adapter: GeneratorAdapter, -) : ManagedStrategyMethodVisitor(fileName, className, methodName, adapter) { - - override fun visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) = adapter.run { - when { - name == "hashCode" && desc == "()I" -> { - invokeIfInTestingCode( - original = { - visitMethodInsn(opcode, owner, name, desc, itf) - }, - code = { - invokeStatic(Injections::hashCodeDeterministic) - } - ) - } - - owner == "java/lang/System" && name == "identityHashCode" && desc == "(Ljava/lang/Object;)I" -> { - invokeIfInTestingCode( - original = { - visitMethodInsn(opcode, owner, name, desc, itf) - }, - code = { - invokeStatic(Injections::identityHashCodeDeterministic) - } - ) - } - - else -> { - visitMethodInsn(opcode, owner, name, desc, itf) - } - } - } - -} - /** * [DeterministicTimeTransformer] tracks invocations of [System.nanoTime] and [System.currentTimeMillis] methods, * and replaces them with stubs to prevent non-determinism. diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterministicInvokeDynamicTransformer.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterministicInvokeDynamicTransformer.kt new file mode 100644 index 000000000..b80412348 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterministicInvokeDynamicTransformer.kt @@ -0,0 +1,150 @@ +/* + * 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.transformation.transformers + +import org.jetbrains.kotlinx.lincheck.transformation.* +import org.objectweb.asm.Handle +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.commons.GeneratorAdapter +import org.objectweb.asm.commons.Method +import sun.nio.ch.lincheck.Injections +import java.lang.invoke.CallSite +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType + + +/** + * A specialized visitor for handling `INVOKEDYNAMIC` instructions in JVM bytecode. + * + * This transformer implements a deterministic execution strategy for dynamic method invocations, + * which normally rely on the call-site creation and caching called done by JVM itself, + * leading to the difference between the first and the following executions. + */ +internal class DeterministicInvokeDynamicTransformer( + fileName: String, + className: String, + methodName: String, + adapter: GeneratorAdapter +) : ManagedStrategyMethodVisitor(fileName, className, methodName, adapter) { + override fun visitInvokeDynamicInsn( + name: String, + descriptor: String, + bootstrapMethodHandle: Handle, + vararg bootstrapMethodArguments: Any? + ) = adapter.run { + invokeIfInTestingCode( + original = { visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments) }, + ) { + val arguments = storeArguments(descriptor) + + // InvokeDynamic execution consists of the following steps: + // 1. Calling bootstrap method, which creates the actual function to call later; + // 2. Caching it by JVM; + // 3. Calling it by JVM. + // On the subsequent runs, JVM uses cached function. + + // The current implementation performs these steps manually each time. + // Once native calls are supported, it would be possible to cache `MethodHandle`s creation. + + // Bootstrap method is a function, which creates the actual call-site. + // Its first three parameters are predefined and are supplied by JVM when it invokes `invokedynamic`. + // After those parameters, there are regular parameters. + // Also, there can be `vararg` parameters. + + // (predefined, predefined, predefined, regular, regular, [Ljava/lang/Object; <- is vararg) + // vararg parameter might exist or not + + // https://openjdk.org/jeps/309 + // https://www.infoq.com/articles/Invokedynamic-Javas-secret-weapon/ + // https://www.baeldung.com/java-string-concatenation-invoke-dynamic + + val bootstrapMethodParameterTypes = Type.getArgumentTypes(bootstrapMethodHandle.desc) + require(bootstrapMethodParameterTypes[0] == Type.getType(MethodHandles.Lookup::class.java)) + require(bootstrapMethodParameterTypes[1] == Type.getType(String::class.java)) + require(bootstrapMethodParameterTypes[2] == Type.getType(MethodType::class.java)) + + // pushing predefined arguments manually + invokeStatic(MethodHandles::lookup) + visitLdcInsn(name) + visitLdcInsn(Type.getMethodType(descriptor)) + val jvmPredefinedParametersCount = 3 + + val isMethodWithVarArgs = isMethodVarArgs(bootstrapMethodHandle) + val notVarargsArgumentsCount = bootstrapMethodParameterTypes.size - jvmPredefinedParametersCount - (if (isMethodWithVarArgs) 1 else 0) + val varargsArgumentsCount = bootstrapMethodArguments.size - notVarargsArgumentsCount + // adding regular arguments + for (arg in bootstrapMethodArguments.take(notVarargsArgumentsCount)) { + visitLdcInsn(arg) + } + // adding vararg + if (isMethodWithVarArgs) { + val varargArguments = bootstrapMethodArguments.takeLast(varargsArgumentsCount) + visitLdcInsn(varargsArgumentsCount) + // [Ljava/lang/Object; -> Ljava/lang/Object;, [[Ljava/lang/Object; -> [Ljava/lang/Object; + val arrayElementType = bootstrapMethodParameterTypes.last().descriptor.drop(1).let(Type::getType) + newArray(arrayElementType) + for ((i, arg) in varargArguments.withIndex()) { + dup() + visitLdcInsn(i) + visitLdcInsn(arg) + if (arg != null && arrayElementType.sort == Type.OBJECT) { + box(Type.getType(arg.javaClass)) + } + arrayStore(arrayElementType) + } + } + + // creating java.lang.invoke.MethodHandle manually + invokeInIgnoredSection { + visitMethodInsn( + Opcodes.INVOKESTATIC, + bootstrapMethodHandle.owner, + bootstrapMethodHandle.name, + bootstrapMethodHandle.desc, + bootstrapMethodHandle.isInterface + ) + invokeVirtual( + Type.getType(CallSite::class.java), + Method.getMethod(CallSite::class.java.getMethod("dynamicInvoker")) + ) + } + // todo cache all above when native calls are available + loadLocals(arguments) + advancingCounter { + visitMethodInsn( + Opcodes.INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", descriptor, false + ) + } + } + } + + private fun isMethodVarArgs( + bootstrapMethodHandle: Handle, + ): Boolean { + val ownerClassName = bootstrapMethodHandle.owner.replace('/', '.') + val methods = Class.forName(ownerClassName).declaredMethods + return methods.single { Type.getMethodDescriptor(it) == bootstrapMethodHandle.desc }.isVarArgs + } + + private fun GeneratorAdapter.advancingCounter(code: GeneratorAdapter.() -> Unit) { + invokeStatic(Injections::getNextObjectId) + val oldId = newLocal(Type.LONG_TYPE) + storeLocal(oldId) + tryCatchFinally( + tryBlock = code, + finallyBlock = { + loadLocal(oldId) + invokeStatic(Injections::advanceCurrentObjectId) + } + ) + } +} diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/KotlinJdkVersionTest.kt b/src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/KotlinJdkVersionTest.kt similarity index 100% rename from src/jvm/test/org/jetbrains/kotlinx/lincheck_test/KotlinJdkVersionTest.kt rename to src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/KotlinJdkVersionTest.kt diff --git a/src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/util/JdkUtils.kt b/src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/util/JdkUtils.kt new file mode 100644 index 000000000..d65f7bc8f --- /dev/null +++ b/src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/util/JdkUtils.kt @@ -0,0 +1,20 @@ +package org.jetbrains.kotlinx.lincheck_test.util + +/* + * 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/. + */ + +/** + * Indicates whether the current Java Development Kit (JDK) version is JDK 8. + * + * This property checks the system's Java specification version + * and determines if the major version corresponds to '8', signifying JDK 8. + */ +// java.specification.version is "1.$x" for Java prior to 8 and "$x" for the newer ones +internal val isJdk8 = System.getProperty("java.specification.version").removePrefix("1.") == "8" diff --git a/src/jvm/test-trace-debugger/AbstractNativeCallTest.kt b/src/jvm/test-trace-debugger/AbstractNativeCallTest.kt new file mode 100644 index 000000000..a11ad3740 --- /dev/null +++ b/src/jvm/test-trace-debugger/AbstractNativeCallTest.kt @@ -0,0 +1,84 @@ +/* + * 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 + +import org.jetbrains.kotlinx.lincheck.ExceptionResult +import org.jetbrains.kotlinx.lincheck.checkImpl +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.execution.parallelResults +import org.jetbrains.kotlinx.lincheck.isLoopDetectorEnabled +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions +import org.jetbrains.kotlinx.lincheck.verifier.Verifier +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +/** + * Checks for absence of non-determinism and absence (or existence) of exceptions. + */ +abstract class AbstractNativeCallTest { + private fun testTraceDebugger() { + val oldStdOut = System.out + val oldErr = System.err + val stdOutOutputCollector = ByteArrayOutputStream() + val myStdOut = PrintStream(stdOutOutputCollector) + val stdErrOutputCollector = ByteArrayOutputStream() + val myStdErr = PrintStream(stdErrOutputCollector) + System.setOut(myStdOut) + System.setErr(myStdErr) + val oldIsLoopDetectorEnabled = isLoopDetectorEnabled + val loopDetectorPropertyName = "lincheck.loopDetectorEnabled" + System.setProperty(loopDetectorPropertyName, "false") + try { + ModelCheckingOptions() + .actorsBefore(0) + .actorsAfter(0) + .iterations(30) + .threads(2) + .minimizeFailedScenario(false) + .actorsPerThread(1) + .verifier(FailingVerifier::class.java) + .customize() + .checkImpl(this::class.java) { lincheckFailure -> + val results = lincheckFailure?.results?.parallelResults?.flatten()?.takeIf { it.isNotEmpty() } + require(results != null) { lincheckFailure.toString() } + if (shouldFail()) { + require(results.all { it is ExceptionResult })// { lincheckFailure.toString() } + } else { + require(results.none { it is ExceptionResult })// { lincheckFailure.toString() } + } + } + } finally { + val forbiddenString = "Non-determinism found." + System.setOut(oldStdOut) + System.setErr(oldErr) + val stdOutOutput = stdOutOutputCollector.toString() + println(stdOutOutput) + val stdErrOutput = stdErrOutputCollector.toString() + System.err.print(stdErrOutput) + require(!stdOutOutput.contains(forbiddenString) && !stdErrOutput.contains(forbiddenString)) + System.setProperty(loopDetectorPropertyName, oldIsLoopDetectorEnabled.toString()) + } + } + + open fun shouldFail() = false + + @Test + fun test() = testTraceDebugger() + + open fun ModelCheckingOptions.customize(): ModelCheckingOptions = this +} + +@Suppress("UNUSED_PARAMETER") +class FailingVerifier(sequentialSpecification: Class<*>) : Verifier { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?) = false +} diff --git a/src/jvm/test-trace-debugger/HashCodeTests.kt b/src/jvm/test-trace-debugger/HashCodeTests.kt new file mode 100644 index 000000000..0f85f212b --- /dev/null +++ b/src/jvm/test-trace-debugger/HashCodeTests.kt @@ -0,0 +1,177 @@ +/* + * 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 + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.check +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions +import org.junit.Test +import java.util.* + +abstract class HashCodeTest : AbstractNativeCallTest() + +class SimpleHashCodeTest : HashCodeTest() { + @Operation + fun operation() = List(100) { Any().hashCode() } +} + +class IdentityHashCodeTest : HashCodeTest() { + @Operation + fun operation(): List = List(100) { System.identityHashCode(Any()) } +} + + +class ObjectsHashCodeTest : HashCodeTest() { + @Operation + fun operation() = List(100) { Objects.hashCode(Any()) } +} + + +class ObjectsHashCodeVarargTest : HashCodeTest() { + @Operation + fun operation() = List(100) { Objects.hash(Any(), Any()) } +} + +class SimpleToStringTest : HashCodeTest() { + @Operation + fun operation() = List(100) { Any().toString() } +} + +class HashCodeToStringTest : HashCodeTest() { + @Operation + fun operation() = List(100) { Any().hashCode().toString() } +} + +class StringBuilderToStringTest : HashCodeTest() { + @Operation + fun operation() = List(100) { buildString { appendLine(Any()) } } +} + +class StringBuilderHashCodeToStringTest : HashCodeTest() { + @Operation + fun operation() = List(100) { buildString { appendLine(Any().hashCode()) } } +} + +class InvokeDynamicToStringTest : HashCodeTest() { + @Operation + fun operation() = List(100) { "${Any()} ${Any()}" } +} + +class InvokeDynamicInnerClassCreationTest : HashCodeTest() { + class A { + override fun toString(): String = Any().hashCode().toString() + } + @Operation + fun operation(): List = List(100) { "${A()} ${A()}" } +} + +class InvokeDynamicHashCodeToStringTest : HashCodeTest() { + @Operation + fun operation(): List = List(100) { "${Any().hashCode()} ${Any().hashCode()}" } +} + +class StringFormatToStringTest : HashCodeTest() { + @Operation + fun operation() = List(100) { String.format("%s %s", Any(), Any()) } +} + +class StringFormatHashCodeToStringTest : HashCodeTest() { + @Operation + fun operation() = List(100) { String.format("%s %s", Any().hashCode(), Any().hashCode()) } +} + +data class Wrapper(val value: Any) + +class WrapperHashCodeTest : HashCodeTest() { + @Operation + fun operation() = List(100) { Wrapper(Any()).hashCode() } +} + +class WrapperToStringTest : HashCodeTest() { + @Operation + fun operation() = List(100) { Wrapper(Any()).toString() } +} + +class InitInternalToStringTest : HashCodeTest() { + class A { + internal val value = "${Any()} ${Any()}" + override fun toString(): String = value + } + @Operation + fun operation() = List(100) { "${A()} ${A()}" } +} + +class ClassInitInternalToStringTest : HashCodeTest() { + object A { + @JvmStatic + internal val value = "${Any()} ${Any()}" + } + fun f(): String = A.value + + @Operation + fun operation() = List(100) { "${f()} ${f()}" } +} + +class FailingInvokeDynamicTest : HashCodeTest() { + private class A { + override fun toString(): String { + throw IllegalStateException() + } + } + @Operation + fun operation(): List { + try { + List(100) { "${A()} ${A()}" } + } catch (_: Throwable) { + // ignore + } + return List(100) { Any().toString() } + } +} + +class FailingInvokeDynamicWithStateTest : HashCodeTest() { + private class A { + var x = 0 + override fun toString(): String { + try { + x = Any().hashCode() + throw IllegalStateException() + } finally { + x = x xor Any().hashCode() + } + } + } + + @Operation + fun operation(): List { + try { + List(100) { "${A()} ${A()} ${A().x}" } + } catch (_: Throwable) { + // ignore + } + return List(100) { Any().toString() + " " + A().x } + } +} + +class IdentityHashCodeDiffersTest() { + @Operation + fun operation() { + val objects = Array(10) { Any() } + // identityHashCode should differ + check(objects.map { it.hashCode() }.distinct().size > 1) + } + + @Test + fun test() = ModelCheckingOptions() + .iterations(1) + .threads(1) + .check(this::class) +} diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CustomThreadsRepresentationTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CustomThreadsRepresentationTest.kt index 119114916..658ef6307 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CustomThreadsRepresentationTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CustomThreadsRepresentationTest.kt @@ -102,7 +102,12 @@ class CustomThreadsRepresentationTest { testClass = this::class, testOperation = this::livelock, invocations = 1_000, - outputFileName = if (isJdk8) "custom_threads_livelock_trace_jdk8.txt" else "custom_threads_livelock_trace.txt", + outputFileName = when { + isJdk8 && TRACE_DEBUGGER_MODE -> "custom_threads_livelock_trace_jdk8_with_trace_debugger.txt" + isJdk8 -> "custom_threads_livelock_trace_jdk8.txt" + TRACE_DEBUGGER_MODE -> "custom_threads_livelock_trace_with_trace_debugger.txt" + else -> "custom_threads_livelock_trace.txt" + }, ) fun incorrectConcurrentLinkedDeque() { diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/HashCodeTests.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/HashCodeTests.kt index 26d343f97..88a3b7977 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/HashCodeTests.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/HashCodeTests.kt @@ -12,7 +12,6 @@ package org.jetbrains.kotlinx.lincheck_test.transformation import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* -import org.jetbrains.kotlinx.lincheck.verifier.* import org.junit.* /** @@ -77,16 +76,3 @@ class IdentityHashCodeOnNullTest { @Test fun test() = ModelCheckingOptions().check(this::class) } - -@Ignore // TODO: easier to support when `javaagent` is merged -class IdentityHashCodeDiffersTest() { - @Operation - fun operation() { - val objects = Array(1000) { Any() } - // identityHashCode should differ - check(objects.map { it.hashCode() }.distinct().size > 1) - } - - @Test - fun test() = ModelCheckingOptions().check(this::class) -} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/util/TestUtils.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/util/TestUtils.kt index b3f19e5d7..b9c11ab5a 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/util/TestUtils.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/util/TestUtils.kt @@ -109,11 +109,3 @@ class StringPoolGenerator(randomProvider: RandomProvider, configuration: String) strings[random.nextInt(strings.size)] } -/** - * Indicates whether the current Java Development Kit (JDK) version is JDK 8. - * - * This property checks the system's Java specification version - * and determines if the major version corresponds to '8', signifying JDK 8. - */ -// java.specification.version is "1.$x" for Java prior to 8 and "$x" for the newer ones -internal val isJdk8 = System.getProperty("java.specification.version").removePrefix("1.") == "8" diff --git a/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_jdk8_with_trace_debugger.txt b/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_jdk8_with_trace_debugger.txt new file mode 100644 index 000000000..71e4df7ed --- /dev/null +++ b/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_jdk8_with_trace_debugger.txt @@ -0,0 +1,73 @@ += The execution has hung = +| ------------------ | +| Thread 1 | +| ------------------ | +| livelock(): | +| ------------------ | + + +The following interleaving leads to the error: +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Thread 1 | Thread 2 | Thread 3 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| livelock(): | | | +| Thread#2.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | | +| Thread#3.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | | +| switch (reason: waiting for Thread 2 to finish) | | | +| | | run() | +| | | SpinLockTestKt.withLock(SpinLock#2,livelock$t2$1$1#1) at CustomThreadsRepresentationTest.livelock$lambda$6(CustomThreadsRepresentationTest.kt:88) | +| | | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | +| | | switch | +| | run() | | +| | SpinLockTestKt.withLock(SpinLock#1,livelock$t1$1$1#1) at CustomThreadsRepresentationTest.livelock$lambda$5(CustomThreadsRepresentationTest.kt:81) | | +| | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | | +| | livelock$t1$1$1#1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | | +| | invoke() at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:81) | | +| | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t1$1$1$1) at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:82) | | +| | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | | +| | /* The following events repeat infinitely: */ | | +| | ┌╶> AtomicBoolean#1.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | | +| | └╶╶ switch (reason: active lock detected) | | +| | | livelock$t2$1$1#1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | +| | | invoke() at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:88) | +| | | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t2$1$1$1) at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:89) | +| | | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | +| | | /* The following events repeat infinitely: */ | +| | | ┌╶> AtomicBoolean#2.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | +| | | └╶╶ switch (reason: active lock detected) | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +All unfinished threads are in deadlock + +Detailed trace: +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Thread 1 | Thread 2 | Thread 3 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| livelock(): | | | +| Thread#2.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | | +| Thread#3.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | | +| switch (reason: waiting for Thread 2 to finish) | | | +| | | run() | +| | | SpinLockTestKt.withLock(SpinLock#2,livelock$t2$1$1#1) at CustomThreadsRepresentationTest.livelock$lambda$6(CustomThreadsRepresentationTest.kt:88) | +| | | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | +| | | AtomicBoolean#1.compareAndSet(false,true): true at SpinLock.lock(SpinLockTest.kt:83) | +| | | switch | +| | run() | | +| | SpinLockTestKt.withLock(SpinLock#1,livelock$t1$1$1#1) at CustomThreadsRepresentationTest.livelock$lambda$5(CustomThreadsRepresentationTest.kt:81) | | +| | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | | +| | AtomicBoolean#2.compareAndSet(false,true): true at SpinLock.lock(SpinLockTest.kt:83) | | +| | livelock$t1$1$1#1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | | +| | invoke() at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:81) | | +| | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t1$1$1$1) at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:82) | | +| | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | | +| | /* The following events repeat infinitely: */ | | +| | ┌╶> AtomicBoolean#1.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | | +| | └╶╶ switch (reason: active lock detected) | | +| | | livelock$t2$1$1#1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | +| | | invoke() at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:88) | +| | | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t2$1$1$1) at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:89) | +| | | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | +| | | /* The following events repeat infinitely: */ | +| | | ┌╶> AtomicBoolean#2.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | +| | | └╶╶ switch (reason: active lock detected) | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +All unfinished threads are in deadlock diff --git a/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_with_trace_debugger.txt b/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_with_trace_debugger.txt new file mode 100644 index 000000000..7f685fa63 --- /dev/null +++ b/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_with_trace_debugger.txt @@ -0,0 +1,73 @@ += The execution has hung = +| ------------------ | +| Thread 1 | +| ------------------ | +| livelock(): | +| ------------------ | + + +The following interleaving leads to the error: +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Thread 1 | Thread 2 | Thread 3 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| livelock(): | | | +| Thread#2.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | | +| Thread#3.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | | +| switch (reason: waiting for Thread 2 to finish) | | | +| | | run() | +| | | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t2$1$1) at CustomThreadsRepresentationTest.livelock$lambda$6(CustomThreadsRepresentationTest.kt:88) | +| | | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | +| | | switch | +| | run() | | +| | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t1$1$1) at CustomThreadsRepresentationTest.livelock$lambda$5(CustomThreadsRepresentationTest.kt:81) | | +| | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | | +| | CustomThreadsRepresentationTest$livelock$t1$1$1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | | +| | invoke() at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:81) | | +| | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t1$1$1$1) at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:82) | | +| | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | | +| | /* The following events repeat infinitely: */ | | +| | ┌╶> AtomicBoolean#1.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | | +| | └╶╶ switch (reason: active lock detected) | | +| | | CustomThreadsRepresentationTest$livelock$t2$1$1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | +| | | invoke() at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:88) | +| | | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t2$1$1$1) at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:89) | +| | | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | +| | | /* The following events repeat infinitely: */ | +| | | ┌╶> AtomicBoolean#2.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | +| | | └╶╶ switch (reason: active lock detected) | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +All unfinished threads are in deadlock + +Detailed trace: +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Thread 1 | Thread 2 | Thread 3 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| livelock(): | | | +| Thread#2.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | | +| Thread#3.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | | +| switch (reason: waiting for Thread 2 to finish) | | | +| | | run() | +| | | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t2$1$1) at CustomThreadsRepresentationTest.livelock$lambda$6(CustomThreadsRepresentationTest.kt:88) | +| | | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | +| | | AtomicBoolean#1.compareAndSet(false,true): true at SpinLock.lock(SpinLockTest.kt:83) | +| | | switch | +| | run() | | +| | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t1$1$1) at CustomThreadsRepresentationTest.livelock$lambda$5(CustomThreadsRepresentationTest.kt:81) | | +| | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | | +| | AtomicBoolean#2.compareAndSet(false,true): true at SpinLock.lock(SpinLockTest.kt:83) | | +| | CustomThreadsRepresentationTest$livelock$t1$1$1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | | +| | invoke() at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:81) | | +| | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t1$1$1$1) at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:82) | | +| | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | | +| | /* The following events repeat infinitely: */ | | +| | ┌╶> AtomicBoolean#1.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | | +| | └╶╶ switch (reason: active lock detected) | | +| | | CustomThreadsRepresentationTest$livelock$t2$1$1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | +| | | invoke() at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:88) | +| | | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t2$1$1$1) at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:89) | +| | | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | +| | | /* The following events repeat infinitely: */ | +| | | ┌╶> AtomicBoolean#2.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | +| | | └╶╶ switch (reason: active lock detected) | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +All unfinished threads are in deadlock