Skip to content

Commit c099090

Browse files
committed
Rename CoroutineState to CoroutineInfo and improve its API
* Rename jobOrNull to job * Replace coroutine with its context * Add check-and-act on isInstalled flag into synchronized blocks in order to avoid spurious memory leaks during uninstall
1 parent 0bf85e6 commit c099090

File tree

9 files changed

+81
-83
lines changed

9 files changed

+81
-83
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
public final class kotlinx/coroutines/debug/CoroutineState {
2-
public final fun component1 ()Lkotlin/coroutines/Continuation;
3-
public final fun copy (Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;J)Lkotlinx/coroutines/debug/CoroutineState;
4-
public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineState;Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineState;
1+
public final class kotlinx/coroutines/debug/CoroutineInfo {
2+
public final fun component1 ()Lkotlin/coroutines/CoroutineContext;
3+
public final fun copy (Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;J)Lkotlinx/coroutines/debug/CoroutineInfo;
4+
public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineInfo;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineInfo;
55
public fun equals (Ljava/lang/Object;)Z
6-
public final fun getContinuation ()Lkotlin/coroutines/Continuation;
6+
public final fun getContext ()Lkotlin/coroutines/CoroutineContext;
77
public final fun getCreationStackTrace ()Ljava/util/List;
8-
public final fun getJobOrNull ()Lkotlinx/coroutines/Job;
8+
public final fun getJob ()Lkotlinx/coroutines/Job;
99
public final fun getState ()Lkotlinx/coroutines/debug/State;
1010
public fun hashCode ()I
1111
public final fun lastObservedStackTrace ()Ljava/util/List;
@@ -16,7 +16,7 @@ public final class kotlinx/coroutines/debug/DebugProbes {
1616
public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes;
1717
public final fun dumpCoroutines (Ljava/io/PrintStream;)V
1818
public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V
19-
public final fun dumpCoroutinesState ()Ljava/util/List;
19+
public final fun dumpCoroutinesInfo ()Ljava/util/List;
2020
public final fun getSanitizeStackTraces ()Z
2121
public final fun install ()V
2222
public final fun jobToString (Lkotlinx/coroutines/Job;)Ljava/lang/String;

kotlinx-coroutines-debug/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Call to [DebugProbes.install] installs debug agent via ByteBuddy and starts spyi
1010

1111
After that, you can use [DebugProbes.dumpCoroutines] to print all active (suspended or running) coroutines, including their state, creation and
1212
suspension stacktraces.
13-
Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesState] or dump isolated parts
13+
Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesInfo] or dump isolated parts
1414
of coroutines hierarchy referenced by a [Job] or [CoroutineScope] instances using [DebugProbes.printJob] and [DebugProbes.printScope] respectively.
1515

1616
### Using in your project
@@ -159,7 +159,7 @@ java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/Mana
159159
[DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html
160160
[DebugProbes.install]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/install.html
161161
[DebugProbes.dumpCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines.html
162-
[DebugProbes.dumpCoroutinesState]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-state.html
162+
[DebugProbes.dumpCoroutinesInfo]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-info.html
163163
[DebugProbes.printJob]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-job.html
164164
[DebugProbes.printScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-scope.html
165165
<!--- INDEX kotlinx.coroutines.debug.junit4 -->

kotlinx-coroutines-debug/src/CoroutineState.kt renamed to kotlinx-coroutines-debug/src/CoroutineInfo.kt

+21-21
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import kotlin.coroutines.*
1212
import kotlin.coroutines.jvm.internal.*
1313

1414
/**
15-
* Class describing coroutine state.
15+
* Class describing coroutine info such as its context, state and stacktrace.
1616
*/
1717
@ExperimentalCoroutinesApi
18-
public data class CoroutineState internal constructor(
19-
public val continuation: Continuation<*>,
18+
public data class CoroutineInfo internal constructor(
19+
val context: CoroutineContext,
2020
private val creationStackBottom: CoroutineStackFrame,
2121
@JvmField internal val sequenceNumber: Long
2222
) {
@@ -25,7 +25,7 @@ public data class CoroutineState internal constructor(
2525
* [Job] associated with a current coroutine or null.
2626
* May be later used in [DebugProbes.printJob].
2727
*/
28-
public val jobOrNull: Job? get() = continuation.context[Job]
28+
public val job: Job? get() = context[Job]
2929

3030
/**
3131
* Creation stacktrace of the coroutine.
@@ -45,15 +45,30 @@ public data class CoroutineState internal constructor(
4545
private var lastObservedFrame: CoroutineStackFrame? = null
4646

4747
// Copy constructor
48-
internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this(
49-
coroutine,
48+
internal constructor(coroutine: Continuation<*>, state: CoroutineInfo) : this(
49+
coroutine.context,
5050
state.creationStackBottom,
5151
state.sequenceNumber
5252
) {
5353
_state = state.state
5454
this.lastObservedFrame = state.lastObservedFrame
5555
}
5656

57+
/**
58+
* Last observed stacktrace of the coroutine captured on its suspension or resumption point.
59+
* It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and
60+
* reflects stacktrace of the resumption point, not the actual current stacktrace.
61+
*/
62+
public fun lastObservedStackTrace(): List<StackTraceElement> {
63+
var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList()
64+
val result = ArrayList<StackTraceElement>()
65+
while (frame != null) {
66+
frame.getStackTraceElement()?.let { result.add(sanitize(it)) }
67+
frame = frame.callerFrame
68+
}
69+
return result
70+
}
71+
5772
private fun creationStackTrace(): List<StackTraceElement> {
5873
// Skip "Coroutine creation stacktrace" frame
5974
return sequence<StackTraceElement> { yieldFrames(creationStackBottom.callerFrame) }.toList()
@@ -79,21 +94,6 @@ public data class CoroutineState internal constructor(
7994
lastObservedThread = null
8095
}
8196
}
82-
83-
/**
84-
* Last observed stacktrace of the coroutine captured on its suspension or resumption point.
85-
* It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and
86-
* reflects stacktrace of the resumption point, not the actual current stacktrace.
87-
*/
88-
public fun lastObservedStackTrace(): List<StackTraceElement> {
89-
var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList()
90-
val result = ArrayList<StackTraceElement>()
91-
while (frame != null) {
92-
frame.getStackTraceElement()?.let { result.add(sanitize(it)) }
93-
frame = frame.callerFrame
94-
}
95-
return result
96-
}
9797
}
9898

9999
/**

kotlinx-coroutines-debug/src/DebugProbes.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ public object DebugProbes {
9595
printJob(scope.coroutineContext[Job] ?: error("Job is not present in the scope"), out)
9696

9797
/**
98-
* Returns all existing coroutine states.
98+
* Returns all existing coroutines info.
9999
* The resulting collection represents a consistent snapshot of all existing coroutines at the moment of invocation.
100100
*/
101-
public fun dumpCoroutinesState(): List<CoroutineState> = DebugProbesImpl.dumpCoroutinesState()
101+
public fun dumpCoroutinesInfo(): List<CoroutineInfo> = DebugProbesImpl.dumpCoroutinesInfo()
102102

103103
/**
104104
* Dumps all active coroutines into the given output stream, providing a consistent snapshot of all existing coroutines at the moment of invocation.

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

+43-42
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // I
2626
internal object DebugProbesImpl {
2727
private const val ARTIFICIAL_FRAME_MESSAGE = "Coroutine creation stacktrace"
2828
private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
29-
private val capturedCoroutines: MutableSet<CoroutineOwner<*>> = Collections.newSetFromMap(HashMap())
29+
private val capturedCoroutines = HashSet<CoroutineOwner<*>>()
3030
@Volatile
3131
private var installations = 0
3232
private val isInstalled: Boolean get() = installations > 0
@@ -41,7 +41,7 @@ internal object DebugProbesImpl {
4141
* Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth).
4242
* To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally.
4343
*/
44-
private val stateCache = WeakHashMap<CoroutineStackFrame, CoroutineState>()
44+
private val infoCache = HashMap<CoroutineStackFrame, CoroutineInfo>()
4545

4646
@Synchronized
4747
public fun install() {
@@ -64,6 +64,7 @@ internal object DebugProbesImpl {
6464
if (--installations != 0) return
6565

6666
capturedCoroutines.clear()
67+
infoCache.clear()
6768
val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt")
6869
val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt")
6970

@@ -79,16 +80,16 @@ internal object DebugProbesImpl {
7980
check(isInstalled) { "Debug probes are not installed" }
8081
val jobToStack = capturedCoroutines
8182
.filter { it.delegate.context[Job] != null }
82-
.associateBy({ it.delegate.context[Job]!! }, {it.state})
83+
.associateBy({ it.delegate.context[Job]!! }, {it.info})
8384
return buildString {
8485
job.build(jobToStack, this, "")
8586
}
8687
}
8788

88-
private fun Job.build(map: Map<Job, CoroutineState>, builder: StringBuilder, indent: String) {
89-
val state = map[this]
89+
private fun Job.build(map: Map<Job, CoroutineInfo>, builder: StringBuilder, indent: String) {
90+
val info = map[this]
9091
val newIndent: String
91-
if (state == null) { // Append coroutine without stacktrace
92+
if (info == null) { // Append coroutine without stacktrace
9293
// Do not print scoped coroutines and do not increase indentation level
9394
@Suppress("INVISIBLE_REFERENCE")
9495
if (this !is kotlinx.coroutines.internal.ScopeCoroutine<*>) {
@@ -99,9 +100,9 @@ internal object DebugProbesImpl {
99100
}
100101
} else {
101102
// Append coroutine with its last stacktrace element
102-
val element = state.lastObservedStackTrace().firstOrNull()
103-
val contState = state.state
104-
builder.append("$indent$debugString, continuation is $contState at line $element\n")
103+
val element = info.lastObservedStackTrace().firstOrNull()
104+
val state = info.state
105+
builder.append("$indent$debugString, continuation is $state at line $element\n")
105106
newIndent = indent + "\t"
106107
}
107108
// Append children with new indent
@@ -114,58 +115,58 @@ internal object DebugProbesImpl {
114115
private val Job.debugString: String get() = if (this is JobSupport) toDebugString() else toString()
115116

116117
@Synchronized
117-
public fun dumpCoroutinesState(): List<CoroutineState> {
118+
public fun dumpCoroutinesInfo(): List<CoroutineInfo> {
118119
check(isInstalled) { "Debug probes are not installed" }
119120
return capturedCoroutines.asSequence()
120-
.map { CoroutineState(it.delegate, it.state) }
121+
.map { CoroutineInfo(it.delegate, it.info) }
121122
.sortedBy { it.sequenceNumber }
122123
.toList()
123124
}
124125

125126
public fun dumpCoroutines(out: PrintStream) {
126-
check(isInstalled) { "Debug probes are not installed" }
127127
// Avoid inference with other out/err invocations by creating a string first
128128
dumpCoroutines().let { out.println(it) }
129129
}
130130

131131
@Synchronized
132132
private fun dumpCoroutines(): String = buildString {
133+
check(isInstalled) { "Debug probes are not installed" }
133134
// Synchronization window can be reduce even more, but no need to do it here
134135
append("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}")
135136
capturedCoroutines
136137
.asSequence()
137-
.sortedBy { it.state.sequenceNumber }
138+
.sortedBy { it.info.sequenceNumber }
138139
.forEach { owner ->
139-
val coroutineState = owner.state
140-
val observedStackTrace = coroutineState.lastObservedStackTrace()
141-
val enhancedStackTrace = enhanceStackTraceWithThreadDump(coroutineState, observedStackTrace)
142-
val state = if (coroutineState.state == State.RUNNING && enhancedStackTrace === observedStackTrace)
143-
"${coroutineState.state} (Last suspension stacktrace, not an actual stacktrace)"
140+
val info = owner.info
141+
val observedStackTrace = info.lastObservedStackTrace()
142+
val enhancedStackTrace = enhanceStackTraceWithThreadDump(info, observedStackTrace)
143+
val state = if (info.state == State.RUNNING && enhancedStackTrace === observedStackTrace)
144+
"${info.state} (Last suspension stacktrace, not an actual stacktrace)"
144145
else
145-
coroutineState.state.toString()
146+
info.state.toString()
146147

147148
append("\n\nCoroutine ${owner.delegate}, state: $state")
148149
if (observedStackTrace.isEmpty()) {
149150
append("\n\tat ${createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE)}")
150-
printStackTrace(coroutineState.creationStackTrace)
151+
printStackTrace(info.creationStackTrace)
151152
} else {
152153
printStackTrace(enhancedStackTrace)
153154
}
154155
}
155156
}
156157

157158
/**
158-
* Tries to enhance [coroutineTrace] (obtained by call to [CoroutineState.lastObservedStackTrace]) with
159-
* thread dump of [CoroutineState.lastObservedThread].
159+
* Tries to enhance [coroutineTrace] (obtained by call to [CoroutineInfo.lastObservedStackTrace]) with
160+
* thread dump of [CoroutineInfo.lastObservedThread].
160161
*
161162
* Returns [coroutineTrace] if enhancement was unsuccessful or the enhancement result.
162163
*/
163164
private fun enhanceStackTraceWithThreadDump(
164-
state: CoroutineState,
165+
info: CoroutineInfo,
165166
coroutineTrace: List<StackTraceElement>
166167
): List<StackTraceElement> {
167-
val thread = state.lastObservedThread
168-
if (state.state != State.RUNNING || thread == null) return coroutineTrace
168+
val thread = info.lastObservedThread
169+
if (info.state != State.RUNNING || thread == null) return coroutineTrace
169170
// Avoid security manager issues
170171
val actualTrace = runCatching { thread.stackTrace }.getOrNull()
171172
?: return coroutineTrace
@@ -259,7 +260,6 @@ internal object DebugProbesImpl {
259260
internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, State.SUSPENDED)
260261

261262
private fun updateState(frame: Continuation<*>, state: State) {
262-
if (!isInstalled) return
263263
// KT-29997 is here only since 1.3.30
264264
if (state == State.RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) {
265265
updateRunningState(frame, state)
@@ -271,17 +271,18 @@ internal object DebugProbesImpl {
271271
updateState(owner, frame, state)
272272
}
273273

274-
@Synchronized // See comment to stateCache
274+
@Synchronized // See comment to infoCache
275275
private fun updateRunningState(continuation: Continuation<*>, state: State) {
276+
if (!isInstalled) return
276277
val frame = continuation as? CoroutineStackFrame ?: return
277-
val coroutineState = stateCache.remove(frame) ?: frame.owner()?.state ?: return
278-
// Do not cache states for proxy-classes such as ScopeCoroutines
278+
val info = infoCache.remove(frame) ?: frame.owner()?.info ?: return
279+
// Do not cache it for proxy-classes such as ScopeCoroutines
279280
val caller = frame.realCaller()
280281
if (caller != null) {
281-
stateCache[caller] = coroutineState
282+
infoCache[caller] = info
282283
}
283284

284-
coroutineState.updateState(state, continuation)
285+
info.updateState(state, continuation)
285286
}
286287

287288
private tailrec fun CoroutineStackFrame.realCaller(): CoroutineStackFrame? {
@@ -292,12 +293,11 @@ internal object DebugProbesImpl {
292293

293294
@Synchronized
294295
private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: State) {
295-
val coroutineState = owner.state
296-
coroutineState.updateState(state, frame)
296+
if (!isInstalled) return
297+
owner.info.updateState(state, frame)
297298
}
298299

299-
private fun Continuation<*>.owner(): CoroutineOwner<*>? =
300-
(this as? CoroutineStackFrame)?.owner()
300+
private fun Continuation<*>.owner(): CoroutineOwner<*>? = (this as? CoroutineStackFrame)?.owner()
301301

302302
private tailrec fun CoroutineStackFrame.owner(): CoroutineOwner<*>? =
303303
if (this is CoroutineOwner<*>) this else callerFrame?.owner()
@@ -328,25 +328,26 @@ internal object DebugProbesImpl {
328328
}
329329

330330
@Synchronized
331-
private fun <T> createOwner(completion: Continuation<T>, frame: CoroutineStackFrame): CoroutineOwner<T> {
332-
val state = CoroutineState(completion, frame, ++sequenceNumber)
333-
val owner = CoroutineOwner(completion, state, frame)
331+
private fun <T> createOwner(completion: Continuation<T>, frame: CoroutineStackFrame): Continuation<T> {
332+
if (!isInstalled) return completion
333+
val info = CoroutineInfo(completion.context, frame, ++sequenceNumber)
334+
val owner = CoroutineOwner(completion, info, frame)
334335
capturedCoroutines += owner
335336
return owner
336337
}
337338

338339
@Synchronized
339-
private fun probeCoroutineCompleted(coroutine: CoroutineOwner<*>) {
340-
capturedCoroutines.remove(coroutine)
340+
private fun probeCoroutineCompleted(owner: CoroutineOwner<*>) {
341+
capturedCoroutines.remove(owner)
341342
}
342343

343344
/**
344345
* This class is injected as completion of all continuations in [probeCoroutineCompleted].
345-
* It is owning the coroutine state and responsible for managing all its external state related to debug agent.
346+
* It is owning the coroutine info and responsible for managing all its external info related to debug agent.
346347
*/
347348
private class CoroutineOwner<T>(
348349
@JvmField val delegate: Continuation<T>,
349-
@JvmField val state: CoroutineState,
350+
@JvmField val info: CoroutineInfo,
350351
frame: CoroutineStackFrame
351352
) : Continuation<T> by delegate, CoroutineStackFrame by frame {
352353
override fun resumeWith(result: Result<T>) {

kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ internal class CoroutinesTimeoutStatement(
7373

7474
private fun cancelIfNecessary() {
7575
if (cancelOnTimeout) {
76-
DebugProbes.dumpCoroutinesState().forEach {
77-
it.jobOrNull?.cancel()
76+
DebugProbes.dumpCoroutinesInfo().forEach {
77+
it.job?.cancel()
7878
}
7979
}
8080
}

0 commit comments

Comments
 (0)