Skip to content
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
7 changes: 6 additions & 1 deletion common/src/main/org/jetbrains/lincheck/trace/TraceContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.jetbrains.lincheck.descriptors.MethodSignature
import org.jetbrains.lincheck.descriptors.VariableDescriptor
import org.jetbrains.lincheck.descriptors.Types
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger

const val UNKNOWN_CODE_LOCATION_ID = -1
// This method type corresponds to the following method descriptor '(V)V'
Expand All @@ -40,8 +41,12 @@ class TraceContext {
val methodPool = DescriptorPool<MethodDescriptor>()
val fieldPool = DescriptorPool<FieldDescriptor>()
val variablePool = DescriptorPool<VariableDescriptor>()
private val anonymousThreadCounter = AtomicInteger(0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do you need a separate counter for anonymous threads?
Why not just print "anonymous-thread-$id"?

Also, consider changing the thread name to "unknown-thread-$id".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To create unique names for displaying.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

image Is it ok for us?


fun setThreadName(id: Int, name: String) { threadNames[id] = name }

fun setThreadName(id: Int, name: String?) {
threadNames[id] = name ?: "anonymous-thread-${anonymousThreadCounter.getAndIncrement()}"
}

fun getThreadName(id: Int): String = threadNames[id] ?: ""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@ class AnalysisProfile(val analyzeStdLib: Boolean) {
className.startsWith("java.util.concurrent.locks.AbstractQueuedSynchronizer") -> SILENT
className == "java.util.concurrent.FutureTask" -> SILENT
!analyzeStdLib && isConcurrentCollectionsLibrary(className) -> SILENT

isSpringRelatedClass(className) -> SILENT

else -> NORMAL
}

Expand All @@ -318,7 +319,7 @@ class AnalysisProfile(val analyzeStdLib: Boolean) {
* @return true if calls should be hidden from results, false otherwise
*/
@Suppress("UNUSED_PARAMETER") // methodName is here for uniformity and might become useful in the future
fun shouldBeHidden(className: String, methodName: String): Boolean =
fun shouldBeHidden(className: String, methodName: String): Boolean =
!analyzeStdLib && (isConcurrentCollectionsLibrary(className) || isCollectionsLibrary(className))

companion object {
Expand Down
37 changes: 36 additions & 1 deletion common/src/main/org/jetbrains/lincheck/util/Libs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -391,4 +391,39 @@ fun isMethodHandleRelatedClass(className: String): Boolean =
* These methods are not ignored because we need to analyze the invoked target method.
*/
fun isIgnoredMethodHandleMethod(className: String, methodName: String): Boolean =
isMethodHandleRelatedClass(className) && !methodName.contains("invoke")
isMethodHandleRelatedClass(className) && !methodName.contains("invoke")


fun isSpringRelatedClass(className: String) = when {
className.startsWith("sun.rmi.") -> true
className.startsWith("javax.") -> true

Comment on lines +398 to +400
Copy link
Collaborator

Choose a reason for hiding this comment

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

These 2 are not spring-related.

Copy link
Collaborator Author

@zhelenskiy zhelenskiy Mar 5, 2026

Choose a reason for hiding this comment

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

Generally, Spring classes are still shown, so none of the logic makes sense.

className.startsWith("org.springframework.data.jpa.") -> true
className.startsWith("org.springframework.boot.sql.") -> true
className.startsWith("org.springframework.boot.autoconfigure.") -> true
className.startsWith("org.springframework.cache.") -> true
className.startsWith("org.springframework.boot.logging.") -> true
className.startsWith("org.springframework.boot.cache.") -> true
className.startsWith("org.springframework.boot.jdbc.") -> true

className.startsWith("org.apache.logging.") -> true
className.startsWith("org.apache.logging.log4j.") -> true
className.startsWith("org.apache.juli.logging.") -> true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's move these to isRecognizedLoggingLibraryClass

className.startsWith("org.apache.tomcat.") -> true
className.startsWith("org.apache.catalina.") -> true
className.startsWith("org.apache.coyote.") -> true
className.startsWith("org.antlr.") -> true

className.startsWith("ch.qos.logback.") -> true
className.startsWith("io.micrometer.") -> true
className.startsWith("io.opentelemetry.") -> true
className.startsWith("com.google.protobuf.") -> true
className.startsWith("com.fasterxml.") -> true
className.startsWith("tools.jackson.") -> true

className.startsWith("org.hibernate.") -> true
className.startsWith("org.h2.") -> true
className.startsWith("jakarta.") -> true

else -> false
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ object LincheckClassFileTransformer : ClassFileTransformer {

private val statsTracker: TransformationStatisticsTracker? =
if (collectTransformationStatistics) TransformationStatisticsTracker() else null

val liveDebuggerSettings = LiveDebuggerSettings()

override fun transform(
Expand All @@ -67,18 +67,23 @@ object LincheckClassFileTransformer : ClassFileTransformer {
// this can be related to the Kotlin compiler bug:
// - https://youtrack.jetbrains.com/issue/KT-16727/
if (internalClassName == null) return null
// If the class should not be transformed, return immediately.
if (!shouldTransform(internalClassName.toCanonicalClassName(), instrumentationMode)) {
return null
}
// If lazy mode is used, transform classes lazily,
// once they are used in the testing code.
if (instrumentationStrategy == InstrumentationStrategy.LAZY &&
// do not re-transform already instrumented classes
internalClassName.toCanonicalClassName() !in instrumentedClasses &&
// always transform eagerly instrumented classes
!isEagerlyInstrumentedClass(internalClassName.toCanonicalClassName())
) {
try {
// If the class should not be transformed, return immediately.
if (!shouldTransform(internalClassName.toCanonicalClassName(), instrumentationMode)) {
return null
}
// If lazy mode is used, transform classes lazily,
// once they are used in the testing code.
if (instrumentationStrategy == InstrumentationStrategy.LAZY &&
// do not re-transform already instrumented classes
internalClassName.toCanonicalClassName() !in instrumentedClasses &&
// always transform eagerly instrumented classes
!isEagerlyInstrumentedClass(internalClassName.toCanonicalClassName())
) {
return null
}
} catch (_: ClassCircularityError) {
// Some bootstrap class
return null
}

Expand Down Expand Up @@ -121,7 +126,10 @@ object LincheckClassFileTransformer : ClassFileTransformer {
)

val writer = SafeClassWriter(reader, loader, ClassWriter.COMPUTE_FRAMES)
val visitor = LincheckClassVisitor(writer, classInfo, instrumentationMode, profile, statsTracker, liveDebuggerSettings, LincheckInstrumentation.context)
val context = LincheckInstrumentation.context
val visitor = LincheckClassVisitor(
writer, classInfo, instrumentationMode, profile, statsTracker, liveDebuggerSettings, context
)

try {
val timeNano = measureTimeNano {
Expand Down Expand Up @@ -163,8 +171,8 @@ object LincheckClassFileTransformer : ClassFileTransformer {

private fun getMethodsLocalVariables(
classNode: ClassNode, profile: TransformationProfile,
): Map<String, MethodVariables> {
return classNode.methods.associateBy(
): Map<String, MethodVariables> = classNode.methods
.associateBy(
keySelector = { m -> m.name + m.desc },
valueTransform = { m ->
val config = profile.getMethodConfiguration(classNode.name.toCanonicalClassName(), m.name, m.desc)
Expand All @@ -183,7 +191,6 @@ object LincheckClassFileTransformer : ClassFileTransformer {
}
)
.mapValues { MethodVariables(it.value) }
}

private fun computeLocalKind(name: String, index: Int, methodNode: MethodNode): LocalKind {
val isStatic = (methodNode.access and Opcodes.ACC_STATIC) != 0
Expand All @@ -196,7 +203,9 @@ object LincheckClassFileTransformer : ClassFileTransformer {
}
}

private fun sanitizeVariableName(owner: String, originalName: String, config: TransformationConfiguration, type: Type): String? {
private fun sanitizeVariableName(
owner: String, originalName: String, config: TransformationConfiguration, type: Type
): String? {
fun callRecursive(originalName: String) = sanitizeVariableName(owner, originalName, config, type)

fun callRecursiveForSuffixAfter(prefix: String): String =
Expand All @@ -215,23 +224,26 @@ object LincheckClassFileTransformer : ClassFileTransformer {
} else {
null
}

originalName.startsWith($$"$i$f$") ->
if (config.trackInlineMethodCalls) callRecursiveForSuffixAfter($$"$i$f$") else null

originalName.endsWith($$"$iv") ->
if (config.trackInlineMethodCalls) callRecursiveForPrefixBefore($$"$iv")
else callRecursive(originalName.removeSuffix($$"$iv"))

originalName.contains('-') -> callRecursive(originalName.substringBeforeLast('-'))
originalName.contains("_u24lambda_u24") ->
callRecursive(originalName.replace("_u24lambda_u24", $$"$lambda$"))

else -> originalName
}
}

private fun getMethodsLabels(
classNode: ClassNode
): Map<String, MethodLabels> {
return classNode.methods.associateBy(
): Map<String, MethodLabels> = classNode.methods
.associateBy(
keySelector = { m -> m.name + m.desc },
valueTransform = { m ->
val labels = mutableMapOf<Label, Int>().also { map ->
Expand All @@ -243,7 +255,6 @@ object LincheckClassFileTransformer : ClassFileTransformer {
}
)
.mapValues { MethodLabels(it.value.first, it.value.second) }
}


/**
Expand Down Expand Up @@ -271,6 +282,7 @@ object LincheckClassFileTransformer : ClassFileTransformer {
}

private val NESTED_LAMBDA_RE = Regex($$"^([^$]+)\\$lambda\\$")

/*
* Collect all line numbers of all methods.
* Some line numbers could be beyond source file line count and need to be mapped.
Expand All @@ -286,9 +298,9 @@ object LincheckClassFileTransformer : ClassFileTransformer {
return a.length == b.length
&& a.length > 3
&& (
(a.startsWith("set") && b.startsWith("get"))
|| (a.startsWith("get") && b.startsWith("set"))
)
(a.startsWith("set") && b.startsWith("get"))
|| (a.startsWith("get") && b.startsWith("set"))
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please rollback

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

why?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's unnecessary indentation change.
The original indentation keeps the same indentation for all disjuncts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But it contradicts the official formatting guidelines.

&& a.substring(3) == b.substring(3)
}

Expand Down Expand Up @@ -320,7 +332,7 @@ object LincheckClassFileTransformer : ClassFileTransformer {
allMethods.sortBy { it.third.first() }

// Special case: on-line setter and getter for same name can share this line
for (i in 0 ..< allMethods.size - 1) {
for (i in 0..<allMethods.size - 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please rollback

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

why?

Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO 0<..N is harder to read, and it is a weird exception from other binary operators that are surrounded by spaces (e.g., you don't write 0<N, but 0 < N).

I know it's recommended formatting by Kotlin styleguide, but I found it inconvenient and inconsistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

BTW, it can be converted to

Suggested change
for (i in 0..<allMethods.size - 1) {
for (i in allMethods.indices) {

val (curName, _, curLines) = allMethods[i]
val (nxtName, _, nxtLines) = allMethods[i + 1]
if (isSetterGetterPair(
Expand All @@ -340,7 +352,7 @@ object LincheckClassFileTransformer : ClassFileTransformer {
}
)

val linesToMethodNames = allMethods
val linesToMethodNames = allMethods
.filter { (it.third.firstOrNull() ?: 0) > 0 && (it.third.lastOrNull() ?: 0) > 0 }
.groupBy(
keySelector = { it.third.first() to it.third.last() },
Expand All @@ -350,7 +362,7 @@ object LincheckClassFileTransformer : ClassFileTransformer {
val (k, v) = it
Triple(k.first, k.second, v.toSet())
}
linesToMethodNames.sortedWith { a, b -> a.first.compareTo(b.first) }
linesToMethodNames.sortedWith { a, b -> a.first.compareTo(b.first) }

return methodsToLines to linesToMethodNames
}
Expand Down Expand Up @@ -432,7 +444,7 @@ object LincheckClassFileTransformer : ClassFileTransformer {
isCoroutineConcurrentKtInternalClass(className)

private fun readUTF(classReader: ClassReader, utfOffset: Int, utfLength: Int, buffer: ByteArray): String {
for (offset in 0 ..< utfLength) {
for (offset in 0..<utfLength) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

please rollback

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

why?

Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO 0<..N is harder to read, and it is a weird exception from other binary operators that are surrounded by spaces (e.g., you don't write 0<N, but 0 < N).

I know it's recommended formatting by Kotlin styleguide, but I found it inconvenient and inconsistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When you write from 1 to 10, do you write 1..10 or 1 .. 10?
image

buffer[offset] = (classReader.readByte(offset + utfOffset) and 0xff).toByte()
}
return String(buffer, 0, utfLength, Charsets.UTF_8)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ protected String getCommonSuperClass(final String type1, final String type2) {
try {
// try to fallback to the default implementation
return super.getCommonSuperClass(type1, type2);
} catch (TypeNotPresentException ignored2) {
return "java/lang/Object";
} catch (Exception e) {
throw new RuntimeException(e.toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ object TraceRecorderDefaultTransformationProfile : TransformationProfile {
if (className.startsWith("com.android.tools.")) return false

if (isRecognizedUninstrumentedClass(className)) return false
if (isSpringRelatedClass(className)) return false

return true
}

Expand Down Expand Up @@ -767,6 +769,8 @@ private fun isRecognizedUninstrumentedClass(className: String): Boolean {

if (isRecognizedUninstrumentedThirdPartyLibraryClass(className)) return true

if (isSpringRelatedClass(className)) return true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's exclude spring classes only for Trace Recorder for now.


// All the classes that were not filtered out are eligible for transformation.
return false
}
Expand Down Expand Up @@ -807,6 +811,8 @@ private fun shouldWrapInIgnoredSection(className: String, methodName: String, de
if (isIntellijRuntimeAgentClass(className))
return true

if (isSpringRelatedClass(className))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's exclude spring classes only for Trace Recorder for now.

return true
return false
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Lincheck
*
* Copyright (C) 2019 - 2026 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.lincheck.trace.recorder

import org.jetbrains.lincheck.tracer.TraceOutputMode
import org.jetbrains.lincheck.tracer.Tracer
import org.jetbrains.lincheck.tracer.TracingSession
import org.jetbrains.lincheck.tracer.isFileMode
import org.jetbrains.lincheck.util.Logger
import java.util.concurrent.atomic.AtomicBoolean

/**
* Manages program-scoped trace recording for trace recorder mode.
* Recording starts at program startup and stops at shutdown via a shutdown hook.
*/
internal object ProgramScopedTraceRecorder {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's just port this logic to Tracer.kt instead of adding a new class here.

I believe this logic is now shared and uniform for both Trace Recorder and Live Debugger, so it makes sense to move it there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can you also comment here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the answer is in this comment: #970 (comment)

Let's not start tracing for premain in case if class and method agent arguments were passed, let's keep the old behavior for this case (at least temporarily).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So, should I exclude passing the class and method from the Idea plugin side?


private val shutdownHookInstalled = AtomicBoolean(false)

fun startRecording(mode: TraceOutputMode, traceDumpFilePath: String? = null, packTrace: Boolean = true) {
try {
// Force BinaryFileDump mode for program-scoped recording.
// BinaryFileStream has a race condition where multiple threads can write descriptors
// in the wrong order due to shared ContextSavingState across threads.
val effectiveMode = when (mode) {
is TraceOutputMode.BinaryFileStream -> TraceOutputMode.BinaryFileDump()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's don't change mode silently. Moreover, binary file dump would not scale as it keeps whole trace in-memory.

If there is a but, then please create a YT issue describing the bug and a reproducer.

else -> mode
}
val session = Tracer.startTracing(
outputMode = effectiveMode,
startMode = TracingSession.StartMode.Static,
)
Logger.info { "Program-scoped trace recording has been started" }

if (mode.isFileMode && traceDumpFilePath != null) {
session.installOnFinishHook {
Tracer.dumpTrace(traceDumpFilePath, packTrace)
}
}
} catch (t: Throwable) {
Logger.error(t) { "Cannot start trace recording in trace recorder mode" }
return
}
registerShutdownHook()
}

fun stopRecording() {
try {
Tracer.stopTracing()
} catch (t: Throwable) {
Logger.error(t) { "Cannot stop trace recording in trace recorder mode" }
}
}

private fun registerShutdownHook() {
if (!shutdownHookInstalled.compareAndSet(false, true)) return
try {
Runtime.getRuntime().addShutdownHook(Thread(::stopRecording))
} catch (e: Exception) {
Logger.error(e) { "Failed to register shutdown hook for trace recorder" }
}
}
}
Loading