You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
IDEA-352355 Asynchronous stack traces for flows in the IDEA debugger
Patched during cherry-pick to 1.10.1. File with conflicts: BufferedChannel.kt
Squashed commits as of version 1.8.0-intellij-11
Don't make unnecessary NULL unboxings
(cherry picked from commit 0267812)
Don't wrap suspend function
(cherry picked from commit 8684cad)
Get rid of changes in the public API -- bring back CoroutineChannel support
(cherry picked from commit 9836a6b)
Get rid of changes in the public API
(cherry picked from commit f852554)
Support SelectImplementation
This brings support for select-based flow operators, such as `timeout`.
(cherry picked from commit db906d8)
Support BufferedChannel
Before, `buffer` operations and such were only supported partially, and in some cases async stack traces were working only in 50% collects. For example, when several flows are merged with `flattenMerge` and emit values simultaneously.
This change seems large, changing a lot of lines in BufferedChannel.kt, but most of them are effectively refactoring (propagation of a wrapped value).
(cherry picked from commit 8be4def)
Discard strict double-wrapping check
We decided not to go with it, as it may dump a lot of error messages to a clueless user's console.
(cherry picked from commit 0efa558)
Enhance support for async stack traces in flows
* simplify instrumentation by making a single insertion point source instead of having one in every class
* handle a double-wrapping case which leads to errors; allow agent to choose how to handle it
* support more commonly used operators (such as `scan`, `buffer`, `debounce` with dynamic timeout)
Unfortunately, this change doesn't cover all possible scenarios of using flows, as many of them interoperate with `Channel`s, and it should be addressed separately.
(cherry picked from commit 00cb4e5)
Prepare shared flows for the debugger agent to support async stack traces
The agent needs three entities to establish a proper asynchronous stack traces connection:
- a capture point -- method that indicates the stack trace that precedes the current stack trace;
- an insertion point -- method within the current stack trace;
- a key -- an object that is present in both points and is unique enough to bridge two points properly.
This change tweaks the code a bit to introduce the three entities in MutableSharedFlow and MutableStateFlow.
The key for MutableSharedFlow is the element itself. For MutableSharedFlow, the element is wrapped into a unique object to prevent bridging mistakes when two equal elements are emitted from different places.
(cherry picked from commit 75107bc)
# Conflicts:
# kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt
# Conflicts:
# IntelliJ-patches.md
Copy file name to clipboardExpand all lines: IntelliJ-patches.md
+70Lines changed: 70 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -19,3 +19,73 @@ We provide a single method `kotlinx.coroutines.internal.intellij.IntellijCorouti
19
19
The invariant is that the result of this method is always equal to `coroutineContext` in suspending environment,
20
20
and it does not change during the non-suspending execution within the same thread.
21
21
22
+
## Parallelism compensation for `CoroutineDispatcher`s
23
+
24
+
If `runBlocking` happens to be invoked on a thread from `CoroutineDispatcher`, it may cause a thread starvation problem
25
+
(Kotlin#3983). This happens because `runBlocking` does not release an associated computational permit while it parks the
26
+
thread. To fix this, a parallelism compensation mechanism is introduced. Some `CoroutineDispatcher`s (such as
27
+
`Dispatchers.Default`, `Dispatchers.IO` and others) support `ParallelismCompensation`, meaning that these dispatchers
28
+
can be notified that they should increase parallelism and parallelism limit, or they should decrease it. It is important that these
29
+
are only requests and dispatchers are in full control on how and when they need to adjust the effective parallelism.
30
+
It also means that the instantaneous parallelism may exceed the current allowed parallelism limit for the given dispatcher.
31
+
32
+
`runBlockingWithParallelismCompensation` (further abbreviated as `rBWPC`) is introduced as a counterpart of `runBlocking`
33
+
with the following behavioral change. When `rBWPC` decides to park a `CoroutineDispatcher` thread, it first increases the allowed parallelism
34
+
limit of the `CoroutineDispatcher`. After the thread unparks, `rBWPC` notifies the dispatcher that the parallelism limit should be lowered back.
35
+
A separate function is introduced because parallelism compensation is not always a desirable behavior.
36
+
37
+
It is easy to see that this behavior cannot be general for `CoroutineDispatcher`s, at least because it breaks the contract
38
+
of `LimitedDispatcher` (one that can be acquired via `.limitedParallelism`). It means that parallelism compensation
39
+
cannot work for `LimitedDispatcher`, so `runBlockingWithParallelismCompensation` can still cause starvation issues there, but it seems rather
40
+
expected.
41
+
42
+
Parallelism compensation support is internal and is implemented for `Dispatchers.Default` and `Dispatchers.IO`.
43
+
To acquire an analogue of `limitedParallelism` dispatcher which supports parallelism compensation, use
44
+
`IntellijCoroutines.softLimitedParallelism`. Be advised that not every `.limitedParallelism` call can be substituted
45
+
with `.softLimitedParallelism`, e.g., `.limitedParallelism(1)` may be used as a synchronization manager and in this case
46
+
exceeding the parallelism limit would eliminate this (likely expected) side effect.
47
+
48
+
### API
49
+
-`runBlockingWithParallelismCompensation` - an analogue of `runBlocking` which also compensates parallelism of the
50
+
associated coroutine dispatcher when it decides to park the thread
51
+
-`CoroutineDispatcher.softLimitedParallelism` – an analogue of `.limitedParallelism` which supports
52
+
parallelism compensation
53
+
54
+
## Asynchronous stack traces for flows in the IDEA debugger
55
+
56
+
The agent needs three entities to establish a proper asynchronous stack traces connection:
57
+
- a capture point — method that indicates the stack trace that precedes the current stack trace;
58
+
- an insertion point — method within the current stack trace;
59
+
- a key — an object that is present in both points and is unique enough to bridge two stack traces properly.
60
+
61
+
The key for MutableStateFlow is the element itself. For MutableSharedFlow, the element is wrapped into a unique object to prevent bridging mistakes when two equal elements are emitted from different places.
62
+
63
+
Most of the operators applicable to flows (such as `map`, `scan`, `debounce`, `timeout`, `buffer`) are supported. As some of them use an intermediary flow inside, the transferred values are wrapped and unwrapped the same way as in MutableSharedFlow.
64
+
It means there may be all-library async stack traces between a stack trace containing `emit` and a stack trace containing `collect`.
65
+
66
+
### API
67
+
68
+
Some logic related to instrumentation was extracted to separate methods so that the debugger agent could instrument it properly:
69
+
70
+
-`kotlinx.coroutines.flow.internal.FlowValueWrapperInternal` -- wrapper class used to create a unique object for the debugger agent
71
+
-`kotlinx.coroutines.flow.internal.FlowValueWrapperInternalKt.wrapInternal` -- returns passed argument by default; the agent instruments it to call `wrapInternalDebuggerCapture` instead
72
+
-`kotlinx.coroutines.flow.internal.FlowValueWrapperInternalKt.wrapInternalDebuggerCapture` -- wraps passed arguments into a `FlowValueWrapperInternal`; only used after transformation.
73
+
-`kotlinx.coroutines.flow.internal.FlowValueWrapperInternalKt.unwrapInternal` -- returns passed argument by default; the agent instruments it to call `unwrapInternalDebuggerCapture` instead
74
+
-`kotlinx.coroutines.flow.internal.FlowValueWrapperInternalKt.unwrapInternalDebuggerCapture` -- unwraps passed argument so it returns the original value; only used after transformation
75
+
-`kotlinx.coroutines.flow.internal.FlowValueWrapperInternalKt.unwrapTyped` -- utility function served to ease casting to a real underlying type
76
+
-`kotlinx.coroutines.flow.internal.FlowValueWrapperInternalKt.emitInternal(FlowCollector, value)` -- alternative of a regular `FlowCollector.emit` that supports insertion points; if there is a `FlowCollector`, its `emit` call can be replaced with `emitInternal` so this case would also be supported for constructing async stack traces
77
+
-`kotlinx.coroutines.flow.internal.FlowValueWrapperInternalKt.debuggerCapture` -- common insertion point for a debugger agent; simplifies instrumentation; the value is always being unwrapped inside.
78
+
79
+
One internal method was added to `BufferedChannel`: `emitAllInternal`. This method ensures the value will be unwrapped in an insertion point.
80
+
81
+
One internal method was added to `flow/Channels.kt`: `emitAllInternal`. It emits all values, like usual, but also considers wrapping/unwrapping supported in `BufferedChannel`.
82
+
83
+
One internal method was added to `ChannelCoroutine`: `emitAllInternal` serves to bridge its delegate and the method above.
84
+
85
+
One internal method was added to `BufferedChannelIterator`: `nextInternal` -- same as `next` but may return a wrapped value. It should only be used with a function that is capable of unwrapping the value (see `BufferedChannel.emitAll` and `BufferedChannelIterator.next`), so there's a guarantee a wrapped value will always unwrap before emitting.
86
+
87
+
Why not just let `next` return a maybe wrapped value? That's because it is heavily used outside a currently supported scope. For example, one may just indirectly call it from a for-loop. In this case, unwrapping will never happen, and a user will get a handful of `ClassCastException`s.
88
+
89
+
Changes were made to lambda parameter `onElementRetrieved` in `BufferedChannel<E>` methods: now they accept `Any?` instead of `E` because now they may be given a wrapped value.
90
+
91
+
`SelectImplementation.complete` now uses `debuggerCapture` to properly propagate value that might come from flows.
0 commit comments