Keep $continuation in LVT

in case of suspend functions and `this` is case of lambdas, since they
are used in async stack trace generation.
Update tests.
 #KT-40661 Open
This commit is contained in:
Ilmir Usmanov
2020-07-28 12:49:38 +02:00
parent db40808186
commit 4fb5f8603a
23 changed files with 128 additions and 104 deletions
@@ -132,7 +132,7 @@ class CoroutineTransformerMethodVisitor(
UninitializedStoresProcessor(methodNode, shouldPreserveClassInitialization).run()
updateLvtAccordingToLiveness(methodNode)
updateLvtAccordingToLiveness(methodNode, isForNamedFunction)
val spilledToVariableMapping = spillVariables(suspensionPoints, methodNode)
@@ -1000,3 +1000,73 @@ internal fun replaceFakeContinuationsWithRealOnes(methodNode: MethodNode, contin
methodNode.instructions.set(fakeContinuation, VarInsnNode(Opcodes.ALOAD, continuationIndex))
}
}
/* We do not want to spill dead variables, thus, we shrink its LVT record to region, where the variable is alive,
* so, the variable will not be visible in debugger. User can still prolong life span of the variable by using it.
*
* This means, that function parameters do not longer span the whole function, including `this`.
* This might and will break some bytecode processors, including old versions of R8. See KT-24510.
*/
private fun updateLvtAccordingToLiveness(method: MethodNode, isForNamedFunction: Boolean) {
val liveness = analyzeLiveness(method)
fun List<LocalVariableNode>.findRecord(insnIndex: Int, variableIndex: Int): LocalVariableNode? {
for (variable in this) {
if (variable.index == variableIndex &&
method.instructions.indexOf(variable.start) <= insnIndex &&
insnIndex < method.instructions.indexOf(variable.end)
) return variable
}
return null
}
fun isAlive(insnIndex: Int, variableIndex: Int): Boolean =
liveness[insnIndex].isAlive(variableIndex)
val oldLvt = arrayListOf<LocalVariableNode>()
for (record in method.localVariables) {
oldLvt += record
}
method.localVariables.clear()
// Skip `this` for suspend lamdba
val start = if (isForNamedFunction) 0 else 1
for (variableIndex in start until method.maxLocals) {
if (oldLvt.none { it.index == variableIndex }) continue
var startLabel: LabelNode? = null
for (insnIndex in 0 until (method.instructions.size() - 1)) {
val insn = method.instructions[insnIndex]
if (!isAlive(insnIndex, variableIndex) && isAlive(insnIndex + 1, variableIndex)) {
startLabel = insn as? LabelNode ?: insn.findNextOrNull { it is LabelNode } as? LabelNode
}
if (isAlive(insnIndex, variableIndex) && !isAlive(insnIndex + 1, variableIndex)) {
// No variable in LVT -> do not add one
val lvtRecord = oldLvt.findRecord(insnIndex, variableIndex) ?: continue
if (lvtRecord.name == CONTINUATION_VARIABLE_NAME) continue
val endLabel = insn as? LabelNode ?: insn.findNextOrNull { it is LabelNode } as? LabelNode ?: continue
// startLabel can be null in case of parameters
@Suppress("NAME_SHADOWING") val startLabel = startLabel ?: lvtRecord.start
// No LINENUMBER in range -> no way to put a breakpoint -> do not bother adding a record
if (InsnSequence(startLabel, endLabel).none { it is LineNumberNode }) continue
method.localVariables.add(
LocalVariableNode(lvtRecord.name, lvtRecord.desc, lvtRecord.signature, startLabel, endLabel, lvtRecord.index)
)
}
}
}
for (variable in oldLvt) {
// $completion, $continuation and $result are dead, but they are used by debugger, as well as fake inliner variables
// For example, $continuation is used to create async stack trace
if (variable.name == SUSPEND_FUNCTION_COMPLETION_PARAMETER_NAME ||
variable.name == CONTINUATION_VARIABLE_NAME ||
variable.name == SUSPEND_CALL_RESULT_NAME ||
isFakeLocalVariableForInline(variable.name)
) {
method.localVariables.add(variable)
}
// this acts like $continuation for lambdas. For example, it is used by debugger to create async stack trace. Keep it.
if (variable.name == "this" && !isForNamedFunction) {
method.localVariables.add(variable)
}
}
}
@@ -5,10 +5,10 @@
package org.jetbrains.kotlin.codegen.optimization.common
import org.jetbrains.kotlin.codegen.coroutines.SUSPEND_CALL_RESULT_NAME
import org.jetbrains.kotlin.codegen.coroutines.SUSPEND_FUNCTION_COMPLETION_PARAMETER_NAME
import org.jetbrains.kotlin.codegen.inline.isFakeLocalVariableForInline
import org.jetbrains.org.objectweb.asm.tree.*
import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode
import org.jetbrains.org.objectweb.asm.tree.IincInsnNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import org.jetbrains.org.objectweb.asm.tree.VarInsnNode
import java.util.*
@@ -59,65 +59,4 @@ private fun useVar(frame: VariableLivenessFrame, insn: AbstractInsnNode) {
} else if (insn is IincInsnNode) {
frame.markAlive(insn.`var`)
}
}
/* We do not want to spill dead variables, thus, we shrink its LVT record to region, where the variable is alive,
* so, the variable will not be visible in debugger. User can still prolong life span of the variable by using it.
*
* This means, that function parameters do not longer span the whole function, including `this`.
* This might and will break some bytecode processors, including old versions of R8. See KT-24510.
*/
fun updateLvtAccordingToLiveness(method: MethodNode) {
val liveness = analyzeLiveness(method)
fun List<LocalVariableNode>.findRecord(insnIndex: Int, variableIndex: Int): LocalVariableNode? {
for (variable in this) {
if (variable.index == variableIndex &&
method.instructions.indexOf(variable.start) <= insnIndex &&
insnIndex < method.instructions.indexOf(variable.end)
) return variable
}
return null
}
fun isAlive(insnIndex: Int, variableIndex: Int): Boolean =
liveness[insnIndex].isAlive(variableIndex)
val oldLvt = arrayListOf<LocalVariableNode>()
for (record in method.localVariables) {
oldLvt += record
}
method.localVariables.clear()
for (variableIndex in 0 until method.maxLocals) {
if (oldLvt.none { it.index == variableIndex }) continue
var startLabel: LabelNode? = null
for (insnIndex in 0 until (method.instructions.size() - 1)) {
val insn = method.instructions[insnIndex]
if (!isAlive(insnIndex, variableIndex) && isAlive(insnIndex + 1, variableIndex)) {
startLabel = insn as? LabelNode ?: insn.findNextOrNull { it is LabelNode } as? LabelNode
}
if (isAlive(insnIndex, variableIndex) && !isAlive(insnIndex + 1, variableIndex)) {
// No variable in LVT -> do not add one
val lvtRecord = oldLvt.findRecord(insnIndex, variableIndex) ?: continue
val endLabel = insn as? LabelNode ?: insn.findNextOrNull { it is LabelNode } as? LabelNode ?: continue
// startLabel can be null in case of parameters
@Suppress("NAME_SHADOWING") val startLabel = startLabel ?: lvtRecord.start
// No LINENUMBER in range -> no way to put a breakpoint -> do not bother adding a record
if (InsnSequence(startLabel, endLabel).none { it is LineNumberNode }) continue
method.localVariables.add(
LocalVariableNode(lvtRecord.name, lvtRecord.desc, lvtRecord.signature, startLabel, endLabel, lvtRecord.index)
)
}
}
}
for (variable in oldLvt) {
// $completion and $result are dead, but they are used by debugger, as well as fake inliner variables
if (variable.name == SUSPEND_FUNCTION_COMPLETION_PARAMETER_NAME ||
variable.name == SUSPEND_CALL_RESULT_NAME ||
isFakeLocalVariableForInline(variable.name)
) {
method.localVariables.add(variable)
}
}
}
}
@@ -5,6 +5,7 @@ class A {
suspend fun foo1(l: Long) {
foo()
foo()
val dead = l
}
}
@@ -12,5 +13,5 @@ class A {
// VARIABLE : NAME=this TYPE=LA; INDEX=0
// VARIABLE : NAME=l TYPE=J INDEX=1
// VARIABLE : NAME=$completion TYPE=Lkotlin/coroutines/Continuation; INDEX=3
// VARIABLE : NAME=$continuation TYPE=Lkotlin/coroutines/Continuation; INDEX=5
// VARIABLE : NAME=$result TYPE=Ljava/lang/Object; INDEX=4
// VARIABLE : NAME=$continuation TYPE=Lkotlin/coroutines/Continuation; INDEX=7
// VARIABLE : NAME=$result TYPE=Ljava/lang/Object; INDEX=6
@@ -4,10 +4,11 @@ suspend fun foo() {}
suspend fun foo1(l: Long) {
foo()
foo()
val dead = l
}
// METHOD : StaticStateMachineKt.foo1(JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
// VARIABLE : NAME=l TYPE=J INDEX=0
// VARIABLE : NAME=$completion TYPE=Lkotlin/coroutines/Continuation; INDEX=2
// VARIABLE : NAME=$continuation TYPE=Lkotlin/coroutines/Continuation; INDEX=4
// VARIABLE : NAME=$result TYPE=Ljava/lang/Object; INDEX=3
// VARIABLE : NAME=$continuation TYPE=Lkotlin/coroutines/Continuation; INDEX=6
// VARIABLE : NAME=$result TYPE=Ljava/lang/Object; INDEX=5
@@ -6,11 +6,12 @@ suspend fun A.foo() {}
suspend fun A.foo1(l: Long) {
foo()
foo()
val dead = l
}
// METHOD : StaticStateMachineReceiverKt.foo1(LA;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
// VARIABLE : NAME=$this$foo1 TYPE=LA; INDEX=0
// VARIABLE : NAME=l TYPE=J INDEX=1
// VARIABLE : NAME=$completion TYPE=Lkotlin/coroutines/Continuation; INDEX=3
// VARIABLE : NAME=$continuation TYPE=Lkotlin/coroutines/Continuation; INDEX=5
// VARIABLE : NAME=$result TYPE=Ljava/lang/Object; INDEX=4
// VARIABLE : NAME=$continuation TYPE=Lkotlin/coroutines/Continuation; INDEX=7
// VARIABLE : NAME=$result TYPE=Ljava/lang/Object; INDEX=6
@@ -18,7 +18,6 @@ suspend fun foo(data: Data, body: suspend (Data) -> Unit) {
// METHOD : DataClassKt$test$2.invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
// JVM_TEMPLATES
// VARIABLE : NAME=$dstr$x_param$y_param TYPE=LData; INDEX=2
// VARIABLE : NAME=x_param TYPE=Ljava/lang/String; INDEX=3
// VARIABLE : NAME=y_param TYPE=I INDEX=4
// VARIABLE : NAME=this TYPE=LDataClassKt$test$2; INDEX=0
@@ -12,7 +12,9 @@ object B {
suspend fun B.bar(): String {
operator fun <R> A<R>.component3() = z
return foo(A("O", "K", 123)) { (x_param, y_param, z_param) -> x_param + y_param + z_param.toString() }
return foo(A("O", "K", 123)) { (x_param, y_param, z_param) ->
x_param + y_param + z_param.toString()
}
}
suspend fun test() = B.bar()
@@ -26,7 +28,6 @@ suspend fun test() = B.bar()
// JVM_TEMPLATES
// METHOD : ExtensionComponentsKt$bar$3.invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
// VARIABLE : NAME=$dstr$x_param$y_param$z_param TYPE=LA; INDEX=2
// VARIABLE : NAME=x_param TYPE=Ljava/lang/String; INDEX=3
// VARIABLE : NAME=y_param TYPE=Ljava/lang/String; INDEX=4
// VARIABLE : NAME=z_param TYPE=I INDEX=5
@@ -4,7 +4,9 @@ data class A<T, F>(val x: T, val y: F)
suspend fun <X, Y> foo(a: A<X, Y>, block: suspend (A<X, Y>) -> String) = block(a)
suspend fun test() = foo(A("OK", 1)) { (x_param, y_param) -> x_param + (y_param.toString()) }
suspend fun test() = foo(A("OK", 1)) { (x_param, y_param) ->
x_param + (y_param.toString())
}
// Parameters (including anonymous destructuring parameters) are moved to fields in the Continuation class for the suspend lambda class.
// However, in non-IR, the fields are first stored in local variables, and they are not read directly (even for destructuring components).
@@ -13,7 +15,6 @@ suspend fun test() = foo(A("OK", 1)) { (x_param, y_param) -> x_param + (y_param.
// METHOD : GenericKt$test$2.invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
// JVM_TEMPLATES
// VARIABLE : NAME=$dstr$x_param$y_param TYPE=LA; INDEX=2
// VARIABLE : NAME=x_param TYPE=Ljava/lang/String; INDEX=3
// VARIABLE : NAME=y_param TYPE=I INDEX=4
// VARIABLE : NAME=this TYPE=LGenericKt$test$2; INDEX=0
@@ -3,7 +3,9 @@ data class A(val x: String, val y: String)
suspend fun foo(a: A, block: suspend (Int, A, String) -> String): String = block(1, a, "#")
suspend fun test() = foo(A("O", "K")) { i_param, (x_param, y_param), v_param -> i_param.toString() + x_param + y_param + v_param }
suspend fun test() = foo(A("O", "K")) { i_param, (x_param, y_param), v_param ->
i_param.toString() + x_param + y_param + v_param
}
// Parameters (including anonymous destructuring parameters) are moved to fields in the Continuation class for the suspend lambda class.
// However, in non-IR, the fields are first stored in local variables, and they are not read directly (even for destructuring components).
@@ -13,7 +15,6 @@ suspend fun test() = foo(A("O", "K")) { i_param, (x_param, y_param), v_param ->
// JVM_TEMPLATES
// VARIABLE : NAME=i_param TYPE=I INDEX=2
// VARIABLE : NAME=$dstr$x_param$y_param TYPE=LA; INDEX=3
// VARIABLE : NAME=v_param TYPE=Ljava/lang/String; INDEX=4
// VARIABLE : NAME=x_param TYPE=Ljava/lang/String; INDEX=5
// VARIABLE : NAME=y_param TYPE=Ljava/lang/String; INDEX=6
@@ -20,7 +20,6 @@ suspend fun foo(data: Data, body: suspend Long.(String, Data, Int) -> Unit) {
// JVM_TEMPLATES
// VARIABLE : NAME=$this$foo TYPE=J INDEX=2
// VARIABLE : NAME=str TYPE=Ljava/lang/String; INDEX=4
// VARIABLE : NAME=$dstr$x$_u24__u24$z TYPE=LData; INDEX=5
// VARIABLE : NAME=i TYPE=I INDEX=6
// VARIABLE : NAME=x TYPE=Ljava/lang/String; INDEX=7
// VARIABLE : NAME=z TYPE=I INDEX=8
@@ -7,7 +7,9 @@ class A {
suspend fun foo(a: A, block: suspend (A) -> String): String = block(a)
suspend fun test() = foo(A()) { (x_param, _, y_param) -> x_param + y_param }
suspend fun test() = foo(A()) { (x_param, _, y_param) ->
x_param + y_param
}
// Parameters (including anonymous destructuring parameters) are moved to fields in the Continuation class for the suspend lambda class.
// However, in non-IR, the fields are first stored in local variables, and they are not read directly (even for destructuring components).
@@ -16,7 +18,6 @@ suspend fun test() = foo(A()) { (x_param, _, y_param) -> x_param + y_param }
// METHOD : UnderscoreNamesKt$test$2.invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
// JVM_TEMPLATES
// VARIABLE : NAME=$dstr$x_param$_u24__u24$y_param TYPE=LA; INDEX=2
// VARIABLE : NAME=x_param TYPE=Ljava/lang/String; INDEX=3
// VARIABLE : NAME=y_param TYPE=Ljava/lang/String; INDEX=4
// VARIABLE : NAME=this TYPE=LUnderscoreNamesKt$test$2; INDEX=0
@@ -22,6 +22,7 @@ fun box(): String {
try {
var i: String = "abc"
i = "123"
println(i)
} finally { }
// This variable should take the same slot as 'i' had
@@ -46,4 +47,5 @@ fun box(): String {
// 0 PUTFIELD VarValueConflictsWithTableSameSort_1_2Kt\$box\$1.L\$0 : Ljava/lang/Object;
/* 1 load in try/finally */
/* 1 load in result = s */
// 2 ALOAD 3
/* 1 load in println */
// 3 ALOAD 3
@@ -5,12 +5,15 @@ suspend fun main() {
val b = 7
none()
foo()
val dead1 = a
val dead2 = b
}
suspend fun foo() {
val x = "foo"
none()
bar()
val dead = x
}
suspend fun bar() {
@@ -19,6 +22,7 @@ suspend fun bar() {
val z = "bar"
//Breakpoint!
val a = 5
val dead = y
}
suspend fun none() {}
@@ -1,11 +1,11 @@
LineBreakpoint created at asyncFunctions.kt:21
LineBreakpoint created at asyncFunctions.kt:24
Run Java
Connected to the target VM
asyncFunctions.kt:21
asyncFunctions.kt:24
Async stack trace:
asyncFunctions.AsyncFunctionsKt:18
asyncFunctions.AsyncFunctionsKt:21
y = "zoo"
asyncFunctions.AsyncFunctionsKt:13
asyncFunctions.AsyncFunctionsKt:15
x = "foo"
asyncFunctions.AsyncFunctionsKt:7
a = 5
@@ -19,7 +19,7 @@ suspend fun test1(i: Int): Int {
suspend fun a(aParam: String) {
val a = "a"
b(a)
a + 1
a + aParam
}
suspend fun b(bParam: String) {
@@ -9,7 +9,7 @@ fun main() {
runBlocking {
val rootCoroutineVal = mainTestVal
TestSuspendA().a()
print("")
print(rootCoroutineVal)
}
}
@@ -18,7 +18,7 @@ class TestSuspendA {
suspend fun a() {
val methodVal = ""
InClassB().b()
print("")
print(methodVal)
}
class InClassB {
@@ -27,7 +27,7 @@ class TestSuspendA {
suspend fun b() {
val inClassBMethodVal = ""
InClassC().c()
print("")
print(inClassBMethodVal)
}
inner class InClassC {
@@ -37,7 +37,7 @@ class TestSuspendA {
yield()
val inClassCMethodVal = ""
//Breakpoint!
print("")
print(inClassCMethodVal)
}
}
}
@@ -4,12 +4,12 @@ Connected to the target VM
suspendFunWithInner.kt:40
Thread stack trace:
CoroutinePreflightFrame c:!LINE_NUMBER!, TestSuspendA$InClassB$InClassC (continuation)
($completion, $continuation, $result, inClassCMethodVal, this)
($completion, $continuation, $result, inClassCMethodVal)
CoroutineInfo: 1 coroutine RUNNING
CoroutineStackFrame b:!LINE_NUMBER!, TestSuspendA$InClassB (continuation)
(inClassBMethodVal, this)
(inClassBMethodVal)
CoroutineStackFrame a:!LINE_NUMBER!, TestSuspendA (continuation)
(methodVal, this)
(methodVal)
CoroutineStackFrame invokeSuspend:!LINE_NUMBER!, SuspendFunWithInnerKt$main$1 (continuation)
(rootCoroutineVal)
CoroutineStackFrame resumeWith:!LINE_NUMBER!, BaseContinuationImpl (kotlin.coroutines.jvm.internal)
@@ -4,7 +4,7 @@ suspend fun main() {
val a = 5
foo()
//Breakpoint!
val b = 4
val b = a
}
suspend fun foo() {}
@@ -14,16 +14,18 @@ fun main() {
suspend fun a() {
val a = "a"
b(a)
val aLate = "a" // to prevent stackFrame to collapse
val aLate = a // to prevent stackFrame to collapse
}
suspend fun b(paramA: String) {
yield()
val b = "b"
c(b)
val dead = paramA
}
suspend fun c(paramB: String) {
val c = "c"
//Breakpoint!
val dead = paramB
}
@@ -1,7 +1,7 @@
LineBreakpoint created at coroutineSuspendFun.kt:29
LineBreakpoint created at coroutineSuspendFun.kt:30
Run Java
Connected to the target VM
coroutineSuspendFun.kt:29
coroutineSuspendFun.kt:30
Thread stack trace:
KotlinStackFrame c:!LINE_NUMBER!, CoroutineSuspendFunKt (continuation)
($completion, c, paramB)
@@ -14,16 +14,18 @@ fun main() {
suspend fun a() {
val a = "a"
b(a)
val aLate = "a" // to prevent stackFrame to collapse
val aLate = a // to prevent stackFrame to collapse
}
suspend fun b(paramA: String) {
yield()
val b = "b"
c(b)
val dead = paramA
}
suspend fun c(paramB: String) {
val c = "c"
//Breakpoint!
val dead = paramB
}
@@ -1,7 +1,7 @@
LineBreakpoint created at coroutineSuspendFun136.kt:29
LineBreakpoint created at coroutineSuspendFun136.kt:30
Run Java
Connected to the target VM
coroutineSuspendFun136.kt:29
coroutineSuspendFun136.kt:30
Thread stack trace:
KotlinStackFrame c:!LINE_NUMBER!, CoroutineSuspendFun136Kt (continuation)
($completion, c, paramB)
@@ -4,7 +4,7 @@ Connected to the target VM
suspendMain.kt:15
Thread stack trace:
CoroutinePreflightFrame invokeSuspend:!LINE_NUMBER!, SuspendMainKt$main$2 (continuation)
($result, $this$coroutineScope, this)
($result, this)
CoroutineInfo: -1 coroutine UNKNOWN
CoroutineStackFrame startUndispatchedOrReturn:!LINE_NUMBER!, UndispatchedKt (kotlinx.coroutines.intrinsics)
($i$a$-undispatchedResult-UndispatchedKt$startUndispatchedOrReturn$2, $i$f$undispatchedResult, $this$startUndispatchedOrReturn, $this$undispatchedResult$iv, block, receiver)