Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
a44bfb1
remove old loop detector implementation
eupp Dec 4, 2025
1621de8
add a comment
eupp Dec 4, 2025
a3dcd65
Counter based loop detector
acturcu Dec 11, 2025
3a5e42e
add comment
acturcu Dec 16, 2025
376bd5f
Add suggestions to LoopDetector.kt
acturcu Jan 18, 2026
c1e4239
Add suggestions in ManagedStrategy.kt
acturcu Jan 19, 2026
7b37623
Add beginning of trace printer
acturcu Jan 20, 2026
8d2f2fc
Commented out removed part of moveSpinCycleStartTracePoints()
acturcu Jan 20, 2026
a6277a9
Add initial try of spin cycle trace printing
acturcu Jan 21, 2026
570427a
Add independent method stack for the LoopDetector
acturcu Jan 26, 2026
bfaa4a8
Add methodId pop afterThreadException
acturcu Jan 26, 2026
fd790f6
Add cleanup for LoopDetector.kt
acturcu Jan 26, 2026
0a11efd
Replace threadDescriptor with threadId in LoopDetector.kt
acturcu Jan 26, 2026
af61fb4
Improve trace printing for loops
acturcu Jan 27, 2026
447199e
Fix for <iteration 1> appearing twice
acturcu Jan 27, 2026
c41ab92
fix missing ignored section at `afterLoopExit`
eupp Jan 28, 2026
aac85af
Fix indentation of loops in printed trace
acturcu Jan 29, 2026
76d8216
Add initial prettifying for loops. Known bug: for some tests the Loop…
acturcu Jan 29, 2026
1632b96
Fix iteration folding bug
acturcu Feb 5, 2026
88196d8
Add recursive calls folding
acturcu Feb 5, 2026
92160cb
Fix trace print for non-ranged iteration in `interleaving leads to er…
acturcu Feb 6, 2026
e7212b4
Merge IterationNode and IterationRangeNode together
acturcu Feb 6, 2026
b0c4060
Remove redundant convertOpenLoopsToSpinCycles() function
acturcu Feb 6, 2026
be8dfcd
Fix event ordering & ranged loops trace print for `interleaving leads…
acturcu Feb 6, 2026
2fc4515
Move code to trace/utils.kt
acturcu Feb 7, 2026
1715729
Fix infinite recursion
acturcu Feb 7, 2026
41cc21e
Change `LIVELOCK_SPINLOOP` to `LIVELOCK` in IdeaPlugin.kt
acturcu Feb 7, 2026
5d3dc9b
Remove comment & make RecursionNodes also unfold
acturcu Feb 7, 2026
9b3d39a
Repair loop folding broken in previous commits
acturcu Feb 7, 2026
f1ef538
Move functions from ManagedStrategy.kt to LoopDetector.kt to make Man…
acturcu Feb 7, 2026
ac8af63
Improve loop folding to also fold structural similar traces
acturcu Feb 9, 2026
9beb7eb
Add support for nested iteration folding
acturcu Feb 11, 2026
5ce0d65
Capture open telemetry trace ID (#935)
bbrockbernd Feb 2, 2026
6a3c0e4
Fix checks for shadowed package names (#938)
eupp Feb 3, 2026
8b8979e
Add `org.omg` to instrumentation filters (#939)
eupp Feb 3, 2026
7e1e03d
fix CFG loop validation check (#936)
eupp Feb 3, 2026
b2a52d5
Live debugger: safety checker for breakpoint conditions (#925)
ndkoval Feb 3, 2026
ee82c30
Refactor `TraceRecordingMode` and related start/stop tracing APIs (#942)
eupp Feb 4, 2026
5cab589
Make sure conditions do not contain loops (#940)
ndkoval Feb 4, 2026
4700195
Fix stupid mistake which can lead to recursive tracing which fails. (…
lev-serebryakov-jetbrains Feb 4, 2026
7ea426e
Live debugger: collect trace points as flat list (#943)
eupp Feb 5, 2026
20e6f4d
Prototype conditions for Live Debugger (#945)
ndkoval Feb 5, 2026
9586468
Add JMX server shutdown hook (#937)
eupp Feb 5, 2026
f40c58f
Trace TCP streaming (#941)
eupp Feb 8, 2026
5eea66b
Add live debugger JMX controller API to add/remove breakpoints (#948)
ndkoval Feb 9, 2026
c1f62c9
Capture Object fields and Array elements (#947)
bbrockbernd Feb 9, 2026
2b8a2f0
quickfix for #948
eupp Feb 9, 2026
fd998f8
Few minor logging infra improvements (#949)
eupp Feb 9, 2026
a803ec3
Fix missing thread name after for TCP streaming (#950)
bbrockbernd Feb 9, 2026
1a41d86
Fix creation of tons of garbage. (#951)
lev-serebryakov-jetbrains Feb 9, 2026
57cfc5b
Disable bytecode caching in Live Debugger mode (#952)
eupp Feb 10, 2026
3e8734f
Bump ASM version to 9.9.1 (#954)
ndkoval Feb 10, 2026
9b9aee9
Avoid duplicate breakpoint hits (#955)
ndkoval Feb 10, 2026
fe41cf6
Surround condition calls with try-catch blocks, assuming conditions f…
ndkoval Feb 11, 2026
29b1426
Update test files - not including ValidationFunctionLongLoopTest and …
acturcu Feb 12, 2026
8b2965b
Add a check to verify if the execution is in Validation in order to s…
acturcu Feb 13, 2026
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.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ void cacheInvokeDynamicCallSite(
void onInlineMethodCallReturn(ThreadDescriptor descriptor, int methodId);
void onInlineMethodCallException(ThreadDescriptor descriptor, int methodId, Throwable t);

void onSnapshotLineBreakpoint(ThreadDescriptor descriptor, int codeLocation, Object[] locals);
void onSnapshotLineBreakpoint(ThreadDescriptor descriptor, int codeLocation, Object[] locals, String traceId);

void onLoopIteration(ThreadDescriptor descriptor, int codeLocation, int loopId);
void afterLoopExit(ThreadDescriptor descriptor, int codeLocation, int loopId, Throwable exception, boolean isReachableFromOutsideLoop);
Expand Down
34 changes: 30 additions & 4 deletions bootstrap/src/sun/nio/ch/lincheck/Injections.java
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,32 @@ public static boolean inAnalyzedCode() {
return (descriptor != null);
}

/**
* Marks the current thread as being inside a breakpoint condition evaluation.
*/
public static void enterBreakpointCondition(ThreadDescriptor descriptor) {
if (descriptor == null) return;
descriptor.enterBreakpointCondition();
}

/**
* Marks the current thread as having exited a breakpoint condition evaluation.
*/
public static void leaveBreakpointCondition(ThreadDescriptor descriptor) {
if (descriptor == null) return;
descriptor.leaveBreakpointCondition();
}

/**
* Checks if the current thread is not inside a breakpoint condition evaluation.
*
* @return true if not inside a condition evaluation, false otherwise.
*/
public static boolean isNotInsideBreakpointCondition(ThreadDescriptor descriptor) {
if (descriptor == null) return true;
return !descriptor.isInsideBreakpointCondition();
}

/**
* Registers a thread for event tracking, creating a new thread descriptor if one does not already exist.
*
Expand Down Expand Up @@ -784,13 +810,13 @@ public static void onMethodCallException(ThreadDescriptor descriptor, int method
* @param descriptor The thread descriptor of the current thread.
* @param codeLocation The location of the breakpoint in the source code. Holds local variable names.
* @param locals An array containing the current values of local variables at the breakpoint location.
* This includes: this, function parameters, and local variables.
* This includes: this, function parameters, and local variables.
* @param traceId ID to correlate snapshot breakpoints. Can be provided by frameworks like OpenTelemetry.
*/

public static void onSnapshotLineBreakpoint(ThreadDescriptor descriptor, int codeLocation, Object[] locals) {
public static void onSnapshotLineBreakpoint(ThreadDescriptor descriptor, int codeLocation, Object[] locals, String traceId) {
EventTracker eventTracker = getEventTracker(descriptor);
if (eventTracker == null || descriptor == null) return;
eventTracker.onSnapshotLineBreakpoint(descriptor, codeLocation, locals);
eventTracker.onSnapshotLineBreakpoint(descriptor, codeLocation, locals, traceId);
}

/**
Expand Down
18 changes: 18 additions & 0 deletions bootstrap/src/sun/nio/ch/lincheck/ThreadDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ public class ThreadDescriptor {
*/
private int ignoredSectionDepth = 0;

/**
* Thread-local variable to track if we are currently evaluating a breakpoint condition.
* When true, breakpoint hits inside the condition evaluation should not be reported.
*/
private boolean insideBreakpointCondition = false;

/**
* Creates a new thread descriptor for the given thread.
*/
Expand Down Expand Up @@ -249,6 +255,18 @@ public void restoreIgnoredSectionDepth(int depth) {
ignoredSectionDepth = depth;
}

public void enterBreakpointCondition() {
insideBreakpointCondition = true;
}

public void leaveBreakpointCondition() {
insideBreakpointCondition = false;
}

public boolean isInsideBreakpointCondition() {
return insideBreakpointCondition;
}

/**
* Thread local variable storing the thread descriptor for each thread.
* <br>
Expand Down
11 changes: 10 additions & 1 deletion buildSrc/src/main/kotlin/TraceAgentTasks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,16 @@ fun Project.registerTraceAgentTasks(fatJarName: String, fatJarTaskName: String,
}
})
from(jarWrapper)


/* IMPORTANT NOTE!
*
* Shadowing will also substitute ALL strings containing package names in ALL source files;
* when adding a new shadowed package, use `listOf(...)` hack to circumvent package shadowing:
* for instance, instead of `org.objectweb.asm`, use `listOf("org", "objectweb", "asm").joinToString(".")`.
*
* To minimize the affected surface area, all package-related string checks should be extracted into
* separate utility functions and be kept in `common/src/main/org/jetbrains/lincheck/util/Utils.kt`.
*/
val packagesToShade = listOf(
"org.objectweb.asm",
"net.bytebuddy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class CodeLocation(
// TODO: this only makes sense for method call code locations,
// consider introducing proper type hierarchy for code locations
val argumentNames: List<AccessPath?>? = null,
val activeLocals: List<String>? = null
val activeLocals: List<ActiveLocal>? = null,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down Expand Up @@ -61,7 +61,7 @@ object CodeLocations {
stackTraceElement: StackTraceElement,
accessPath: AccessPath? = null,
argumentNames: List<AccessPath?>? = null,
activeLocals: List<String>? = null
activeLocals: List<ActiveLocal>? = null
): Int =
context.newCodeLocation(stackTraceElement, accessPath, argumentNames, activeLocals)

Expand All @@ -81,5 +81,5 @@ object CodeLocations {

@JvmStatic
@Synchronized
fun activeLocals(context: TraceContext, codeLocationId: Int): List<String>? = context.activeLocalsNames(codeLocationId)
fun activeLocals(context: TraceContext, codeLocationId: Int): List<ActiveLocal>? = context.activeLocals(codeLocationId)
}
12 changes: 12 additions & 0 deletions common/src/main/org/jetbrains/lincheck/descriptors/Descriptors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,15 @@ data class VariableDescriptor(
val name: String,
val type: Types.Type
)


data class ActiveLocal(
val localName: String,
val localKind: LocalKind,
)

enum class LocalKind {
THIS,
PARAMETER,
VARIABLE,
}
5 changes: 3 additions & 2 deletions common/src/main/org/jetbrains/lincheck/trace/TraceContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package org.jetbrains.lincheck.trace

import org.jetbrains.lincheck.descriptors.AccessPath
import org.jetbrains.lincheck.descriptors.ActiveLocal
import org.jetbrains.lincheck.descriptors.ClassDescriptor
import org.jetbrains.lincheck.descriptors.CodeLocation
import org.jetbrains.lincheck.descriptors.FieldDescriptor
Expand Down Expand Up @@ -142,7 +143,7 @@ class TraceContext {
stackTraceElement: StackTraceElement,
accessPath: AccessPath? = null,
argumentNames: List<AccessPath?>? = null,
activeLocals: List<String>? = null
activeLocals: List<ActiveLocal>? = null
): Int {
val id = locations.size
val location = CodeLocation(stackTraceElement, accessPath, argumentNames, activeLocals)
Expand Down Expand Up @@ -180,7 +181,7 @@ class TraceContext {
return loc.argumentNames
}

fun activeLocalsNames(codeLocationId: Int): List<String>? {
fun activeLocals(codeLocationId: Int): List<ActiveLocal>? {
if (codeLocationId == UNKNOWN_CODE_LOCATION_ID) return null
val loc = locations[codeLocationId] ?: error("Invalid code location id $codeLocationId")
return loc.activeLocals
Expand Down
17 changes: 11 additions & 6 deletions common/src/main/org/jetbrains/lincheck/util/AnalysisSections.kt
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,11 @@ class AnalysisProfile(val analyzeStdLib: Boolean) {
if (isThreadContainerClass(className)) return true
return false
}
// Old legacy Java std library for CORBA,
// for instance, `org/omg/stub/javax/management`;
// can appear on Java 8 when JMX is used.
if (className.startsWith("org.omg.")) return false

// We do not need to instrument most standard Kotlin classes.
// However, we need to inject the Lincheck analysis into the classes
// related to collections, iterators, random and coroutines.
Expand All @@ -346,23 +351,23 @@ class AnalysisProfile(val analyzeStdLib: Boolean) {
if (isIntellijRuntimeAgentClass(className)) return false
// We should never instrument the JetBrains coverage package classes (for instance, relocated ASM library).
if (isJetBrainsCoverageClass(className)) return false

// Do not instrument bytecode-manipulation libraries
// (special care required to circumvent package shadowing, see `TraceAgentTasks.kt`).
if (isAsmClass(className) || isByteBuddyClass(className)) return false

// We can also safely do not instrument some libraries for performance reasons.
if (className.startsWith("com.esotericsoftware.kryo.")) return false
if (className.startsWith("net.bytebuddy.")) return false
if (className.startsWith("net.rubygrapefruit.platform.")) return false
if (className.startsWith("io.mockk.")) return false
if (className.startsWith("it.unimi.dsi.fastutil.")) return false
if (className.startsWith("worker.org.gradle.")) return false
if (className.startsWith("org.objectweb.asm.")) return false
if (className.startsWith("org.gradle.")) return false
if (className.startsWith("org.slf4j.")) return false
if (className.startsWith("org.apache.commons.lang.")) return false
if (className.startsWith("org.junit.")) return false
if (className.startsWith("junit.framework.")) return false
// Finally, we should never instrument the Lincheck classes.
if (className.startsWith(LINCHECK_PACKAGE_NAME)) return false
if (className.startsWith(LINCHECK_KOTLINX_PACKAGE_NAME)) return false
if (className.startsWith(LINCHECK_RELOCATED_PACKAGE_PREFIX)) return false

// All the classes that were not filtered out are eligible for transformation.
return true
}
Expand Down
101 changes: 76 additions & 25 deletions common/src/main/org/jetbrains/lincheck/util/Logger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@
package org.jetbrains.lincheck.util

import java.io.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

/**
* Logging utilities for the Lincheck framework.
*
* Supports logging to `stderr` or to a specified file.
* If a log file is specified through the `lincheck.logFile` system property,
* then the file-based logging is used. Otherwise, messages are logged to the standard error stream.
*
* Logging levels can be configured using the `lincheck.logLevel` system property, see [LoggingLevel].
*
* NOTE: when stderr is used, log messages from shutdown hooks are not guaranteed to be written.
*/
object Logger {

val logFile: File? = System.getProperty("lincheck.logFile")?.let { fileName ->
File(fileName).also { runCatching { initFile(it) }.getOrNull() }
}
Expand All @@ -28,60 +38,101 @@ object Logger {
runCatching { LoggingLevel.valueOf(it) }.getOrElse { DEFAULT_LOG_LEVEL }
} ?: DEFAULT_LOG_LEVEL

inline fun error(lazyMessage: () -> String) = log(LoggingLevel.ERROR, lazyMessage)
inline fun error(lazyMessage: () -> String) {
log(LoggingLevel.ERROR, lazyMessage)
}

inline fun warn(lazyMessage: () -> String) = log(LoggingLevel.WARN, lazyMessage)
inline fun warn(lazyMessage: () -> String) {
log(LoggingLevel.WARN, lazyMessage)
}

inline fun info(lazyMessage: () -> String) = log(LoggingLevel.INFO, lazyMessage)
inline fun info(lazyMessage: () -> String) {
log(LoggingLevel.INFO, lazyMessage)
}

inline fun debug(lazyMessage: () -> String) = log(LoggingLevel.DEBUG, lazyMessage)
inline fun debug(lazyMessage: () -> String) {
log(LoggingLevel.DEBUG, lazyMessage)
}

fun error(e: Throwable) = log(LoggingLevel.ERROR, e)
inline fun verbose(lazyMessage: () -> String) {
log(LoggingLevel.VERBOSE, lazyMessage)
}

fun warn(e: Throwable) = log(LoggingLevel.WARN, e)
inline fun error(e: Throwable, lazyMessage: () -> String = { e.message ?: "" }) {
log(LoggingLevel.ERROR, e, lazyMessage)
}

fun info(e: Throwable) = log(LoggingLevel.INFO, e)
inline fun warn(e: Throwable, lazyMessage: () -> String = { e.message ?: "" }) {
log(LoggingLevel.WARN, e, lazyMessage)
}

fun debug(e: Throwable) = log(LoggingLevel.DEBUG, e)
inline fun info(e: Throwable, lazyMessage: () -> String = { e.message ?: "" }) {
log(LoggingLevel.INFO, e, lazyMessage)
}

inline fun debug(e: Throwable, lazyMessage: () -> String = { e.message ?: "" }) {
log(LoggingLevel.DEBUG, e, lazyMessage)
}

inline fun verbose(e: Throwable, lazyMessage: () -> String = { e.message ?: "" }) {
log(LoggingLevel.VERBOSE, e, lazyMessage)
}

inline fun log(logLevel: LoggingLevel, lazyMessage: () -> String) {
if (logLevel >= this.logLevel) {
write(logLevel, lazyMessage(), logWriter)
write("[${Logger.logLevel.name}] ${lazyMessage()}$LINE_SEPARATOR")
}
}

inline fun log(logLevel: LoggingLevel, throwable: Throwable, lazyMessage: () -> String = { throwable.message ?: "" }) {
log(logLevel) {
createExceptionMessage(lazyMessage(), throwable)
}
}

fun write(logLevel: LoggingLevel, s: String, writer: Writer) {
fun write(message: String) {
try {
writer.write("[${logLevel.name}] $s$LINE_SEPARATOR")
writer.flush()
logWriter.write(message)
logWriter.flush()
} catch (e: IOException) {
e.printStackTrace()
}
}

private fun log(logLevel: LoggingLevel, throwable: Throwable) {
log(logLevel) {
StringWriter().use { writer ->
throwable.printStackTrace(PrintWriter(writer))
writer.toString()
fun createExceptionMessage(message: String, throwable: Throwable): String {
val writer = StringWriter().apply {
PrintWriter(this).use { printer ->
throwable.printStackTrace(printer)
}
}
val stackTrace = writer.toString()
if (message.isNotEmpty()) {
val paddedStackTrace = stackTrace.lines().joinToString(separator = LINE_SEPARATOR) { TAB + it }
return "${message}${LINE_SEPARATOR}${paddedStackTrace}"
} else {
return stackTrace
}
}

private fun initFile(file: File) {
// create parent directories
file.parentFile?.let { if (!it.exists()) it.mkdirs() }
file.parentFile?.let { parent ->
if (!parent.exists()) parent.mkdirs()
}

// create file
if (file.exists()) file.delete()
// create the file
if (file.exists()) {
file.delete()
}
file.createNewFile()
}
}

private val LINE_SEPARATOR = System.lineSeparator()
val TAB: String = "\t"
val LINE_SEPARATOR: String = System.lineSeparator()
}

@JvmField val DEFAULT_LOG_LEVEL = LoggingLevel.WARN

enum class LoggingLevel {
DEBUG, INFO, WARN, ERROR, OFF
VERBOSE, DEBUG, INFO, WARN, ERROR, OFF
}
Loading