From 39a3eaf7f0e624e6087c7fb3f3a720c2e0d120b5 Mon Sep 17 00:00:00 2001 From: Denis Berestinskii Date: Wed, 12 Aug 2026 14:56:30 +0300 Subject: [PATCH] implement lazy caching for continuations --- CLAUDE.md | 6 +- .../main/kotlin/internal/classTransformer.kt | 162 +++++++++++++++--- common/src/main/kotlin/internal/awakener.kt | 19 +- common/src/main/kotlin/internal/di-common.kt | 9 + .../src/main/kotlin/internal/provider-impl.kt | 50 +++++- .../src/main/kotlin/internal/utils-common.kt | 2 +- .../src/main/kotlin/groovy-dsl-initializer.kt | 3 +- provider/src/main/kotlin/internal/provider.kt | 17 +- provider/src/main/kotlin/provider-api.kt | 46 ++++- .../src/main/kotlin/runtime-settings.kt | 3 + .../main/kotlin/internal/specMethodBuilder.kt | 3 +- 11 files changed, 254 insertions(+), 66 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 45c80d08..4a6dcd84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,7 +121,11 @@ Custom `_plugins/bytecode-processor` Gradle plugin applies compile-time transfor **`SpecCache.specMethod` benign race**: Written without synchronization (two threads may both compute and write the same handle), which is benign since MethodHandle instances are immutable and the computation is pure. -**`ManualContinuation.$decoroutinator$cacheField` is nullable at runtime**: The bytecode transformer (`tryAddManualContinuation`) only runs the `PUTSTATIC` for `$decoroutinator$cacheField` in `` when `fillUnknownElementsWithClassName()` is true at class-load time. When false, the JVM reference field stays null (default). The interface property is declared `SpecCache?` and the getter uses `?: return null` to handle this. Any code path that reads `$decoroutinator$cacheField` must handle null even though the Kotlin type looks non-nullable from the source. +**`ManualContinuation.$decoroutinator$cacheField` can be JVM-null despite its non-null Kotlin type**: The bytecode transformer (`tryAddManualContinuation`) only runs the `PUTSTATIC` for the (`static`) `$decoroutinator$cacheField` in `` when `fillUnknownElementsWithClassName` is `true` at class-load time; when false, the raw JVM field stays at its default `null`, even though the interface declares it `val $decoroutinator$cacheField: SpecCache` (non-null). This is only sound because `ManualContinuation.$decoroutinator$cache`'s getter checks `provider.fillUnknownElementsWithClassName` itself and returns `provider.nullElementSpecCache` *before* ever touching `$decoroutinator$cacheField` — since that flag is a process-lifetime-constant `val`, the ``-time gate and the getter's runtime gate always agree. Never read `$decoroutinator$cacheField` directly without first replicating that same flag check. + +**`LazilyCachedContinuation` vs `ManualContinuation`**: Both bytecode-inject a `ContinuationCached` implementation into external (non-project) classes via `class-transformer/.../classTransformer.kt`, but for different situations. `ManualContinuation` (`tryAddManualContinuation`, whitelist `manualContinuationsInternalClassNames`, e.g. `kotlinx.coroutines.internal.ScopeCoroutine`) is for classes whose real `getStackTraceElement()` is class-wide constant (e.g. always `null`) — it eagerly bakes one synthetic `SpecCache` into a *static* field at ``. `LazilyCachedContinuation` (`tryAddLazilyCachedContinuation`, whitelist `lazilyCachedContinuationsInternalClassNames`, e.g. `kotlinx.coroutines.debug.internal.DebugProbesImpl$CoroutineOwner`) is for classes whose real `getStackTraceElement()` carries genuine, varying per-*instance* data — it lazily computes and caches a `SpecCache` into an *instance* field the first time `$decoroutinator$cache` is read, which is always correct because a real per-instance value never changes after construction. Both mechanisms also patch the class's own `getStackTraceElement()` (via the shared `updateGetStackTraceElementMethod`) to serve the cache once populated, so external callers of that method benefit too — but the two patches call *different* things to fetch the cache, and this difference is load-bearing: `ManualContinuation`'s patch calls the smart `$decoroutinator$cache` getter (safe — its computation never calls back into `getStackTraceElement()`), while `LazilyCachedContinuation`'s patch calls the *raw* `$decoroutinator$cacheField` getter, not the smart `$decoroutinator$cache` getter. Calling the smart getter there would recurse infinitely: `$decoroutinator$cache`'s own computation calls `getStackTraceElement()` to get the real value, which — if patched to call the smart getter — would re-enter `$decoroutinator$cache` before the field is ever populated. + +**`SpecCache.element` is nullable; `provider.nullElementSpecCache`** is the shared singleton (`element = null`, `specMethod` pre-set to `methodHandleInvoker.unknownSpecMethodHandle`) used whenever a `ManualContinuation`/`LazilyCachedContinuation` frame has no usable element and `fillUnknownElementsWithClassName` is off. Reusing one singleton (rather than returning plain `null`) means the caller — the injected `$decoroutinator$cache` field-based caching — never has to distinguish "not yet computed" from "computed, nothing useful," so it never recomputes. **`Method.invoke` wraps checked exceptions in `InvocationTargetException`**: When calling a protected/private method via reflection (e.g., `ClassLoader.findClass`), any checked exception thrown by the method is wrapped in `InvocationTargetException`. Direct `catch (e: ClassNotFoundException)` at the call site of `Method.invoke(...)` will NOT catch it — you must catch `InvocationTargetException` and rethrow `e.cause`. See `generator-android/specMethodsFactory-generator-android.kt`'s `ClassLoader.findClass` extension for the canonical pattern. diff --git a/class-transformer/src/main/kotlin/internal/classTransformer.kt b/class-transformer/src/main/kotlin/internal/classTransformer.kt index edc58964..fafb5fd8 100644 --- a/class-transformer/src/main/kotlin/internal/classTransformer.kt +++ b/class-transformer/src/main/kotlin/internal/classTransformer.kt @@ -16,6 +16,7 @@ import dev.reformator.stacktracedecoroutinator.intrinsics.LABEL_FIELD_NAME import dev.reformator.stacktracedecoroutinator.intrinsics.UNKNOWN_LINE_NUMBER import dev.reformator.stacktracedecoroutinator.provider.BaseContinuationExtractor import dev.reformator.stacktracedecoroutinator.provider.DecoroutinatorTransformed +import dev.reformator.stacktracedecoroutinator.provider.LazilyCachedContinuation import dev.reformator.stacktracedecoroutinator.provider.ManualContinuation import dev.reformator.stacktracedecoroutinator.provider.SpecCache import dev.reformator.stacktracedecoroutinator.provider.internal.BaseContinuationAccessor @@ -35,6 +36,7 @@ import java.io.InputStream import java.lang.invoke.MethodHandles import kotlin.coroutines.Continuation import kotlin.coroutines.jvm.internal.CoroutineStackFrame +import kotlin.jvm.java class ClassBodyTransformationStatus( val updatedBody: ByteArray?, @@ -68,7 +70,11 @@ fun transformClassBody( node.transformBaseContinuation() doTransformation = true } else { - if (node.tryAddBaseContinuationExtractor() || node.tryAddManualContinuation(lineNumbersBySpecMethodName)) { + if ( + node.tryAddBaseContinuationExtractor() || + node.tryAddManualContinuation(lineNumbersBySpecMethodName) || + node.tryAddLazilyCachedContinuation() + ) { doTransformation = true } @@ -106,8 +112,14 @@ private val manualContinuationsInternalClassNames = "kotlinx.coroutines.internal.ScopeCoroutine" ).map { it.internalName }.toHashSet() +private val lazilyCachedContinuationsInternalClassNames = + sequenceOf( + "kotlinx.coroutines.debug.internal.DebugProbesImpl\$CoroutineOwner" + ).map { it.internalName }.toHashSet() + private const val baseContinuationCachesFieldName = "\$decoroutinator\$caches" private const val manualContinuationCacheFieldName = "\$decoroutinator\$cache" +private const val lazilyCachedContinuationCacheFieldName = "\$decoroutinator\$cache" @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") private fun Metadata.getNonSuspendFunctionSignatures(): List { @@ -305,6 +317,24 @@ private fun ClassNode.tryAddManualContinuation( } } + updateGetStackTraceElementMethod( + isUsingElementCacheForGetElementMethodEnabledMethodName = isUsingElementCacheForManualContinuationGetElementMethodEnabledMethodName, + getSpecCacheMethodOwnerInterfaceClass = ManualContinuation::class.java, + getSpecCacheMethodName = manualContinuationGetCacheMethodName + ) + + lineNumbersBySpecMethodName.computeIfAbsent(Continuation<*>::resumeWith.name) { + hashSetOf(UNKNOWN_LINE_NUMBER) + }.add(UNKNOWN_LINE_NUMBER) + + return true +} + +private fun ClassNode.updateGetStackTraceElementMethod( + isUsingElementCacheForGetElementMethodEnabledMethodName: String, + getSpecCacheMethodOwnerInterfaceClass: Class<*>, + getSpecCacheMethodName: String, +) { val getStackTraceElementMethod = methods.find { method -> method.name == CoroutineStackFrame::getStackTraceElement.name && !method.isStatic && method.desc == "()${Type.getDescriptor(StackTraceElement::class.java)}" && @@ -313,31 +343,103 @@ private fun ClassNode.tryAddManualContinuation( @Suppress("IfThenToSafeAccess") if (getStackTraceElementMethod != null) { - getStackTraceElementMethod.instructions.insertBefore(getStackTraceElementMethod.instructions.first, InsnList().apply { - add(MethodInsnNode( - Opcodes.INVOKESTATIC, - Type.getInternalName(providerApiClass), - isUsingElementCacheForManualContinuationGetElementMethodEnabledMethodName, - "()${Type.BOOLEAN_TYPE.descriptor}" - )) - val disabledLabel = LabelNode() - add(JumpInsnNode(Opcodes.IFEQ, disabledLabel)) + getStackTraceElementMethod.instructions.insertBefore( + getStackTraceElementMethod.instructions.first, + InsnList().apply { + add(MethodInsnNode( + Opcodes.INVOKESTATIC, + Type.getInternalName(providerApiClass), + isUsingElementCacheForGetElementMethodEnabledMethodName, + "()${Type.BOOLEAN_TYPE.descriptor}" + )) + val disabledLabel = LabelNode() + add(JumpInsnNode(Opcodes.IFEQ, disabledLabel)) + + add(VarInsnNode(Opcodes.ALOAD, 0)) + add(MethodInsnNode( + Opcodes.INVOKEINTERFACE, + Type.getInternalName(getSpecCacheMethodOwnerInterfaceClass), + getSpecCacheMethodName, + "()${Type.getDescriptor(SpecCache::class.java)}" + )) + add(InsnNode(Opcodes.DUP)) + val cacheIsNullLabel = LabelNode() + add(JumpInsnNode(Opcodes.IFNULL, cacheIsNullLabel)) + + add(MethodInsnNode( + Opcodes.INVOKEVIRTUAL, + Type.getInternalName(SpecCache::class.java), + specCacheGetElementMethodName, + "()${Type.getDescriptor(StackTraceElement::class.java)}" + )) + add(InsnNode(Opcodes.ARETURN)) + + add(cacheIsNullLabel) + add(FrameNode( + Opcodes.F_SAME1, + 0, + null, + 1, + arrayOf(Type.getInternalName(SpecCache::class.java)) + )) + add(InsnNode(Opcodes.POP)) + add(disabledLabel) + add(FrameNode(Opcodes.F_SAME, 0, null, 0, null)) + } + ) + } +} + +private fun ClassNode.tryAddLazilyCachedContinuation(): Boolean { + if (isInterface || name !in lazilyCachedContinuationsInternalClassNames) return false + + interfaces = interfaces.orEmpty() + Type.getInternalName(LazilyCachedContinuation::class.java) + + fields = fields.orEmpty() + FieldNode( + Opcodes.ASM9, + Opcodes.ACC_PRIVATE or Opcodes.ACC_SYNTHETIC, + lazilyCachedContinuationCacheFieldName, + Type.getDescriptor(SpecCache::class.java), + null, + null + ) + + methods = methods.orEmpty() + MethodNode(Opcodes.ASM9).apply { + access = Opcodes.ACC_PUBLIC or Opcodes.ACC_SYNTHETIC + name = lazilyCachedContinuationGetCacheFieldMethodName + desc = "()${Type.getDescriptor(SpecCache::class.java)}" + instructions = InsnList().apply { add(VarInsnNode(Opcodes.ALOAD, 0)) - add(MethodInsnNode( - Opcodes.INVOKEVIRTUAL, - this@tryAddManualContinuation.name, - continuationCachedGetCacheElementMethodName, - "()${Type.getDescriptor(StackTraceElement::class.java)}" + add(FieldInsnNode( + Opcodes.GETFIELD, + this@tryAddLazilyCachedContinuation.name, + lazilyCachedContinuationCacheFieldName, + Type.getDescriptor(SpecCache::class.java) )) add(InsnNode(Opcodes.ARETURN)) - add(disabledLabel) - add(FrameNode(Opcodes.F_SAME, 0, null, 0, null)) - }) + } + } + MethodNode(Opcodes.ASM9).apply { + access = Opcodes.ACC_PUBLIC or Opcodes.ACC_SYNTHETIC + name = lazilyCachedContinuationSetCacheFieldMethodName + desc = "(${Type.getDescriptor(SpecCache::class.java)})V" + instructions = InsnList().apply { + add(VarInsnNode(Opcodes.ALOAD, 0)) + add(VarInsnNode(Opcodes.ALOAD, 1)) + add(FieldInsnNode( + Opcodes.PUTFIELD, + this@tryAddLazilyCachedContinuation.name, + lazilyCachedContinuationCacheFieldName, + Type.getDescriptor(SpecCache::class.java) + )) + add(InsnNode(Opcodes.RETURN)) + } } - lineNumbersBySpecMethodName.computeIfAbsent(Continuation<*>::resumeWith.name) { - hashSetOf(UNKNOWN_LINE_NUMBER) - }.add(UNKNOWN_LINE_NUMBER) + updateGetStackTraceElementMethod( + isUsingElementCacheForGetElementMethodEnabledMethodName = isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabledMethodName, + getSpecCacheMethodOwnerInterfaceClass = LazilyCachedContinuation::class.java, + getSpecCacheMethodName = lazilyCachedContinuationGetCacheFieldMethodName + ) return true } @@ -832,9 +934,6 @@ private val debugMetadataMethodNameMethodName: String private val debugMetadataClassNameMethodName: String @LoadConstant("debugMetadataClassNameMethodName") get() = fail() -private val continuationCachedGetCacheElementMethodName: String - @LoadConstant("continuationCachedGetCacheElementMethodName") get() = fail() - private val isDecoroutinatorEnabledMethodName: String @LoadConstant("isDecoroutinatorEnabledMethodName") get() = fail() @@ -891,3 +990,18 @@ private val fillUnknownElementsWithClassNameMethodName: String private val isUsingElementCacheForManualContinuationGetElementMethodEnabledMethodName: String @LoadConstant("isUsingElementCacheForManualContinuationGetElementMethodEnabledMethodName") get() = fail() + +private val manualContinuationGetCacheMethodName: String + @LoadConstant("manualContinuationGetCacheMethodName") get() = fail() + +private val lazilyCachedContinuationGetCacheFieldMethodName: String + @LoadConstant("lazilyCachedContinuationGetCacheFieldMethodName") get() = fail() + +private val lazilyCachedContinuationSetCacheFieldMethodName: String + @LoadConstant("lazilyCachedContinuationSetCacheFieldMethodName") get() = fail() + +private val specCacheGetElementMethodName: String + @LoadConstant("specCacheGetElementMethodName") get() = fail() + +private val isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabledMethodName: String + @LoadConstant("isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabledMethodName") get() = fail() diff --git a/common/src/main/kotlin/internal/awakener.kt b/common/src/main/kotlin/internal/awakener.kt index abff3105..801ae00f 100644 --- a/common/src/main/kotlin/internal/awakener.kt +++ b/common/src/main/kotlin/internal/awakener.kt @@ -59,8 +59,9 @@ private inline fun CoroutineStackFrame.getElementAndSpecMethod( val cache = (this as? ContinuationCached)?.`$decoroutinator$cache` if (cache != null) { val specMethod = cache.specMethod ?: run { - val specMethod = specMethodsFactory.getSpecMethodHandle(cache.element) ?: - methodHandleInvoker.unknownSpecMethodHandle + val specMethod = cache.element + ?.let { specMethodsFactory.getSpecMethodHandle(it) } + ?: methodHandleInvoker.unknownSpecMethodHandle cache.specMethod = specMethod specMethod } @@ -93,20 +94,6 @@ private fun BaseContinuation.getElementsAndSpecMethods(): List element - fillUnknownElementsWithClassName -> StackTraceElement( - javaClass.name, - Continuation<*>::resumeWith.name, - null, - UNKNOWN_LINE_NUMBER - ) - else -> null - } -} - private fun BaseContinuation.stdlibAwake(accessor: BaseContinuationAccessor, result: Any?) { var newResult = result var baseContinuation = this diff --git a/common/src/main/kotlin/internal/di-common.kt b/common/src/main/kotlin/internal/di-common.kt index 6f575ad7..f72910d8 100644 --- a/common/src/main/kotlin/internal/di-common.kt +++ b/common/src/main/kotlin/internal/di-common.kt @@ -78,6 +78,15 @@ internal val isUsingElementFactoryForBaseContinuationEnabled: Boolean = ).toBoolean() } +internal val isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled = + fillUnknownElementsWithClassName && + getRuntimeSettingsValue({ isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled }) { + System.getProperty( + "dev.reformator.stacktracedecoroutinator.isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled", + "true" + ).toBoolean() + } + @Suppress("ObjectPropertyName") private val _transformedClassesRegistry: TransformedClassesRegistry? = if (enabled) TransformedClassesRegistryImpl() else null diff --git a/common/src/main/kotlin/internal/provider-impl.kt b/common/src/main/kotlin/internal/provider-impl.kt index b1b9d4e4..f10daf05 100644 --- a/common/src/main/kotlin/internal/provider-impl.kt +++ b/common/src/main/kotlin/internal/provider-impl.kt @@ -2,7 +2,16 @@ package dev.reformator.stacktracedecoroutinator.common.internal +import dev.reformator.stacktracedecoroutinator.common.internal.isUsingElementCacheForManualContinuationGetElementMethodEnabled + as cachedIsUsingElementCacheForManualContinuationGetElementMethodEnabled +import dev.reformator.stacktracedecoroutinator.common.internal.isUsingElementFactoryForBaseContinuationEnabled + as cachedIsUsingElementFactoryForBaseContinuationEnabled +import dev.reformator.stacktracedecoroutinator.common.internal.isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled + as cachedIsUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled +import dev.reformator.stacktracedecoroutinator.common.internal.fillUnknownElementsWithClassName + as cachedFillUnknownElementsWithClassName import dev.reformator.stacktracedecoroutinator.intrinsics.BaseContinuation +import dev.reformator.stacktracedecoroutinator.intrinsics.UNKNOWN_LINE_NUMBER import dev.reformator.stacktracedecoroutinator.provider.BaseContinuationExtractor import dev.reformator.stacktracedecoroutinator.provider.ContinuationCached import dev.reformator.stacktracedecoroutinator.provider.SpecCache @@ -12,6 +21,7 @@ import java.lang.invoke.MethodHandles import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock import kotlin.coroutines.Continuation +import kotlin.coroutines.jvm.internal.CoroutineStackFrame internal class Provider: DecoroutinatorProvider { private val prepareBaseContinuationAccessorLock = ReentrantLock() @@ -65,18 +75,40 @@ internal class Provider: DecoroutinatorProvider { } override val isUsingElementFactoryForBaseContinuationEnabled: Boolean - get() = dev.reformator.stacktracedecoroutinator.common.internal.isUsingElementFactoryForBaseContinuationEnabled + get() = cachedIsUsingElementFactoryForBaseContinuationEnabled - override fun getElementFactoryStacktraceElement(baseContinuation: Any): StackTraceElement? = - if (baseContinuation is ContinuationCached) { - baseContinuation.`$decoroutinator$cache`?.element - } else { - stacktraceElementsFactory.getStacktraceElement(baseContinuation as BaseContinuation) - } + override fun getElementFactoryStacktraceElement(baseContinuation: Any): StackTraceElement? { + (baseContinuation as? ContinuationCached)?.`$decoroutinator$cache`?.let { return it.element } + return stacktraceElementsFactory.getStacktraceElement(baseContinuation as BaseContinuation) + } override val fillUnknownElementsWithClassName: Boolean - get() = dev.reformator.stacktracedecoroutinator.common.internal.fillUnknownElementsWithClassName + get() = cachedFillUnknownElementsWithClassName override val isUsingElementCacheForManualContinuationGetElementMethodEnabled: Boolean - get() = dev.reformator.stacktracedecoroutinator.common.internal.isUsingElementCacheForManualContinuationGetElementMethodEnabled + get() = cachedIsUsingElementCacheForManualContinuationGetElementMethodEnabled + + override fun getCoroutineStackFrameStackTraceElement(coroutineStackFrame: Any): StackTraceElement? = + (coroutineStackFrame as CoroutineStackFrame).getStackTraceElement() + + override val nullElementSpecCache = SpecCache(null).also { cache -> + cache.specMethod = methodHandleInvoker.unknownSpecMethodHandle + } + + override val isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled: Boolean + get() = cachedIsUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled +} + +internal fun CoroutineStackFrame.getNormalizedStackTraceElement(): StackTraceElement? { + val element = getStackTraceElement() + return when { + element != null -> element + cachedFillUnknownElementsWithClassName -> StackTraceElement( + javaClass.name, + Continuation<*>::resumeWith.name, + null, + UNKNOWN_LINE_NUMBER + ) + else -> null + } } diff --git a/common/src/main/kotlin/internal/utils-common.kt b/common/src/main/kotlin/internal/utils-common.kt index 20f7bf72..dceb3203 100644 --- a/common/src/main/kotlin/internal/utils-common.kt +++ b/common/src/main/kotlin/internal/utils-common.kt @@ -112,7 +112,7 @@ internal class TailCallDeoptimizedContinuation( override fun invokeSuspend(result: Any?): Any? = result - override fun getStackTraceElement(): StackTraceElement = + override fun getStackTraceElement(): StackTraceElement? = `$decoroutinator$cache`.element } diff --git a/gradle-plugin/src/main/kotlin/groovy-dsl-initializer.kt b/gradle-plugin/src/main/kotlin/groovy-dsl-initializer.kt index 2c67e6e0..e3e24521 100644 --- a/gradle-plugin/src/main/kotlin/groovy-dsl-initializer.kt +++ b/gradle-plugin/src/main/kotlin/groovy-dsl-initializer.kt @@ -10,4 +10,5 @@ import dev.reformator.bytecodeprocessor.intrinsics.ChangeClassName import dev.reformator.bytecodeprocessor.intrinsics.fail import org.gradle.api.Project -internal fun initGroovyDsl(@Suppress("unused") target: Project): Unit = fail() +@Suppress("UNUSED_PARAMETER") +internal fun initGroovyDsl(target: Project): Unit = fail() diff --git a/provider/src/main/kotlin/internal/provider.kt b/provider/src/main/kotlin/internal/provider.kt index 942814fb..696dcc61 100644 --- a/provider/src/main/kotlin/internal/provider.kt +++ b/provider/src/main/kotlin/internal/provider.kt @@ -18,6 +18,9 @@ interface DecoroutinatorProvider { fun getElementFactoryStacktraceElement(baseContinuation: Any): StackTraceElement? val fillUnknownElementsWithClassName: Boolean val isUsingElementCacheForManualContinuationGetElementMethodEnabled: Boolean + fun getCoroutineStackFrameStackTraceElement(coroutineStackFrame: Any): StackTraceElement? + val nullElementSpecCache: SpecCache + val isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled: Boolean } internal val provider: DecoroutinatorProvider = @@ -34,8 +37,7 @@ private class NoopProvider: DecoroutinatorProvider { override val baseContinuationAccessor: BaseContinuationAccessor get() = error("not supported") - override fun prepareBaseContinuationAccessor(lookup: MethodHandles.Lookup): BaseContinuationAccessor = - error("not supported") + override fun prepareBaseContinuationAccessor(lookup: MethodHandles.Lookup) = error("not supported") override fun awakeBaseContinuation( accessor: BaseContinuationAccessor, @@ -58,14 +60,21 @@ private class NoopProvider: DecoroutinatorProvider { override val isUsingElementFactoryForBaseContinuationEnabled: Boolean get() = false - override fun getElementFactoryStacktraceElement(baseContinuation: Any): StackTraceElement = - error("not supported") + override fun getElementFactoryStacktraceElement(baseContinuation: Any) = error("not supported") override val fillUnknownElementsWithClassName: Boolean get() = false override val isUsingElementCacheForManualContinuationGetElementMethodEnabled: Boolean get() = false + + override fun getCoroutineStackFrameStackTraceElement(coroutineStackFrame: Any) = error("not supported") + + override val nullElementSpecCache: SpecCache + get() = error("not supported") + + override val isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled: Boolean + get() = false } interface BaseContinuationAccessor { diff --git a/provider/src/main/kotlin/provider-api.kt b/provider/src/main/kotlin/provider-api.kt index 4e4ff27e..af462d0a 100644 --- a/provider/src/main/kotlin/provider-api.kt +++ b/provider/src/main/kotlin/provider-api.kt @@ -56,11 +56,6 @@ annotation class DecoroutinatorTransformed( interface ContinuationCached { @Suppress("unused", "PropertyName") val `$decoroutinator$cache`: SpecCache? - - @Suppress("unused", "PropertyName") - @get:MethodNameConstant("continuationCachedGetCacheElementMethodName") - val `$decoroutinator$cacheElement`: StackTraceElement? - get() = `$decoroutinator$cache`?.element } interface BaseContinuationExtractor: ContinuationCached { @@ -79,18 +74,46 @@ interface BaseContinuationExtractor: ContinuationCached { interface ManualContinuation: ContinuationCached { @Suppress("PropertyName") @get:MethodNameConstant("manualContinuationGetCacheFieldMethodName") - val `$decoroutinator$cacheField`: SpecCache? + val `$decoroutinator$cacheField`: SpecCache + @get:MethodNameConstant("manualContinuationGetCacheMethodName") override val `$decoroutinator$cache`: SpecCache? get() { - val cache = `$decoroutinator$cacheField` ?: return null - return if (javaClass.name == cache.element.className) cache else null + if (!provider.fillUnknownElementsWithClassName) return provider.nullElementSpecCache + + val cache = `$decoroutinator$cacheField` + if (javaClass.name != cache.element!!.className) return null + + return cache + } +} + +interface LazilyCachedContinuation: ContinuationCached { + @Suppress("PropertyName") + @get:MethodNameConstant("lazilyCachedContinuationGetCacheFieldMethodName") + @set:MethodNameConstant("lazilyCachedContinuationSetCacheFieldMethodName") + var `$decoroutinator$cacheField`: SpecCache? + + override val `$decoroutinator$cache`: SpecCache + get() { + val cacheField = `$decoroutinator$cacheField` + return if (cacheField == null) { + val element = provider.getCoroutineStackFrameStackTraceElement(this) + val result = if (element == null) { + if (provider.fillUnknownElementsWithClassName) { + SpecCache(javaClass.name, "resumeWith", null, -1) + } else provider.nullElementSpecCache + } else SpecCache(element) + `$decoroutinator$cacheField` = result + result + } else cacheField } } @Suppress("unused") class SpecCache( - val element: StackTraceElement + @get:MethodNameConstant("specCacheGetElementMethodName") + val element: StackTraceElement? ) { var specMethod: MethodHandle? = null @@ -142,5 +165,10 @@ val isUsingElementCacheForManualContinuationGetElementMethodEnabled: Boolean @MethodNameConstant("isUsingElementCacheForManualContinuationGetElementMethodEnabledMethodName") get() = provider.isUsingElementCacheForManualContinuationGetElementMethodEnabled +@Suppress("unused") +val isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled: Boolean + @MethodNameConstant("isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabledMethodName") + get() = provider.isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled + val providerApiClass: Class<*> @GetOwnerClass get() { fail() } diff --git a/runtime-settings/src/main/kotlin/runtime-settings.kt b/runtime-settings/src/main/kotlin/runtime-settings.kt index 64e9efff..9797214f 100644 --- a/runtime-settings/src/main/kotlin/runtime-settings.kt +++ b/runtime-settings/src/main/kotlin/runtime-settings.kt @@ -31,6 +31,9 @@ interface DecoroutinatorRuntimeSettingsProvider { val isUsingElementCacheForManualContinuationGetElementMethodEnabled: Boolean get() = defaultValue() + val isUsingElementCacheForLazilyCachedContinuationGetElementMethodEnabled: Boolean + get() = defaultValue() + // JVM Agent settings val isBaseContinuationRedefinitionAllowed: Boolean diff --git a/spec-method-builder/src/main/kotlin/internal/specMethodBuilder.kt b/spec-method-builder/src/main/kotlin/internal/specMethodBuilder.kt index b99fff89..44423f6f 100644 --- a/spec-method-builder/src/main/kotlin/internal/specMethodBuilder.kt +++ b/spec-method-builder/src/main/kotlin/internal/specMethodBuilder.kt @@ -212,6 +212,7 @@ private fun getThrowInvalidLineNumberInstructions() = InsnList().apply { add(InsnNode(Opcodes.ATHROW)) } +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") private fun getResumeNextAndReturnInstructions(lineNumbers: List) = InsnList().apply { add(VarInsnNode(Opcodes.ALOAD, SPEC_VAR_INDEX)) add(VarInsnNode(Opcodes.ALOAD, RESULT_VAR_INDEX)) @@ -243,7 +244,7 @@ private fun getResumeNextAndReturnInstructions(lineNumbers: List) = InsnLis 0, null, 1, - arrayOf(Type.getType(Object::class.java).internalName) + arrayOf(Type.getInternalName(Object::class.java)) )) add(InsnNode(Opcodes.ARETURN)) add(invalidLabel)