Skip to content

Commit e1e2628

Browse files
committed
Get rid of quadratic complexity during stack unrolling in debug agent
1 parent ffaedf1 commit e1e2628

File tree

1 file changed

+30
-1
lines changed

1 file changed

+30
-1
lines changed

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

+30-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 owner of such chains in this map and update it incrementally.
42+
*/
43+
private val ownersCache = WeakHashMap<CoroutineStackFrame, ArtificialStackFrame<*>>()
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,14 +258,33 @@ internal object DebugProbesImpl {
248258

249259
private fun updateState(frame: Continuation<*>, state: State) {
250260
if (!isInstalled) return
261+
if (state == State.RUNNING) {
262+
updateRunningState(frame, state)
263+
return
264+
}
265+
251266
// Find ArtificialStackFrame of the coroutine
252267
val owner = frame.owner()
253268
updateState(owner, frame, state)
254269
}
255270

271+
@Synchronized // See comment to ownersCache
272+
private fun updateRunningState(continuation: Continuation<*>, state: State) {
273+
val frame = continuation as? CoroutineStackFrame ?: return
274+
val owner = ownersCache.remove(frame) ?: frame.owner() ?: return
275+
val completion = frame.callerFrame
276+
if (completion != null) {
277+
ownersCache[completion] = owner
278+
}
279+
280+
val coroutineState = capturedCoroutines[owner] ?: return
281+
coroutineState.updateState(state, continuation)
282+
}
283+
256284
@Synchronized
257285
private fun updateState(owner: ArtificialStackFrame<*>?, frame: Continuation<*>, state: State) {
258286
val coroutineState = capturedCoroutines[owner] ?: return
287+
if (coroutineState.state == State.RUNNING && frame is CoroutineStackFrame) ownersCache.remove(frame)
259288
coroutineState.updateState(state, frame)
260289
}
261290

0 commit comments

Comments
 (0)