-
Notifications
You must be signed in to change notification settings - Fork 41
Support Spring framework integration #970
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These 2 are not spring-related.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move these to |
||
| 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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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<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) | ||||||
|
|
@@ -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,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 -> | ||||||
|
|
@@ -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")) | ||||||
| ) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rollback
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's unnecessary indentation change.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -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) { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rollback
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO I know it's recommended formatting by Kotlin styleguide, but I found it inconvenient and inconsistent.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW, it can be converted to
Suggested change
|
||||||
| val (curName, _, curLines) = allMethods[i] | ||||||
| val (nxtName, _, nxtLines) = allMethods[i + 1] | ||||||
| if (isSetterGetterPair( | ||||||
|
|
@@ -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() }, | ||||||
|
|
@@ -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..<utfLength) { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please rollback
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO I know it's recommended formatting by Kotlin styleguide, but I found it inconvenient and inconsistent.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| buffer[offset] = (classReader.readByte(offset + utfOffset) and 0xff).toByte() | ||||||
| } | ||||||
| return String(buffer, 0, utfLength, Charsets.UTF_8) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
|
|
@@ -807,6 +811,8 @@ private fun shouldWrapInIgnoredSection(className: String, methodName: String, de | |
| if (isIntellijRuntimeAgentClass(className)) | ||
| return true | ||
|
|
||
| if (isSpringRelatedClass(className)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
|
|
||
|
|
||
| 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 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's just port this logic to I believe this logic is now shared and uniform for both Trace Recorder and Live Debugger, so it makes sense to move it there.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also comment here?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" } | ||
| } | ||
| } | ||
| } | ||

There was a problem hiding this comment.
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".There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.