4
4
5
5
package kotlinx.coroutines.debug.internal
6
6
7
+ import kotlinx.atomicfu.*
7
8
import kotlinx.coroutines.*
8
9
import kotlinx.coroutines.debug.*
9
10
import net.bytebuddy.*
@@ -12,8 +13,10 @@ import net.bytebuddy.dynamic.loading.*
12
13
import java.io.*
13
14
import java.text.*
14
15
import java.util.*
16
+ import java.util.concurrent.*
17
+ import java.util.concurrent.locks.*
15
18
import kotlin.collections.ArrayList
16
- import kotlin.collections.HashMap
19
+ import kotlin.concurrent.*
17
20
import kotlin.coroutines.*
18
21
import kotlin.coroutines.jvm.internal.*
19
22
import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // IDEA bug workaround
@@ -26,12 +29,20 @@ import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // I
26
29
internal object DebugProbesImpl {
27
30
private const val ARTIFICIAL_FRAME_MESSAGE = " Coroutine creation stacktrace"
28
31
private val dateFormat = SimpleDateFormat (" yyyy/MM/dd HH:mm:ss" )
29
- private val capturedCoroutines = HashSet <CoroutineOwner <* >>( )
32
+ private val capturedCoroutines = Collections .newSetFromMap( ConcurrentHashMap <CoroutineOwner <* >, Boolean > () )
30
33
@Volatile
31
34
private var installations = 0
32
35
internal val isInstalled: Boolean get() = installations > 0
33
36
// To sort coroutines by creation order, used as unique id
34
- private var sequenceNumber: Long = 0
37
+ private val sequenceNumber = atomic(0L )
38
+ /*
39
+ * RW-lock that guards all debug probes state changes.
40
+ * All individual coroutine state transitions are guarded by read-lock
41
+ * and do not interfere with each other.
42
+ * All state reads are guarded by the write lock to guarantee a strongly-consistent
43
+ * snapshot of the system.
44
+ */
45
+ private val coroutineStateLock = ReentrantReadWriteLock ()
35
46
36
47
/*
37
48
* This is an optimization in the face of KT-29997:
@@ -41,10 +52,9 @@ internal object DebugProbesImpl {
41
52
* Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth).
42
53
* To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally.
43
54
*/
44
- private val callerInfoCache = HashMap <CoroutineStackFrame , CoroutineInfo >()
55
+ private val callerInfoCache = ConcurrentHashMap <CoroutineStackFrame , CoroutineInfo >()
45
56
46
- @Synchronized
47
- public fun install () {
57
+ public fun install (): Unit = coroutineStateLock.write {
48
58
if (++ installations > 1 ) return
49
59
50
60
ByteBuddyAgent .install()
@@ -58,8 +68,7 @@ internal object DebugProbesImpl {
58
68
.load(cl.classLoader, ClassReloadingStrategy .fromInstalledAgent())
59
69
}
60
70
61
- @Synchronized
62
- public fun uninstall () {
71
+ public fun uninstall (): Unit = coroutineStateLock.write {
63
72
check(isInstalled) { " Agent was not installed" }
64
73
if (-- installations != 0 ) return
65
74
@@ -75,8 +84,7 @@ internal object DebugProbesImpl {
75
84
.load(cl.classLoader, ClassReloadingStrategy .fromInstalledAgent())
76
85
}
77
86
78
- @Synchronized
79
- public fun hierarchyToString (job : Job ): String {
87
+ public fun hierarchyToString (job : Job ): String = coroutineStateLock.write {
80
88
check(isInstalled) { " Debug probes are not installed" }
81
89
val jobToStack = capturedCoroutines
82
90
.filter { it.delegate.context[Job ] != null }
@@ -114,28 +122,26 @@ internal object DebugProbesImpl {
114
122
@Suppress(" DEPRECATION_ERROR" ) // JobSupport
115
123
private val Job .debugString: String get() = if (this is JobSupport ) toDebugString() else toString()
116
124
117
- @Synchronized
118
- public fun dumpCoroutinesInfo (): List <CoroutineInfo > {
125
+ public fun dumpCoroutinesInfo (): List <CoroutineInfo > = coroutineStateLock.write {
119
126
check(isInstalled) { " Debug probes are not installed" }
120
127
return capturedCoroutines.asSequence()
121
128
.map { it.info.copy() } // Copy as CoroutineInfo can be mutated concurrently by DebugProbes
122
129
.sortedBy { it.sequenceNumber }
123
130
.toList()
124
131
}
125
132
126
- public fun dumpCoroutines (out : PrintStream ) = synchronized(out ) {
133
+ public fun dumpCoroutines (out : PrintStream ): Unit = synchronized(out ) {
127
134
/*
128
135
* This method synchronizes both on `out` and `this` for a reason:
129
- * 1) Synchronization on `this` is required to have a consistent snapshot of coroutines.
136
+ * 1) Taking a write lock is required to have a consistent snapshot of coroutines.
130
137
* 2) Synchronization on `out` is not required, but prohibits interleaving with any other
131
138
* (asynchronous) attempt to write to this `out` (System.out by default).
132
139
* Yet this prevents the progress of coroutines until they are fully dumped to the out which we find acceptable compromise.
133
140
*/
134
141
dumpCoroutinesSynchronized(out )
135
142
}
136
143
137
- @Synchronized
138
- private fun dumpCoroutinesSynchronized (out : PrintStream ) {
144
+ private fun dumpCoroutinesSynchronized (out : PrintStream ): Unit = coroutineStateLock.write {
139
145
check(isInstalled) { " Debug probes are not installed" }
140
146
out .print (" Coroutines dump ${dateFormat.format(System .currentTimeMillis())} " )
141
147
capturedCoroutines
@@ -277,8 +283,8 @@ internal object DebugProbesImpl {
277
283
updateState(owner, frame, state)
278
284
}
279
285
280
- @Synchronized // See comment to callerInfoCache
281
- private fun updateRunningState (frame : CoroutineStackFrame , state : State ) {
286
+ // See comment to callerInfoCache
287
+ private fun updateRunningState (frame : CoroutineStackFrame , state : State ): Unit = coroutineStateLock.read {
282
288
if (! isInstalled) return
283
289
// Lookup coroutine info in cache or by traversing stack frame
284
290
val info: CoroutineInfo
@@ -288,7 +294,8 @@ internal object DebugProbesImpl {
288
294
} else {
289
295
info = frame.owner()?.info ? : return
290
296
// Guard against improper implementations of CoroutineStackFrame and bugs in the compiler
291
- callerInfoCache.remove(info.lastObservedFrame?.realCaller())
297
+ val realCaller = info.lastObservedFrame?.realCaller()
298
+ if (realCaller != null ) callerInfoCache.remove(realCaller)
292
299
}
293
300
294
301
info.updateState(state, frame as Continuation <* >)
@@ -302,8 +309,7 @@ internal object DebugProbesImpl {
302
309
return if (caller.getStackTraceElement() != null ) caller else caller.realCaller()
303
310
}
304
311
305
- @Synchronized
306
- private fun updateState (owner : CoroutineOwner <* >, frame : Continuation <* >, state : State ) {
312
+ private fun updateState (owner : CoroutineOwner <* >, frame : Continuation <* >, state : State ) = coroutineStateLock.read {
307
313
if (! isInstalled) return
308
314
owner.info.updateState(state, frame)
309
315
}
@@ -313,6 +319,7 @@ internal object DebugProbesImpl {
313
319
private tailrec fun CoroutineStackFrame.owner (): CoroutineOwner <* >? =
314
320
if (this is CoroutineOwner <* >) this else callerFrame?.owner()
315
321
322
+ // Not guarded by the lock at all, does not really affect consistency
316
323
internal fun <T > probeCoroutineCreated (completion : Continuation <T >): Continuation <T > {
317
324
if (! isInstalled) return completion
318
325
/*
@@ -338,23 +345,23 @@ internal object DebugProbesImpl {
338
345
return createOwner(completion, frame)
339
346
}
340
347
341
- @Synchronized
342
348
private fun <T > createOwner (completion : Continuation <T >, frame : CoroutineStackFrame ): Continuation <T > {
343
349
if (! isInstalled) return completion
344
- val info = CoroutineInfo (completion.context, frame, ++ sequenceNumber)
350
+ val info = CoroutineInfo (completion.context, frame, sequenceNumber.incrementAndGet() )
345
351
val owner = CoroutineOwner (completion, info, frame)
346
352
capturedCoroutines + = owner
353
+ if (! isInstalled) capturedCoroutines.clear()
347
354
return owner
348
355
}
349
356
350
- @Synchronized
357
+ // Not guarded by the lock at all, does not really affect consistency
351
358
private fun probeCoroutineCompleted (owner : CoroutineOwner <* >) {
352
359
capturedCoroutines.remove(owner)
353
360
/*
354
361
* This removal is a guard against improperly implemented CoroutineStackFrame
355
362
* and bugs in the compiler.
356
363
*/
357
- val caller = owner.info.lastObservedFrame?.realCaller()
364
+ val caller = owner.info.lastObservedFrame?.realCaller() ? : return
358
365
callerInfoCache.remove(caller)
359
366
}
360
367
0 commit comments