Skip to content

Commit e35baa4

Browse files
committed
Get rid of quadratic complexity during stack unrolling in debug agent in 1.3.30
1 parent 3c56ff5 commit e35baa4

File tree

1 file changed

+36
-1
lines changed

1 file changed

+36
-1
lines changed

kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt

+36-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ internal object DebugProbesImpl {
3232
// To sort coroutines by creation order, used as unique id
3333
private var sequenceNumber: Long = 0
3434

35+
/*
36+
* This is an optimization in the face of KT-29997:
37+
* Consider suspending call stack a()->b()->c() and c() completes its execution and every call is
38+
* "almost" in tail position.
39+
*
40+
* Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth).
41+
* To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally.
42+
*/
43+
private val stateCache = WeakHashMap<CoroutineStackFrame, CoroutineState>()
44+
3545
@Synchronized
3646
public fun install() {
3747
if (++installations > 1) return
@@ -172,7 +182,7 @@ internal object DebugProbesImpl {
172182
* 4) Prepend dumped stacktrace (trimmed by target frame) to continuation stacktrace
173183
*
174184
* Heuristic may fail on recursion and overloads, but it will be automatically improved
175-
* with KT-29997
185+
* with KT-29997.
176186
*/
177187
val indexOfResumeWith = actualTrace.indexOfFirst {
178188
it.className == "kotlin.coroutines.jvm.internal.BaseContinuationImpl" &&
@@ -248,11 +258,36 @@ internal object DebugProbesImpl {
248258

249259
private fun updateState(frame: Continuation<*>, state: State) {
250260
if (!isInstalled) return
261+
// KT-29997 is here only since 1.3.30
262+
if (state == State.RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) {
263+
updateRunningState(frame, state)
264+
return
265+
}
266+
251267
// Find ArtificialStackFrame of the coroutine
252268
val owner = frame.owner()
253269
updateState(owner, frame, state)
254270
}
255271

272+
@Synchronized // See comment to stateCache
273+
private fun updateRunningState(continuation: Continuation<*>, state: State) {
274+
val frame = continuation as? CoroutineStackFrame ?: return
275+
val coroutineState = stateCache.remove(frame) ?: capturedCoroutines[frame.owner()] ?: return
276+
// Do not cache states for proxy-classes such as ScopeCoroutines
277+
val caller = frame.realCaller()
278+
if (caller != null) {
279+
stateCache[caller] = coroutineState
280+
}
281+
282+
coroutineState.updateState(state, continuation)
283+
}
284+
285+
private tailrec fun CoroutineStackFrame.realCaller(): CoroutineStackFrame? {
286+
val caller = callerFrame ?: return null
287+
if (caller.getStackTraceElement() != null) return caller
288+
else return caller.realCaller()
289+
}
290+
256291
@Synchronized
257292
private fun updateState(owner: ArtificialStackFrame<*>?, frame: Continuation<*>, state: State) {
258293
val coroutineState = capturedCoroutines[owner] ?: return

0 commit comments

Comments
 (0)