From f5f9675a73c1e271f11dfdb94081d0e6f31d4945 Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Sat, 28 Feb 2026 04:50:07 +0100 Subject: [PATCH 1/2] Support Spring by starting recording from the program start and finishing with its shutdown. Also, fix several negligible bugs. --- .../jetbrains/lincheck/trace/TraceContext.kt | 7 +- .../jvm/agent/LincheckClassFileTransformer.kt | 68 ++++++++++-------- .../lincheck/jvm/agent/SafeClassWriter.java | 2 + .../recorder/ProgramScopedTraceRecorder.kt | 71 +++++++++++++++++++ .../trace/recorder/TraceRecorderAgent.kt | 18 +++-- .../lincheck/trace/SerializationPack.kt | 3 +- .../jetbrains/lincheck/tracer/TracerAgent.kt | 12 ++++ 7 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 trace-recorder/src/main/org/jetbrains/lincheck/trace/recorder/ProgramScopedTraceRecorder.kt diff --git a/common/src/main/org/jetbrains/lincheck/trace/TraceContext.kt b/common/src/main/org/jetbrains/lincheck/trace/TraceContext.kt index 59b8780bbd..fe89380399 100644 --- a/common/src/main/org/jetbrains/lincheck/trace/TraceContext.kt +++ b/common/src/main/org/jetbrains/lincheck/trace/TraceContext.kt @@ -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' @@ -40,8 +41,12 @@ class TraceContext { val methodPool = DescriptorPool() val fieldPool = DescriptorPool() val variablePool = DescriptorPool() + private val anonymousThreadCounter = AtomicInteger(0) - 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] ?: "" diff --git a/jvm-agent/src/main/org/jetbrains/lincheck/jvm/agent/LincheckClassFileTransformer.kt b/jvm-agent/src/main/org/jetbrains/lincheck/jvm/agent/LincheckClassFileTransformer.kt index a9695d3433..e895687125 100644 --- a/jvm-agent/src/main/org/jetbrains/lincheck/jvm/agent/LincheckClassFileTransformer.kt +++ b/jvm-agent/src/main/org/jetbrains/lincheck/jvm/agent/LincheckClassFileTransformer.kt @@ -48,7 +48,7 @@ object LincheckClassFileTransformer : ClassFileTransformer { private val statsTracker: TransformationStatisticsTracker? = if (collectTransformationStatistics) TransformationStatisticsTracker() else null - + val liveDebuggerSettings = LiveDebuggerSettings() override fun transform( @@ -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 } @@ -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 { @@ -163,8 +171,8 @@ object LincheckClassFileTransformer : ClassFileTransformer { private fun getMethodsLocalVariables( classNode: ClassNode, profile: TransformationProfile, - ): Map { - return classNode.methods.associateBy( + ): Map = classNode.methods + .associateBy( keySelector = { m -> m.name + m.desc }, valueTransform = { m -> val config = profile.getMethodConfiguration(classNode.name.toCanonicalClassName(), m.name, m.desc) @@ -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 @@ -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 = @@ -215,8 +224,10 @@ 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")) @@ -224,14 +235,15 @@ object LincheckClassFileTransformer : ClassFileTransformer { originalName.contains('-') -> callRecursive(originalName.substringBeforeLast('-')) originalName.contains("_u24lambda_u24") -> callRecursive(originalName.replace("_u24lambda_u24", $$"$lambda$")) + else -> originalName } } private fun getMethodsLabels( classNode: ClassNode - ): Map { - return classNode.methods.associateBy( + ): Map = classNode.methods + .associateBy( keySelector = { m -> m.name + m.desc }, valueTransform = { m -> val labels = mutableMapOf().also { map -> @@ -243,7 +255,6 @@ object LincheckClassFileTransformer : ClassFileTransformer { } ) .mapValues { MethodLabels(it.value.first, it.value.second) } - } /** @@ -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. @@ -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")) + ) && a.substring(3) == b.substring(3) } @@ -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.. 0 && (it.third.lastOrNull() ?: 0) > 0 } .groupBy( keySelector = { it.third.first() to it.third.last() }, @@ -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 } @@ -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.. TraceOutputMode.BinaryFileDump() + 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" } + } + } +} diff --git a/trace-recorder/src/main/org/jetbrains/lincheck/trace/recorder/TraceRecorderAgent.kt b/trace-recorder/src/main/org/jetbrains/lincheck/trace/recorder/TraceRecorderAgent.kt index 724dcc0598..a2ef939f1e 100644 --- a/trace-recorder/src/main/org/jetbrains/lincheck/trace/recorder/TraceRecorderAgent.kt +++ b/trace-recorder/src/main/org/jetbrains/lincheck/trace/recorder/TraceRecorderAgent.kt @@ -22,6 +22,7 @@ import org.jetbrains.lincheck.jvm.agent.TraceAgentParameters.ARGUMENT_JMX_MBEAN import org.jetbrains.lincheck.jvm.agent.TraceAgentParameters.ARGUMENT_PACK import org.jetbrains.lincheck.jvm.agent.TracingEntryPointMethodVisitorProvider import org.jetbrains.lincheck.trace.jmx.TracingJmxRegistrator +import org.jetbrains.lincheck.tracer.TraceOutputMode import org.jetbrains.lincheck.tracer.TracerAgent import org.jetbrains.lincheck.tracer.jmx.AbstractTracingJmxController import org.jetbrains.lincheck.util.TRACE_RECORDER_MODE_PROPERTY @@ -56,9 +57,18 @@ internal object TraceRecorderAgent { override fun validateArguments(attachType: JavaAgentAttachType) { TraceAgentParameters.validateMode() + } - if (attachType == JavaAgentAttachType.STATIC) { - TraceAgentParameters.validateClassAndMethodArgumentsAreProvided() + override fun afterInstrumentationInstalled(attachType: JavaAgentAttachType) { + // start recording at program start if trace dump file path was provided + if (TraceAgentParameters.traceDumpFilePath != null) { + val mode = TraceOutputMode.parse( + outputMode = TraceAgentParameters.getArg(ARGUMENT_FORMAT), + outputOption = TraceAgentParameters.getArg(ARGUMENT_FOPTION), + outputFilePath = TraceAgentParameters.traceDumpFilePath, + ) + val packTrace = (TraceAgentParameters.getArg(ARGUMENT_PACK) ?: "true").toBoolean() + ProgramScopedTraceRecorder.startRecording(mode, TraceAgentParameters.traceDumpFilePath, packTrace) } } @@ -69,8 +79,8 @@ internal object TraceRecorderAgent { override fun onStreamingDisconnect() {} } - override val tracingEntryPointMethodVisitorProvider: TracingEntryPointMethodVisitorProvider - get() = ::TraceRecorderMethodTransformer + override val tracingEntryPointMethodVisitorProvider: TracingEntryPointMethodVisitorProvider? + get() = null } // entry point for a statically attached java agent diff --git a/trace/src/main/org/jetbrains/lincheck/trace/SerializationPack.kt b/trace/src/main/org/jetbrains/lincheck/trace/SerializationPack.kt index 61d3e6b44f..f19706a15f 100644 --- a/trace/src/main/org/jetbrains/lincheck/trace/SerializationPack.kt +++ b/trace/src/main/org/jetbrains/lincheck/trace/SerializationPack.kt @@ -264,7 +264,8 @@ internal class TraceDataProvider(val traceFileName: String) : AutoCloseable { } } - val dataFileName: String = tmpDataFile?.absolutePath ?: traceFileName + val dataFileName: String + get() = tmpDataFile?.absolutePath ?: traceFileName override fun close() { idMapDataInput?.close() diff --git a/tracer/src/main/org/jetbrains/lincheck/tracer/TracerAgent.kt b/tracer/src/main/org/jetbrains/lincheck/tracer/TracerAgent.kt index 7953052b71..bcddb21b44 100644 --- a/tracer/src/main/org/jetbrains/lincheck/tracer/TracerAgent.kt +++ b/tracer/src/main/org/jetbrains/lincheck/tracer/TracerAgent.kt @@ -47,6 +47,9 @@ abstract class TracerAgent { // install instrumentation installInstrumentation() + + // call post-installation hook + afterInstrumentationInstalled(JavaAgentAttachType.STATIC) } // entry point for a dynamically attached java agent @@ -65,6 +68,9 @@ abstract class TracerAgent { // install instrumentation and re-transform already loaded classes installInstrumentation() + + // call post-installation hook + afterInstrumentationInstalled(JavaAgentAttachType.DYNAMIC) } protected abstract val modeSystemPropertyName: String @@ -105,4 +111,10 @@ abstract class TracerAgent { private fun installInstrumentation() { LincheckInstrumentation.install(instrumentationMode) } + + /** + * Hook called after instrumentation is installed. + * Can be used for post-initialization tasks like starting program-scoped recording. + */ + protected open fun afterInstrumentationInstalled(attachType: JavaAgentAttachType) {} } \ No newline at end of file From 068ea3239539a1495362202f30975975cd2e40a2 Mon Sep 17 00:00:00 2001 From: "Evgeniy.Zhelenskiy" Date: Sat, 28 Feb 2026 20:21:06 +0100 Subject: [PATCH 2/2] Filter out Spring (in progress) --- .../lincheck/util/AnalysisSections.kt | 5 ++- .../main/org/jetbrains/lincheck/util/Libs.kt | 37 ++++++++++++++++++- .../jvm/agent/TransformationProfile.kt | 6 +++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/common/src/main/org/jetbrains/lincheck/util/AnalysisSections.kt b/common/src/main/org/jetbrains/lincheck/util/AnalysisSections.kt index e6448f3138..f1df252c7c 100644 --- a/common/src/main/org/jetbrains/lincheck/util/AnalysisSections.kt +++ b/common/src/main/org/jetbrains/lincheck/util/AnalysisSections.kt @@ -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 } @@ -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 { diff --git a/common/src/main/org/jetbrains/lincheck/util/Libs.kt b/common/src/main/org/jetbrains/lincheck/util/Libs.kt index 79544980b2..898e6ad2b0 100644 --- a/common/src/main/org/jetbrains/lincheck/util/Libs.kt +++ b/common/src/main/org/jetbrains/lincheck/util/Libs.kt @@ -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") \ No newline at end of file + isMethodHandleRelatedClass(className) && !methodName.contains("invoke") + + +fun isSpringRelatedClass(className: String) = when { + className.startsWith("sun.rmi.") -> true + className.startsWith("javax.") -> true + + 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 + 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 +} diff --git a/jvm-agent/src/main/org/jetbrains/lincheck/jvm/agent/TransformationProfile.kt b/jvm-agent/src/main/org/jetbrains/lincheck/jvm/agent/TransformationProfile.kt index 1b4408aa10..b94a552455 100644 --- a/jvm-agent/src/main/org/jetbrains/lincheck/jvm/agent/TransformationProfile.kt +++ b/jvm-agent/src/main/org/jetbrains/lincheck/jvm/agent/TransformationProfile.kt @@ -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 } @@ -767,6 +769,8 @@ private fun isRecognizedUninstrumentedClass(className: String): Boolean { if (isRecognizedUninstrumentedThirdPartyLibraryClass(className)) return true + if (isSpringRelatedClass(className)) return true + // All the classes that were not filtered out are eligible for transformation. return false } @@ -807,6 +811,8 @@ private fun shouldWrapInIgnoredSection(className: String, methodName: String, de if (isIntellijRuntimeAgentClass(className)) return true + if (isSpringRelatedClass(className)) + return true return false }