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

Deterministic System.identityHashCode for Trace Debugger. #444

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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: 2 additions & 0 deletions bootstrap/src/sun/nio/ch/lincheck/EventTracker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
34 changes: 34 additions & 0 deletions bootstrap/src/sun/nio/ch/lincheck/Injections.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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.
Expand Down
30 changes: 22 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -105,7 +115,7 @@ sourceSets.main {
}

sourceSets.test {
java.srcDirs("src/jvm/test")
java.configureTestSources()
resources {
srcDir("src/jvm/test/resources")
}
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jdkToolchainVersion=17
runAllTestsInSeparateJVMs=false
instrumentAllClasses=false
withEventIdSequentialCheck=false
testInTraceDebuggerMode=false

kotlinVersion=1.9.25
kotlinxCoroutinesVersion=1.7.3
Expand Down
14 changes: 14 additions & 0 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/TraceDebugger.kt
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,13 @@ abstract class ManagedStrategy(
protected val currentActorId = mutableThreadMapOf<Int>()

// 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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -260,7 +263,7 @@ abstract class ManagedStrategy(
POST -> 0
VALIDATION -> 0
}
loopDetector.beforePart(nextThread)
loopDetector?.beforePart(nextThread)
threadScheduler.scheduleThread(nextThread)
}

Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -614,7 +618,7 @@ abstract class ManagedStrategy(
currentActorId[iThread] = actorId
callStackTrace[iThread]!!.clear()
suspendedFunctionsStack[iThread]!!.clear()
loopDetector.onActorStart(iThread)
loopDetector?.onActorStart(iThread)
enterTestingCode()
}

Expand Down Expand Up @@ -708,7 +712,7 @@ abstract class ManagedStrategy(

@JvmName("setNextThread")
private fun setCurrentThread(nextThread: Int) {
loopDetector.onThreadSwitch(nextThread)
loopDetector?.onThreadSwitch(nextThread)
threadScheduler.scheduleThread(nextThread)
}

Expand Down Expand Up @@ -943,7 +947,7 @@ abstract class ManagedStrategy(
lastReadTracePoint[iThread] = tracePoint
}
newSwitchPoint(iThread, codeLocation, tracePoint)
loopDetector.beforeReadField(obj)
loopDetector?.beforeReadField(obj)
return@runInIgnoredSection true
}

Expand All @@ -970,7 +974,7 @@ abstract class ManagedStrategy(
lastReadTracePoint[iThread] = tracePoint
}
newSwitchPoint(iThread, codeLocation, tracePoint)
loopDetector.beforeReadArrayElement(array, index)
loopDetector?.beforeReadArrayElement(array, index)
true
}

Expand All @@ -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,
Expand Down Expand Up @@ -1010,7 +1015,7 @@ abstract class ManagedStrategy(
null
}
newSwitchPoint(iThread, codeLocation, tracePoint)
loopDetector.beforeWriteField(obj, value)
loopDetector?.beforeWriteField(obj, value)
return@runInIgnoredSection true
}

Expand All @@ -1036,7 +1041,7 @@ abstract class ManagedStrategy(
null
}
newSwitchPoint(iThread, codeLocation, tracePoint)
loopDetector.beforeWriteArrayElement(array, index, value)
loopDetector?.beforeWriteArrayElement(array, index, value)
true
}

Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -1278,7 +1290,7 @@ abstract class ManagedStrategy(

override fun onMethodCallException(t: Throwable) {
runInIgnoredSection {
loopDetector.afterMethodCall()
loopDetector?.afterMethodCall()
}
if (collectTrace) {
runInIgnoredSection {
Expand Down Expand Up @@ -1788,6 +1800,7 @@ abstract class ManagedStrategy(
}

fun checkActiveLockDetected() {
if (loopDetector == null) return
val currentThreadId = threadScheduler.getCurrentThreadId()
if (!loopDetector.replayModeCurrentlyInSpinCycle) return
if (spinCycleStartAdded) {
Expand Down
Loading