Skip to content

Commit

Permalink
Introduce CoroutineStackFrame and implement it in SuspendFunctionGun …
Browse files Browse the repository at this point in the history
…reusable completion in order to properly walk through stackframes of interceptors
  • Loading branch information
qwwdfsad committed Dec 11, 2018
1 parent 168e24f commit f33f66f
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 1 deletion.
10 changes: 10 additions & 0 deletions ktor-utils/ktor-utils-ios/src/io/ktor/util/StackFramesIos.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.ktor.util

@Suppress("UNUSED")
internal actual interface CoroutineStackFrame {
public actual val callerFrame: CoroutineStackFrame?
public actual fun getStackTraceElement(): StackTraceElement?
}

@Suppress("ACTUAL_WITHOUT_EXPECT")
actual typealias StackTraceElement = Any
10 changes: 10 additions & 0 deletions ktor-utils/ktor-utils-js/src/io/ktor/util/StackFramesJs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.ktor.util

@Suppress("UNUSED")
internal actual interface CoroutineStackFrame {
public actual val callerFrame: CoroutineStackFrame?
public actual fun getStackTraceElement(): StackTraceElement?
}

@Suppress("ACTUAL_WITHOUT_EXPECT")
actual typealias StackTraceElement = Any
7 changes: 7 additions & 0 deletions ktor-utils/ktor-utils-jvm/src/io/ktor/util/StackFramesJvm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.ktor.util

@Suppress("ACTUAL_WITHOUT_EXPECT")
actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame

@Suppress("ACTUAL_WITHOUT_EXPECT")
actual typealias StackTraceElement = java.lang.StackTraceElement
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.ktor.tests.utils

import io.ktor.util.*
import io.ktor.util.pipeline.*
import kotlinx.coroutines.*
import org.junit.Test
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
import kotlin.test.*

class PipelineStackFramesTest {
private val phase = PipelinePhase("StubPhase")
private lateinit var interceptorContinuation: Continuation<Unit>

@Test
fun testStackTraceWalking() {
val pipeline = Pipeline<Unit, Unit>(phase)
pipeline.intercept(phase) {
captureContinuation()
}

runBlocking {
runPipeline(pipeline)
}

val frame = interceptorContinuation as CoroutineStackFrame
val stacktrace = buildString {
var currentTop: CoroutineStackFrame? = frame
while (currentTop != null) {
val element = currentTop.getStackTraceElement()
if (element != null) {
append(element)
append('\n')
}
currentTop = currentTop.callerFrame
}
}

val filtered = stacktrace
.replace(Regex(":[0-9]+"), "") // line numbers
.replace(Regex("\n.*invokeSuspend.*\n"), "\n") // lambdas
assertEquals(filtered,
"io/ktor/tests/utils/PipelineStackFramesTest.nestedCapture(PipelineStackFramesTest.kt)\n" +
"io/ktor/tests/utils/PipelineStackFramesTest.captureContinuation(PipelineStackFramesTest.kt)\n" +
"io/ktor/tests/utils/PipelineStackFramesTest.runPipeline(PipelineStackFramesTest.kt)\n"
)
}

private suspend fun runPipeline(pipeline: Pipeline<Unit, Unit>) {
pipeline.execute(Unit, Unit)
preventTailCall()
}

private suspend fun captureContinuation() {
nestedCapture()
preventTailCall()
}

private suspend fun nestedCapture() {
suspendCoroutineUninterceptedOrReturn<Unit> {
interceptorContinuation = it
Unit
}
preventTailCall()
}

private fun preventTailCall() {
}
}
8 changes: 8 additions & 0 deletions ktor-utils/src/io/ktor/util/StackFrames.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.ktor.util

internal expect class StackTraceElement

internal expect interface CoroutineStackFrame {
public val callerFrame: CoroutineStackFrame?
public fun getStackTraceElement(): StackTraceElement?
}
10 changes: 9 additions & 1 deletion ktor-utils/src/io/ktor/util/pipeline/PipelineContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ private class SuspendFunctionGun<TSubject : Any, TContext : Any>(

// this is impossible to inline because of property name clash
// between PipelineContext.context and Continuation.context
private val continuation: Continuation<Unit> = object : Continuation<Unit> {
private val continuation: Continuation<Unit> = object : Continuation<Unit>, CoroutineStackFrame {
override val callerFrame: CoroutineStackFrame?
get() = proceedContinuation as? CoroutineStackFrame

override fun getStackTraceElement(): StackTraceElement? = null

@Suppress("UNCHECKED_CAST")
override val context: CoroutineContext
get () {
Expand All @@ -97,13 +102,16 @@ private class SuspendFunctionGun<TSubject : Any, TContext : Any>(
private set

private var rootContinuation: Any? = null
// Continuation where 'proceed' was called in order to properly walk over stacktrace
private var proceedContinuation: Continuation<*>? = null
private var index = 0

override fun finish() {
index = blocks.size
}

override suspend fun proceed(): TSubject = suspendCoroutineUninterceptedOrReturn { continuation ->
proceedContinuation = continuation
if (index == blocks.size) return@suspendCoroutineUninterceptedOrReturn subject

addContinuation(continuation)
Expand Down

0 comments on commit f33f66f

Please sign in to comment.