Skip to content

Commit 321280c

Browse files
committed
Speed-up installed debug probes by splitting global probes lock to RW-lock, guard all state transitions with read lock and all read operations with write lock to guarantee a consistent snapshot
1 parent 737ad1e commit 321280c

File tree

2 files changed

+33
-26
lines changed

2 files changed

+33
-26
lines changed

kotlinx-coroutines-debug/src/DebugProbes.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,5 @@ public object DebugProbes {
132132
internal fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame)
133133

134134
internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame)
135-
internal fun <T> probeCoroutineCreated(completion: kotlin.coroutines.Continuation<T>): kotlin.coroutines.Continuation<T> =
135+
internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> =
136136
DebugProbesImpl.probeCoroutineCreated(completion)

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

+32-25
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.coroutines.debug.internal
66

7+
import kotlinx.atomicfu.*
78
import kotlinx.coroutines.*
89
import kotlinx.coroutines.debug.*
910
import net.bytebuddy.*
@@ -12,8 +13,10 @@ import net.bytebuddy.dynamic.loading.*
1213
import java.io.*
1314
import java.text.*
1415
import java.util.*
16+
import java.util.concurrent.*
17+
import java.util.concurrent.locks.*
1518
import kotlin.collections.ArrayList
16-
import kotlin.collections.HashMap
19+
import kotlin.concurrent.*
1720
import kotlin.coroutines.*
1821
import kotlin.coroutines.jvm.internal.*
1922
import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // IDEA bug workaround
@@ -26,12 +29,20 @@ import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // I
2629
internal object DebugProbesImpl {
2730
private const val ARTIFICIAL_FRAME_MESSAGE = "Coroutine creation stacktrace"
2831
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>())
3033
@Volatile
3134
private var installations = 0
3235
internal val isInstalled: Boolean get() = installations > 0
3336
// 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()
3546

3647
/*
3748
* This is an optimization in the face of KT-29997:
@@ -41,10 +52,9 @@ internal object DebugProbesImpl {
4152
* Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth).
4253
* To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally.
4354
*/
44-
private val callerInfoCache = HashMap<CoroutineStackFrame, CoroutineInfo>()
55+
private val callerInfoCache = ConcurrentHashMap<CoroutineStackFrame, CoroutineInfo>()
4556

46-
@Synchronized
47-
public fun install() {
57+
public fun install(): Unit = coroutineStateLock.write {
4858
if (++installations > 1) return
4959

5060
ByteBuddyAgent.install()
@@ -58,8 +68,7 @@ internal object DebugProbesImpl {
5868
.load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent())
5969
}
6070

61-
@Synchronized
62-
public fun uninstall() {
71+
public fun uninstall(): Unit = coroutineStateLock.write {
6372
check(isInstalled) { "Agent was not installed" }
6473
if (--installations != 0) return
6574

@@ -75,8 +84,7 @@ internal object DebugProbesImpl {
7584
.load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent())
7685
}
7786

78-
@Synchronized
79-
public fun hierarchyToString(job: Job): String {
87+
public fun hierarchyToString(job: Job): String = coroutineStateLock.write {
8088
check(isInstalled) { "Debug probes are not installed" }
8189
val jobToStack = capturedCoroutines
8290
.filter { it.delegate.context[Job] != null }
@@ -114,28 +122,26 @@ internal object DebugProbesImpl {
114122
@Suppress("DEPRECATION_ERROR") // JobSupport
115123
private val Job.debugString: String get() = if (this is JobSupport) toDebugString() else toString()
116124

117-
@Synchronized
118-
public fun dumpCoroutinesInfo(): List<CoroutineInfo> {
125+
public fun dumpCoroutinesInfo(): List<CoroutineInfo> = coroutineStateLock.write {
119126
check(isInstalled) { "Debug probes are not installed" }
120127
return capturedCoroutines.asSequence()
121128
.map { it.info.copy() } // Copy as CoroutineInfo can be mutated concurrently by DebugProbes
122129
.sortedBy { it.sequenceNumber }
123130
.toList()
124131
}
125132

126-
public fun dumpCoroutines(out: PrintStream) = synchronized(out) {
133+
public fun dumpCoroutines(out: PrintStream): Unit = synchronized(out) {
127134
/*
128135
* 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.
130137
* 2) Synchronization on `out` is not required, but prohibits interleaving with any other
131138
* (asynchronous) attempt to write to this `out` (System.out by default).
132139
* Yet this prevents the progress of coroutines until they are fully dumped to the out which we find acceptable compromise.
133140
*/
134141
dumpCoroutinesSynchronized(out)
135142
}
136143

137-
@Synchronized
138-
private fun dumpCoroutinesSynchronized(out: PrintStream) {
144+
private fun dumpCoroutinesSynchronized(out: PrintStream): Unit = coroutineStateLock.write {
139145
check(isInstalled) { "Debug probes are not installed" }
140146
out.print("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}")
141147
capturedCoroutines
@@ -277,8 +283,8 @@ internal object DebugProbesImpl {
277283
updateState(owner, frame, state)
278284
}
279285

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 {
282288
if (!isInstalled) return
283289
// Lookup coroutine info in cache or by traversing stack frame
284290
val info: CoroutineInfo
@@ -288,7 +294,8 @@ internal object DebugProbesImpl {
288294
} else {
289295
info = frame.owner()?.info ?: return
290296
// 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)
292299
}
293300

294301
info.updateState(state, frame as Continuation<*>)
@@ -302,8 +309,7 @@ internal object DebugProbesImpl {
302309
return if (caller.getStackTraceElement() != null) caller else caller.realCaller()
303310
}
304311

305-
@Synchronized
306-
private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: State) {
312+
private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: State) = coroutineStateLock.read {
307313
if (!isInstalled) return
308314
owner.info.updateState(state, frame)
309315
}
@@ -313,6 +319,7 @@ internal object DebugProbesImpl {
313319
private tailrec fun CoroutineStackFrame.owner(): CoroutineOwner<*>? =
314320
if (this is CoroutineOwner<*>) this else callerFrame?.owner()
315321

322+
// Not guarded by the lock at all, does not really affect consistency
316323
internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
317324
if (!isInstalled) return completion
318325
/*
@@ -338,23 +345,23 @@ internal object DebugProbesImpl {
338345
return createOwner(completion, frame)
339346
}
340347

341-
@Synchronized
342348
private fun <T> createOwner(completion: Continuation<T>, frame: CoroutineStackFrame): Continuation<T> {
343349
if (!isInstalled) return completion
344-
val info = CoroutineInfo(completion.context, frame, ++sequenceNumber)
350+
val info = CoroutineInfo(completion.context, frame, sequenceNumber.incrementAndGet())
345351
val owner = CoroutineOwner(completion, info, frame)
346352
capturedCoroutines += owner
353+
if (!isInstalled) capturedCoroutines.clear()
347354
return owner
348355
}
349356

350-
@Synchronized
357+
// Not guarded by the lock at all, does not really affect consistency
351358
private fun probeCoroutineCompleted(owner: CoroutineOwner<*>) {
352359
capturedCoroutines.remove(owner)
353360
/*
354361
* This removal is a guard against improperly implemented CoroutineStackFrame
355362
* and bugs in the compiler.
356363
*/
357-
val caller = owner.info.lastObservedFrame?.realCaller()
364+
val caller = owner.info.lastObservedFrame?.realCaller() ?: return
358365
callerInfoCache.remove(caller)
359366
}
360367

0 commit comments

Comments
 (0)