From 6e810837e9b19a403446b49108a10fdf7739d590 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 26 Apr 2019 13:16:56 +0300 Subject: [PATCH 01/56] Expand only package.json file during prepareNodePackage, otherwise files like webpack configs may become corrupted --- gradle/node-js.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gradle/node-js.gradle b/gradle/node-js.gradle index 046ffd3932..208f4ad293 100644 --- a/gradle/node-js.gradle +++ b/gradle/node-js.gradle @@ -15,12 +15,16 @@ node { task prepareNodePackage(type: Copy) { from("npm") { + include 'package.json' // Postpone expansion of package.json until we configure version property in build.gradle def copySpec = it afterEvaluate { copySpec.expand(project.properties + [kotlinDependency: ""]) } } + from("npm") { + exclude 'package.json' + } into "$node.nodeModulesDir" } From f8eac76dee6318b3f87a03686ea336e6ac36fe32 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Sat, 27 Apr 2019 17:44:00 +0300 Subject: [PATCH 02/56] Rename transform parameters for consistency with stdlib transformer -> transform --- .../common/src/flow/operators/Transform.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index f75f3df8fb..3818efeb16 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -12,8 +12,8 @@ import kotlin.jvm.* import kotlinx.coroutines.flow.unsafeFlow as flow /** - * Applies [transformer] function to each value of the given flow. - * [transformer] is a generic function that may transform emitted element, skip it or emit it multiple times. + * Applies [transform] function to each value of the given flow. + * [transform] is a generic function that may transform emitted element, skip it or emit it multiple times. * * This operator is useless by itself, but can be used as a building block of user-specific operators: * ``` @@ -26,10 +26,10 @@ import kotlinx.coroutines.flow.unsafeFlow as flow * ``` */ @FlowPreview -public fun Flow.transform(@BuilderInference transformer: suspend FlowCollector.(value: T) -> Unit): Flow { +public fun Flow.transform(@BuilderInference transform: suspend FlowCollector.(value: T) -> Unit): Flow { return flow { collect { value -> - transformer(value) + transform(value) } } } @@ -70,17 +70,19 @@ public fun Flow.filterNotNull(): Flow = flow { } /** - * Returns a flow containing the results of applying the given [transformer] function to each value of the original flow. + * Returns a flow containing the results of applying the given [transform] function to each value of the original flow. */ @FlowPreview -public fun Flow.map(transformer: suspend (value: T) -> R): Flow = transform { value -> emit(transformer(value)) } +public fun Flow.map(transform: suspend (value: T) -> R): Flow = transform { value -> + emit(transform(value)) +} /** - * Returns a flow that contains only non-null results of applying the given [transformer] function to each value of the original flow. + * Returns a flow that contains only non-null results of applying the given [transform] function to each value of the original flow. */ @FlowPreview -public fun Flow.mapNotNull(transformer: suspend (value: T) -> R?): Flow = transform { value -> - val transformed = transformer(value) ?: return@transform +public fun Flow.mapNotNull(transform: suspend (value: T) -> R?): Flow = transform { value -> + val transformed = transform(value) ?: return@transform emit(transformed) } From e569bd318cfe4b6f2d8223947b363adf4bc449ce Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 30 Apr 2019 16:57:49 +0300 Subject: [PATCH 03/56] Fix exception types for channels to ensure transparency & reporting (#1158) * Fix exception types for channels to ensure transparency & reporting * ReceiveChannel.cancel always closes channel with CancellationException, so sending or receiving from a cancelled channel produces the corresponding CancellationException. * Cancelling produce builder has similar effect, but an more specific instance of JobCancellationException is created. * This ensure that produce/consumeEach pair is transparent with respect to cancellation and can be used to build "identity" transformation of the flow (the corresponding test is added). * ClosedSendChannelException is now a subclass of IllegalStateException, so that trying to send to a channel that was closed normally is reported as program error and is not eaten (test is added). Fixes #957 Fixes #1128 * Exceptions for channels cleanup * Documentation improved * Better exception message * Simplified flowOn implementation * Avoid exception instantiation on happy path in zip --- .../kotlinx-coroutines-core.txt | 2 +- .../common/src/channels/AbstractChannel.kt | 2 +- .../common/src/channels/Channel.kt | 18 ++++-- .../common/src/channels/ChannelCoroutine.kt | 11 +++- .../src/flow/internal/AbortFlowException.kt | 16 +++++ .../common/src/flow/operators/Context.kt | 13 +--- .../common/src/flow/operators/Limit.kt | 16 ++--- .../common/src/flow/operators/Zip.kt | 25 ++++---- .../channels/ArrayBroadcastChannelTest.kt | 14 ++-- .../common/test/channels/ArrayChannelTest.kt | 2 +- .../test/channels/BasicOperationsTest.kt | 23 ++++++- .../test/channels/ConflatedChannelTest.kt | 2 +- .../test/channels/LinkedListChannelTest.kt | 2 +- .../common/test/channels/ProduceTest.kt | 5 +- .../test/channels/RendezvousChannelTest.kt | 2 +- .../common/test/flow/IdFlowTest.kt | 64 +++++++++++++++++++ .../common/test/flow/operators/ZipTest.kt | 2 - .../channels/DoubleChannelCloseStressTest.kt | 6 +- .../test/channels/TickerChannelCommonTest.kt | 3 +- .../test/exceptions/ProduceExceptionsTest.kt | 4 +- 20 files changed, 166 insertions(+), 66 deletions(-) create mode 100644 kotlinx-coroutines-core/common/src/flow/internal/AbortFlowException.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/IdFlowTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index b1375b257e..366906acd6 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -692,7 +692,7 @@ public final class kotlinx/coroutines/channels/ClosedReceiveChannelException : j public fun (Ljava/lang/String;)V } -public final class kotlinx/coroutines/channels/ClosedSendChannelException : java/util/concurrent/CancellationException { +public final class kotlinx/coroutines/channels/ClosedSendChannelException : java/lang/IllegalStateException { public fun (Ljava/lang/String;)V } diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 05bfbca98d..f3fb7cd288 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -669,7 +669,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel { /** * Cancels reception of remaining elements from this channel with an optional [cause]. * This function closes the channel and removes all buffered sent elements from it. + * * A cause can be used to specify an error message or to provide other details on * a cancellation reason for debugging purposes. + * If the cause is not specified, then an instance of [CancellationException] with a + * default message is created to [close][SendChannel.close] the channel. * * Immediately after invocation of this function [isClosedForReceive] and * [isClosedForSend][SendChannel.isClosedForSend] - * on the side of [SendChannel] start returning `true`, so all attempts to send to this channel - * afterwards will throw [ClosedSendChannelException], while attempts to receive will throw - * [ClosedReceiveChannelException]. + * on the side of [SendChannel] start returning `true`. All attempts to send to this channel + * or receive from this channel will throw [CancellationException]. */ public fun cancel(cause: CancellationException? = null) @@ -382,14 +384,18 @@ public fun Channel(capacity: Int = RENDEZVOUS): Channel = * Indicates attempt to [send][SendChannel.send] on [isClosedForSend][SendChannel.isClosedForSend] channel * that was closed without a cause. A _failed_ channel rethrows the original [close][SendChannel.close] cause * exception on send attempts. + * + * This exception is a subclass of [IllegalStateException] because, conceptually, sender is responsible + * for closing the channel and not be trying to send anything after the channel was close. Attempts to + * send into the closed channel indicate logical error in the sender's code. */ -public class ClosedSendChannelException(message: String?) : CancellationException(message) +public class ClosedSendChannelException(message: String?) : IllegalStateException(message) /** * Indicates attempt to [receive][ReceiveChannel.receive] on [isClosedForReceive][ReceiveChannel.isClosedForReceive] * channel that was closed without a cause. A _failed_ channel rethrows the original [close][SendChannel.close] cause * exception on receive attempts. * - * This exception is subclass of [NoSuchElementException] to be consistent with plain collections. + * This exception is a subclass of [NoSuchElementException] to be consistent with plain collections. */ -public class ClosedReceiveChannelException(message: String?) : NoSuchElementException(message) +public class ClosedReceiveChannelException(message: String?) : NoSuchElementException(message) \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt index 9382600af8..dc2cc5b69f 100644 --- a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt @@ -28,8 +28,15 @@ internal open class ChannelCoroutine( } override fun cancelInternal(cause: Throwable?): Boolean { - _channel.cancel(cause?.toCancellationException()) // cancel the channel - cancelCoroutine(cause) // cancel the job + val exception = cause?.toCancellationException() + ?: JobCancellationException("$classSimpleName was cancelled", null, this) + _channel.cancel(exception) // cancel the channel + cancelCoroutine(exception) // cancel the job return true // does not matter - result is used in DEPRECATED functions only } + + @Suppress("UNCHECKED_CAST") + suspend fun sendFair(element: E) { + (_channel as AbstractSendChannel).sendFair(element) + } } diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbortFlowException.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbortFlowException.kt new file mode 100644 index 0000000000..e432c18f8c --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/AbortFlowException.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.coroutines.* + +/** + * This exception is thrown when operator need no more elements from the flow. + * This exception should never escape outside of operator's implementation. + */ +internal class AbortFlowException : CancellationException("Flow was aborted, no more elements needed") { + // TODO expect/actual + // override fun fillInStackTrace(): Throwable = this +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 6f676d27a9..3dc021b635 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -53,17 +53,8 @@ public fun Flow.flowOn(flowContext: CoroutineContext, bufferSize: Int = 1 send(value) } } - - // TODO semantics doesn't play well here and we pay for that with additional object - (channel as Job).invokeOnCompletion { if (it is CancellationException && it.cause == null) cancel() } - for (element in channel) { - emit(element) - } - - val producer = channel as Job - if (producer.isCancelled) { - producer.join() - throw producer.getCancellationException() + channel.consumeEach { value -> + emit(value) } } } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt index 80ef4b66e6..c60d105b02 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -8,9 +8,10 @@ package kotlinx.coroutines.flow -import kotlinx.coroutines.flow.unsafeFlow as flow import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* +import kotlinx.coroutines.flow.unsafeFlow as flow /** * Returns a flow that ignores first [count] elements. @@ -57,10 +58,10 @@ public fun Flow.take(count: Int): Flow { collect { value -> emit(value) if (++consumed == count) { - throw TakeLimitException() + throw AbortFlowException() } } - } catch (e: TakeLimitException) { + } catch (e: AbortFlowException) { // Nothing, bail out } } @@ -74,14 +75,9 @@ public fun Flow.takeWhile(predicate: suspend (T) -> Boolean): Flow = f try { collect { value -> if (predicate(value)) emit(value) - else throw TakeLimitException() + else throw AbortFlowException() } - } catch (e: TakeLimitException) { + } catch (e: AbortFlowException) { // Nothing, bail out } } - -private class TakeLimitException : CancellationException("Flow limit is reached, cancelling") { - // TODO expect/actual - // override fun fillInStackTrace(): Throwable = this -} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt index ab0bee3f96..ea46a09833 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt @@ -78,7 +78,7 @@ public fun Flow.combineLatest(other: Flow, transform: suspen private inline fun SelectBuilder.onReceive( isClosed: Boolean, - channel: Channel, + channel: ReceiveChannel, crossinline onClosed: () -> Unit, noinline onReceive: suspend (value: Any) -> Unit ) { @@ -90,18 +90,11 @@ private inline fun SelectBuilder.onReceive( } // Channel has any type due to onReceiveOrNull. This will be fixed after receiveOrClosed -private fun CoroutineScope.asFairChannel(flow: Flow<*>): Channel { - val channel = RendezvousChannel() // Explicit type - launch { - try { - flow.collect { value -> - channel.sendFair(value ?: NullSurrogate) - } - } finally { - channel.close() - } +private fun CoroutineScope.asFairChannel(flow: Flow<*>): ReceiveChannel = produce { + val channel = channel as ChannelCoroutine + flow.collect { value -> + channel.sendFair(value ?: NullSurrogate) } - return channel } @@ -133,7 +126,9 @@ public fun Flow.zip(other: Flow, transform: suspend (T1, T2) * * Invariant: this clause is invoked only when all elements from the channel were processed (=> rendezvous restriction). */ - (second as SendChannel<*>).invokeOnClose { first.cancel() } + (second as SendChannel<*>).invokeOnClose { + if (!first.isClosedForReceive) first.cancel(AbortFlowException()) + } val otherIterator = second.iterator() try { @@ -144,8 +139,10 @@ public fun Flow.zip(other: Flow, transform: suspend (T1, T2) val secondValue = NullSurrogate.unbox(otherIterator.next()) emit(transform(NullSurrogate.unbox(value), NullSurrogate.unbox(secondValue))) } + } catch (e: AbortFlowException) { + // complete } finally { - second.cancel() + if (!second.isClosedForReceive) second.cancel(AbortFlowException()) } } } diff --git a/kotlinx-coroutines-core/common/test/channels/ArrayBroadcastChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ArrayBroadcastChannelTest.kt index 9867ead560..a7084296bb 100644 --- a/kotlinx-coroutines-core/common/test/channels/ArrayBroadcastChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ArrayBroadcastChannelTest.kt @@ -170,23 +170,25 @@ class ArrayBroadcastChannelTest : TestBase() { // start consuming val sub = channel.openSubscription() var expected = 0 - sub.consumeEach { - check(it == ++expected) - if (it == 2) { - sub.cancel() + assertFailsWith { + sub.consumeEach { + check(it == ++expected) + if (it == 2) { + sub.cancel() + } } } check(expected == 2) } @Test - fun testReceiveFromClosedSub() = runTest({ it is ClosedReceiveChannelException }) { + fun testReceiveFromCancelledSub() = runTest { val channel = BroadcastChannel(1) val sub = channel.openSubscription() assertFalse(sub.isClosedForReceive) sub.cancel() assertTrue(sub.isClosedForReceive) - sub.receive() + assertFailsWith { sub.receive() } } @Test diff --git a/kotlinx-coroutines-core/common/test/channels/ArrayChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ArrayChannelTest.kt index bcff1edfa0..2b948dfa25 100644 --- a/kotlinx-coroutines-core/common/test/channels/ArrayChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ArrayChannelTest.kt @@ -134,7 +134,7 @@ class ArrayChannelTest : TestBase() { q.cancel() check(q.isClosedForSend) check(q.isClosedForReceive) - check(q.receiveOrNull() == null) + assertFailsWith { q.receiveOrNull() } finish(12) } diff --git a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt index a5e180aeee..820fad67f6 100644 --- a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.* import kotlin.test.* class BasicOperationsTest : TestBase() { - @Test fun testSimpleSendReceive() = runTest { // Parametrized common test :( @@ -20,6 +19,11 @@ class BasicOperationsTest : TestBase() { TestChannelKind.values().forEach { kind -> testOffer(kind) } } + @Test + fun testSendAfterClose() = runTest { + TestChannelKind.values().forEach { kind -> testSendAfterClose(kind) } + } + @Test fun testReceiveOrNullAfterClose() = runTest { TestChannelKind.values().forEach { kind -> testReceiveOrNull(kind) } @@ -128,6 +132,23 @@ class BasicOperationsTest : TestBase() { d.await() } + /** + * [ClosedSendChannelException] should not be eaten. + * See [https://github.com/Kotlin/kotlinx.coroutines/issues/957] + */ + private suspend fun testSendAfterClose(kind: TestChannelKind) { + assertFailsWith { + coroutineScope { + val channel = kind.create() + channel.close() + + launch { + channel.send(1) + } + } + } + } + private suspend fun testSendReceive(kind: TestChannelKind, iterations: Int) = coroutineScope { val channel = kind.create() launch { diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt index 666b706499..6b5e020d27 100644 --- a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt @@ -73,7 +73,7 @@ class ConflatedChannelTest : TestBase() { q.cancel() check(q.isClosedForSend) check(q.isClosedForReceive) - check(q.receiveOrNull() == null) + assertFailsWith { q.receiveOrNull() } finish(2) } diff --git a/kotlinx-coroutines-core/common/test/channels/LinkedListChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/LinkedListChannelTest.kt index 700ea96c46..4233a35084 100644 --- a/kotlinx-coroutines-core/common/test/channels/LinkedListChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/LinkedListChannelTest.kt @@ -31,7 +31,7 @@ class LinkedListChannelTest : TestBase() { q.cancel() check(q.isClosedForSend) check(q.isClosedForReceive) - check(q.receiveOrNull() == null) + assertFailsWith { q.receiveOrNull() } } @Test diff --git a/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt b/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt index f286ba5d24..5137dd740d 100644 --- a/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt @@ -38,7 +38,7 @@ class ProduceTest : TestBase() { expectUnreached() } catch (e: Throwable) { expect(7) - check(e is ClosedSendChannelException) + check(e is CancellationException) throw e } expectUnreached() @@ -48,7 +48,7 @@ class ProduceTest : TestBase() { expect(4) c.cancel() expect(5) - assertNull(c.receiveOrNull()) + assertFailsWith { c.receiveOrNull() } expect(6) yield() // to produce finish(8) @@ -107,7 +107,6 @@ class ProduceTest : TestBase() { produced.cancel() try { source.receive() - // TODO shouldn't it be ClosedReceiveChannelException ? } catch (e: CancellationException) { finish(4) } diff --git a/kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt index d7ca753e0b..54d6938481 100644 --- a/kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/RendezvousChannelTest.kt @@ -272,7 +272,7 @@ class RendezvousChannelTest : TestBase() { q.cancel() check(q.isClosedForSend) check(q.isClosedForReceive) - check(q.receiveOrNull() == null) + assertFailsWith { q.receiveOrNull() } finish(12) } diff --git a/kotlinx-coroutines-core/common/test/flow/IdFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/IdFlowTest.kt new file mode 100644 index 0000000000..a7299cc8d9 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/IdFlowTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913 + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +// See https://github.com/Kotlin/kotlinx.coroutines/issues/1128 +class IdFlowTest : TestBase() { + @Test + fun testCancelInCollect() = runTest( + expected = { it is CancellationException } + ) { + expect(1) + flow { + expect(2) + emit(1) + expect(3) + hang { finish(6) } + }.idScoped().collect { value -> + expect(4) + assertEquals(1, value) + kotlin.coroutines.coroutineContext.cancel() + expect(5) + } + expectUnreached() + } + + @Test + fun testCancelInFlow() = runTest( + expected = { it is CancellationException } + ) { + expect(1) + flow { + expect(2) + emit(1) + kotlin.coroutines.coroutineContext.cancel() + expect(3) + }.idScoped().collect { value -> + finish(4) + assertEquals(1, value) + } + expectUnreached() + } +} + +/** + * This flow should be "identity" function with respect to cancellation. + */ +private fun Flow.idScoped(): Flow = flow { + coroutineScope { + val channel = produce { + collect { send(it) } + } + channel.consumeEach { + emit(it) + } + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt index 7024961128..e02da811e4 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt @@ -227,6 +227,4 @@ class ZipTest : TestBase() { assertFailsWith(flow) finish(2) } - - private suspend fun sum(s: String?, i: Int?): String = s + i } diff --git a/kotlinx-coroutines-core/jvm/test/channels/DoubleChannelCloseStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/DoubleChannelCloseStressTest.kt index 3c9af50b14..01cace720c 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/DoubleChannelCloseStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/DoubleChannelCloseStressTest.kt @@ -17,7 +17,11 @@ class DoubleChannelCloseStressTest : TestBase() { // empty -- just closes channel } GlobalScope.launch(CoroutineName("sender")) { - actor.send(1) + try { + actor.send(1) + } catch (e: ClosedSendChannelException) { + // ok -- closed before send + } } Thread.sleep(1) actor.close() diff --git a/kotlinx-coroutines-core/jvm/test/channels/TickerChannelCommonTest.kt b/kotlinx-coroutines-core/jvm/test/channels/TickerChannelCommonTest.kt index 392d982700..cffe6c0e20 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/TickerChannelCommonTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/TickerChannelCommonTest.kt @@ -48,8 +48,7 @@ class TickerChannelCommonTest(private val channelFactory: Channel) : TestBase() delayChannel.cancel() delay(5100) - delayChannel.checkEmpty() - delayChannel.cancel() + assertFailsWith { delayChannel.poll() } } } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt index b4df58ac34..83bd72355f 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/ProduceExceptionsTest.kt @@ -77,7 +77,7 @@ class ProduceExceptionsTest : TestBase() { channel!!.cancel() try { send(1) - } catch (e: ClosedSendChannelException) { + } catch (e: CancellationException) { expect(3) throw e } @@ -87,7 +87,7 @@ class ProduceExceptionsTest : TestBase() { yield() try { channel.receive() - } catch (e: ClosedReceiveChannelException) { + } catch (e: CancellationException) { assertTrue(e.suppressed.isEmpty()) finish(4) } From 5ea1bedea1aef1344ba55b80a92c279d20a24383 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 30 Apr 2019 14:11:38 +0300 Subject: [PATCH 04/56] Rename TimedRunnable to TimedRunnableObsolete in obsolete kotlinx.coroutines.test package to avoid FQN clash Fixes #1159 --- .../jvm/src/test_/TestCoroutineContext.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt index b07efffea7..3a1bf3a664 100644 --- a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt @@ -43,7 +43,7 @@ class TestCoroutineContext(private val name: String? = null) : CoroutineContext } // The ordered queue for the runnable tasks. - private val queue = ThreadSafeHeap() + private val queue = ThreadSafeHeap() // The per-scheduler global order counter. private var counter = 0L @@ -184,10 +184,10 @@ class TestCoroutineContext(private val name: String? = null) : CoroutineContext } private fun enqueue(block: Runnable) = - queue.addLast(TimedRunnable(block, counter++)) + queue.addLast(TimedRunnableObsolete(block, counter++)) private fun postDelayed(block: Runnable, delayTime: Long) = - TimedRunnable(block, counter++, time + TimeUnit.MILLISECONDS.toNanos(delayTime)) + TimedRunnableObsolete(block, counter++, time + TimeUnit.MILLISECONDS.toNanos(delayTime)) .also { queue.addLast(it) } @@ -245,17 +245,17 @@ class TestCoroutineContext(private val name: String? = null) : CoroutineContext } } -private class TimedRunnable( +private class TimedRunnableObsolete( private val run: Runnable, private val count: Long = 0, @JvmField internal val time: Long = 0 -) : Comparable, Runnable by run, ThreadSafeHeapNode { +) : Comparable, Runnable by run, ThreadSafeHeapNode { override var heap: ThreadSafeHeap<*>? = null override var index: Int = 0 override fun run() = run.run() - override fun compareTo(other: TimedRunnable) = if (time == other.time) { + override fun compareTo(other: TimedRunnableObsolete) = if (time == other.time) { count.compareTo(other.count) } else { time.compareTo(other.time) From 6e3faa71bfc4b91494d4bebf5c0f8e1d5aded837 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 24 Apr 2019 17:02:14 +0300 Subject: [PATCH 05/56] Fixed spurious exception during select.onJoin clause registration * Removed #1130 workaround from debounce * Fixed race in debounce Fixes #1130 --- .../common/src/JobSupport.kt | 1 - .../common/src/flow/operators/Delay.kt | 11 ++--- .../common/test/selects/SelectLoopTest.kt | 42 +++++++++++++++++++ 3 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index d6834e9cfb..b3250b4e1d 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -529,7 +529,6 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren if (state !is Incomplete) { // already complete -- select result if (select.trySelect(null)) { - select.completion.context.checkCompletion() // always check for our completion block.startCoroutineUnintercepted(select.completion) } return diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 111ef7cf9a..e283f9f31e 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -64,13 +64,8 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { coroutineScope { val values = Channel(Channel.CONFLATED) // Actually Any, KT-30796 // Channel is not closed deliberately as there is no close with value - val collector = launch { - try { - collect { value -> values.send(value ?: NullSurrogate) } - } catch (e: Throwable) { - values.close(e) // Workaround for #1130 - throw e - } + val collector = async { + collect { value -> values.send(value ?: NullSurrogate) } } var isDone = false @@ -89,7 +84,7 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { } // Close with value 'idiom' - collector.onJoin { + collector.onAwait { if (lastValue != null) emit(NullSurrogate.unbox(lastValue)) isDone = true } diff --git a/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt new file mode 100644 index 0000000000..5af68f6be5 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913 + +package kotlinx.coroutines.selects + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +class SelectLoopTest : TestBase() { + // https://github.com/Kotlin/kotlinx.coroutines/issues/1130 + @Test + fun testChannelSelectLoop() = runTest( + expected = { it is TestException } + ) { + expect(1) + val channel = Channel() + val job = launch { + expect(2) + channel.send(Unit) + expect(3) + throw TestException() + } + var isDone = false + while (!isDone) { + select { + channel.onReceiveOrNull { + expect(4) + assertEquals(Unit, it) + } + job.onJoin { + expect(5) + isDone = true + } + } + } + finish(6) + } +} \ No newline at end of file From 9614536ff093c99fc7febd5e31547ede8c8ceb5d Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 1 May 2019 00:27:20 +0300 Subject: [PATCH 06/56] Fixes kotlin-coroutines-test TestCoroutineDispatcher docs Fixes #1163 --- kotlinx-coroutines-test/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index c309a359a6..d791a5c27c 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -325,7 +325,7 @@ when the class under test allows a test to provide a [CoroutineDispatcher] but d [CoroutineScope]. Since [TestCoroutineDispatcher] is stateful in order to keep track of executing coroutines, it is -important to ensure that [cleanupTestCoroutines][TestCoroutineDispatcher.cleanupTestCoroutines] is called after every test case. +important to ensure that [cleanupTestCoroutines][DelayController.cleanupTestCoroutines] is called after every test case. ```kotlin class TestClass { @@ -340,7 +340,7 @@ class TestClass { @After fun cleanUp() { Dispatchers.resetMain() - testScope.cleanupTestCoroutines() + testDispatcher.cleanupTestCoroutines() } @Test @@ -449,5 +449,5 @@ If you have any suggestions for improvements to this experimental API please sha [TestCoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/index.html [TestCoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-exception-handler/index.html [TestCoroutineScope.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/cleanup-test-coroutines.html -[TestCoroutineDispatcher.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-dispatcher/cleanup-test-coroutines.html +[DelayController.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/cleanup-test-coroutines.html From b81f5f0ad73bc163a01a2ec88e2cb2e3d61927e1 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 7 May 2019 02:46:46 +0300 Subject: [PATCH 07/56] Knit: fix handling of backquote when making anchor reference --- knit/src/Knit.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/knit/src/Knit.kt b/knit/src/Knit.kt index e212206b3b..c2e9fef106 100644 --- a/knit/src/Knit.kt +++ b/knit/src/Knit.kt @@ -352,6 +352,7 @@ fun makeSectionRef(name: String): String = name .replace(",", "") .replace("(", "") .replace(")", "") + .replace("`", "") .toLowerCase() class Include(val regex: Regex, val lines: MutableList = arrayListOf()) From 485eab1611433e49bd1d7c4a302b4825daf0d2e9 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 3 May 2019 02:56:40 +0300 Subject: [PATCH 08/56] Ensure that there are no references to atomicfu in classes * Updated to 0.12.7 version of atmoicfu with fixes. * Added test that that is no references to atomicfu in any of the resulting class files. Fixes #1155 --- .../test/PublicApiTest.kt | 32 +++++++++++++++++-- gradle.properties | 2 +- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/binary-compatibility-validator/test/PublicApiTest.kt b/binary-compatibility-validator/test/PublicApiTest.kt index b5ecdc09e1..fb4f55cc17 100644 --- a/binary-compatibility-validator/test/PublicApiTest.kt +++ b/binary-compatibility-validator/test/PublicApiTest.kt @@ -43,14 +43,18 @@ class PublicApiTest( @Test fun testApi() { val libsDir = File("../$rootDir/$moduleName/build/libs").absoluteFile.normalize() - val jarFile = getJarPath(libsDir) + val jarPath = getJarPath(libsDir) val kotlinJvmMappingsFiles = listOf(libsDir.resolve("../visibilities.json")) val visibilities = kotlinJvmMappingsFiles .map { readKotlinVisibilities(it) } .reduce { m1, m2 -> m1 + m2 } - val api = getBinaryAPI(JarFile(jarFile), visibilities).filterOutNonPublic(nonPublicPackages) - api.dumpAndCompareWith(File("reference-public-api").resolve("$moduleName.txt")) + JarFile(jarPath).use { jarFile -> + val api = getBinaryAPI(jarFile, visibilities).filterOutNonPublic(nonPublicPackages) + api.dumpAndCompareWith(File("reference-public-api").resolve("$moduleName.txt")) + // check for atomicfu leaks + jarFile.checkForAtomicFu() + } } private fun getJarPath(libsDir: File): File { @@ -68,3 +72,25 @@ class PublicApiTest( error("No single file matching $regex in $libsDir:\n${files.joinToString("\n")}") } } + +private val ATOMIC_FU_REF = "Lkotlinx/atomicfu/".toByteArray() + +private fun JarFile.checkForAtomicFu() { + val foundClasses = mutableListOf() + for (e in entries()) { + if (!e.name.endsWith(".class")) continue + val bytes = getInputStream(e).use { it.readBytes() } + loop@for (i in 0 until bytes.size - ATOMIC_FU_REF.size) { + for (j in 0 until ATOMIC_FU_REF.size) { + if (bytes[i + j] != ATOMIC_FU_REF[j]) continue@loop + } + foundClasses += e.name // report error at the end with all class names + break@loop + } + } + if (foundClasses.isNotEmpty()) { + error("Found references to atomicfu in jar file $name in the following class files: ${ + foundClasses.joinToString("") { "\n\t\t" + it } + }") + } +} diff --git a/gradle.properties b/gradle.properties index 29bf0f4560..bc2a75778a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ kotlin_version=1.3.30 # Dependencies junit_version=4.12 -atomicfu_version=0.12.5 +atomicfu_version=0.12.7 html_version=0.6.8 lincheck_version=2.0 dokka_version=0.9.16-rdev-2-mpp-hacks From a2499a3ffe7522b8881a41ada45ce1779494be51 Mon Sep 17 00:00:00 2001 From: Andrew Orobator Date: Sat, 27 Apr 2019 15:32:57 -0400 Subject: [PATCH 09/56] Fixed Dispatcher docs * Fixed typos * Added reference to kotlinx-coroutines-test for testing `Main` dispatcher --- kotlinx-coroutines-core/jvm/src/Dispatchers.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt index cb877a3a80..5b6adcd20a 100644 --- a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt @@ -41,14 +41,17 @@ public actual object Dispatchers { * * Depending on platform and classpath it can be mapped to different dispatchers: * - On JS and Native it is equivalent of [Default] dispatcher. - * - On JVM it either Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by + * - On JVM it is either Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by * [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html). * - * In order to work with `Main` dispatcher, following artifact should be added to project runtime dependencies: + * In order to work with `Main` dispatcher, the following artifacts should be added to project runtime dependencies: * - `kotlinx-coroutines-android` for Android Main thread dispatcher * - `kotlinx-coroutines-javafx` for JavaFx Application thread dispatcher * - `kotlinx-coroutines-swing` for Swing EDT dispatcher * + * In order to set a custom `Main` dispatcher for testing purposes, add the `kotlinx-coroutines-test` artifact to + * project test dependencies. + * * Implementation note: [MainCoroutineDispatcher.immediate] is not supported on Native and JS platforms. */ @JvmStatic From 725addfbe66c5ce1ec85163fde5960b686836d54 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 13 May 2019 18:17:59 +0300 Subject: [PATCH 10/56] Flow documentation improvements --- kotlinx-coroutines-core/common/src/flow/Builders.kt | 5 +++++ kotlinx-coroutines-core/common/src/flow/operators/Limit.kt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index c79a5ac255..1416589854 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -76,6 +76,11 @@ public fun (() -> T).asFlow(): Flow = unsafeFlow { /** * Creates a flow that produces a single value from the given functional type. + * Example of usage: + * ``` + * suspend fun remoteCall(): R = ... + * suspend fun remoteCallFlow(): Flow = ::remoteCall.asFlow() + * ``` */ @FlowPreview public fun (suspend () -> T).asFlow(): Flow = unsafeFlow { diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt index c60d105b02..bc4a75bef2 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -47,7 +47,7 @@ public fun Flow.dropWhile(predicate: suspend (T) -> Boolean): Flow = f /** * Returns a flow that contains first [count] elements. * When [count] elements are consumed, the original flow is cancelled. - * Throws [IllegalArgumentException] if [count] is negative. + * Throws [IllegalArgumentException] if [count] is not positive. */ @FlowPreview public fun Flow.take(count: Int): Flow { From 71fa64f4975bbf628b886a4483c31d3bdfbed60c Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 15 May 2019 16:40:27 +0300 Subject: [PATCH 11/56] Use downloaded package list for dokka (and knit, transitively) to avoid dependency on (frequently) unstable external JavaDoc resources Fixes #648 --- .../kotlinx-coroutines-guava/build.gradle | 1 + .../kotlinx-coroutines-guava/package.list | 16 ++ .../kotlinx-coroutines-reactive/build.gradle | 1 + .../kotlinx-coroutines-reactive/package.list | 1 + .../kotlinx-coroutines-reactor/build.gradle | 3 +- .../kotlinx-coroutines-reactor/package.list | 9 + reactive/kotlinx-coroutines-rx2/build.gradle | 1 + reactive/kotlinx-coroutines-rx2/package.list | 14 ++ ui/kotlinx-coroutines-android/build.gradle | 1 + ui/kotlinx-coroutines-android/package.list | 211 ++++++++++++++++++ 10 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 integration/kotlinx-coroutines-guava/package.list create mode 100644 reactive/kotlinx-coroutines-reactive/package.list create mode 100644 reactive/kotlinx-coroutines-reactor/package.list create mode 100644 reactive/kotlinx-coroutines-rx2/package.list create mode 100644 ui/kotlinx-coroutines-android/package.list diff --git a/integration/kotlinx-coroutines-guava/build.gradle b/integration/kotlinx-coroutines-guava/build.gradle index 90f2898f05..48fd0f56b1 100644 --- a/integration/kotlinx-coroutines-guava/build.gradle +++ b/integration/kotlinx-coroutines-guava/build.gradle @@ -11,5 +11,6 @@ dependencies { tasks.withType(dokka.getClass()) { externalDocumentationLink { url = new URL("https://google.github.io/guava/releases/$guava_version/api/docs/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } \ No newline at end of file diff --git a/integration/kotlinx-coroutines-guava/package.list b/integration/kotlinx-coroutines-guava/package.list new file mode 100644 index 0000000000..9ad26f4474 --- /dev/null +++ b/integration/kotlinx-coroutines-guava/package.list @@ -0,0 +1,16 @@ +com.google.common.annotations +com.google.common.base +com.google.common.cache +com.google.common.collect +com.google.common.escape +com.google.common.eventbus +com.google.common.graph +com.google.common.hash +com.google.common.html +com.google.common.io +com.google.common.math +com.google.common.net +com.google.common.primitives +com.google.common.reflect +com.google.common.util.concurrent +com.google.common.xml diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle b/reactive/kotlinx-coroutines-reactive/build.gradle index d5139a1015..f544ab448b 100644 --- a/reactive/kotlinx-coroutines-reactive/build.gradle +++ b/reactive/kotlinx-coroutines-reactive/build.gradle @@ -29,5 +29,6 @@ test { tasks.withType(dokka.getClass()) { externalDocumentationLink { url = new URL("https://www.reactive-streams.org/reactive-streams-$reactive_streams_version-javadoc/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactive/package.list b/reactive/kotlinx-coroutines-reactive/package.list new file mode 100644 index 0000000000..6a8ba62f50 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/package.list @@ -0,0 +1 @@ +org.reactivestreams diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle b/reactive/kotlinx-coroutines-reactor/build.gradle index b0f2292360..72ef6e5623 100644 --- a/reactive/kotlinx-coroutines-reactor/build.gradle +++ b/reactive/kotlinx-coroutines-reactor/build.gradle @@ -9,6 +9,7 @@ dependencies { tasks.withType(dokka.getClass()) { externalDocumentationLink { - url = new URL('https://projectreactor.io/docs/core/3.2.5.RELEASE/api/') + url = new URL("https://projectreactor.io/docs/core/$reactor_vesion/api/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactor/package.list b/reactive/kotlinx-coroutines-reactor/package.list new file mode 100644 index 0000000000..9809a3f5f1 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactor/package.list @@ -0,0 +1,9 @@ +reactor.adapter +reactor.core +reactor.core.publisher +reactor.core.scheduler +reactor.util +reactor.util.annotation +reactor.util.concurrent +reactor.util.context +reactor.util.function diff --git a/reactive/kotlinx-coroutines-rx2/build.gradle b/reactive/kotlinx-coroutines-rx2/build.gradle index 0bb02b2b45..8fa85092b5 100644 --- a/reactive/kotlinx-coroutines-rx2/build.gradle +++ b/reactive/kotlinx-coroutines-rx2/build.gradle @@ -12,6 +12,7 @@ dependencies { tasks.withType(dokka.getClass()) { externalDocumentationLink { url = new URL('http://reactivex.io/RxJava/javadoc/') + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } diff --git a/reactive/kotlinx-coroutines-rx2/package.list b/reactive/kotlinx-coroutines-rx2/package.list new file mode 100644 index 0000000000..5be2c9e9e3 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx2/package.list @@ -0,0 +1,14 @@ +io.reactivex +io.reactivex.annotations +io.reactivex.disposables +io.reactivex.exceptions +io.reactivex.flowables +io.reactivex.functions +io.reactivex.observables +io.reactivex.observers +io.reactivex.parallel +io.reactivex.plugins +io.reactivex.processors +io.reactivex.schedulers +io.reactivex.subjects +io.reactivex.subscribers diff --git a/ui/kotlinx-coroutines-android/build.gradle b/ui/kotlinx-coroutines-android/build.gradle index 40d51e4f4a..195d6b53b7 100644 --- a/ui/kotlinx-coroutines-android/build.gradle +++ b/ui/kotlinx-coroutines-android/build.gradle @@ -17,5 +17,6 @@ dependencies { tasks.withType(dokka.getClass()) { externalDocumentationLink { url = new URL("https://developer.android.com/reference/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/package.list b/ui/kotlinx-coroutines-android/package.list new file mode 100644 index 0000000000..349cdcd89c --- /dev/null +++ b/ui/kotlinx-coroutines-android/package.list @@ -0,0 +1,211 @@ +android +android.accessibilityservice +android.accounts +android.animation +android.annotation +android.app +android.app.admin +android.app.assist +android.app.backup +android.app.job +android.app.role +android.app.slice +android.app.usage +android.appwidget +android.bluetooth +android.bluetooth.le +android.companion +android.content +android.content.pm +android.content.res +android.database +android.database.sqlite +android.drm +android.gesture +android.graphics +android.graphics.drawable +android.graphics.drawable.shapes +android.graphics.fonts +android.graphics.pdf +android.graphics.text +android.hardware +android.hardware.biometrics +android.hardware.camera2 +android.hardware.camera2.params +android.hardware.display +android.hardware.fingerprint +android.hardware.input +android.hardware.usb +android.icu.lang +android.icu.math +android.icu.text +android.icu.util +android.inputmethodservice +android.location +android.media +android.media.audiofx +android.media.browse +android.media.effect +android.media.midi +android.media.projection +android.media.session +android.media.tv +android.mtp +android.net +android.net.http +android.net.nsd +android.net.rtp +android.net.sip +android.net.ssl +android.net.wifi +android.net.wifi.aware +android.net.wifi.hotspot2 +android.net.wifi.hotspot2.omadm +android.net.wifi.hotspot2.pps +android.net.wifi.p2p +android.net.wifi.p2p.nsd +android.net.wifi.rtt +android.nfc +android.nfc.cardemulation +android.nfc.tech +android.opengl +android.os +android.os.health +android.os.storage +android.os.strictmode +android.preference +android.print +android.print.pdf +android.printservice +android.provider +android.renderscript +android.sax +android.se.omapi +android.security +android.security.keystore +android.service.autofill +android.service.carrier +android.service.chooser +android.service.dreams +android.service.media +android.service.notification +android.service.quicksettings +android.service.restrictions +android.service.textservice +android.service.voice +android.service.vr +android.service.wallpaper +android.speech +android.speech.tts +android.system +android.telecom +android.telephony +android.telephony.cdma +android.telephony.data +android.telephony.emergency +android.telephony.euicc +android.telephony.gsm +android.telephony.mbms +android.test +android.test.mock +android.test.suitebuilder +android.test.suitebuilder.annotation +android.text +android.text.format +android.text.method +android.text.style +android.text.util +android.transition +android.util +android.view +android.view.accessibility +android.view.animation +android.view.autofill +android.view.inputmethod +android.view.inspector +android.view.textclassifier +android.view.textservice +android.webkit +android.widget +dalvik.annotation +dalvik.bytecode +dalvik.system +java.awt.font +java.beans +java.io +java.lang +java.lang.annotation +java.lang.invoke +java.lang.ref +java.lang.reflect +java.math +java.net +java.nio +java.nio.channels +java.nio.channels.spi +java.nio.charset +java.nio.charset.spi +java.nio.file +java.nio.file.attribute +java.nio.file.spi +java.security +java.security.acl +java.security.cert +java.security.interfaces +java.security.spec +java.sql +java.text +java.time +java.time.chrono +java.time.format +java.time.temporal +java.time.zone +java.util +java.util.concurrent +java.util.concurrent.atomic +java.util.concurrent.locks +java.util.function +java.util.jar +java.util.logging +java.util.prefs +java.util.regex +java.util.stream +java.util.zip +javax.crypto +javax.crypto.interfaces +javax.crypto.spec +javax.microedition.khronos.egl +javax.microedition.khronos.opengles +javax.net +javax.net.ssl +javax.security.auth +javax.security.auth.callback +javax.security.auth.login +javax.security.auth.x500 +javax.security.cert +javax.sql +javax.xml +javax.xml.datatype +javax.xml.namespace +javax.xml.parsers +javax.xml.transform +javax.xml.transform.dom +javax.xml.transform.sax +javax.xml.transform.stream +javax.xml.validation +javax.xml.xpath +junit.framework +junit.runner +org.apache.http.conn +org.apache.http.conn.scheme +org.apache.http.conn.ssl +org.apache.http.params +org.json +org.w3c.dom +org.w3c.dom.ls +org.xml.sax +org.xml.sax.ext +org.xml.sax.helpers +org.xmlpull.v1 +org.xmlpull.v1.sax2 + From c022ab69e2b4f853311205b48ec7f1a6c227377d Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 14 May 2019 15:10:09 +0300 Subject: [PATCH 12/56] Make withContext cancellable on return (instead of atomically cancellable). * Reasoning about cancellation is simplified in sequential scenarios, if 'cancel' was invoked before withContext return it will throw an exception, thus "isActive == false" cannot be observed in sequential scenarios after cancellation * withContext now complies its own documentation Fixes #1177 --- .../common/src/Builders.common.kt | 4 +-- .../common/src/Dispatched.kt | 23 +++++++------- .../common/test/TestBase.common.kt | 2 ++ .../common/test/WithContextTest.kt | 30 +++++++++++++++---- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index d7a66fe3de..26fd8e7050 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -156,7 +156,7 @@ public suspend fun withContext( } } // SLOW PATH -- use new dispatcher - val coroutine = DispatchedCoroutine(newContext, uCont) // MODE_ATOMIC_DEFAULT + val coroutine = DispatchedCoroutine(newContext, uCont) // MODE_CANCELLABLE coroutine.initParentJob() block.startCoroutineCancellable(coroutine, coroutine) coroutine.getResult() @@ -215,7 +215,7 @@ private class DispatchedCoroutine( context: CoroutineContext, uCont: Continuation ) : ScopeCoroutine(context, uCont) { - override val defaultResumeMode: Int get() = MODE_ATOMIC_DEFAULT + override val defaultResumeMode: Int get() = MODE_CANCELLABLE // this is copy-and-paste of a decision state machine inside AbstractionContinuation // todo: we may some-how abstract it via inline class diff --git a/kotlinx-coroutines-core/common/src/Dispatched.kt b/kotlinx-coroutines-core/common/src/Dispatched.kt index b767cc135d..450163c668 100644 --- a/kotlinx-coroutines-core/common/src/Dispatched.kt +++ b/kotlinx-coroutines-core/common/src/Dispatched.kt @@ -218,32 +218,35 @@ internal abstract class DispatchedTask( public final override fun run() { val taskContext = this.taskContext - var exception: Throwable? = null + var fatalException: Throwable? = null try { val delegate = delegate as DispatchedContinuation val continuation = delegate.continuation val context = continuation.context - val job = if (resumeMode.isCancellableMode) context[Job] else null val state = takeState() // NOTE: Must take state in any case, even if cancelled withCoroutineContext(context, delegate.countOrElement) { - if (job != null && !job.isActive) { + val exception = getExceptionalResult(state) + val job = if (resumeMode.isCancellableMode) context[Job] else null + /* + * Check whether continuation was originally resumed with an exception. + * If so, it dominates cancellation, otherwise the original exception + * will be silently lost. + */ + if (exception == null && job != null && !job.isActive) { val cause = job.getCancellationException() cancelResult(state, cause) continuation.resumeWithStackTrace(cause) } else { - val exception = getExceptionalResult(state) - if (exception != null) - continuation.resumeWithStackTrace(exception) - else - continuation.resume(getSuccessfulResult(state)) + if (exception != null) continuation.resumeWithStackTrace(exception) + else continuation.resume(getSuccessfulResult(state)) } } } catch (e: Throwable) { // This instead of runCatching to have nicer stacktrace and debug experience - exception = e + fatalException = e } finally { val result = runCatching { taskContext.afterTask() } - handleFatalException(exception, result.exceptionOrNull()) + handleFatalException(fatalException, result.exceptionOrNull()) } } diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt index 449d958073..cef74c1704 100644 --- a/kotlinx-coroutines-core/common/test/TestBase.common.kt +++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt @@ -74,3 +74,5 @@ public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { } } +public suspend fun wrapperDispatcher(): CoroutineContext = wrapperDispatcher(coroutineContext) + diff --git a/kotlinx-coroutines-core/common/test/WithContextTest.kt b/kotlinx-coroutines-core/common/test/WithContextTest.kt index be6bde4a09..55127e5c0b 100644 --- a/kotlinx-coroutines-core/common/test/WithContextTest.kt +++ b/kotlinx-coroutines-core/common/test/WithContextTest.kt @@ -15,7 +15,7 @@ class WithContextTest : TestBase() { fun testThrowException() = runTest { expect(1) try { - withContext(coroutineContext) { + withContext(coroutineContext) { expect(2) throw AssertionError() } @@ -31,7 +31,7 @@ class WithContextTest : TestBase() { fun testThrowExceptionFromWrappedContext() = runTest { expect(1) try { - withContext(wrapperDispatcher(coroutineContext)) { + withContext(wrapperDispatcher(coroutineContext)) { expect(2) throw AssertionError() } @@ -151,7 +151,7 @@ class WithContextTest : TestBase() { expect(2) try { // Same dispatcher, different context - withContext(CoroutineName("testRunCancellationUndispatchedVsException")) { + withContext(CoroutineName("testRunCancellationUndispatchedVsException")) { expect(3) yield() // must suspend expect(5) @@ -176,7 +176,7 @@ class WithContextTest : TestBase() { expect(2) try { // "Different" dispatcher (schedules execution back and forth) - withContext(wrapperDispatcher(coroutineContext)) { + withContext(wrapperDispatcher(coroutineContext)) { expect(4) yield() // must suspend expect(6) @@ -204,7 +204,7 @@ class WithContextTest : TestBase() { job = launch(Job()) { try { expect(3) - withContext(wrapperDispatcher(coroutineContext)) { + withContext(wrapperDispatcher(coroutineContext)) { require(isActive) expect(5) job!!.cancel() @@ -349,6 +349,26 @@ class WithContextTest : TestBase() { expectUnreached() } + @Test + fun testSequentialCancellation() = runTest { + val job = launch { + expect(1) + withContext(wrapperDispatcher()) { + expect(2) + } + expectUnreached() + } + + yield() + val job2 = launch { + expect(3) + job.cancel() + } + + joinAll(job, job2) + finish(4) + } + private class Wrapper(val value: String) : Incomplete { override val isActive: Boolean get() = error("") From 7af7ede7a06ae1a644c17dbb2ca12172af316ec7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 13 May 2019 18:38:13 +0300 Subject: [PATCH 13/56] Add overload with long for CoroutinesTimeout.seconds Fixes #1184 --- .../reference-public-api/kotlinx-coroutines-debug.txt | 2 ++ kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt | 8 +++++++- .../src/junit4/CoroutinesTimeoutStatement.kt | 7 +++---- .../test/junit4/CoroutinesTimeoutTest.kt | 1 - .../test/junit4/TestFailureValidation.kt | 3 +-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt index 183495af5c..604e6cd253 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt @@ -47,6 +47,8 @@ public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion { public final fun seconds (IZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout; + public final fun seconds (JZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout; public static synthetic fun seconds$default (Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;IZILjava/lang/Object;)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout; + public static synthetic fun seconds$default (Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;JZILjava/lang/Object;)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout; } diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt index c69becb24c..ef81cd4b7e 100644 --- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt @@ -49,7 +49,13 @@ public class CoroutinesTimeout( * Creates [CoroutinesTimeout] rule with the given timeout in seconds. */ public fun seconds(seconds: Int, cancelOnTimeout: Boolean = false): CoroutinesTimeout = - CoroutinesTimeout(TimeUnit.SECONDS.toMillis(seconds.toLong()), cancelOnTimeout) + seconds(seconds.toLong(), cancelOnTimeout) + + /** + * Creates [CoroutinesTimeout] rule with the given timeout in seconds. + */ + public fun seconds(seconds: Long, cancelOnTimeout: Boolean = false): CoroutinesTimeout = + CoroutinesTimeout(TimeUnit.SECONDS.toMillis(seconds), cancelOnTimeout) // Overflow is properly handled by TimeUnit } /** diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt index a00a17d58d..88864309b6 100644 --- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt @@ -52,8 +52,7 @@ internal class CoroutinesTimeoutStatement( "${testTimeoutMs / 1000} seconds" else "$testTimeoutMs milliseconds" - val message = "Test ${description.methodName} timed out after $units" - System.err.println("\n$message\n") + System.err.println("\nTest ${description.methodName} timed out after $units\n") System.err.flush() DebugProbes.dumpCoroutines() @@ -65,7 +64,7 @@ internal class CoroutinesTimeoutStatement( * 2) Cancel all coroutines via debug agent API (changing system state!) * 3) Throw created exception */ - val exception = createTimeoutException(message, testThread) + val exception = createTimeoutException(testThread) cancelIfNecessary() // If timed out test throws an exception, we can't do much except ignoring it throw exception @@ -79,7 +78,7 @@ internal class CoroutinesTimeoutStatement( } } - private fun createTimeoutException(message: String, thread: Thread): Exception { + private fun createTimeoutException(thread: Thread): Exception { val stackTrace = thread.stackTrace val exception = TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS) exception.stackTrace = stackTrace diff --git a/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt index 8d50c723cc..fb170c071c 100644 --- a/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt +++ b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt @@ -4,7 +4,6 @@ package kotlinx.coroutines.debug.junit4 -import junit4.* import kotlinx.coroutines.* import org.junit.* import org.junit.runners.model.* diff --git a/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt b/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt index 9084926993..a10c51183e 100644 --- a/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt +++ b/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt @@ -2,10 +2,9 @@ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package junit4 +package kotlinx.coroutines.debug.junit4 import kotlinx.coroutines.debug.* -import kotlinx.coroutines.debug.junit4.* import org.junit.rules.* import org.junit.runner.* import org.junit.runners.model.* From b7e93f0f2032a981bd56ea742c1d7cfc794fd532 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 29 Apr 2019 17:10:52 +0300 Subject: [PATCH 14/56] Update Kotlin to 1.3.31 --- README.md | 8 ++++---- gradle.properties | 2 +- kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt | 8 +++----- kotlinx-coroutines-debug/test/DebugProbesTest.kt | 2 -- kotlinx-coroutines-debug/test/SanitizedProbesTest.kt | 1 - .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2a0f1f3dec..e8e37a8c41 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.2.1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.2.1) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for Kotlin `1.3.30` release. +This is a companion version for Kotlin `1.3.31` release. ```kotlin suspend fun main() = coroutineScope { @@ -89,7 +89,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.3.30 + 1.3.31 ``` @@ -107,7 +107,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.3.30' + ext.kotlin_version = '1.3.31' } ``` @@ -133,7 +133,7 @@ And make sure that you use the latest Kotlin version: ```groovy plugins { - kotlin("jvm") version "1.3.30" + kotlin("jvm") version "1.3.31" } ``` diff --git a/gradle.properties b/gradle.properties index bc2a75778a..13b510dcef 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Kotlin version=1.2.1-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.3.30 +kotlin_version=1.3.31 # Dependencies junit_version=4.12 diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt index 35e9d1e9f2..ec727014cb 100644 --- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt +++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -24,7 +24,6 @@ class CoroutinesDumpTest : DebugTestBase() { "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: SUSPENDED\n" + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingNestedMethod(CoroutinesDumpTest.kt:95)\n" + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingOuterMethod(CoroutinesDumpTest.kt:88)\n" + - "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testSuspendedCoroutine\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:29)\n" + "\t(Coroutine creation stacktrace)\n" + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + @@ -39,13 +38,13 @@ class CoroutinesDumpTest : DebugTestBase() { fun testRunningCoroutine() = synchronized(monitor) { val deferred = GlobalScope.async { activeMethod(shouldSuspend = false) + assertTrue(true) } awaitCoroutineStarted() verifyDump( - "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + - "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutine\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:49)\n" + - "\t(Coroutine creation stacktrace)\n" + + "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@227d9994, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\t(Coroutine creation stacktrace)\n" + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + @@ -72,7 +71,6 @@ class CoroutinesDumpTest : DebugTestBase() { "\tat java.lang.Thread.sleep(Native Method)\n" + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" + - "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutineWithSuspensionPoint\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:71)\n" + "\t(Coroutine creation stacktrace)\n" + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + diff --git a/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/kotlinx-coroutines-debug/test/DebugProbesTest.kt index 9dd4d7cec0..35d024178d 100644 --- a/kotlinx-coroutines-debug/test/DebugProbesTest.kt +++ b/kotlinx-coroutines-debug/test/DebugProbesTest.kt @@ -45,7 +45,6 @@ class DebugProbesTest : TestBase() { "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsyncWithProbes\$1\$1.invokeSuspend(DebugProbesTest.kt:43)\n" + "\t(Coroutine creation stacktrace)\n" + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + @@ -76,7 +75,6 @@ class DebugProbesTest : TestBase() { "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsyncWithSanitizedProbes\$1\$1.invokeSuspend(DebugProbesTest.kt:43)\n" + "\t(Coroutine creation stacktrace)\n" + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + diff --git a/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt b/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt index 3ee80ad38f..c990c3e085 100644 --- a/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt +++ b/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt @@ -86,7 +86,6 @@ class SanitizedProbesTest : DebugTestBase() { "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)", "Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@1b68b9a4, state: SUSPENDED\n" + - "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$launchSelector\$1\$1\$1.invokeSuspend(SanitizedProbesTest.kt:105)\n" + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$launchSelector\$1.invokeSuspend(SanitizedProbesTest.kt:143)\n" + "\t(Coroutine creation stacktrace)\n" + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index bd2f170459..342b103ab2 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -18,6 +18,6 @@ org.gradle.jvmargs=-Xmx1536m kotlin.coroutines=enable -kotlin_version=1.3.30 +kotlin_version=1.3.31 coroutines_version=1.2.1 diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index bd2f170459..342b103ab2 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -18,6 +18,6 @@ org.gradle.jvmargs=-Xmx1536m kotlin.coroutines=enable -kotlin_version=1.3.30 +kotlin_version=1.3.31 coroutines_version=1.2.1 From 641d6715cb5d2ec34b73423f543dfedff6b63e7a Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 29 Apr 2019 17:13:57 +0300 Subject: [PATCH 15/56] Flow performance improvements: mark crucial Flow DSL (unsafeFlow, collect, transform, map, mapNotNull, filter, filterNot, filterNotNull) as inline --- .../reference-public-api/kotlinx-coroutines-core.txt | 1 - kotlinx-coroutines-core/common/src/flow/Builders.kt | 2 +- .../common/src/flow/operators/Transform.kt | 10 +++++----- .../common/src/flow/terminal/Collect.kt | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 366906acd6..c66814f33b 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -792,7 +792,6 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun asFlow ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; - public static final fun collect (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun combineLatest (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 1416589854..31efea83ee 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -58,7 +58,7 @@ public fun flow(@BuilderInference block: suspend FlowCollector.() -> Unit */ @FlowPreview @PublishedApi -internal fun unsafeFlow(@BuilderInference block: suspend FlowCollector.() -> Unit): Flow { +internal inline fun unsafeFlow(@BuilderInference crossinline block: suspend FlowCollector.() -> Unit): Flow { return object : Flow { override suspend fun collect(collector: FlowCollector) { collector.block() diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index 3818efeb16..41eb638009 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.unsafeFlow as flow * ``` */ @FlowPreview -public fun Flow.transform(@BuilderInference transform: suspend FlowCollector.(value: T) -> Unit): Flow { +public inline fun Flow.transform(@BuilderInference crossinline transform: suspend FlowCollector.(value: T) -> Unit): Flow { return flow { collect { value -> transform(value) @@ -38,7 +38,7 @@ public fun Flow.transform(@BuilderInference transform: suspend FlowCol * Returns a flow containing only values of the original flow that matches the given [predicate]. */ @FlowPreview -public fun Flow.filter(predicate: suspend (T) -> Boolean): Flow = flow { +public inline fun Flow.filter(crossinline predicate: suspend (T) -> Boolean): Flow = flow { collect { value -> if (predicate(value)) emit(value) } @@ -48,7 +48,7 @@ public fun Flow.filter(predicate: suspend (T) -> Boolean): Flow = flow * Returns a flow containing only values of the original flow that do not match the given [predicate]. */ @FlowPreview -public fun Flow.filterNot(predicate: suspend (T) -> Boolean): Flow = flow { +public inline fun Flow.filterNot(crossinline predicate: suspend (T) -> Boolean): Flow = flow { collect { value -> if (!predicate(value)) emit(value) } @@ -73,7 +73,7 @@ public fun Flow.filterNotNull(): Flow = flow { * Returns a flow containing the results of applying the given [transform] function to each value of the original flow. */ @FlowPreview -public fun Flow.map(transform: suspend (value: T) -> R): Flow = transform { value -> +public inline fun Flow.map(crossinline transform: suspend (value: T) -> R): Flow = transform { value -> emit(transform(value)) } @@ -81,7 +81,7 @@ public fun Flow.map(transform: suspend (value: T) -> R): Flow = tra * Returns a flow that contains only non-null results of applying the given [transform] function to each value of the original flow. */ @FlowPreview -public fun Flow.mapNotNull(transform: suspend (value: T) -> R?): Flow = transform { value -> +public inline fun Flow.mapNotNull(crossinline transform: suspend (value: T) -> R?): Flow = transform { value -> val transformed = transform(value) ?: return@transform emit(transformed) } diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt index d0e04ff286..624b51f683 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -28,7 +28,7 @@ import kotlin.jvm.* * ``` */ @FlowPreview -public suspend fun Flow.collect(action: suspend (value: T) -> Unit): Unit = +public suspend inline fun Flow.collect(crossinline action: suspend (value: T) -> Unit): Unit = collect(object : FlowCollector { override suspend fun emit(value: T) = action(value) }) From 2596414ce3c28e283752eb81ca82e1d4ad56f0fc Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 29 Apr 2019 18:25:19 +0300 Subject: [PATCH 16/56] Add flowOf(value), use unsafeFlow in trivial flow builders --- .../kotlinx-coroutines-core.txt | 1 + .../common/src/flow/Builders.kt | 20 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index c66814f33b..7ddbfb93c3 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -813,6 +813,7 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun flattenMerge (Lkotlinx/coroutines/flow/Flow;II)Lkotlinx/coroutines/flow/Flow; public static synthetic fun flattenMerge$default (Lkotlinx/coroutines/flow/Flow;IIILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun flowOf (Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flowOf ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flowOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/Flow; public static synthetic fun flowOn$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 31efea83ee..06a5c00e2f 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -127,6 +127,18 @@ public fun flowOf(vararg elements: T): Flow = unsafeFlow { } } +/** + * Creates flow that produces a given [value]. + */ +@FlowPreview +public fun flowOf(value: T): Flow = unsafeFlow { + /* + * Implementation note: this is just an "optimized" overload of flowOf(vararg) + * which significantly reduce the footprint of widespread single-value flows. + */ + emit(value) +} + /** * Returns an empty flow. */ @@ -141,7 +153,7 @@ private object EmptyFlow : Flow { * Creates a flow that produces values from the given array. */ @FlowPreview -public fun Array.asFlow(): Flow = flow { +public fun Array.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) } @@ -151,7 +163,7 @@ public fun Array.asFlow(): Flow = flow { * Creates flow that produces values from the given array. */ @FlowPreview -public fun IntArray.asFlow(): Flow = flow { +public fun IntArray.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) } @@ -161,7 +173,7 @@ public fun IntArray.asFlow(): Flow = flow { * Creates flow that produces values from the given array. */ @FlowPreview -public fun LongArray.asFlow(): Flow = flow { +public fun LongArray.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) } @@ -171,7 +183,7 @@ public fun LongArray.asFlow(): Flow = flow { * Creates flow that produces values from the given range. */ @FlowPreview -public fun IntRange.asFlow(): Flow = flow { +public fun IntRange.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) } From a9f8c0d63b0227533532cdf0d58a6675d7bcf497 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 30 Apr 2019 15:05:18 +0300 Subject: [PATCH 17/56] Make Flow.fold inlineable * It is not that useful in an application code (-> is extracted into method), so won't bloat bytecode too much * It is crucial enough as building block to avoid excess allocations * Additionally mark inlined builders with labeled return to workaround KT-28938 --- .../common/src/flow/operators/Context.kt | 4 ++-- .../common/src/flow/operators/Transform.kt | 13 +++++++------ .../common/src/flow/terminal/Reduce.kt | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 3dc021b635..17c1a4c4d6 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -50,7 +50,7 @@ public fun Flow.flowOn(flowContext: CoroutineContext, bufferSize: Int = 1 coroutineScope { val channel = produce(flowContext, capacity = bufferSize) { collect { value -> - send(value) + return@collect send(value) } } channel.consumeEach { value -> @@ -98,7 +98,7 @@ public fun Flow.flowWith( val originalContext = coroutineContext.minusKey(Job) val prepared = source.flowOn(originalContext, bufferSize) builder(prepared).flowOn(flowContext, bufferSize).collect { value -> - emit(value) + return@collect emit(value) } } } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index 41eb638009..aff523dd99 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -29,7 +29,8 @@ import kotlinx.coroutines.flow.unsafeFlow as flow public inline fun Flow.transform(@BuilderInference crossinline transform: suspend FlowCollector.(value: T) -> Unit): Flow { return flow { collect { value -> - transform(value) + // kludge, without it Unit will be returned and TCE won't kick in, KT-28938 + return@collect transform(value) } } } @@ -40,7 +41,7 @@ public inline fun Flow.transform(@BuilderInference crossinline transfo @FlowPreview public inline fun Flow.filter(crossinline predicate: suspend (T) -> Boolean): Flow = flow { collect { value -> - if (predicate(value)) emit(value) + if (predicate(value)) return@collect emit(value) } } @@ -50,7 +51,7 @@ public inline fun Flow.filter(crossinline predicate: suspend (T) -> Boole @FlowPreview public inline fun Flow.filterNot(crossinline predicate: suspend (T) -> Boolean): Flow = flow { collect { value -> - if (!predicate(value)) emit(value) + if (!predicate(value)) return@collect emit(value) } } @@ -66,7 +67,7 @@ public inline fun Flow<*>.filterIsInstance(): Flow = filter { it */ @FlowPreview public fun Flow.filterNotNull(): Flow = flow { - collect { value -> if (value != null) emit(value) } + collect { value -> if (value != null) return@collect emit(value) } } /** @@ -74,7 +75,7 @@ public fun Flow.filterNotNull(): Flow = flow { */ @FlowPreview public inline fun Flow.map(crossinline transform: suspend (value: T) -> R): Flow = transform { value -> - emit(transform(value)) + return@transform emit(transform(value)) } /** @@ -83,7 +84,7 @@ public inline fun Flow.map(crossinline transform: suspend (value: T) - @FlowPreview public inline fun Flow.mapNotNull(crossinline transform: suspend (value: T) -> R?): Flow = transform { value -> val transformed = transform(value) ?: return@transform - emit(transformed) + return@transform emit(transformed) } /** diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index ac3c93cc0d..4afd0959b7 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -38,9 +38,9 @@ public suspend fun Flow.reduce(operation: suspend (accumulator: S, * Accumulates value starting with [initial] value and applying [operation] current accumulator value and each element */ @FlowPreview -public suspend fun Flow.fold( +public suspend inline fun Flow.fold( initial: R, - operation: suspend (acc: R, value: T) -> R + crossinline operation: suspend (acc: R, value: T) -> R ): R { var accumulator = initial collect { value -> From c42d3380be08a78fb4e46b1dabc75d703a31a72b Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 30 Apr 2019 15:18:36 +0300 Subject: [PATCH 18/56] Reactive scrabble benchmarks * RxJava2 by David Karnok * FlowPlaysScrabbleBase and FlowPlaysScrabbleOpt as flow counterparts * Lower bounds for Flow scrabble benchmark --- benchmarks/build.gradle | 48 ++- .../flow/scrabble/RxJava2PlaysScrabble.java | 163 +++++++++ .../scrabble/RxJava2PlaysScrabbleOpt.java | 174 ++++++++++ .../optimizations/FlowableCharSequence.java | 149 ++++++++ .../scrabble/optimizations/FlowableSplit.java | 327 ++++++++++++++++++ .../optimizations/StringFlowable.java | 82 +++++ .../flow/scrabble/FlowPlaysScrabbleBase.kt | 134 +++++++ .../flow/scrabble/FlowPlaysScrabbleOpt.kt | 195 +++++++++++ .../flow/scrabble/IterableSpliterator.kt | 12 + .../kotlin/benchmarks/flow/scrabble/README.md | 42 +++ .../flow/scrabble/ReactorPlaysScrabble.kt | 146 ++++++++ .../flow/scrabble/SaneFlowPlaysScrabble.kt | 104 ++++++ .../flow/scrabble/SequencePlaysScrabble.kt | 103 ++++++ .../flow/scrabble/ShakespearePlaysScrabble.kt | 85 +++++ benchmarks/src/jmh/resources/ospd.txt.gz | Bin 0 -> 197126 bytes .../jmh/resources/words.shakespeare.txt.gz | Bin 0 -> 81824 bytes .../kotlinx-coroutines-core.txt | 1 - 17 files changed, 1760 insertions(+), 5 deletions(-) create mode 100644 benchmarks/src/jmh/java/benchmarks/flow/scrabble/RxJava2PlaysScrabble.java create mode 100644 benchmarks/src/jmh/java/benchmarks/flow/scrabble/RxJava2PlaysScrabbleOpt.java create mode 100644 benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/FlowableCharSequence.java create mode 100644 benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/FlowableSplit.java create mode 100644 benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/StringFlowable.java create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleBase.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/IterableSpliterator.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/README.md create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ReactorPlaysScrabble.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SaneFlowPlaysScrabble.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ShakespearePlaysScrabble.kt create mode 100644 benchmarks/src/jmh/resources/ospd.txt.gz create mode 100644 benchmarks/src/jmh/resources/words.shakespeare.txt.gz diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 728804ad4b..fb10ad1e05 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -1,6 +1,8 @@ /* * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +sourceCompatibility = 1.8 +targetCompatibility = 1.8 apply plugin: "net.ltgt.apt" apply plugin: "com.github.johnrengelman.shadow" @@ -10,15 +12,49 @@ repositories { maven { url "https://repo.typesafe.com/typesafe/releases/" } } -jmh.jmhVersion = '1.21' +compileJmhKotlin { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs += ['-Xjvm-default=enable'] + } +} + +/* + * Due to a bug in the inliner it sometimes does not remove inlined symbols (that are later renamed) from unused code paths, + * and it breaks JMH that tries to post-process these symbols and fails because they are renamed. + */ +task removeRedundantFiles(type: Delete) { + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$2\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$2\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$2\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$1\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$2\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class" + + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class" + + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble//SaneFlowPlaysScrabble\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class" + +} + +jmhRunBytecodeGenerator.dependsOn(removeRedundantFiles) // It is better to use the following to run benchmarks, otherwise you may get unexpected errors: -// ../gradlew --no-daemon cleanJmhJar jmh +// ./gradlew --no-daemon cleanJmhJar jmh -Pjmh="MyBenchmark" jmh { + jmhVersion = '1.21' duplicateClassesStrategy DuplicatesStrategy.INCLUDE failOnError = true resultFormat = 'CSV' -// include = ['.*ChannelProducerConsumer.*'] + if (project.hasProperty('jmh')) { + include = ".*" + project.jmh + ".*" + } +// includeTests = false } jmhJar { @@ -29,8 +65,12 @@ jmhJar { } dependencies { + compile "org.openjdk.jmh:jmh-core:1.21" + compile "io.projectreactor:reactor-core:$reactor_vesion" + compile 'io.reactivex.rxjava2:rxjava:2.1.9' + compile "com.github.akarnokd:rxjava2-extensions:0.20.8" + compile "org.openjdk.jmh:jmh-core:1.21" compile 'com.typesafe.akka:akka-actor_2.12:2.5.0' compile project(':kotlinx-coroutines-core') } - diff --git a/benchmarks/src/jmh/java/benchmarks/flow/scrabble/RxJava2PlaysScrabble.java b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/RxJava2PlaysScrabble.java new file mode 100644 index 0000000000..2a85d0dbdd --- /dev/null +++ b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/RxJava2PlaysScrabble.java @@ -0,0 +1,163 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble; + +import benchmarks.flow.scrabble.IterableSpliterator; +import benchmarks.flow.scrabble.ShakespearePlaysScrabble; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import io.reactivex.functions.Function; +import org.openjdk.jmh.annotations.*; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +/** + * Shakespeare plays Scrabble with RxJava 2 Flowable. + * @author José + * @author akarnokd + */ +@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +public class RxJava2PlaysScrabble extends ShakespearePlaysScrabble { + + @Benchmark + @Override + public List>> play() throws Exception { + + // Function to compute the score of a given word + Function> scoreOfALetter = letter -> Flowable.just(letterScores[letter - 'a']) ; + + // score of the same letters in a word + Function, Flowable> letterScore = + entry -> + Flowable.just( + letterScores[entry.getKey() - 'a'] * + Integer.min( + (int)entry.getValue().get(), + scrabbleAvailableLetters[entry.getKey() - 'a'] + ) + ) ; + + Function> toIntegerFlowable = + string -> Flowable.fromIterable(IterableSpliterator.of(string.chars().boxed().spliterator())) ; + + // Histogram of the letters in a given word + Function>> histoOfLetters = + word -> toIntegerFlowable.apply(word) + .collect( + () -> new HashMap<>(), + (HashMap map, Integer value) -> + { + LongWrapper newValue = map.get(value) ; + if (newValue == null) { + newValue = () -> 0L ; + } + map.put(value, newValue.incAndSet()) ; + } + + ) ; + + // number of blanks for a given letter + Function, Flowable> blank = + entry -> + Flowable.just( + Long.max( + 0L, + entry.getValue().get() - + scrabbleAvailableLetters[entry.getKey() - 'a'] + ) + ) ; + + // number of blanks for a given word + Function> nBlanks = + word -> histoOfLetters.apply(word) + .flatMapPublisher(map -> Flowable.fromIterable(() -> map.entrySet().iterator())) + .flatMap(blank) + .reduce(Long::sum) ; + + + // can a word be written with 2 blanks? + Function> checkBlanks = + word -> nBlanks.apply(word) + .flatMap(l -> Maybe.just(l <= 2L)) ; + + // score taking blanks into account letterScore1 + Function> score2 = + word -> histoOfLetters.apply(word) + .flatMapPublisher(map -> Flowable.fromIterable(() -> map.entrySet().iterator())) + .flatMap(letterScore) + .reduce(Integer::sum) ; + + // Placing the word on the board + // Building the streams of first and last letters + Function> first3 = + word -> Flowable.fromIterable(IterableSpliterator.of(word.chars().boxed().limit(3).spliterator())) ; + Function> last3 = + word -> Flowable.fromIterable(IterableSpliterator.of(word.chars().boxed().skip(3).spliterator())) ; + + + // Stream to be maxed + Function> toBeMaxed = + word -> Flowable.just(first3.apply(word), last3.apply(word)) + .flatMap(observable -> observable) ; + + // Bonus for double letter + Function> bonusForDoubleLetter = + word -> toBeMaxed.apply(word) + .flatMap(scoreOfALetter) + .reduce(Integer::max) ; + + // score of the word put on the board + Function> score3 = + word -> + Maybe.merge(Arrays.asList( + score2.apply(word), + score2.apply(word), + bonusForDoubleLetter.apply(word), + bonusForDoubleLetter.apply(word), + Maybe.just(word.length() == 7 ? 50 : 0) + ) + ) + .reduce(Integer::sum) ; + + Function>, Single>>> buildHistoOnScore = + score -> Flowable.fromIterable(() -> shakespeareWords.iterator()) + .filter(scrabbleWords::contains) + .filter(word -> checkBlanks.apply(word).blockingGet()) + .collect( + () -> new TreeMap<>(Comparator.reverseOrder()), + (TreeMap> map, String word) -> { + Integer key = score.apply(word).blockingGet() ; + List list = map.get(key) ; + if (list == null) { + list = new ArrayList<>() ; + map.put(key, list) ; + } + list.add(word) ; + } + ) ; + + // best key / value pairs + List>> finalList2 = + buildHistoOnScore.apply(score3) + .flatMapPublisher(map -> Flowable.fromIterable(() -> map.entrySet().iterator())) + .take(3) + .collect( + () -> new ArrayList>>(), + (list, entry) -> { + list.add(entry) ; + } + ) + .blockingGet() ; + return finalList2 ; + } +} \ No newline at end of file diff --git a/benchmarks/src/jmh/java/benchmarks/flow/scrabble/RxJava2PlaysScrabbleOpt.java b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/RxJava2PlaysScrabbleOpt.java new file mode 100644 index 0000000000..7a7cb1aa4e --- /dev/null +++ b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/RxJava2PlaysScrabbleOpt.java @@ -0,0 +1,174 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +import hu.akarnokd.rxjava2.math.MathFlowable; +import org.openjdk.jmh.annotations.*; +import benchmarks.flow.scrabble.optimizations.*; +import io.reactivex.*; +import io.reactivex.functions.Function; + +/** + * Shakespeare plays Scrabble with RxJava 2 Flowable optimized. + * @author José + * @author akarnokd + */ +@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +public class RxJava2PlaysScrabbleOpt extends ShakespearePlaysScrabble { + static Flowable chars(String word) { +// return Flowable.range(0, word.length()).map(i -> (int)word.charAt(i)); + return StringFlowable.characters(word); + } + + @Benchmark + @Override + public List>> play() throws Exception { + + // to compute the score of a given word + Function scoreOfALetter = letter -> letterScores[letter - 'a']; + + // score of the same letters in a word + Function, Integer> letterScore = + entry -> + letterScores[entry.getKey() - 'a'] * + Integer.min( + (int)entry.getValue().get(), + scrabbleAvailableLetters[entry.getKey() - 'a'] + ) + ; + + + Function> toIntegerFlowable = + string -> chars(string); + + Map>> histoCache = new HashMap<>(); + // Histogram of the letters in a given word + Function>> histoOfLetters = + word -> { Single> s = histoCache.get(word); + if (s == null) { + s = toIntegerFlowable.apply(word) + .collect( + () -> new HashMap<>(), + (HashMap map, Integer value) -> + { + MutableLong newValue = map.get(value) ; + if (newValue == null) { + newValue = new MutableLong(); + map.put(value, newValue); + } + newValue.incAndSet(); + } + + ); + histoCache.put(word, s); + } + return s; + }; + + // number of blanks for a given letter + Function, Long> blank = + entry -> + Long.max( + 0L, + entry.getValue().get() - + scrabbleAvailableLetters[entry.getKey() - 'a'] + ) + ; + + // number of blanks for a given word + Function> nBlanks = + word -> MathFlowable.sumLong( + histoOfLetters.apply(word).flattenAsFlowable( + map -> map.entrySet() + ) + .map(blank) + ) + ; + + + // can a word be written with 2 blanks? + Function> checkBlanks = + word -> nBlanks.apply(word) + .map(l -> l <= 2L) ; + + // score taking blanks into account letterScore1 + Function> score2 = + word -> MathFlowable.sumInt( + histoOfLetters.apply(word).flattenAsFlowable( + map -> map.entrySet() + ) + .map(letterScore) + ) ; + + // Placing the word on the board + // Building the streams of first and last letters + Function> first3 = + word -> chars(word).take(3) ; + Function> last3 = + word -> chars(word).skip(3) ; + + + // Stream to be maxed + Function> toBeMaxed = + word -> Flowable.concat(first3.apply(word), last3.apply(word)) + ; + + // Bonus for double letter + Function> bonusForDoubleLetter = + word -> MathFlowable.max(toBeMaxed.apply(word) + .map(scoreOfALetter) + ) ; + + // score of the word put on the board + Function> score3 = + word -> + MathFlowable.sumInt(Flowable.concat( + score2.apply(word), + bonusForDoubleLetter.apply(word) + )).map(v -> v * 2 + (word.length() == 7 ? 50 : 0)); + + Function>, Single>>> buildHistoOnScore = + score -> Flowable.fromIterable(shakespeareWords) + .filter(scrabbleWords::contains) + .filter(word -> checkBlanks.apply(word).blockingFirst()) + .collect( + () -> new TreeMap>(Comparator.reverseOrder()), + (TreeMap> map, String word) -> { + Integer key = score.apply(word).blockingFirst() ; + List list = map.get(key) ; + if (list == null) { + list = new ArrayList<>() ; + map.put(key, list) ; + } + list.add(word) ; + } + ) ; + + // best key / value pairs + List>> finalList2 = + buildHistoOnScore.apply(score3).flattenAsFlowable( + map -> map.entrySet() + ) + .take(3) + .collect( + () -> new ArrayList>>(), + (list, entry) -> { + list.add(entry) ; + } + ) + .blockingGet(); + + return finalList2 ; + } +} \ No newline at end of file diff --git a/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/FlowableCharSequence.java b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/FlowableCharSequence.java new file mode 100644 index 0000000000..a45dbdd2c5 --- /dev/null +++ b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/FlowableCharSequence.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble.optimizations; + +import io.reactivex.Flowable; +import io.reactivex.internal.fuseable.QueueFuseable; +import io.reactivex.internal.subscriptions.BasicQueueSubscription; +import io.reactivex.internal.subscriptions.SubscriptionHelper; +import io.reactivex.internal.util.BackpressureHelper; +import org.reactivestreams.Subscriber; + +final class FlowableCharSequence extends Flowable { + + final CharSequence string; + + FlowableCharSequence(CharSequence string) { + this.string = string; + } + + @Override + public void subscribeActual(Subscriber s) { + s.onSubscribe(new CharSequenceSubscription(s, string)); + } + + static final class CharSequenceSubscription + extends BasicQueueSubscription { + + private static final long serialVersionUID = -4593793201463047197L; + + final Subscriber downstream; + + final CharSequence string; + + final int end; + + int index; + + volatile boolean cancelled; + + CharSequenceSubscription(Subscriber downstream, CharSequence string) { + this.downstream = downstream; + this.string = string; + this.end = string.length(); + } + + @Override + public void cancel() { + cancelled = true; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (BackpressureHelper.add(this, n) == 0) { + if (n == Long.MAX_VALUE) { + fastPath(); + } else { + slowPath(n); + } + } + } + } + + void fastPath() { + int e = end; + CharSequence s = string; + Subscriber a = downstream; + + for (int i = index; i != e; i++) { + if (cancelled) { + return; + } + + a.onNext((int)s.charAt(i)); + } + + if (!cancelled) { + a.onComplete(); + } + } + + void slowPath(long r) { + long e = 0L; + int i = index; + int f = end; + CharSequence s = string; + Subscriber a = downstream; + + for (;;) { + + while (e != r && i != f) { + if (cancelled) { + return; + } + + a.onNext((int)s.charAt(i)); + + i++; + e++; + } + + if (i == f) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + r = get(); + if (e == r) { + index = i; + r = addAndGet(-e); + if (r == 0L) { + break; + } + e = 0L; + } + } + } + + @Override + public int requestFusion(int requestedMode) { + return requestedMode & QueueFuseable.SYNC; + } + + @Override + public Integer poll() { + int i = index; + if (i != end) { + index = i + 1; + return (int)string.charAt(i); + } + return null; + } + + @Override + public boolean isEmpty() { + return index == end; + } + + @Override + public void clear() { + index = end; + } + } + +} diff --git a/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/FlowableSplit.java b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/FlowableSplit.java new file mode 100644 index 0000000000..83c203e42f --- /dev/null +++ b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/FlowableSplit.java @@ -0,0 +1,327 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble.optimizations; + +import io.reactivex.Flowable; +import io.reactivex.FlowableTransformer; +import io.reactivex.exceptions.Exceptions; +import io.reactivex.internal.fuseable.ConditionalSubscriber; +import io.reactivex.internal.fuseable.SimplePlainQueue; +import io.reactivex.internal.queue.SpscArrayQueue; +import io.reactivex.internal.subscriptions.SubscriptionHelper; +import io.reactivex.internal.util.BackpressureHelper; +import io.reactivex.plugins.RxJavaPlugins; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; + +final class FlowableSplit extends Flowable implements FlowableTransformer { + + final Publisher source; + + final Pattern pattern; + + final int bufferSize; + + FlowableSplit(Publisher source, Pattern pattern, int bufferSize) { + this.source = source; + this.pattern = pattern; + this.bufferSize = bufferSize; + } + + @Override + public Publisher apply(Flowable upstream) { + return new FlowableSplit(upstream, pattern, bufferSize); + } + + @Override + protected void subscribeActual(Subscriber s) { + source.subscribe(new SplitSubscriber(s, pattern, bufferSize)); + } + + static final class SplitSubscriber + extends AtomicInteger + implements ConditionalSubscriber, Subscription { + + static final String[] EMPTY = new String[0]; + + private static final long serialVersionUID = -5022617259701794064L; + + final Subscriber downstream; + + final Pattern pattern; + + final SimplePlainQueue queue; + + final AtomicLong requested; + + final int bufferSize; + + final int limit; + + Subscription upstream; + + volatile boolean cancelled; + + String leftOver; + + String[] current; + + int index; + + int produced; + + volatile boolean done; + Throwable error; + + int empty; + + SplitSubscriber(Subscriber downstream, Pattern pattern, int bufferSize) { + this.downstream = downstream; + this.pattern = pattern; + this.bufferSize = bufferSize; + this.limit = bufferSize - (bufferSize >> 2); + this.queue = new SpscArrayQueue(bufferSize); + this.requested = new AtomicLong(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + + if (getAndIncrement() == 0) { + current = null; + queue.clear(); + } + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(bufferSize); + } + } + + @Override + public void onNext(String t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(String t) { + String lo = leftOver; + String[] a; + try { + if (lo == null || lo.isEmpty()) { + a = pattern.split(t, -1); + } else { + a = pattern.split(lo + t, -1); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + this.upstream.cancel(); + onError(ex); + return true; + } + + if (a.length == 0) { + leftOver = null; + return false; + } else + if (a.length == 1) { + leftOver = a[0]; + return false; + } + leftOver = a[a.length - 1]; + queue.offer(a); + drain(); + return true; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + String lo = leftOver; + if (lo != null && !lo.isEmpty()) { + leftOver = null; + queue.offer(new String[] { lo, null }); + } + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + String lo = leftOver; + if (lo != null && !lo.isEmpty()) { + leftOver = null; + queue.offer(new String[] { lo, null }); + } + drain(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + SimplePlainQueue q = queue; + + int missed = 1; + int consumed = produced; + String[] array = current; + int idx = index; + int emptyCount = empty; + + Subscriber a = downstream; + + for (;;) { + long r = requested.get(); + long e = 0; + + while (e != r) { + if (cancelled) { + current = null; + q.clear(); + return; + } + + boolean d = done; + + if (array == null) { + array = q.poll(); + if (array != null) { + current = array; + if (++consumed == limit) { + consumed = 0; + upstream.request(limit); + } + } + } + + boolean empty = array == null; + + if (d && empty) { + current = null; + Throwable ex = error; + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + return; + } + + if (empty) { + break; + } + + if (array.length == idx + 1) { + array = null; + current = null; + idx = 0; + continue; + } + + String v = array[idx]; + + if (v.isEmpty()) { + emptyCount++; + idx++; + } else { + while (emptyCount != 0 && e != r) { + if (cancelled) { + current = null; + q.clear(); + return; + } + a.onNext(""); + e++; + emptyCount--; + } + + if (e != r && emptyCount == 0) { + a.onNext(v); + + e++; + idx++; + } + } + } + + if (e == r) { + if (cancelled) { + current = null; + q.clear(); + return; + } + + boolean d = done; + + if (array == null) { + array = q.poll(); + if (array != null) { + current = array; + if (++consumed == limit) { + consumed = 0; + upstream.request(limit); + } + } + } + + boolean empty = array == null; + + if (d && empty) { + current = null; + Throwable ex = error; + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + return; + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + + empty = emptyCount; + produced = consumed; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } +} diff --git a/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/StringFlowable.java b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/StringFlowable.java new file mode 100644 index 0000000000..3d36a0d8e7 --- /dev/null +++ b/benchmarks/src/jmh/java/benchmarks/flow/scrabble/optimizations/StringFlowable.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble.optimizations; + +import io.reactivex.Flowable; +import io.reactivex.FlowableTransformer; +import io.reactivex.internal.functions.ObjectHelper; +import io.reactivex.plugins.RxJavaPlugins; + +import java.util.regex.Pattern; + +public final class StringFlowable { + /** Utility class. */ + private StringFlowable() { + throw new IllegalStateException("No instances!"); + } + + /** + * Signals each character of the given string CharSequence as Integers. + * @param string the source of characters + * @return the new Flowable instance + */ + public static Flowable characters(CharSequence string) { + ObjectHelper.requireNonNull(string, "string is null"); + return RxJavaPlugins.onAssembly(new FlowableCharSequence(string)); + } + + /** + * Splits the input sequence of strings based on a pattern even across subsequent + * elements if needed. + * @param pattern the Rexexp pattern to split along + * @return the new FlowableTransformer instance + * + * @since 0.13.0 + */ + public static FlowableTransformer split(Pattern pattern) { + return split(pattern, Flowable.bufferSize()); + } + + /** + * Splits the input sequence of strings based on a pattern even across subsequent + * elements if needed. + * @param pattern the Rexexp pattern to split along + * @param bufferSize the number of items to prefetch from the upstream + * @return the new FlowableTransformer instance + * + * @since 0.13.0 + */ + public static FlowableTransformer split(Pattern pattern, int bufferSize) { + ObjectHelper.requireNonNull(pattern, "pattern is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return new FlowableSplit(null, pattern, bufferSize); + } + + /** + * Splits the input sequence of strings based on a pattern even across subsequent + * elements if needed. + * @param pattern the Rexexp pattern to split along + * @return the new FlowableTransformer instance + * + * @since 0.13.0 + */ + public static FlowableTransformer split(String pattern) { + return split(pattern, Flowable.bufferSize()); + } + + /** + * Splits the input sequence of strings based on a pattern even across subsequent + * elements if needed. + * @param pattern the Rexexp pattern to split along + * @param bufferSize the number of items to prefetch from the upstream + * @return the new FlowableTransformer instance + * + * @since 0.13.0 + */ + public static FlowableTransformer split(String pattern, int bufferSize) { + return split(Pattern.compile(pattern), bufferSize); + } + +} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleBase.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleBase.kt new file mode 100644 index 0000000000..b556053b5d --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleBase.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.openjdk.jmh.annotations.* +import java.lang.Long.* +import java.lang.Long.max +import java.util.* +import java.util.concurrent.* +import kotlin.math.* + +@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +open class FlowPlaysScrabbleBase : ShakespearePlaysScrabble() { + + @Benchmark + public override fun play(): List>> { + val scoreOfALetter = { letter: Int -> flowOf(letterScores[letter - 'a'.toInt()]) } + + val letterScore = { entry: Map.Entry -> + flowOf( + letterScores[entry.key - 'a'.toInt()] * Integer.min( + entry.value.get().toInt(), + scrabbleAvailableLetters[entry.key - 'a'.toInt()] + ) + ) + } + + val toIntegerStream = { string: String -> + IterableSpliterator.of(string.chars().boxed().spliterator()).asFlow() + } + + val histoOfLetters = { word: String -> + flow { + emit(toIntegerStream(word).fold(HashMap()) { accumulator, value -> + var newValue: LongWrapper? = accumulator[value] + if (newValue == null) { + newValue = LongWrapper.zero() + } + accumulator[value] = newValue.incAndSet() + accumulator + }) + } + } + + val blank = { entry: Map.Entry -> + flowOf(max(0L, entry.value.get() - scrabbleAvailableLetters[entry.key - 'a'.toInt()])) + } + + val nBlanks = { word: String -> + flow { + emit(histoOfLetters(word) + .flatMapConcat { map -> map.entries.iterator().asFlow() } + .flatMapConcat({ blank(it) }) + .reduce { a, b -> a + b }) + } + } + + val checkBlanks = { word: String -> + nBlanks(word).flatMapConcat { l -> flowOf(l <= 2L) } + } + + val score2 = { word: String -> + flow { + emit(histoOfLetters(word) + .flatMapConcat { map -> map.entries.iterator().asFlow() } + .flatMapConcat { letterScore(it) } + .reduce { a, b -> a + b }) + } + } + + val first3 = { word: String -> + IterableSpliterator.of(word.chars().boxed().limit(3).spliterator()).asFlow() + } + + val last3 = { word: String -> + IterableSpliterator.of(word.chars().boxed().skip(3).spliterator()).asFlow() + } + + val toBeMaxed = { word: String -> flowOf(first3(word), last3(word)).flattenConcat() } + + // Bonus for double letter + val bonusForDoubleLetter = { word: String -> + flow { + emit(toBeMaxed(word) + .flatMapConcat { scoreOfALetter(it) } + .reduce { a, b -> max(a, b) }) + } + } + + val score3 = { word: String -> + flow { + emit(flowOf( + score2(word), score2(word), + bonusForDoubleLetter(word), + bonusForDoubleLetter(word), + flowOf(if (word.length == 7) 50 else 0) + ).flattenConcat().reduce { a, b -> a + b }) + } + } + + val buildHistoOnScore: (((String) -> Flow) -> Flow>>) = { score -> + flow { + emit(shakespeareWords.asFlow() + .filter({ scrabbleWords.contains(it) && checkBlanks(it).single() }) + .fold(TreeMap>(Collections.reverseOrder())) { acc, value -> + val key = score(value).single() + var list = acc[key] as MutableList? + if (list == null) { + list = ArrayList() + acc[key] = list + } + list.add(value) + acc + }) + } + } + + return runBlocking { + buildHistoOnScore(score3) + .flatMapConcat { map -> map.entries.iterator().asFlow() } + .take(3) + .toList() + } + } +} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt.kt new file mode 100644 index 0000000000..921f390dce --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. and contributors Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.openjdk.jmh.annotations.* +import java.lang.Long.max +import java.util.* +import java.util.concurrent.* +import java.util.stream.* +import kotlin.math.* + +@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +open class FlowPlaysScrabbleOpt : ShakespearePlaysScrabble() { + + @Benchmark + public override fun play(): List>> { + val histoOfLetters = { word: String -> + flow { + emit(word.asFlow().fold(HashMap()) { accumulator, value -> + var newValue: MutableLong? = accumulator[value] + if (newValue == null) { + newValue = MutableLong() + accumulator[value] = newValue + } + newValue.incAndSet() + accumulator + }) + } + } + + val blank = { entry: Map.Entry -> + max(0L, entry.value.get() - scrabbleAvailableLetters[entry.key - 'a'.toInt()]) + } + + val nBlanks = { word: String -> + flow { + emit(histoOfLetters(word) + .flatMapConcatIterable { it.entries } + .map({ blank(it) }) + .sum() + ) + } + } + + val checkBlanks = { word: String -> + nBlanks(word).map { it <= 2L } + } + + val letterScore = { entry: Map.Entry -> + letterScores[entry.key - 'a'.toInt()] * Integer.min( + entry.value.get().toInt(), + scrabbleAvailableLetters[entry.key - 'a'.toInt()] + ) + } + + val score2 = { word: String -> + flow { + emit(histoOfLetters(word) + .flatMapConcatIterable { it.entries } + .map { letterScore(it) } + .sum()) + } + } + + val first3 = { word: String -> word.asFlow(endIndex = 3) } + val last3 = { word: String -> word.asFlow(startIndex = 3) } + val toBeMaxed = { word: String -> concat(first3(word), last3(word)) } + + val bonusForDoubleLetter = { word: String -> + flow { + emit(toBeMaxed(word) + .map { letterScores[it.toInt() - 'a'.toInt()] } + .max()) + } + } + + val score3 = { word: String -> + flow { + val sum = score2(word).single() + bonusForDoubleLetter(word).single() + emit(sum * 2 + if (word.length == 7) 50 else 0) + } + } + + val buildHistoOnScore: (((String) -> Flow) -> Flow>>) = { score -> + flow { + emit(shakespeareWords.asFlow() + .filter({ scrabbleWords.contains(it) && checkBlanks(it).single() }) + .fold(TreeMap>(Collections.reverseOrder())) { acc, value -> + val key = score(value).single() + var list = acc[key] as MutableList? + if (list == null) { + list = ArrayList() + acc[key] = list + } + list.add(value) + acc + }) + } + } + + return runBlocking { + buildHistoOnScore(score3) + .flatMapConcatIterable { it.entries } + .take(3) + .toList() + } + } +} + +public fun String.asFlow() = flow { + forEach { + emit(it.toInt()) + } +} + +public fun String.asFlow(startIndex: Int = 0, endIndex: Int = length) = + StringByCharFlow(this, startIndex, endIndex.coerceAtMost(this.length)) + +public suspend inline fun Flow.sum(): Int { + val collector = object : FlowCollector { + public var sum = 0 + + override suspend fun emit(value: Int) { + sum += value + } + } + collect(collector) + return collector.sum +} + +public suspend inline fun Flow.max(): Int { + val collector = object : FlowCollector { + public var max = 0 + + override suspend fun emit(value: Int) { + max = max(max, value) + } + } + collect(collector) + return collector.max +} + +@JvmName("longSum") +public suspend inline fun Flow.sum(): Long { + val collector = object : FlowCollector { + public var sum = 0L + + override suspend fun emit(value: Long) { + sum += value + } + } + collect(collector) + return collector.sum +} + +public class StringByCharFlow(private val source: String, private val startIndex: Int, private val endIndex: Int): Flow { + override suspend fun collect(collector: FlowCollector) { + for (i in startIndex until endIndex) collector.emit(source[i]) + } +} + +public fun concat(first: Flow, second: Flow): Flow = flow { + first.collect { value -> + return@collect emit(value) + } + + second.collect { value -> + return@collect emit(value) + } +} + +public fun Flow.flatMapConcatIterable(transformer: (T) -> Iterable): Flow = flow { + collect { value -> + transformer(value).forEach { r -> + emit(r) + } + } +} + +public inline fun flow(@BuilderInference crossinline block: suspend FlowCollector.() -> Unit): Flow { + return object : Flow { + override suspend fun collect(collector: FlowCollector) { + collector.block() + } + } +} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/IterableSpliterator.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/IterableSpliterator.kt new file mode 100644 index 0000000000..434ea1e19d --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/IterableSpliterator.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble + +import java.util.* + +object IterableSpliterator { + @JvmStatic + public fun of(spliterator: Spliterator): Iterable = Iterable { Spliterators.iterator(spliterator) } +} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/README.md b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/README.md new file mode 100644 index 0000000000..13e016fd8b --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/README.md @@ -0,0 +1,42 @@ +## Reactive scrabble benchmarks + +This package contains reactive scrabble benchmarks. + +Reactive Scrabble benchmarks were originally developed by José Paumard and are [available](https://github.com/JosePaumard/jdk8-stream-rx-comparison-reloaded) under Apache 2.0, +Flow version is adaptation of this work. +All Rx and Reactive benchmarks are based on (or copied from) [David Karnok work](https://github.com/akarnokd/akarnokd-misc). + +### Benchmark classes + +The package (split into two sourcesets, `kotlin` and `java`), contains different benchmarks with different purposes + + * `RxJava2PlaysScrabble` and `RxJava2PlaysScrabbleOpt` are copied as is and used for comparison. The infrastructure (e.g. `FlowableSplit`) + is copied from `akarnokd-misc` in order for the latter benchmark to work. + This is the original benchmark for `RxJava`. + * `ReactorPlaysScrabble` is an original benchmark for `Reactor`, but rewritten into Kotlin. + It is disabled by default and had the only purpose -- verify that Kotlin version performs as the original Java version + (which could have been different due to lambdas translation, implicit boxing, etc.). It is disabled because + it has almost no difference compared to `RxJava` benchmark. + * `FlowPlaysScrabbleBase` is a scrabble benchmark rewritten on top of the `Flow` API without using any optimizations or tricky internals. + * `FlowPlaysScrabbleOpt` is an optimized version of benchmark that follows the same guidelines as `RxJava2PlaysScrabbleOpt`: it still is + lazy, reactive and uses only `Flow` abstraction. + * `SequencePlaysScrabble` is a version of benchmark built on top of `Sequence` without suspensions, used as a lower bound. + * `SaneFlowPlaysScrabble` is a `SequencePlaysScrabble` that produces `Flow`. + This benchmark is not identical (in terms of functions pipelining) to `FlowPlaysScrabbleOpt`, but rather is used as a lower bound of `Flow` performance + on this particular task. + +### Results + +Benchmark results for throughput mode, Java `1.8.162`. +Full command: `taskset -c 0,1 java -jar benchmarks.jar -f 2 -jvmArgsPrepend "-XX:+UseParallelGC" .*Scrabble.*`. + +``` +FlowPlaysScrabbleBase.play avgt 14 94.845 ± 1.345 ms/op +FlowPlaysScrabbleOpt.play avgt 14 20.587 ± 0.173 ms/op + +RxJava2PlaysScrabble.play avgt 14 114.253 ± 3.450 ms/op +RxJava2PlaysScrabbleOpt.play avgt 14 30.795 ± 0.144 ms/op + +SaneFlowPlaysScrabble.play avgt 14 18.825 ± 0.231 ms/op +SequencePlaysScrabble.play avgt 14 13.787 ± 0.111 ms/op +``` diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ReactorPlaysScrabble.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ReactorPlaysScrabble.kt new file mode 100644 index 0000000000..9adc4f1f59 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ReactorPlaysScrabble.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble + +import org.openjdk.jmh.annotations.* +import reactor.core.publisher.* +import java.lang.Long.* +import java.util.* +import java.util.concurrent.* +import java.util.function.Function + +/*@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark)*/ +open class ReactorPlaysScrabble : ShakespearePlaysScrabble() { + +// @Benchmark + public override fun play(): List>> { + val scoreOfALetter = Function> { letter -> Flux.just(letterScores[letter - 'a'.toInt()]) } + + val letterScore = Function, Flux> { entry -> + Flux.just( + letterScores[entry.key - 'a'.toInt()] * Integer.min( + entry.value.get().toInt(), + scrabbleAvailableLetters[entry.key - 'a'.toInt()] + ) + ) + } + + val toIntegerStream = Function> { string -> + Flux.fromIterable(IterableSpliterator.of(string.chars().boxed().spliterator())) + } + + val histoOfLetters = Function>> { word -> + Flux.from(toIntegerStream.apply(word) + .collect( + { HashMap() }, + { map: HashMap, value: Int -> + var newValue: LongWrapper? = map[value] + if (newValue == null) { + newValue = LongWrapper.zero() + } + map[value] = newValue.incAndSet() + } + + )) + } + + val blank = Function, Flux> { entry -> + Flux.just(max(0L, entry.value.get() - scrabbleAvailableLetters[entry.key - 'a'.toInt()])) + } + + val nBlanks = Function> { word -> + Flux.from(histoOfLetters.apply(word) + .flatMap> { map -> Flux.fromIterable>(Iterable { map.entries.iterator() }) } + .flatMap(blank) + .reduce { a, b -> sum(a, b) }) + } + + val checkBlanks = Function> { word -> + nBlanks.apply(word) + .flatMap { l -> Flux.just(l <= 2L) } + } + + + val score2 = Function> { word -> + Flux.from(histoOfLetters.apply(word) + .flatMap> { map -> Flux.fromIterable>(Iterable { map.entries.iterator() }) } + .flatMap(letterScore) + .reduce { a, b -> Integer.sum(a, b) }) + + } + + val first3 = Function> { word -> Flux.fromIterable( + IterableSpliterator.of( + word.chars().boxed().limit(3).spliterator() + ) + ) } + val last3 = Function> { word -> Flux.fromIterable( + IterableSpliterator.of( + word.chars().boxed().skip(3).spliterator() + ) + ) } + + val toBeMaxed = Function> { word -> + Flux.just(first3.apply(word), last3.apply(word)) + .flatMap { Stream -> Stream } + } + + // Bonus for double letter + val bonusForDoubleLetter = Function> { word -> + Flux.from(toBeMaxed.apply(word) + .flatMap(scoreOfALetter) + .reduce { a, b -> Integer.max(a, b) } + ) + } + + val score3 = Function> { word -> + Flux.from(Flux.just( + score2.apply(word), + score2.apply(word), + bonusForDoubleLetter.apply(word), + bonusForDoubleLetter.apply(word), + Flux.just(if (word.length == 7) 50 else 0) + ) + .flatMap { Stream -> Stream } + .reduce { a, b -> Integer.sum(a, b) }) + } + + val buildHistoOnScore = Function>, Flux>>> { score -> + Flux.from(Flux.fromIterable(Iterable { shakespeareWords.iterator() }) + .filter( { scrabbleWords.contains(it) }) + .filter({ word -> checkBlanks.apply(word).toIterable().iterator().next() }) + .collect( + { TreeMap>(Collections.reverseOrder()) }, + { map: TreeMap>, word: String -> + val key = score.apply(word).toIterable().iterator().next() + var list = map[key] as MutableList? + if (list == null) { + list = ArrayList() + map[key] = list + } + list.add(word) + } + )) + } + + val finalList2 = Flux.from>>>(buildHistoOnScore.apply(score3) + .flatMap>> { map -> Flux.fromIterable>>(Iterable { map.entries.iterator() }) } + .take(3) + .collect>>>( + { ArrayList() }, + { list, entry -> list.add(entry) } + ) + ).toIterable().iterator().next() + + return finalList2 + } + +} + diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SaneFlowPlaysScrabble.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SaneFlowPlaysScrabble.kt new file mode 100644 index 0000000000..597667c1c6 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SaneFlowPlaysScrabble.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.openjdk.jmh.annotations.* +import java.lang.Long.* +import java.util.* +import java.util.concurrent.* + +@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +open class SaneFlowPlaysScrabble : ShakespearePlaysScrabble() { + + @Benchmark + public override fun play(): List>> { + val score3: suspend (String) -> Int = { word: String -> + val sum = score2(word) + bonusForDoubleLetter(word) + sum * 2 + if (word.length == 7) 50 else 0 + } + + val buildHistoOnScore: ((suspend (String) -> Int) -> Flow>>) = { score -> + flow { + emit(shakespeareWords.asFlow() + .filter({ scrabbleWords.contains(it) && checkBlanks(it) }) + .fold(TreeMap>(Collections.reverseOrder())) { acc, value -> + val key = score(value) + var list = acc[key] as MutableList? + if (list == null) { + list = ArrayList() + acc[key] = list + } + list.add(value) + acc + }) + } + } + + return runBlocking { + buildHistoOnScore(score3) + .flatMapConcatIterable { it.entries } + .take(3) + .toList() + } + } + + private suspend inline fun score2(word: String): Int { + return buildHistogram(word) + .map { it.letterScore() } + .sum() + } + + private suspend inline fun bonusForDoubleLetter(word: String): Int { + return toBeMaxed(word) + .map { letterScores[it - 'a'.toInt()] } + .max() + } + + private fun Map.Entry.letterScore(): Int = letterScores[key - 'a'.toInt()] * Integer.min( + value.get().toInt(), + scrabbleAvailableLetters[key - 'a'.toInt()]) + + private fun toBeMaxed(word: String) = concat(word.asSequence(), word.asSequence(endIndex = 3)) + + private suspend inline fun checkBlanks(word: String) = numBlanks(word) <= 2L + + private suspend fun numBlanks(word: String): Long { + return buildHistogram(word) + .map { blanks(it) } + .sum() + } + + private fun blanks(entry: Map.Entry): Long = + max(0L, entry.value.get() - scrabbleAvailableLetters[entry.key - 'a'.toInt()]) + + private suspend inline fun buildHistogram(word: String): HashMap { + return word.asSequence().fold(HashMap()) { accumulator, value -> + var newValue: MutableLong? = accumulator[value] + if (newValue == null) { + newValue = MutableLong() + accumulator[value] = newValue + } + newValue.incAndSet() + accumulator + } + } + + private fun String.asSequence(startIndex: Int = 0, endIndex: Int = length) = flow { + for (i in startIndex until endIndex.coerceAtMost(length)) { + emit(get(i).toInt()) + } + } +} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt new file mode 100644 index 0000000000..5f4f4c2d1a --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/SequencePlaysScrabble.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.openjdk.jmh.annotations.* +import java.lang.Long.* +import java.util.* +import java.util.concurrent.* + +@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +open class SequencePlaysScrabble : ShakespearePlaysScrabble() { + + @Benchmark + public override fun play(): List>> { + val score2: (String) -> Int = { word: String -> + buildHistogram(word) + .map { it.letterScore() } + .sum() + } + + val bonusForDoubleLetter: (String) -> Int = { word: String -> + toBeMaxed(word) + .map { letterScores[it - 'a'.toInt()] } + .max()!! + } + + val score3: (String) -> Int = { word: String -> + val sum = score2(word) + bonusForDoubleLetter(word) + sum * 2 + if (word.length == 7) 50 else 0 + } + + val buildHistoOnScore: (((String) -> Int) -> Flow>>) = { score -> + flow { + emit(shakespeareWords.asSequence() + .filter({ scrabbleWords.contains(it) && checkBlanks(it) }) + .fold(TreeMap>(Collections.reverseOrder())) { acc, value -> + val key = score(value) + var list = acc[key] as MutableList? + if (list == null) { + list = ArrayList() + acc[key] = list + } + list.add(value) + acc + }) + } + } + + return runBlocking { + buildHistoOnScore(score3) + .flatMapConcatIterable { it.entries } + .take(3) + .toList() + } + } + + private fun Map.Entry.letterScore(): Int = letterScores[key - 'a'.toInt()] * Integer.min( + value.get().toInt(), + scrabbleAvailableLetters[key - 'a'.toInt()]) + + private fun toBeMaxed(word: String) = word.asSequence(startIndex = 3) + word.asSequence(endIndex = 3) + + private fun checkBlanks(word: String) = numBlanks(word) <= 2L + + private fun numBlanks(word: String): Long { + return buildHistogram(word) + .map { blanks(it) } + .sum() + } + + private fun blanks(entry: Map.Entry): Long = + max(0L, entry.value.get() - scrabbleAvailableLetters[entry.key - 'a'.toInt()]) + + private fun buildHistogram(word: String): HashMap { + return word.asSequence().fold(HashMap()) { accumulator, value -> + var newValue: MutableLong? = accumulator[value] + if (newValue == null) { + newValue = MutableLong() + accumulator[value] = newValue + } + newValue.incAndSet() + accumulator + } + } + + private fun String.asSequence(startIndex: Int = 0, endIndex: Int = length) = object : Sequence { + override fun iterator(): Iterator = object : Iterator { + private val _endIndex = endIndex.coerceAtMost(length) + private var currentIndex = startIndex + override fun hasNext(): Boolean = currentIndex < _endIndex + override fun next(): Int = get(currentIndex++).toInt() + } + } +} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ShakespearePlaysScrabble.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ShakespearePlaysScrabble.kt new file mode 100644 index 0000000000..7eaa3f0a5d --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ShakespearePlaysScrabble.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.scrabble + +import org.openjdk.jmh.annotations.* +import java.io.* +import java.util.stream.* +import java.util.zip.* + +@State(Scope.Benchmark) +abstract class ShakespearePlaysScrabble { + @Throws(Exception::class) + abstract fun play(): List>> + + public class MutableLong { + var value: Long = 0 + fun get(): Long { + return value + } + + fun incAndSet(): MutableLong { + value++ + return this + } + + fun add(other: MutableLong): MutableLong { + value += other.value + return this + } + } + + public interface LongWrapper { + fun get(): Long + + @JvmDefault + fun incAndSet(): LongWrapper { + return object : LongWrapper { + override fun get(): Long = this@LongWrapper.get() + 1L + } + } + + @JvmDefault + fun add(other: LongWrapper): LongWrapper { + return object : LongWrapper { + override fun get(): Long = this@LongWrapper.get() + other.get() + } + } + + companion object { + fun zero(): LongWrapper { + return object : LongWrapper { + override fun get(): Long = 0L + } + } + } + } + + @JvmField + public val letterScores: IntArray = intArrayOf(1, 3, 3, 2, 1, 4, 2, 4, 1, 8, 5, 1, 3, 1, 1, 3, 10, 1, 1, 1, 1, 4, 4, 8, 4, 10) + + @JvmField + public val scrabbleAvailableLetters: IntArray = + intArrayOf(9, 2, 2, 1, 12, 2, 3, 2, 9, 1, 1, 4, 2, 6, 8, 2, 1, 6, 4, 6, 4, 2, 2, 1, 2, 1) + + @JvmField + public val scrabbleWords: Set = readResource("ospd.txt.gz") + + @JvmField + public val shakespeareWords: Set = readResource("words.shakespeare.txt.gz") + + private fun readResource(path: String) = + BufferedReader(InputStreamReader(GZIPInputStream(this.javaClass.classLoader.getResourceAsStream(path)))).lines() + .map { it.toLowerCase() }.collect(Collectors.toSet()) + + init { + val expected = listOf(120 to listOf("jezebel", "quickly"), + 118 to listOf("zephyrs"), 116 to listOf("equinox")) + val actual = play().map { it.key to it.value } + if (expected != actual) { + error("Incorrect benchmark, output: $actual") + } + } +} diff --git a/benchmarks/src/jmh/resources/ospd.txt.gz b/benchmarks/src/jmh/resources/ospd.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..8a3074e927a9658bdac49e74fe86d4fc10c5cafd GIT binary patch literal 197126 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

E%@8=dkhz(g)z-E8r7iGHTJ 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 N{{xXIj*kst2>_`lZIl22 literal 0 HcmV?d00001 diff --git a/benchmarks/src/jmh/resources/words.shakespeare.txt.gz b/benchmarks/src/jmh/resources/words.shakespeare.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..456f56cbeb8c2112175d6ecb91bc21196526b3a2 GIT binary patch literal 81824 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 r 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 Gj{^W5+z(*@ literal 0 HcmV?d00001 diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 7ddbfb93c3..558b62ed8e 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -821,7 +821,6 @@ public final class kotlinx/coroutines/flow/FlowKt { public static synthetic fun flowViaChannel$default (ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flowWith (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun flowWith$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static final fun fold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun map (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun mapNotNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun onEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; From c438a0e8ffefb6682898415ef967c5165937948a Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 30 Apr 2019 16:36:14 +0300 Subject: [PATCH 19/56] Numbers benchmark that exposes some of Flow weaknesses --- benchmarks/build.gradle | 5 +- .../kotlin/benchmarks/flow/misc/Numbers.kt | 123 ++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/misc/Numbers.kt diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index fb10ad1e05..157eb88aa6 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -34,12 +34,13 @@ task removeRedundantFiles(type: Delete) { delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$1\$1.class" delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$2\$1.class" delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class" delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble//SaneFlowPlaysScrabble\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class" + // Primes + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$2\$1.class" + delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$1.class" } jmhRunBytecodeGenerator.dependsOn(removeRedundantFiles) diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/misc/Numbers.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/misc/Numbers.kt new file mode 100644 index 0000000000..cbdbca55ef --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/misc/Numbers.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + + +package benchmarks.flow.misc + +import benchmarks.flow.scrabble.flow +import io.reactivex.* +import io.reactivex.functions.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.openjdk.jmh.annotations.* +import java.util.concurrent.* + +/* + * Results: + * + * // Throw FlowAborted overhead + * Numbers.primes avgt 7 4106.837 ± 59.672 us/op + * Numbers.primesRx avgt 7 2777.232 ± 85.357 us/op + * + * // On par + * Numbers.transformations avgt 7 20.290 ± 1.367 us/op + * Numbers.transformationsRx avgt 7 22.932 ± 1.863 us/op + * + * // Channels overhead + * Numbers.zip avgt 7 470.737 ± 10.838 us/op + * Numbers.zipRx avgt 7 104.811 ± 9.073 us/op + * + */ +@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +open class Numbers { + + companion object { + private const val primes = 100 + private const val natural = 1000 + } + + private fun numbers() = flow { + for (i in 2L..Long.MAX_VALUE) emit(i) + } + + private fun primesFlow(): Flow = flow { + var source = numbers() + while (true) { + val next = source.take(1).single() + emit(next) + source = source.filter { it % next != 0L } + } + } + + private fun rxNumbers() = + Flowable.generate(Callable { 1L }, BiFunction, Long> { state, emitter -> + val newState = state + 1 + emitter.onNext(newState) + newState + }) + + private fun generateRxPrimes(): Flowable = Flowable.generate(Callable { rxNumbers() }, + BiFunction, Emitter, Flowable> { state, emitter -> + // Not the most fair comparison, but here we go + val prime = state.firstElement().blockingGet() + emitter.onNext(prime) + state.filter { it % prime != 0L } + }) + + @Benchmark + fun primes() = runBlocking { + primesFlow().take(primes).count() + } + + @Benchmark + fun primesRx() = generateRxPrimes().take(primes.toLong()).count().blockingGet() + + @Benchmark + fun zip() = runBlocking { + val numbers = numbers().take(natural) + val first = numbers + .filter { it % 2L != 0L } + .map { it * it } + val second = numbers + .filter { it % 2L == 0L } + .map { it * it } + first.zip(second) { v1, v2 -> v1 + v2 }.filter { it % 3 == 0L }.count() + } + + @Benchmark + fun zipRx() { + val numbers = rxNumbers().take(natural.toLong()) + val first = numbers + .filter { it % 2L != 0L } + .map { it * it } + val second = numbers + .filter { it % 2L == 0L } + .map { it * it } + first.zipWith(second, BiFunction { v1, v2 -> v1 + v2 }).filter { it % 3 == 0L }.count() + .blockingGet() + } + + @Benchmark + fun transformations(): Int = runBlocking { + numbers() + .take(natural) + .filter { it % 2L != 0L } + .map { it * it } + .filter { (it + 1) % 3 == 0L }.count() + } + + @Benchmark + fun transformationsRx(): Long { + return rxNumbers().take(natural.toLong()) + .filter { it % 2L != 0L } + .map { it * it } + .filter { (it + 1) % 3 == 0L }.count() + .blockingGet() + } +} From 78f3e23594e43d999015b397a85b13d34cc89186 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 16 May 2019 11:54:54 +0300 Subject: [PATCH 20/56] Safe flow benchmark --- .../benchmarks/flow/misc/SafeFlowBenchmark.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/misc/SafeFlowBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/misc/SafeFlowBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/misc/SafeFlowBenchmark.kt new file mode 100644 index 0000000000..f62af91eb8 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/misc/SafeFlowBenchmark.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow.misc + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.openjdk.jmh.annotations.* +import java.util.concurrent.* +import benchmarks.flow.scrabble.flow as unsafeFlow +import kotlinx.coroutines.flow.flow as safeFlow + +@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +open class SafeFlowBenchmark { + + private fun numbersSafe() = safeFlow { + for (i in 2L..1000L) emit(i) + } + + private fun numbersUnsafe() = unsafeFlow { + for (i in 2L..1000L) emit(i) + } + + @Benchmark + fun safeNumbers(): Int = runBlocking { + numbersSafe().count() + } + + @Benchmark + fun unsafeNumbers(): Int = runBlocking { + numbersUnsafe().count() + } +} From 218dc97f02041adafe1ea05fa675721316f2d5a5 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 16 May 2019 14:39:25 +0300 Subject: [PATCH 21/56] Make ChannelIterator.next non-suspending * Mark ReceiveChannel.map with @ObsoleteCoroutinesApi Fixes #1162 --- .../kotlinx-coroutines-core.txt | 7 +++- .../common/src/channels/AbstractChannel.kt | 6 +-- .../common/src/channels/Channel.kt | 41 +++++++++++-------- .../common/src/channels/Channels.common.kt | 2 +- .../test/channels/BasicOperationsTest.kt | 12 ++++++ 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 366906acd6..16ba32a385 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -561,7 +561,12 @@ public final class kotlinx/coroutines/channels/Channel$Factory { public abstract interface class kotlinx/coroutines/channels/ChannelIterator { public abstract fun hasNext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun next (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun next ()Ljava/lang/Object; + public abstract synthetic fun next (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class kotlinx/coroutines/channels/ChannelIterator$DefaultImpls { + public static synthetic fun next (Lkotlinx/coroutines/channels/ChannelIterator;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class kotlinx/coroutines/channels/ChannelKt { diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index f3fb7cd288..d6ae8e4416 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -901,15 +901,15 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) throw recoverStackTrace(result.receiveException) if (result !== POLL_FAILED) { this.result = POLL_FAILED return result as E } - // rare case when hasNext was not invoked yet -- just delegate to receive (leave state as is) - return channel.receive() + + throw IllegalStateException("'hasNext' should be called prior to 'next' invocation") } } diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index 257d526845..030fb6b07a 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -295,25 +295,34 @@ public interface ChannelIterator { */ public suspend operator fun hasNext(): Boolean + @Deprecated(message = "Since 1.3.0, binary compatibility with versions <= 1.2.x", level = DeprecationLevel.HIDDEN) + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("next") + public suspend fun next0(): E { + /* + * Before 1.3.0 the "next()" could have been used without invoking "hasNext" first and there were code samples + * demonstrating this behavior, so we preserve this logic for full binary backwards compatibility with previously + * compiled code. + */ + if (!hasNext()) throw ClosedReceiveChannelException(DEFAULT_CLOSE_MESSAGE) + return next() + } + /** - * Retrieves and removes the element from this channel suspending the caller while this channel - * is empty or throws [ClosedReceiveChannelException] if the channel - * [isClosedForReceive][ReceiveChannel.isClosedForReceive] without cause. - * It throws the original [close][SendChannel.close] cause exception if the channel has _failed_. - * - * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this - * function is suspended, this function immediately resumes with [CancellationException]. - * - * *Cancellation of suspended receive is atomic* -- when this function - * throws [CancellationException] it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this receive operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * Retrieves the element from the current iterator previously removed from the channel by preceding call to [hasNext] or + * throws [IllegalStateException] if [hasNext] was not invoked. + * [next] should only be used in pair with [hasNext]: + * ``` + * while (iterator.hasNext()) { + * val element = iterator.next() + * // ... handle element ... + * } + * ``` * - * Note that this function does not check for cancellation when it is not suspended. - * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. + * This method throws [ClosedReceiveChannelException] if the channel [isClosedForReceive][ReceiveChannel.isClosedForReceive] without cause. + * It throws the original [close][SendChannel.close] cause exception if the channel has _failed_. */ - public suspend operator fun next(): E + public operator fun next(): E } /** diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index 17c19ff1d3..b13dce2704 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -1196,7 +1196,7 @@ public suspend inline fun >> Receiv * The operation is _intermediate_ and _stateless_. * This function [consumes][ReceiveChannel.consume] all elements of the original [ReceiveChannel]. */ -// todo: mark transform with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@ObsoleteCoroutinesApi public fun ReceiveChannel.map(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (E) -> R): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { consumeEach { diff --git a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt index 820fad67f6..167edba53e 100644 --- a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt @@ -73,6 +73,18 @@ class BasicOperationsTest : TestBase() { finish(4) } + @Test + fun testIterator() = runTest { + TestChannelKind.values().forEach { kind -> + val channel = kind.create() + val iterator = channel.iterator() + assertFailsWith { iterator.next() } + channel.close() + assertFailsWith { iterator.next() } + assertFalse(iterator.hasNext()) + } + } + private suspend fun testReceiveOrNull(kind: TestChannelKind) = coroutineScope { val channel = kind.create() val d = async(NonCancellable) { From e35637a9faabb021d7a5efdc5509910cb499cd7a Mon Sep 17 00:00:00 2001 From: Eduard Wolf Date: Mon, 20 May 2019 09:45:03 +0200 Subject: [PATCH 22/56] Fix overflow bug in Flow.drop --- kotlinx-coroutines-core/common/src/flow/operators/Limit.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt index bc4a75bef2..61d4c590c6 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -23,7 +23,7 @@ public fun Flow.drop(count: Int): Flow { return flow { var skipped = 0 collect { value -> - if (++skipped > count) emit(value) + if (skipped >= count) emit(value) else ++skipped } } } From 3fe7bd231b127b4225e731485f7e02a2dd799f02 Mon Sep 17 00:00:00 2001 From: Sean McQuillan Date: Thu, 16 May 2019 16:38:04 -0700 Subject: [PATCH 23/56] Update docs based on feedback @ I/O --- kotlinx-coroutines-test/README.md | 16 ++- .../src/DelayController.kt | 125 ++++++++++++++++++ .../src/TestCoroutineDispatcher.kt | 92 ------------- .../test/TestCoroutineDispatcherOrderTest.kt | 39 ++++++ .../test/TestModuleHelpers.kt | 4 +- .../test/TestRunBlockingOrderTest.kt | 9 ++ 6 files changed, 185 insertions(+), 100 deletions(-) create mode 100644 kotlinx-coroutines-test/src/DelayController.kt create mode 100644 kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 85d6133a4c..f286cab296 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -69,7 +69,7 @@ builder that provides extra test control to coroutines. ### Testing regular suspend functions To test regular suspend functions, which may have a delay, you can use the [runBlockingTest] builder to start a testing -coroutine. Any calls to `delay` will automatically advance time. +coroutine. Any calls to `delay` will automatically advance virtual time by the amount delayed. ```kotlin @Test @@ -79,7 +79,7 @@ fun testFoo() = runBlockingTest { // a coroutine with an extra test control } suspend fun foo() { - delay(1_000) // auto-advances without delay due to runBlockingTest + delay(1_000) // auto-advances virtual time by 1_000ms due to runBlockingTest // ... } ``` @@ -92,7 +92,7 @@ Inside of [runBlockingTest], both [launch] and [async] will start a new coroutin test case. To make common testing situations easier, by default the body of the coroutine is executed *eagerly* until -the first [delay]. +the first call to [delay] or [yield]. ```kotlin @Test @@ -113,8 +113,10 @@ fun CoroutineScope.foo() { suspend fun bar() {} ``` -`runBlockingTest` will auto-progress time until all coroutines are completed before returning. If any coroutines are not -able to complete, an [UncompletedCoroutinesError] will be thrown. +`runBlockingTest` will auto-progress virtual time until all coroutines are completed before returning. If any coroutines +are not able to complete, an [UncompletedCoroutinesError] will be thrown. + +*Note:* The default eager behavior of [runBlockingTest] will ignore [CoroutineStart] parameters. ### Testing `launch` or `async` with `delay` @@ -130,7 +132,7 @@ fun testFooWithLaunchAndDelay() = runBlockingTest { foo() // the coroutine launched by foo has not completed here, it is suspended waiting for delay(1_000) advanceTimeBy(1_000) // progress time, this will cause the delay to resume - // foo() coroutine launched by foo has completed here + // the coroutine launched by foo has completed here // ... } @@ -434,6 +436,8 @@ If you have any suggestions for improvements to this experimental API please sha [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html [delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html +[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html +[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html diff --git a/kotlinx-coroutines-test/src/DelayController.kt b/kotlinx-coroutines-test/src/DelayController.kt new file mode 100644 index 0000000000..54e9c8ae5e --- /dev/null +++ b/kotlinx-coroutines-test/src/DelayController.kt @@ -0,0 +1,125 @@ +package kotlinx.coroutines.test + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** + * Control the virtual clock time of a [CoroutineDispatcher]. + * + * Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher]. + */ +@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 +public interface DelayController { + /** + * Returns the current virtual clock-time as it is known to this Dispatcher. + * + * @return The virtual clock-time + */ + @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + public val currentTime: Long + + /** + * Moves the Dispatcher's virtual clock forward by a specified amount of time. + * + * The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses + * blocking coroutines. + * + * The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested + * `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different + * calls to delay. + * + * ``` + * @Test + * fun advanceTimeTest() = runBlockingTest { + * foo() + * advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays + * // virtual time is 2_000, next resume is at 2_001 + * advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced) + * // virtual time is 2_0002 + * } + * + * fun CoroutineScope.foo() { + * launch { + * delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000) + * // virtual time is 1_000 + * delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500) + * // virtual time is 1_500 + * delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001) + * // virtual time is 2_001 + * } + * } + * ``` + * + * @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward. + * @return The amount of delay-time that this Dispatcher's clock has been forwarded. + */ + @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + public fun advanceTimeBy(delayTimeMillis: Long): Long + + /** + * Immediately execute all pending tasks and advance the virtual clock-time to the last delay. + * + * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle` + * returns. + * + * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds. + */ + @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + public fun advanceUntilIdle(): Long + + /** + * Run any tasks that are pending at or before the current virtual clock-time. + * + * Calling this function will never advance the clock. + */ + @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + public fun runCurrent() + + /** + * Call after test code completes to ensure that the dispatcher is properly cleaned up. + * + * @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended + * coroutines. + */ + @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + @Throws(UncompletedCoroutinesError::class) + public fun cleanupTestCoroutines() + + /** + * Run a block of code in a paused dispatcher. + * + * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher + * will resume auto-advancing. + * + * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or + * setup may be done between the time the coroutine is created and started. + */ + @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + public suspend fun pauseDispatcher(block: suspend () -> Unit) + + /** + * Pause the dispatcher. + * + * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or + * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines. + */ + @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + public fun pauseDispatcher() + + /** + * Resume the dispatcher from a paused state. + * + * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance + * time and execute coroutines scheduled in the future use, one of [advanceTimeBy], + * or [advanceUntilIdle]. + */ + @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 + public fun resumeDispatcher() +} + +/** + * Thrown when a test has completed and there are tasks that are not completed or cancelled. + */ +// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type) +@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 +public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause) diff --git a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt index ef9b6682a3..386fc8380d 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt +++ b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt @@ -10,98 +10,6 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.math.* -/** - * Control the virtual clock time of a [CoroutineDispatcher]. - * - * Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher]. - */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public interface DelayController { - /** - * Returns the current virtual clock-time as it is known to this Dispatcher. - * - * @return The virtual clock-time - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public val currentTime: Long - - /** - * Moves the Dispatcher's virtual clock forward by a specified amount of time. - * - * The amount the clock is progressed may be larger than the requested delayTimeMillis if the code under test uses - * blocking coroutines. - * - * @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward. - * @return The amount of delay-time that this Dispatcher's clock has been forwarded. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun advanceTimeBy(delayTimeMillis: Long): Long - - /** - * Immediately execute all pending tasks and advance the virtual clock-time to the last delay. - * - * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun advanceUntilIdle(): Long - - /** - * Run any tasks that are pending at or before the current virtual clock-time. - * - * Calling this function will never advance the clock. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun runCurrent() - - /** - * Call after test code completes to ensure that the dispatcher is properly cleaned up. - * - * @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended - * coroutines. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - @Throws(UncompletedCoroutinesError::class) - public fun cleanupTestCoroutines() - - /** - * Run a block of code in a paused dispatcher. - * - * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher - * will resume auto-advancing. - * - * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or - * setup may be done between the time the coroutine is created and started. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public suspend fun pauseDispatcher(block: suspend () -> Unit) - - /** - * Pause the dispatcher. - * - * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or - * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun pauseDispatcher() - - /** - * Resume the dispatcher from a paused state. - * - * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance - * time and execute coroutines scheduled in the future use, one of [advanceTimeBy], - * or [advanceUntilIdle]. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun resumeDispatcher() -} - -/** - * Thrown when a test has completed and there are tasks that are not completed or cancelled. - */ -// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type) -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause) - /** * [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests * and implements [DelayController] to control its virtual clock. diff --git a/kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt b/kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt new file mode 100644 index 0000000000..116aadcf8d --- /dev/null +++ b/kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt @@ -0,0 +1,39 @@ +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import org.junit.* +import kotlin.coroutines.* +import kotlin.test.assertEquals + +class TestCoroutineDispatcherOrderTest : TestBase() { + + @Test + fun testAdvanceTimeBy_progressesOnEachDelay() { + val dispatcher = TestCoroutineDispatcher() + val scope = TestCoroutineScope(dispatcher) + + expect(1) + scope.launch { + expect(2) + delay(1_000) + assertEquals(1_000, dispatcher.currentTime) + expect(4) + delay(5_00) + assertEquals(1_500, dispatcher.currentTime) + expect(5) + delay(501) + assertEquals(2_001, dispatcher.currentTime) + expect(7) + } + expect(3) + assertEquals(0, dispatcher.currentTime) + dispatcher.advanceTimeBy(2_000) + expect(6) + assertEquals(2_000, dispatcher.currentTime) + dispatcher.advanceTimeBy(2) + expect(8) + assertEquals(2_002, dispatcher.currentTime) + scope.cleanupTestCoroutines() + finish(9) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/test/TestModuleHelpers.kt b/kotlinx-coroutines-test/test/TestModuleHelpers.kt index 6a9ae9ac92..12541bd90f 100644 --- a/kotlinx-coroutines-test/test/TestModuleHelpers.kt +++ b/kotlinx-coroutines-test/test/TestModuleHelpers.kt @@ -15,8 +15,8 @@ const val SLOW = 10_000L */ suspend fun CoroutineScope.assertRunsFast(block: suspend CoroutineScope.() -> Unit) { val start = Instant.now().toEpochMilli() - // don''t need to be fancy with timeouts here since anything longer than a few ms is an error - this.block() + // don't need to be fancy with timeouts here since anything longer than a few ms is an error + block() val duration = Instant.now().minusMillis(start).toEpochMilli() Assert.assertTrue("All tests must complete within 2000ms (use longer timeouts to cause failure)", duration < 2_000) } diff --git a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt index c58c21cf0d..0013a654a6 100644 --- a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt +++ b/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt @@ -67,4 +67,13 @@ class TestRunBlockingOrderTest : TestBase() { } expect(2) } + + @Test + fun testAdvanceUntilIdle_inRunBlocking() = runBlockingTest { + expect(1) + assertRunsFast { + advanceUntilIdle() // ensure this doesn't block forever + } + finish(2) + } } From da2cf23435de30bb17760c23838f0960ca376b51 Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Wed, 22 May 2019 09:04:22 -0400 Subject: [PATCH 24/56] spelling --- docs/debugging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/debugging.md b/docs/debugging.md index 1067a37063..fc8570126d 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -72,7 +72,7 @@ The full tutorial of how to use debug agent can be found in the corresponding [r Unfortunately, Android runtime does not support Instrument API necessary for `kotlinx-coroutines-debug` to function, triggering `java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/ManagementFactory;`. -Nevertheless, it will be possible to support debug agent on Android as soon as [GradleAspectJ-Android](https://github.com/Archinamon/android-gradle-aspectj) will support androin-gradle 3.3 +Nevertheless, it will be possible to support debug agent on Android as soon as [GradleAspectJ-Android](https://github.com/Archinamon/android-gradle-aspectj) will support android-gradle 3.3 diff --git a/kotlinx-coroutines-core/common/README.md b/kotlinx-coroutines-core/common/README.md index ea4e53b5e2..b84cedf4d4 100644 --- a/kotlinx-coroutines-core/common/README.md +++ b/kotlinx-coroutines-core/common/README.md @@ -1,6 +1,6 @@ # Module kotlinx-coroutines-core -Core primitives to work with coroutines. +Core primitives to work with coroutines available on all platforms. Coroutine builder functions: @@ -10,7 +10,6 @@ Coroutine builder functions: | [async] | [Deferred] | [CoroutineScope] | Returns a single value with the future result | [produce][kotlinx.coroutines.channels.produce] | [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [ProducerScope][kotlinx.coroutines.channels.ProducerScope] | Produces a stream of elements | [actor][kotlinx.coroutines.channels.actor] | [SendChannel][kotlinx.coroutines.channels.SendChannel] | [ActorScope][kotlinx.coroutines.channels.ActorScope] | Processes a stream of messages -| [runBlocking] | `T` | [CoroutineScope] | Blocks the thread while the coroutine runs Coroutine dispatchers implementing [CoroutineDispatcher]: @@ -96,14 +95,6 @@ Select expression to perform multiple suspending operations simultaneously until Low-level primitives for finer-grained control of coroutines. -# Package kotlinx.coroutines.timeunit - -Optional time unit support for multiplatform projects. - -# Package kotlinx.coroutines.test - -Components to ease writing unit-tests for code that contains coroutines with delays and timeouts. - [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html @@ -111,7 +102,6 @@ Components to ease writing unit-tests for code that contains coroutines with del [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html [Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html -[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html [CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html [Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html [Dispatchers.Unconfined]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index 26fd8e7050..4c42e5e183 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -60,6 +60,9 @@ public fun CoroutineScope.launch( /** * Creates a coroutine and returns its future result as an implementation of [Deferred]. * The running coroutine is cancelled when the resulting deferred is [cancelled][Job.cancel]. + * The resulting coroutine has a key difference compared with similar primitives in other languages + * and frameworks: it cancels the parent job (or outer scope) on failure to enforce *structured concurrency* paradigm. + * To change that behaviour, supervising parent ([SupervisorJob] or [supervisorScope]) can be used. * * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument. * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. @@ -72,8 +75,6 @@ public fun CoroutineScope.launch( * the resulting [Deferred] is created in _new_ state. It can be explicitly started with [start][Job.start] * function and will be started implicitly on the first invocation of [join][Job.join], [await][Deferred.await] or [awaitAll]. * - * @param context additional to [CoroutineScope.coroutineContext] context of the coroutine. - * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. * @param block the coroutine code. */ public fun CoroutineScope.async( diff --git a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt index 3a73d239fd..2a20095aee 100644 --- a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt @@ -25,6 +25,9 @@ public abstract class MainCoroutineDispatcher : CoroutineDispatcher() { * /* * * If it is known that updateUiElement can be invoked both from the Main thread and from other threads, * * `immediate` dispatcher is used as a performance optimization to avoid unnecessary dispatch. + * * + * * In that case, when `updateUiElement` is invoked from the Main thread, `uiElement.text` will be + * * invoked immediately without any dispatching, otherwise, the `Dispatchers.Main` dispatch cycle will be triggered. * */ * withContext(Dispatchers.Main.immediate) { * uiElement.text = text diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 8d34265f92..d7e01aba0b 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -100,8 +100,16 @@ public fun CoroutineScope.produce( } /** - * @suppress **This an internal API and should not be used from general code.** - * onCompletion parameter will be redesigned. + * This an internal API and should not be used from general code.** + * onCompletion parameter will be redesigned. + * If you have to use `onCompletion` operator, please report to https://github.com/Kotlin/kotlinx.coroutines/issues/. + * As a temporary solution, [invokeOnCompletion][Job.invokeOnCompletion] can be used instead: + * ``` + * fun ReceiveChannel.myOperator(): ReceiveChannel = GlobalScope.produce(Dispatchers.Unconfined) { + * coroutineContext[Job]?.invokeOnCompletion { consumes() } + * } + * ``` + * @suppress */ @InternalCoroutinesApi public fun CoroutineScope.produce( diff --git a/kotlinx-coroutines-core/native/README.md b/kotlinx-coroutines-core/native/README.md deleted file mode 100644 index a1bd39ed3a..0000000000 --- a/kotlinx-coroutines-core/native/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# Module kotlinx-coroutines-core-native - -Core primitives to work with coroutines on Kotlin/Native. - -Coroutine builder functions: - -| **Name** | **Result** | **Scope** | **Description** -| ------------- | ------------- | ---------------- | --------------- -| [launch] | [Job] | [CoroutineScope] | Launches coroutine that does not have any result -| [async] | [Deferred] | [CoroutineScope] | Returns a single value with the future result -| [produce][kotlinx.coroutines.channels.produce] | [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [ProducerScope][kotlinx.coroutines.channels.ProducerScope] | Produces a stream of elements -| [runBlocking] | `T` | [CoroutineScope] | Blocks the thread while the coroutine runs - -Coroutine dispatchers implementing [CoroutineDispatcher]: - -| **Name** | **Description** -| --------------------------- | --------------- -| [Dispatchers.Default] | References current [runBlocking] event loop -| [Dispatchers.Unconfined] | Does not confine coroutine execution in any way - -More context elements: - -| **Name** | **Description** -| --------------------------- | --------------- -| [NonCancellable] | A non-cancelable job that is always active -| [CoroutineExceptionHandler] | Handler for uncaught exception - -Synchronization primitives for coroutines: - -| **Name** | **Suspending functions** | **Description** -| ---------- | ----------------------------------------------------------- | --------------- -| [Mutex][kotlinx.coroutines.sync.Mutex] | [lock][kotlinx.coroutines.sync.Mutex.lock] | Mutual exclusion -| [Channel][kotlinx.coroutines.channels.Channel] | [send][kotlinx.coroutines.channels.SendChannel.send], [receive][kotlinx.coroutines.channels.ReceiveChannel.receive] | Communication channel (aka queue or exchanger) - -Top-level suspending functions: - -| **Name** | **Description** -| ------------------- | --------------- -| [delay] | Non-blocking sleep -| [yield] | Yields thread in single-threaded dispatchers -| [withContext] | Switches to a different context -| [withTimeout] | Set execution time-limit with exception on timeout -| [withTimeoutOrNull] | Set execution time-limit will null result on timeout -| [awaitAll] | Awaits for successful completion of all given jobs or exceptional completion of any -| [joinAll] | Joins on all given jobs - -Cancellation support for user-defined suspending functions is available with [suspendCancellableCoroutine] -helper function. [NonCancellable] job object is provided to suppress cancellation with -`run(NonCancellable) {...}` block of code. - -[Select][kotlinx.coroutines.selects.select] expression waits for the result of multiple suspending functions simultaneously: - -| **Receiver** | **Suspending function** | **Select clause** | **Non-suspending version** -| ---------------- | --------------------------------------------- | ------------------------------------------------ | -------------------------- -| [Job] | [join][Job.join] | [onJoin][Job.onJoin] | [isCompleted][Job.isCompleted] -| [Deferred] | [await][Deferred.await] | [onAwait][Deferred.onAwait] | [isCompleted][Job.isCompleted] -| [SendChannel][kotlinx.coroutines.channels.SendChannel] | [send][kotlinx.coroutines.channels.SendChannel.send] | [onSend][kotlinx.coroutines.channels.SendChannel.onSend] | [offer][kotlinx.coroutines.channels.SendChannel.offer] -| [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receive][kotlinx.coroutines.channels.ReceiveChannel.receive] | [onReceive][kotlinx.coroutines.channels.ReceiveChannel.onReceive] | [poll][kotlinx.coroutines.channels.ReceiveChannel.poll] -| [ReceiveChannel][kotlinx.coroutines.channels.ReceiveChannel] | [receiveOrNull][kotlinx.coroutines.channels.ReceiveChannel.receiveOrNull] | [onReceiveOrNull][kotlinx.coroutines.channels.ReceiveChannel.onReceiveOrNull] | [poll][kotlinx.coroutines.channels.ReceiveChannel.poll] -| [Mutex][kotlinx.coroutines.sync.Mutex] | [lock][kotlinx.coroutines.sync.Mutex.lock] | [onLock][kotlinx.coroutines.sync.Mutex.onLock] | [tryLock][kotlinx.coroutines.sync.Mutex.tryLock] -| none | [delay] | [onTimeout][kotlinx.coroutines.selects.SelectBuilder.onTimeout] | none - - - -[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html -[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html -[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html -[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html -[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html -[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html -[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html -[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html -[Dispatchers.Unconfined]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html -[NonCancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable.html -[CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html -[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html -[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html -[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html -[withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html -[withTimeoutOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html -[awaitAll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await-all.html -[joinAll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/join-all.html -[suspendCancellableCoroutine]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html -[Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html -[Job.onJoin]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/on-join.html -[Job.isCompleted]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/is-completed.html -[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html -[Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html - -[kotlinx.coroutines.sync.Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html -[kotlinx.coroutines.sync.Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html -[kotlinx.coroutines.sync.Mutex.onLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/on-lock.html -[kotlinx.coroutines.sync.Mutex.tryLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/try-lock.html - -[kotlinx.coroutines.channels.produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html -[kotlinx.coroutines.channels.ReceiveChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html -[kotlinx.coroutines.channels.ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html -[kotlinx.coroutines.channels.Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html -[kotlinx.coroutines.channels.SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html -[kotlinx.coroutines.channels.ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html -[kotlinx.coroutines.channels.SendChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/index.html -[kotlinx.coroutines.channels.SendChannel.onSend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html -[kotlinx.coroutines.channels.SendChannel.offer]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/offer.html -[kotlinx.coroutines.channels.ReceiveChannel.onReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html -[kotlinx.coroutines.channels.ReceiveChannel.poll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/poll.html -[kotlinx.coroutines.channels.ReceiveChannel.receiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive-or-null.html -[kotlinx.coroutines.channels.ReceiveChannel.onReceiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive-or-null.html - -[kotlinx.coroutines.selects.select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html -[kotlinx.coroutines.selects.SelectBuilder.onTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/-select-builder/on-timeout.html - diff --git a/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt b/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt index a2e9d51b96..41d14d0841 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt +++ b/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt @@ -40,6 +40,7 @@ public class TestCoroutineExceptionHandler : { private val _exceptions = mutableListOf() + /** @suppress **/ override fun handleException(context: CoroutineContext, exception: Throwable) { synchronized(_exceptions) { _exceptions += exception diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index fb0ee47684..f656b353c5 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -34,6 +34,10 @@ public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay { * /* * * If it is known that updateUiElement can be invoked both from the Main thread and from other threads, * * `immediate` dispatcher is used as a performance optimization to avoid unnecessary dispatch. + * * + * * In that case, when `updateUiElement` is invoked from the Main thread, `uiElement.text` will be + * * invoked immediately without any dispatching, otherwise, the `Dispatchers.Main` dispatch cycle via + * * `Handler.post` will be triggered. * */ * withContext(Dispatchers.Main.immediate) { * uiElement.text = text From b7e1499b966175782cf2a5045c5bd3bb95a63b8c Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 1 Mar 2019 14:25:23 +0300 Subject: [PATCH 36/56] Amortize the cost of coroutine dispatch using message queue in all JS dispatchers. Use Promise.resolve and process.nextTick as dispatch mechanism for cold starts --- .../js/src/CoroutineContext.kt | 6 +- .../js/src/JSDispatcher.kt | 75 +++++++++++++------ .../js/test/MessageQueueTest.kt | 4 + 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index 87ed1f4ceb..de02723a81 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -16,16 +16,16 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { // For details see https://github.com/Kotlin/kotlinx.coroutines/issues/236 // The check for ReactNative is based on https://github.com/facebook/react-native/commit/3c65e62183ce05893be0822da217cb803b121c61 jsTypeOf(navigator) != UNDEFINED && navigator != null && navigator.product == "ReactNative" -> - NodeDispatcher() + NodeDispatcher // Check if we are running under jsdom. WindowDispatcher doesn't work under jsdom because it accesses MessageEvent#source. // It is not implemented in jsdom, see https://github.com/jsdom/jsdom/blob/master/Changelog.md // "It's missing a few semantics, especially around origins, as well as MessageEvent source." - isJsdom() -> NodeDispatcher() + isJsdom() -> NodeDispatcher // Check if we are in the browser and must use window.postMessage to avoid setTimeout throttling jsTypeOf(window) != UNDEFINED && window.asDynamic() != null && jsTypeOf(window.asDynamic().addEventListener) != UNDEFINED -> window.asCoroutineDispatcher() // Fallback to NodeDispatcher when browser environment is not detected - else -> NodeDispatcher() + else -> NodeDispatcher } private fun isJsdom() = jsTypeOf(navigator) != UNDEFINED && diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index f2e3b9026a..e11377718c 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -5,18 +5,17 @@ package kotlinx.coroutines import kotlinx.coroutines.internal.* -import kotlin.coroutines.* import org.w3c.dom.* +import kotlin.coroutines.* +import kotlin.js.* private const val MAX_DELAY = Int.MAX_VALUE.toLong() private fun delayToInt(timeMillis: Long): Int = timeMillis.coerceIn(0, MAX_DELAY).toInt() -internal class NodeDispatcher : CoroutineDispatcher(), Delay { - override fun dispatch(context: CoroutineContext, block: Runnable) { - setTimeout({ block.run() }, 0) - } +internal object NodeDispatcher : CoroutineDispatcher(), Delay { + override fun dispatch(context: CoroutineContext, block: Runnable) = NodeJsMessageQueue.enqueue(block) override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) @@ -37,48 +36,77 @@ internal class NodeDispatcher : CoroutineDispatcher(), Delay { } internal class WindowDispatcher(private val window: Window) : CoroutineDispatcher(), Delay { - private val messageName = "dispatchCoroutine" + private val queue = WindowMessageQueue(window) + + override fun dispatch(context: CoroutineContext, block: Runnable) = queue.enqueue(block) + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) + } - private val queue = object : MessageQueue() { - override fun schedule() { - window.postMessage(messageName, "*") + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis)) + return object : DisposableHandle { + override fun dispose() { + window.clearTimeout(handle) + } } } +} + +private class WindowMessageQueue(private val window: Window) : MessageQueue() { + private val messageName = "dispatchCoroutine" init { window.addEventListener("message", { event: dynamic -> if (event.source == window && event.data == messageName) { event.stopPropagation() - queue.process() + process() } }, true) } - override fun dispatch(context: CoroutineContext, block: Runnable) { - queue.enqueue(block) + override fun schedule() { + Promise.resolve(Unit).then({ process() }) } - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) + override fun reschedule() { + window.postMessage(messageName, "*") } +} - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { - val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis)) - return object : DisposableHandle { - override fun dispose() { - window.clearTimeout(handle) - } - } +private object NodeJsMessageQueue : MessageQueue() { + override fun schedule() { + // next tick is even faster than resolve + process.nextTick({ process() }) + } + + override fun reschedule() { + setTimeout({ process() }, 0) } } +/** + * An abstraction over JS scheduling mechanism that leverages micro-batching of [dispatch] blocks without + * paying the cost of JS callbacks scheduling on every dispatch. + * + * Queue uses two scheduling mechanisms: + * 1) [schedule] is used to schedule the initial processing of the message queue. + * JS engine-specific microtask mechanism is used in order to boost performance on short runs and a dispatch batch + * 2) [reschedule] is used to schedule processing of the queue after yield to the JS event loop. + * JS engine-specific macrotask mechanism is used not to starve animations and non-coroutines macrotasks. + * + * Yet there could be a long tail of "slow" reschedules, but it should be amortized by the queue size. + */ internal abstract class MessageQueue : ArrayQueue() { - val yieldEvery = 16 // yield to JS event loop after this many processed messages + val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages private var scheduled = false abstract fun schedule() + abstract fun reschedule() + fun enqueue(element: Runnable) { addLast(element) if (!scheduled) { @@ -98,7 +126,7 @@ internal abstract class MessageQueue : ArrayQueue() { if (isEmpty) { scheduled = false } else { - schedule() + reschedule() } } } @@ -108,3 +136,4 @@ internal abstract class MessageQueue : ArrayQueue() { // using them via "window" (which only works in browser) private external fun setTimeout(handler: dynamic, timeout: Int = definedExternally): Int private external fun clearTimeout(handle: Int = definedExternally) +private external val process: dynamic diff --git a/kotlinx-coroutines-core/js/test/MessageQueueTest.kt b/kotlinx-coroutines-core/js/test/MessageQueueTest.kt index 4943f747ad..de514c7628 100644 --- a/kotlinx-coroutines-core/js/test/MessageQueueTest.kt +++ b/kotlinx-coroutines-core/js/test/MessageQueueTest.kt @@ -15,6 +15,10 @@ class MessageQueueTest { assertFalse(scheduled) scheduled = true } + + override fun reschedule() { + schedule() + } } inner class Box(val i: Int): Runnable { From a3f150e66f7c47e0a0cefed4d9aa03199dff9e7f Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 31 May 2019 14:39:08 +0300 Subject: [PATCH 37/56] Use identity hash code on K/N for debug strings Fixes #1237 --- kotlinx-coroutines-core/native/src/Debug.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/kotlinx-coroutines-core/native/src/Debug.kt b/kotlinx-coroutines-core/native/src/Debug.kt index 866798e459..e81e89af01 100644 --- a/kotlinx-coroutines-core/native/src/Debug.kt +++ b/kotlinx-coroutines-core/native/src/Debug.kt @@ -4,11 +4,12 @@ package kotlinx.coroutines -private var counter = 0 +import kotlin.math.* -internal actual val Any.hexAddress: String - get() { - return "" // :todo: - } +internal actual val Any.hexAddress: String get() = abs(id().let { if (it == Int.MIN_VALUE) 0 else it }).toString(16) internal actual val Any.classSimpleName: String get() = this::class.simpleName ?: "Unknown" + + +@SymbolName("Kotlin_Any_hashCode") +external fun Any.id(): Int // Note: can return negative value on K/N From 46e41f25f792d9ce2bc487155cc88aa6474fbd5d Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 31 May 2019 17:59:53 +0300 Subject: [PATCH 38/56] Use nanosleep in runBlocking's delay on Native (#1228) Fixes #1225 --- kotlinx-coroutines-core/native/src/Builders.kt | 13 +++++++++---- .../native/test/DelayExceptionTest.kt | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt index afeed7a508..f7d12065a8 100644 --- a/kotlinx-coroutines-core/native/src/Builders.kt +++ b/kotlinx-coroutines-core/native/src/Builders.kt @@ -4,6 +4,8 @@ package kotlinx.coroutines +import kotlinx.cinterop.* +import platform.posix.* import kotlin.coroutines.* /** @@ -56,21 +58,24 @@ private class BlockingCoroutine( get() = false // it throws exception to parent instead of cancelling it @Suppress("UNCHECKED_CAST") - fun joinBlocking(): T { + fun joinBlocking(): T = memScoped { try { eventLoop?.incrementUseCount() + val timespec = alloc() while (true) { val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE // note: process next even may loose unpark flag, so check if completed before parking if (isCompleted) break - // todo: LockSupport.parkNanos(this, parkNanos) + timespec.tv_sec = parkNanos / 1000000000L // 1e9 ns -> sec + timespec.tv_nsec = (parkNanos % 1000000000L).convert() // % 1e9 + nanosleep(timespec.ptr, null) } } finally { // paranoia eventLoop?.decrementUseCount() } // now return result - val state = this.state + val state = state (state as? CompletedExceptionally)?.let { throw it.cause } - return state as T + state as T } } diff --git a/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt b/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt index 463712ce47..a39a59e134 100644 --- a/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt +++ b/kotlinx-coroutines-core/native/test/DelayExceptionTest.kt @@ -5,10 +5,9 @@ package kotlinx.coroutines import kotlin.coroutines.* -import kotlin.test.Test -import kotlin.test.assertTrue +import kotlin.test.* -class DelayExceptionTest { +class DelayExceptionTest : TestBase() { private object Dispatcher : CoroutineDispatcher() { override fun isDispatchNeeded(context: CoroutineContext): Boolean = true override fun dispatch(context: CoroutineContext, block: Runnable) { block.run() } @@ -25,4 +24,16 @@ class DelayExceptionTest { assertTrue(exception is IllegalStateException) } + + @Test + fun testMaxDelay() = runBlocking { + expect(1) + val job = launch { + expect(2) + delay(Long.MAX_VALUE) + } + yield() + job.cancel() + finish(3) + } } From b9b7d828b18b2462817b6ed0c1a56233a4b6fcf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojtek=20Kalici=C5=84ski?= Date: Thu, 30 May 2019 14:17:51 +0200 Subject: [PATCH 39/56] Enable R8 optimization of Dispatchers.Main loading The developer still has to disable the FastServiceLoader manually if they're using R8. R8 is then able to optimize away the ServiceLoader and reflection entirely, resulting in direct class instantiation and no extra I/O on calling thread. Fixes #1231 --- .../jvm/src/CoroutineExceptionHandlerImpl.kt | 11 +++++++--- .../jvm/src/internal/FastServiceLoader.kt | 10 --------- .../jvm/src/internal/MainDispatchers.kt | 22 +++++++++++++++++-- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt index cabb8a4cc1..90c6a84980 100644 --- a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt @@ -13,10 +13,15 @@ import kotlin.coroutines.* * Note that Android may have dummy [Thread.contextClassLoader] which is used by one-argument [ServiceLoader.load] function, * see (https://stackoverflow.com/questions/13407006/android-class-loader-may-fail-for-processes-that-host-multiple-applications). * So here we explicitly use two-argument `load` with a class-loader of [CoroutineExceptionHandler] class. + * + * We are explicitly using the `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()` + * form of the ServiceLoader call to enable R8 optimization when compiled on Android. */ -private val handlers: List = CoroutineExceptionHandler::class.java.let { serviceClass -> - ServiceLoader.load(serviceClass, serviceClass.classLoader).toList() -} +private val handlers: List = ServiceLoader.load( + CoroutineExceptionHandler::class.java, + CoroutineExceptionHandler::class.java.classLoader +).iterator().asSequence().toList() + internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) { // use additional extension handlers diff --git a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt index 6cf1b4750b..ecb2213e4a 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt @@ -6,11 +6,6 @@ import java.util.* import java.util.jar.* import java.util.zip.* -/** - * Name of the boolean property that enables using of [FastServiceLoader]. - */ -private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.fast.service.loader" - /** * A simplified version of [ServiceLoader]. * FastServiceLoader locates and instantiates all service providers named in configuration @@ -25,12 +20,7 @@ private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.fast.s internal object FastServiceLoader { private const val PREFIX: String = "META-INF/services/" - private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true) - internal fun load(service: Class, loader: ClassLoader): List { - if (!FAST_SERVICE_LOADER_ENABLED) { - return ServiceLoader.load(service, loader).toList() - } return try { loadProviders(service, loader) } catch (e: Throwable) { diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index f2f0af7aa8..63e38cb084 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -4,15 +4,33 @@ import kotlinx.coroutines.* import java.util.* import kotlin.coroutines.* +/** + * Name of the boolean property that enables using of [FastServiceLoader]. + */ +private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.fast.service.loader" + // Lazy loader for the main dispatcher internal object MainDispatcherLoader { + + private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true) + @JvmField val dispatcher: MainCoroutineDispatcher = loadMainDispatcher() private fun loadMainDispatcher(): MainCoroutineDispatcher { return try { - val factories = MainDispatcherFactory::class.java.let { clz -> - FastServiceLoader.load(clz, clz.classLoader) + val factories = if (FAST_SERVICE_LOADER_ENABLED) { + MainDispatcherFactory::class.java.let { clz -> + FastServiceLoader.load(clz, clz.classLoader) + } + } else { + //We are explicitly using the + //`ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()` + //form of the ServiceLoader call to enable R8 optimization when compiled on Android. + ServiceLoader.load( + MainDispatcherFactory::class.java, + MainDispatcherFactory::class.java.classLoader + ).iterator().asSequence().toList() } factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories) ?: MissingMainCoroutineDispatcher(null) From 8f6c03a2eb2da255350b3339b76f636aebe8618a Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Sun, 2 Jun 2019 16:54:17 +0300 Subject: [PATCH 40/56] Fix compilation on 32-bit platforms --- kotlinx-coroutines-core/native/src/Builders.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt index f7d12065a8..82fd81a08f 100644 --- a/kotlinx-coroutines-core/native/src/Builders.kt +++ b/kotlinx-coroutines-core/native/src/Builders.kt @@ -66,7 +66,7 @@ private class BlockingCoroutine( val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE // note: process next even may loose unpark flag, so check if completed before parking if (isCompleted) break - timespec.tv_sec = parkNanos / 1000000000L // 1e9 ns -> sec + timespec.tv_sec = (parkNanos / 1000000000L).convert() // 1e9 ns -> sec timespec.tv_nsec = (parkNanos % 1000000000L).convert() // % 1e9 nanosleep(timespec.ptr, null) } From daf8502ee746569d5d00a889f5eb0dcb89f1b198 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 3 Jun 2019 14:39:32 +0300 Subject: [PATCH 41/56] Disable binary compatibility tests in snapshot trains --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index e5a79bb3df..a1813d18a6 100644 --- a/build.gradle +++ b/build.gradle @@ -145,6 +145,7 @@ if (build_snapshot_train) { allprojects { tasks.withType(Test).all { exclude '**/*LinearizabilityTest*' + exclude '**/*PublicApiTest*' // KT-30956 exclude '**/*LFTest*' exclude '**/*StressTest*' exclude '**/*scheduling*' From f44942ab3e36b0393cf6179f38a85cb3c3f809cd Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Sun, 2 Jun 2019 16:38:10 +0300 Subject: [PATCH 42/56] Deprecate flowWith operator --- .../common/src/flow/Flow.kt | 5 +-- .../common/src/flow/Migration.kt | 21 ++------- .../common/src/flow/operators/Context.kt | 16 ++++++- .../test/flow/operators/CombineLatestTest.kt | 11 +++-- .../test/flow/operators/DebounceTest.kt | 5 +-- .../test/flow/operators/FlatMapBaseTest.kt | 8 +--- .../test/flow/operators/FlowContextTest.kt | 1 + .../test/flow/operators/FlowWithTest.kt | 1 + .../common/test/flow/operators/SampleTest.kt | 6 +-- .../common/test/flow/operators/ZipTest.kt | 44 ++----------------- 10 files changed, 34 insertions(+), 84 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index eb67d12f7c..0b5df8df8d 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -30,9 +30,8 @@ import kotlinx.coroutines.* * The flow has a context preservation property: it encapsulates its own execution context and never propagates or leaks it downstream, thus making * reasoning about the execution context of particular transformations or terminal operations trivial. * - * There are two ways to change the context of a flow: [flowOn][Flow.flowOn] and [flowWith][Flow.flowWith]. - * The former changes the upstream context ("everything above the flowOn operator") while the latter - * changes the context of the flow within [flowWith] body. For additional information refer to these operators' documentation. + * There is the only way to change the context of a flow: [flowOn][Flow.flowOn] operator, + * that changes the upstream context ("everything above the flowOn operator"). For additional information refer to its documentation. * * This reasoning can be demonstrated in practice: * ``` diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 7570911006..77beb3779c 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -71,7 +71,7 @@ public fun Flow.publishOn(context: CoroutineContext): Flow = error("Sh * .doOnEach { value -> println("Processing $value in computation") * .subscribe() * ``` - * has the following Flow equivalents: + * has the following Flow equivalent: * ``` * withContext(Dispatchers.Default) { * flow @@ -82,25 +82,10 @@ public fun Flow.publishOn(context: CoroutineContext): Flow = error("Sh * } * } * ``` - * or - * - * ``` - * withContext(Dispatchers.Default) { - * flow - * .flowWith(Dispatchers.IO) { map { value -> println("Doing map in IO"); value } } - * .collect { value -> - * println("Processing $value in computation") - * } - * } - * ``` - * - * The difference is that [flowWith] encapsulates ("preserves") the context within its lambda - * while [flowOn] changes the context of all preceding operators. - * Opposed to subscribeOn, it it **possible** to use multiple `flowOn` operators in the one flow. - * + * Opposed to subscribeOn, it it **possible** to use multiple `flowOn` operators in the one flow * @suppress */ -@Deprecated(message = "Use flowWith or flowOn instead", level = DeprecationLevel.ERROR) +@Deprecated(message = "Use flowOn instead", level = DeprecationLevel.ERROR) public fun Flow.subscribeOn(context: CoroutineContext): Flow = error("Should not be called") /** @suppress **/ diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 17c1a4c4d6..c5a6a361a5 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -77,11 +77,23 @@ public fun Flow.flowOn(flowContext: CoroutineContext, bufferSize: Int = 1 * For more explanation of context preservation please refer to [Flow] documentation. * * This operator uses channel of the specific [bufferSize] in order to switch between contexts, - * but it is not guaranteed that channel will be created, implementation is free to optimize it away in case of fusing. + * but it is not guaranteed that channel will be created, implementation is free to optimize it away in case of fusing.* * - * @throws [IllegalArgumentException] if provided context contains [Job] instance. + * This operator is deprecated without replacement because it was discovered that it doesn't play well with coroutines and flow semantics: + * 1) It doesn't prevent context elements from the downstream to leak into its body + * ``` + * flowOf(1).flowWith(EmptyCoroutineContext) { + * onEach { println(kotlin.coroutines.coroutineContext[CoroutineName]) } // Will print 42 + * }.flowOn(CoroutineName(42)) + * ``` + * 2) To avoid such leaks, new primitive should be introduced to `kotlinx.coroutines` -- the subtraction of contexts. + * And this will become a new concept to learn, maintain and explain. + * 3) It defers the execution of declarative [builder] until the moment of [collection][Flow.collect] similarly + * to `Observable.defer`. But it is unexpected because nothing in the name `flowWith` reflects this fact. + * 4) It can be confused with [flowOn] operator, though [flowWith] is much rarer. */ @FlowPreview +@Deprecated(message = "flowWith is deprecated without replacement, please refer to its KDoc for an explanation", level = DeprecationLevel.WARNING) // Error in beta release, removal in 1.4 public fun Flow.flowWith( flowContext: CoroutineContext, bufferSize: Int = 16, diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CombineLatestTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CombineLatestTest.kt index 5e50a95365..bda9927c79 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CombineLatestTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CombineLatestTest.kt @@ -5,8 +5,8 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* -import kotlinx.coroutines.flow.combineLatest as combineLatestOriginal import kotlin.test.* +import kotlinx.coroutines.flow.combineLatest as combineLatestOriginal /* * Replace: { i, j -> i + j } -> { i, j -> i + j } as soon as KT-30991 is fixed @@ -133,12 +133,11 @@ abstract class CombineLatestTestBase : TestBase() { emit(1) assertEquals("second", NamedDispatchers.name()) expect(3) - }.flowOn(NamedDispatchers("second")).flowWith(NamedDispatchers("with")) { - onEach { - assertEquals("with", NamedDispatchers.name()) + }.flowOn(NamedDispatchers("second")) + .onEach { + assertEquals("onEach", NamedDispatchers.name()) expect(4) - } - } + }.flowOn(NamedDispatchers("onEach")) val value = withContext(NamedDispatchers("main")) { f1.combineLatest(f2) { i, j -> diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index a31854bade..607d4cd661 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -152,12 +152,9 @@ class DebounceTest : TestBase() { emit(1) expect(2) throw TestException() - }.flowWith(NamedDispatchers("unused")) { - debounce(Long.MAX_VALUE).map { + }.flowOn(NamedDispatchers("source")).debounce(Long.MAX_VALUE).map { expectUnreached() - } } - assertFailsWith(flow) finish(3) } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapBaseTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapBaseTest.kt index 4c3f646d42..ca1fa25afa 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapBaseTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapBaseTest.kt @@ -5,8 +5,6 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.* import kotlin.test.* abstract class FlatMapBaseTest : TestBase() { @@ -75,14 +73,12 @@ abstract class FlatMapBaseTest : TestBase() { fun testIsolatedContext() = runTest { val flow = flowOf(1) .flowOn(NamedDispatchers("irrelevant")) - .flowWith(NamedDispatchers("inner")) { - flatMap { + .flatMap { flow { assertEquals("inner", NamedDispatchers.name()) emit(it) } - } - }.flowOn(NamedDispatchers("irrelevant")) + }.flowOn(NamedDispatchers("inner")) .flatMap { flow { assertEquals("outer", NamedDispatchers.name()) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt index 944f930fdc..004827d2e7 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.channels.* import kotlin.coroutines.* import kotlin.test.* +@Suppress("DEPRECATION") class FlowContextTest : TestBase() { private val captured = ArrayList() diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt index e31a1db66f..055f84741c 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlin.test.* +@Suppress("DEPRECATION") class FlowWithTest : TestBase() { private fun mapper(name: String, index: Int): suspend (Int) -> Int = { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt index 814aec669e..e77b128f76 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt @@ -227,10 +227,8 @@ class SampleTest : TestBase() { emit(1) expect(2) throw TestException() - }.flowWith(NamedDispatchers("unused")) { - sample(Long.MAX_VALUE).map { - expectUnreached() - } + }.flowOn(NamedDispatchers("unused")).sample(Long.MAX_VALUE).map { + expectUnreached() } assertFailsWith(flow) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt index e02da811e4..decd2307c6 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt @@ -112,52 +112,15 @@ class ZipTest : TestBase() { } @Test - fun testContextIsIsolated() = runTest { + fun testContextIsIsolatedReversed() = runTest { val f1 = flow { emit("a") assertEquals("first", NamedDispatchers.name()) expect(1) }.flowOn(NamedDispatchers("first")).onEach { - assertEquals("nested", NamedDispatchers.name()) + assertEquals("with", NamedDispatchers.name()) expect(2) - }.flowOn(NamedDispatchers("nested")) - - val f2 = flow { - emit(1) - assertEquals("second", NamedDispatchers.name()) - expect(3) - }.flowOn(NamedDispatchers("second")).flowWith(NamedDispatchers("with")) { - onEach { - assertEquals("with", NamedDispatchers.name()) - expect(4) - } - } - - val value = withContext(NamedDispatchers("main")) { - f1.zip(f2) { i, j -> - assertEquals("main", NamedDispatchers.name()) - expect(5) - i + j - }.single() - } - - assertEquals("a1", value) - finish(6) - } - - @Test - fun testContextIsIsolatedReversed() = runTest { - val f1 = flow { - emit("a") - assertEquals("first", NamedDispatchers.name()) - expect(1) - }.flowOn(NamedDispatchers("first")) - .flowWith(NamedDispatchers("with")) { - onEach { - assertEquals("with", NamedDispatchers.name()) - expect(2) - } - } + }.flowOn(NamedDispatchers("with")) val f2 = flow { emit(1) @@ -180,7 +143,6 @@ class ZipTest : TestBase() { finish(6) } - @Test fun testErrorInDownstreamCancelsUpstream() = runTest { val f1 = flow { From b73ebdc5cf024e4a59eb97d8dd9c89618802c7c0 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 31 May 2019 12:57:08 +0300 Subject: [PATCH 43/56] Adjust behavior of conflated channel to deliver last value Fixes #1235 Fixes #332 --- .../common/src/channels/AbstractChannel.kt | 32 +------------ .../common/src/channels/ConflatedChannel.kt | 48 +++++++++++++------ .../test/channels/ConflatedChannelTest.kt | 8 +++- .../test/flow/channels/ChannelFlowTest.kt | 4 +- 4 files changed, 44 insertions(+), 48 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 3086c61759..61bc090817 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -104,35 +104,6 @@ internal abstract class AbstractSendChannel : SendChannel { return null } - /** - * Queues conflated element, returns null on success or - * returns node reference if it was already closed or is waiting for receive. - * @suppress **This is unstable API and it is subject to change.** - */ - protected fun sendConflated(element: E): ReceiveOrClosed<*>? { - val node = SendBuffered(element) - queue.addLastIfPrev(node, { prev -> - if (prev is ReceiveOrClosed<*>) return@sendConflated prev - true - }) - conflatePreviousSendBuffered(node) - return null - } - - protected fun conflatePreviousSendBuffered(node: LockFreeLinkedListNode) { - /* - * Conflate all previous SendBuffered, - * helping other sends to coflate - */ - var prev = node.prevNode - while (prev is SendBuffered<*>) { - if (!prev.remove()) { - prev.helpRemove() - } - prev = prev.prevNode - } - } - /** * @suppress **This is unstable API and it is subject to change.** */ @@ -331,7 +302,6 @@ internal abstract class AbstractSendChannel : SendChannel { previous as Receive // type assertion previous.resumeReceiveClosed(closed) } - onClosedIdempotent(closed) } @@ -499,7 +469,7 @@ internal abstract class AbstractSendChannel : SendChannel { override fun toString(): String = "SendSelect($pollResult)[$channel, $select]" } - private class SendBuffered( + internal class SendBuffered( @JvmField val element: E ) : LockFreeLinkedListNode(), Send { override val pollResult: Any? get() = element diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt index 339bfd2c08..21a18832a4 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt @@ -25,7 +25,35 @@ internal open class ConflatedChannel : AbstractChannel() { protected final override val isBufferFull: Boolean get() = false override fun onClosedIdempotent(closed: LockFreeLinkedListNode) { - conflatePreviousSendBuffered(closed) + @Suppress("UNCHECKED_CAST") + (closed.prevNode as? SendBuffered)?.let { lastBuffered -> + conflatePreviousSendBuffered(lastBuffered) + } + } + + /** + * Queues conflated element, returns null on success or + * returns node reference if it was already closed or is waiting for receive. + */ + private fun sendConflated(element: E): ReceiveOrClosed<*>? { + val node = SendBuffered(element) + queue.addLastIfPrev(node) { prev -> + if (prev is ReceiveOrClosed<*>) return@sendConflated prev + true + } + conflatePreviousSendBuffered(node) + return null + } + + private fun conflatePreviousSendBuffered(node: SendBuffered) { + // Conflate all previous SendBuffered, helping other sends to conflate + var prev = node.prevNode + while (prev is SendBuffered<*>) { + if (!prev.remove()) { + prev.helpRemove() + } + prev = prev.prevNode + } } // result is always `OFFER_SUCCESS | Closed` @@ -35,20 +63,13 @@ internal open class ConflatedChannel : AbstractChannel() { when { result === OFFER_SUCCESS -> return OFFER_SUCCESS result === OFFER_FAILED -> { // try to buffer - val sendResult = sendConflated(element) - when (sendResult) { + when (val sendResult = sendConflated(element)) { null -> return OFFER_SUCCESS - is Closed<*> -> { - conflatePreviousSendBuffered(sendResult) - return sendResult - } + is Closed<*> -> return sendResult } // otherwise there was receiver in queue, retry super.offerInternal } - result is Closed<*> -> { - conflatePreviousSendBuffered(result) - return result - } + result is Closed<*> -> return result else -> error("Invalid offerInternal result $result") } } @@ -64,10 +85,7 @@ internal open class ConflatedChannel : AbstractChannel() { result === ALREADY_SELECTED -> return ALREADY_SELECTED result === OFFER_SUCCESS -> return OFFER_SUCCESS result === OFFER_FAILED -> {} // retry - result is Closed<*> -> { - conflatePreviousSendBuffered(result) - return result - } + result is Closed<*> -> return result else -> error("Invalid result $result") } } diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt index 6b5e020d27..4deb3858f0 100644 --- a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt @@ -31,7 +31,13 @@ class ConflatedChannelTest : TestBase() { fun testConflatedClose() = runTest { val q = Channel(Channel.CONFLATED) q.send(1) - q.close() // shall conflate sent item and become closed + q.close() // shall become closed but do not conflate last sent item yet + assertTrue(q.isClosedForSend) + assertFalse(q.isClosedForReceive) + assertEquals(1, q.receive()) + // not it is closed for receive, too + assertTrue(q.isClosedForSend) + assertTrue(q.isClosedForReceive) assertNull(q.receiveOrNull()) } diff --git a/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt index 5d0292ef1e..e8754e1db4 100644 --- a/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt @@ -34,8 +34,10 @@ class ChannelFlowTest : TestBase() { val flow = channelFlow(bufferSize = Channel.CONFLATED) { assertTrue(offer(1)) assertTrue(offer(2)) + assertTrue(offer(3)) + assertTrue(offer(4)) } - assertEquals(listOf(1), flow.toList()) + assertEquals(listOf(1, 4), flow.toList()) // two elements in the middle got conflated } @Test From b77a80cd85a6acc1d011f75ef49a22cba4938f31 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 29 May 2019 17:42:58 +0300 Subject: [PATCH 44/56] Flow: decouple buffer size from various operators and fuse * Introduce buffer operator * Remove buffer size from all the other operators * Fuse all adjacent operators that create channels * Introduce Channel.BUFFERED buffer size marker to request buffered channel with a default (unspecified) size Fixes #1233 --- .../kotlinx-coroutines-core.txt | 32 +-- .../common/src/channels/BroadcastChannel.kt | 5 + .../common/src/channels/Channel.kt | 28 ++- .../common/src/flow/Builders.kt | 98 +++++---- .../common/src/flow/ChannelFlow.kt | 35 ++-- .../common/src/flow/Flow.kt | 110 +++++----- .../common/src/flow/internal/ChannelFlow.kt | 174 ++++++++++++++++ .../common/src/flow/internal/Concurrent.kt | 75 +++++++ .../common/src/flow/operators/Context.kt | 186 +++++++++++++---- .../common/src/flow/operators/Merge.kt | 176 ++++++++-------- .../channels/BroadcastChannelFactoryTest.kt | 2 +- .../test/channels/ChannelFactoryTest.kt | 2 +- .../test/flow/channels/ChannelFlowTest.kt | 9 +- .../common/test/flow/operators/BufferTest.kt | 188 ++++++++++++++++++ .../operators/FlowContextOptimizationsTest.kt | 69 ++++--- .../test/flow/operators/FlowContextTest.kt | 3 - .../jvm/src/channels/Actor.kt | 2 +- .../jvm/test/flow/CallbackFlowTest.kt | 2 +- .../jvm/test/flow/FlatMapStressTest.kt | 4 +- 19 files changed, 924 insertions(+), 276 deletions(-) create mode 100644 kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/internal/Concurrent.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index dc3180b996..aa0faba080 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -542,7 +542,9 @@ public final class kotlinx/coroutines/channels/BroadcastKt { } public abstract interface class kotlinx/coroutines/channels/Channel : kotlinx/coroutines/channels/ReceiveChannel, kotlinx/coroutines/channels/SendChannel { + public static final field BUFFERED I public static final field CONFLATED I + public static final field DEFAULT_BUFFER_PROPERTY_NAME Ljava/lang/String; public static final field Factory Lkotlinx/coroutines/channels/Channel$Factory; public static final field RENDEZVOUS I public static final field UNLIMITED I @@ -553,7 +555,9 @@ public final class kotlinx/coroutines/channels/Channel$DefaultImpls { } public final class kotlinx/coroutines/channels/Channel$Factory { + public static final field BUFFERED I public static final field CONFLATED I + public static final field DEFAULT_BUFFER_PROPERTY_NAME Ljava/lang/String; public static final field RENDEZVOUS I public static final field UNLIMITED I } @@ -785,6 +789,7 @@ public abstract interface class kotlinx/coroutines/flow/FlowCollector { } public final class kotlinx/coroutines/flow/FlowKt { + public static final field DEFAULT_CONCURRENCY_PROPERTY_NAME Ljava/lang/String; public static final fun asFlow (Ljava/lang/Iterable;)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow (Ljava/util/Iterator;)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow (Lkotlin/jvm/functions/Function0;)Lkotlinx/coroutines/flow/Flow; @@ -796,12 +801,12 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun asFlow ([I)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow ([J)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; - public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; - public static final fun callbackFlow (ILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun callbackFlow$default (ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static final fun channelFlow (ILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun channelFlow$default (ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; + public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; + public static final fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun callbackFlow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun channelFlow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun combineLatest (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun combineLatest (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function4;)Lkotlinx/coroutines/flow/Flow; public static final fun combineLatest (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function5;)Lkotlinx/coroutines/flow/Flow; @@ -821,20 +826,20 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun filterNot (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun filterNotNull (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun flatMapConcat (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; - public static final fun flatMapMerge (Lkotlinx/coroutines/flow/Flow;IILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun flatMapMerge$default (Lkotlinx/coroutines/flow/Flow;IILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun flatMapMerge (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun flatMapMerge$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flattenConcat (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; - public static final fun flattenMerge (Lkotlinx/coroutines/flow/Flow;II)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun flattenMerge$default (Lkotlinx/coroutines/flow/Flow;IIILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun flattenMerge (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun flattenMerge$default (Lkotlinx/coroutines/flow/Flow;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun flowOf (Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flowOf ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static final fun flowOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun flowOn$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun flowOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static final fun flowViaChannel (ILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun flowViaChannel$default (ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun flowWith (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun flowWith$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun getDEFAULT_CONCURRENCY ()I public static final fun map (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun mapNotNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun onEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; @@ -842,8 +847,7 @@ public final class kotlinx/coroutines/flow/FlowKt { public static synthetic fun onErrorCollect$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun onErrorReturn (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun onErrorReturn$default (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static final fun produceIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;I)Lkotlinx/coroutines/channels/ReceiveChannel; - public static synthetic fun produceIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;IILjava/lang/Object;)Lkotlinx/coroutines/channels/ReceiveChannel; + public static final fun produceIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/channels/ReceiveChannel; public static final fun reduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun retry (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt index bdb06b74d3..2981d8394e 100644 --- a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt @@ -8,6 +8,8 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.channels.Channel.Factory.CHANNEL_DEFAULT_CAPACITY import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED /** @@ -50,9 +52,11 @@ public interface BroadcastChannel : SendChannel { * Creates a broadcast channel with the specified buffer capacity. * * The resulting channel type depends on the specified [capacity] parameter: + * * * when `capacity` positive, but less than [UNLIMITED] -- creates `ArrayBroadcastChannel` with a buffer of given capacity. * **Note:** this channel looses all items that are send to it until the first subscriber appears; * * when `capacity` is [CONFLATED] -- creates [ConflatedBroadcastChannel] that conflates back-to-back sends; + * * when `capacity` is [BUFFERED] -- creates `ArrayBroadcastChannel` with a default capacity. * * otherwise -- throws [IllegalArgumentException]. * * **Note: This is an experimental api.** It may be changed in the future updates. @@ -63,5 +67,6 @@ public fun BroadcastChannel(capacity: Int): BroadcastChannel = 0 -> throw IllegalArgumentException("Unsupported 0 capacity for BroadcastChannel") UNLIMITED -> throw IllegalArgumentException("Unsupported UNLIMITED capacity for BroadcastChannel") CONFLATED -> ConflatedBroadcastChannel() + BUFFERED -> ArrayBroadcastChannel(CHANNEL_DEFAULT_CAPACITY) else -> ArrayBroadcastChannel(capacity) } diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index 030fb6b07a..9fa341830d 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -10,6 +10,9 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.RENDEZVOUS import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.channels.Channel.Factory.CHANNEL_DEFAULT_CAPACITY +import kotlinx.coroutines.internal.systemProp import kotlinx.coroutines.selects.* import kotlin.jvm.* @@ -372,6 +375,27 @@ public interface Channel : SendChannel, ReceiveChannel { * Requests conflated channel in `Channel(...)` factory function -- the `ConflatedChannel` gets created. */ public const val CONFLATED = -1 + + /** + * Requests buffered channel with a default buffer capacity in `Channel(...)` factory function -- + * the `ArrayChannel` gets created with a default capacity. + * This capacity is equal to 16 by default and can be overridden by setting + * [DEFAULT_BUFFER_PROPERTY_NAME] on JVM. + */ + public const val BUFFERED = -2 + + // only for internal use, cannot be used with Channel(...) + internal const val OPTIONAL_CHANNEL = -3 + + /** + * Name of the property that defines the default channel capacity when + * [BUFFERED] is used as parameter in `Channel(...)` factory function. + */ + public const val DEFAULT_BUFFER_PROPERTY_NAME = "kotlinx.coroutines.channels.defaultBuffer" + + internal val CHANNEL_DEFAULT_CAPACITY = systemProp(DEFAULT_BUFFER_PROPERTY_NAME, + 16, 1, UNLIMITED - 1 + ) } } @@ -379,13 +403,15 @@ public interface Channel : SendChannel, ReceiveChannel { * Creates a channel with the specified buffer capacity (or without a buffer by default). * See [Channel] interface documentation for details. * - * @throws IllegalArgumentException when [capacity] < -1 + * @param capacity either a positive channel capacity or one of the constants defined in [Channel.Factory]. + * @throws IllegalArgumentException when [capacity] < -2 */ public fun Channel(capacity: Int = RENDEZVOUS): Channel = when (capacity) { RENDEZVOUS -> RendezvousChannel() UNLIMITED -> LinkedListChannel() CONFLATED -> ConflatedChannel() + BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) else -> ArrayChannel(capacity) } diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 733bf63e6a..6147b65202 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -9,6 +9,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED import kotlinx.coroutines.flow.internal.* import kotlin.coroutines.* import kotlin.jvm.* @@ -206,24 +207,15 @@ public fun LongRange.asFlow(): Flow = flow { @Deprecated( message = "Use channelFlow instead", level = DeprecationLevel.WARNING, - replaceWith = ReplaceWith("channelFlow(bufferSize, block)") + replaceWith = ReplaceWith("channelFlow(block)") ) public fun flowViaChannel( - bufferSize: Int = 16, + bufferSize: Int = BUFFERED, @BuilderInference block: CoroutineScope.(channel: SendChannel) -> Unit ): Flow { - return flow { - coroutineScope { - val channel = Channel(bufferSize) - launch { - block(channel) - } - - channel.consumeEach { value -> - emit(value) - } - } - } + return channelFlow { + block(channel) + }.buffer(bufferSize) } /** @@ -233,28 +225,37 @@ public fun flowViaChannel( * The resulting flow is _cold_, which means that [block] is called on each call of a terminal operator * on the resulting flow. * - * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope] can be used concurrently from different contexts. - * The resulting flow will complete as soon as [ProducerScope], to artificially prolong it [awaitClose] can be used. + * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope] can be used + * concurrently from different contexts. + * The resulting flow completes as soon as the code in the [block] and all its children complete. + * Use [awaitClose] as the last statement to keep it running. * For more detailed example please refer to [callbackFlow] documentation. * - * To control backpressure, [bufferSize] is used and matches directly the `capacity` parameter of [Channel] factory. - * The provided channel can later be used by any external service to communicate with the flow and its buffer determines - * backpressure buffer size or its behaviour (e.g. in the case when [Channel.CONFLATED] was used). + * A channel with [default][Channel.BUFFERED] buffer size is used. Use [buffer] operator on the + * resulting flow to specify a value other than default and to control what happens when data is produced faster + * than it is consumed, that is to control backpressure behavior. + * + * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are + * always fused so that only one properly configured channel is used for execution. * * Examples of usage: + * * ``` * fun Flow.merge(other: Flow): Flow = channelFlow { + * // collect from one coroutine and send it * launch { - * collect { value -> send(value) } + * collect { send(it) } * } - * other.collect { value -> send(value) } + * // collect and send from this coroutine, too, concurrently + * other.collect { send(it) } * } * * fun contextualFlow(): Flow = channelFlow { + * // send from one coroutine * launch(Dispatchers.IO) { * send(computeIoValue()) * } - * + * // send from another coroutine, concurrently * launch(Dispatchers.Default) { * send(computeCpuValue()) * } @@ -262,15 +263,8 @@ public fun flowViaChannel( * ``` */ @FlowPreview -public fun channelFlow(bufferSize: Int = 16, @BuilderInference block: suspend ProducerScope.() -> Unit): Flow = - flow { - coroutineScope { - val channel = produce(capacity = bufferSize, block = block) - channel.consumeEach { value -> - emit(value) - } - } - } +public fun channelFlow(@BuilderInference block: suspend ProducerScope.() -> Unit): Flow = + ChannelFlowBuilder(block) /** * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] @@ -280,23 +274,28 @@ public fun channelFlow(bufferSize: Int = 16, @BuilderInference block: suspen * The resulting flow is _cold_, which means that [block] is called on each call of a terminal operator * on the resulting flow. * - * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope] can be used from any context, - * e.g. from the callback-based API. The flow completes as soon as its scope completes, thus if you are using channel from the - * callback-based API, to artificially prolong scope lifetime and avoid memory-leaks related to unregistered resources, - * [awaitClose] extension should be used. [awaitClose] argument will be invoked when either flow consumer cancels flow collection + * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope] can be used + * from any context, e.g. from the callback-based API. + * The resulting flow completes as soon as the code in the [block] and all its children complete. + * Use [awaitClose] as the last statement to keep it running. + * [awaitClose] argument is called when either flow consumer cancels flow collection * or when callback-based API invokes [SendChannel.close] manually. * - * To control backpressure, [bufferSize] is used and matches directly the `capacity` parameter of [Channel] factory. - * The provided channel can later be used by any external service to communicate with the flow and its buffer determines - * backpressure buffer size or its behaviour (e.g. in the case when [Channel.CONFLATED] was used). + * A channel with [default][Channel.BUFFERED] buffer size is used. Use [buffer] operator on the + * resulting flow to specify a value other than default and to control what happens when data is produced faster + * than it is consumed, that is to control backpressure behavior. + * + * Adjacent applications of [callbackFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are + * always fused so that only one properly configured channel is used for execution. * * Example of usage: + * * ``` * fun flowFrom(api: CallbackBasedApi): Flow = callbackFlow { * val callback = object : Callback { // implementation of some callback interface * override fun onNextValue(value: T) { * // Note: offer drops value when buffer is full - * // Channel.UNLIMITED can be used to avoid overfill + * // Use either buffer(Channel.CONFLATED) or buffer(Channel.UNLIMITED) to avoid overfill * offer(value) * } * override fun onApiError(cause: Throwable) { @@ -310,5 +309,22 @@ public fun channelFlow(bufferSize: Int = 16, @BuilderInference block: suspen * } * ``` */ -public inline fun callbackFlow(bufferSize: Int = 16, @BuilderInference crossinline block: suspend ProducerScope.() -> Unit): Flow = - channelFlow(bufferSize) { block() } +@Suppress("NOTHING_TO_INLINE") +public inline fun callbackFlow(@BuilderInference noinline block: suspend ProducerScope.() -> Unit): Flow = + channelFlow(block) + +// ChannelFlow implementation that is the first in the chain of flow operations and introduces (builds) a flow +private class ChannelFlowBuilder( + private val block: suspend ProducerScope.() -> Unit, + context: CoroutineContext = EmptyCoroutineContext, + capacity: Int = BUFFERED +) : ChannelFlow(context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelFlowBuilder(block, context, capacity) + + override suspend fun collectTo(scope: ProducerScope) = + block(scope) + + override fun toString(): String = + "block[$block] -> ${super.toString()}" +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt index 9034aec4c8..d0d22432e7 100644 --- a/kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt @@ -9,6 +9,11 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.channels.Channel.Factory.OPTIONAL_CHANNEL +import kotlinx.coroutines.flow.internal.* +import kotlin.coroutines.* import kotlin.jvm.* /** @@ -33,30 +38,32 @@ public fun BroadcastChannel.asFlow(): Flow = flow { * * This transformation is **stateful**, it launches a [broadcast] coroutine * that collects the given flow and thus resulting channel should be properly closed or cancelled. + * + * A channel with [default][Channel.Factory.BUFFERED] buffer size is created. + * Use [buffer] operator on the flow before calling `produce` to specify a value other than + * default and to control what happens when data is produced faster than it is consumed, + * that is to control backpressure behavior. */ @FlowPreview public fun Flow.broadcastIn( - scope: CoroutineScope, capacity: Int = 1, + scope: CoroutineScope, start: CoroutineStart = CoroutineStart.LAZY -): BroadcastChannel = scope.broadcast(capacity = capacity, start = start) { - collect { value -> - send(value) - } -} +): BroadcastChannel = + asChannelFlow().broadcastImpl(scope, start) /** * Creates a [produce] coroutine that collects the given flow. * * This transformation is **stateful**, it launches a [produce] coroutine * that collects the given flow and thus resulting channel should be properly closed or cancelled. + * + * A channel with [default][Channel.Factory.BUFFERED] buffer size is created. + * Use [buffer] operator on the flow before calling `produce` to specify a value other than + * default and to control what happens when data is produced faster than it is consumed, + * that is to control backpressure behavior. */ @FlowPreview public fun Flow.produceIn( - scope: CoroutineScope, - capacity: Int = 1 -): ReceiveChannel = scope.produce(capacity = capacity) { - // TODO it would be nice to have it with start = lazy as well - collect { value -> - send(value) - } -} + scope: CoroutineScope +): ReceiveChannel = + asChannelFlow().produceImpl(scope) diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index 0b5df8df8d..0d7271d7b9 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -7,11 +7,17 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* /** - * A cold asynchronous data stream emitting zero to N (where N can be unbounded) values and completing normally or with an exception. + * A cold asynchronous data stream that sequentially emits values + * and completes normally or with an exception. * - * Transformations on a flow, such as [map] and [filter], do not trigger its collection or execution (which is only done by terminal operators like [single]). + * _Cold flow_ means that intermediate operators on a flow such as [map] and [filter] do not trigger its execution, + * which is only done by terminal operators like [single]. By default, flows are _sequential_ and all flow + * operations are executed sequentially in the same coroutine, see [buffer] for details. + * + * _Collecting the flow_ means executing all its operations. + * Flow values can be collected in a suspending manner without actual blocking using the [collect] extension that + * completes normally or exceptionally: * - * A flow can be collected in a suspending manner, without actual blocking, using the [collect] extension that will complete normally or exceptionally: * ``` * try { * flow.collect { value -> @@ -21,27 +27,45 @@ import kotlinx.coroutines.* * println("The flow has thrown an exception: $e") * } * ``` + * * Additionally, the library provides a rich set of terminal operators such as [single], [reduce] and others. * * Flows don't carry information whether they are cold streams (which can be collected repeatedly and * trigger their evaluation every time [collect] is executed) or hot ones, but, conventionally, they represent cold streams. - * Transitions between hot and cold streams are supported via channels and the corresponding API: [flowViaChannel], [broadcastIn], [produceIn]. + * Transitions between hot and cold streams are supported via channels and the corresponding API: + * [channelFlow], [produceIn], [broadcastIn]. + * + * ### Flow builders + * + * There are the following basic ways to create a flow: + * + * * [flowOf(...)][flowOf] functions to create a flow from a fixed set of values. + * * [asFlow()][asFlow] extension functions on various types to convert them into flows. + * * [flow { ... }][flow] builder function to construct arbitrary flows from + * sequential calls to [emit][FlowCollector.emit] function. + * * [channelFlow { ... }][channelFlow] builder function to construct arbitrary flows from + * potentially concurrent calls to [send][kotlinx.coroutines.channels.SendChannel.send] function. + * + * ### Flow context * - * The flow has a context preservation property: it encapsulates its own execution context and never propagates or leaks it downstream, thus making - * reasoning about the execution context of particular transformations or terminal operations trivial. + * The flow has a context preservation property: it encapsulates its own execution context and never propagates or leaks + * it downstream, thus making reasoning about the execution context of particular transformations or terminal + * operations trivial. * * There is the only way to change the context of a flow: [flowOn][Flow.flowOn] operator, - * that changes the upstream context ("everything above the flowOn operator"). For additional information refer to its documentation. + * that changes the upstream context ("everything above the flowOn operator"). + * For additional information refer to its documentation. * * This reasoning can be demonstrated in practice: + * * ``` - * val flow = flowOf(1, 2, 3) - * .map { it + 1 } // Will be executed in ctx_1 - * .flowOn(ctx_1) // Changes the upstream context: flowOf and map + * val flowA = flowOf(1, 2, 3) + * .map { it + 1 } // Will be executed in ctxA + * .flowOn(ctxA) // Changes the upstream context: flowOf and map * * // Now we have a context-preserving flow: it is executed somewhere but this information is encapsulated in the flow itself * - * val filtered = flow // ctx_1 is inaccessible + * val filtered = flowA // ctxA is encapsulated in flowA * .filter { it == 3 } // Pure operator without a context yet * * withContext(Dispatchers.Main) { @@ -51,44 +75,39 @@ import kotlinx.coroutines.* * } * ``` * - * From the implementation point of view it means that all intermediate operators on [Flow] should abide by the following constraints: - * 1) If an operator is trivial and does not start any coroutines, regular [flow] builder should be used. Its implementation - * efficiently enforces all the invariants and prevents most of the development mistakes. - * - * 2) If the collection and emission of the flow are to be separated into multiple coroutines, [channelFlow] should be used. - * [channelFlow] encapsulates all the context preservation work and allows you to focus on your domain-specific problem, - * rather than invariant implementation details. It is possible to use any combination of coroutine builders from within [channelFlow]. - * - * 3) If you are looking for the performance and are sure that no concurrent emits and context jumps will happen, [flow] builder - * alongside with [coroutineScope] or [supervisorScope] can be used instead: + * From the implementation point of view it means that all flow implementations should + * emit only from the same coroutine. + * This constraint is efficiently enforced by the default [flow] builder. + * The [flow] builder should be used if flow implementation does not start any coroutines. + * Its implementation prevents most of the development mistakes: * - * - Scoped primitive should be used to provide a [CoroutineScope] - * - Changing the context of emission is prohibited, no matter whether it is `withContext(ctx)` or builder argument (e.g. `launch(ctx)`) - * - Changing the context of collection is allowed, but it has the same effect as [flowOn] operator and changes the upstream context. - * - * These constraints are enforced by the default [flow] builder. - * Example of the proper `buffer` implementation: * ``` - * fun Flow.buffer(bufferSize: Int): Flow = flow { - * coroutineScope { // coroutine scope is necessary, withContext is prohibited - * // GlobalScope.produce { is prohibited - * val channel = produce(bufferSize) { - * collect { value -> // Collect from started coroutine -- OK - * channel.send(value) - * } - * } - * - * for (i in channel) { - * emit(i) // Emission from the enclosing scope -- OK - * // launch { emit(i) } -- prohibited - * // withContext(Dispatchers.IO) { emit(i) } - * } - * } + * val myFlow = flow { + * // GlobalScope.launch { // is prohibited + * // launch(Dispatchers.IO) { // is prohibited + * // withContext(CoroutineName("myFlow")) // is prohibited + * emit(1) // OK + * coroutineScope { + * emit(2) // OK -- still the same coroutine + * } * } * ``` * - * Flow is [Reactive Streams](http://www.reactive-streams.org/) compliant, you can safely interop it with reactive streams using [Flow.asPublisher] and [Publisher.asFlow] from - * kotlinx-coroutines-reactive module. + * Use [channelFlow] if the collection and emission of the flow are to be separated into multiple coroutines. + * It encapsulates all the context preservation work and allows you to focus on your + * domain-specific problem, rather than invariant implementation details. + * It is possible to use any combination of coroutine builders from within [channelFlow]. + * + * If you are looking for the performance and are sure that no concurrent emits and context jumps will happen, + * [flow] builder alongside with [coroutineScope] or [supervisorScope] can be used instead: + * - Scoped primitive should be used to provide a [CoroutineScope]. + * - Changing the context of emission is prohibited, no matter whether it is `withContext(ctx)` or + * builder argument (e.g. `launch(ctx)`). + * - Collecting another flow from a separate context is allowed, but it has the same effect as + * [flowOn] operator on that flow, which is more efficient. + * + * Flow is [Reactive Streams](http://www.reactive-streams.org/) compliant, you can safely interop it with + * reactive streams using [Flow.asPublisher] and [Publisher.asFlow] from kotlinx-coroutines-reactive module. */ @FlowPreview public interface Flow { @@ -101,7 +120,8 @@ public interface Flow { * The emission should happen in the context of the [collect] call. * Please refer to the top-level [Flow] documentation for more details. * - * 2) It should serialize calls to [emit][FlowCollector.emit] as [FlowCollector] implementations are not thread safe by default. + * 2) It should serialize calls to [emit][FlowCollector.emit] as [FlowCollector] implementations are not + * thread safe by default. * To automatically serialize emissions [channelFlow] builder can be used instead of [flow] */ public suspend fun collect(collector: FlowCollector) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt new file mode 100644 index 0000000000..30005afa5d --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* +import kotlin.jvm.* + +internal fun Flow.asChannelFlow(): ChannelFlow = + this as? ChannelFlow ?: ChannelFlowOperatorImpl(this) + +// Operators that use channels extend this ChannelFlow and are always fused with each other +internal abstract class ChannelFlow( + // upstream context + @JvmField val context: CoroutineContext, + // buffer capacity between upstream and downstream context + @JvmField val capacity: Int +) : Flow { + public fun update( + context: CoroutineContext = EmptyCoroutineContext, + capacity: Int = Channel.OPTIONAL_CHANNEL + ): ChannelFlow { + // note: previous upstream context (specified before) takes precedence + val newContext = context + this.context + val newCapacity = when { + this.capacity == Channel.OPTIONAL_CHANNEL -> capacity + capacity == Channel.OPTIONAL_CHANNEL -> this.capacity + this.capacity == Channel.BUFFERED -> capacity + capacity == Channel.BUFFERED -> this.capacity + this.capacity == Channel.CONFLATED -> Channel.CONFLATED + capacity == Channel.CONFLATED -> Channel.CONFLATED + else -> { + // sanity checks + check(this.capacity >= 0) { "Unexpected capacity ${this.capacity}" } + check(capacity >= 0) { "Unexpected capacity $capacity" } + // combine capacities clamping to UNLIMITED on overflow + val sum = this.capacity + capacity + if (sum >= 0) sum else Channel.UNLIMITED // unlimited on int overflow + } + } + if (newContext == this.context && newCapacity == this.capacity) return this + return create(newContext, newCapacity) + } + + protected abstract fun create(context: CoroutineContext, capacity: Int): ChannelFlow + + protected abstract suspend fun collectTo(scope: ProducerScope) + + // shared code to create a suspend lambda from collectTo function in one place + private val collectToFun: suspend (ProducerScope) -> Unit + get() = { collectTo(it) } + + private val produceCapacity: Int + get() = if (capacity == Channel.OPTIONAL_CHANNEL) Channel.BUFFERED else capacity + + fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel = + scope.broadcast(context, produceCapacity, start, block = collectToFun) + + fun produceImpl(scope: CoroutineScope): ReceiveChannel = + scope.produce(context, produceCapacity, block = collectToFun) + + override suspend fun collect(collector: FlowCollector) = + coroutineScope { // todo: flowScope + val channel = produceImpl(this) + channel.consumeEach { collector.emit(it) } + } + + // debug toString + override fun toString(): String = + "$classSimpleName[${additionalToStringProps()}context=$context, capacity=$capacity]" + + open fun additionalToStringProps() = "" +} + +// ChannelFlow implementation that operates on another flow before it +internal abstract class ChannelFlowOperator( + @JvmField val flow: Flow, + context: CoroutineContext, + capacity: Int +) : ChannelFlow(context, capacity) { + protected abstract suspend fun flowCollect(collector: FlowCollector) + + // Changes collecting context upstream to the specified newContext, while collecting in the original context + private suspend fun collectWithContextUndispatched(collector: FlowCollector, newContext: CoroutineContext) { + val originalContextCollector = collector.withUndispatchedContextCollector(coroutineContext) + // invoke flowCollect(originalContextCollector) in the newContext + return withContextUndispatched(newContext, block = { flowCollect(it) }, value = originalContextCollector) + } + + // Slow path when output channel is required + protected override suspend fun collectTo(scope: ProducerScope) = + flowCollect(SendingCollector(scope)) + + // Optimizations for fast-path when channel creation is optional + override suspend fun collect(collector: FlowCollector) { + // Fast-path: When channel creation is optional (flowOn/flowWith operators without buffer) + if (capacity == Channel.OPTIONAL_CHANNEL) { + val collectContext = coroutineContext + val newContext = collectContext + context // compute resulting collect context + // #1: If the resulting context happens to be the same as it was -- fallback to plain collect + if (newContext == collectContext) + return flowCollect(collector) + // #2: If we don't need to change the dispatcher we can go without channels + if (newContext[ContinuationInterceptor] == collectContext[ContinuationInterceptor]) + return collectWithContextUndispatched(collector, newContext) + } + // Slow-path: create the actual channel + super.collect(collector) + } + + // debug toString + override fun toString(): String = "$flow -> ${super.toString()}" +} + +// Simple channel flow operator: flowOn, buffer, or their fused combination +internal class ChannelFlowOperatorImpl( + flow: Flow, + context: CoroutineContext = EmptyCoroutineContext, + capacity: Int = Channel.OPTIONAL_CHANNEL +) : ChannelFlowOperator(flow, context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelFlowOperatorImpl(flow, context, capacity) + + override suspend fun flowCollect(collector: FlowCollector) = + flow.collect(collector) +} + +// Now if the underlying collector was accepting concurrent emits, then this one is too +// todo: we might need to generalize this pattern for "thread-safe" operators that can fuse with channels +private fun FlowCollector.withUndispatchedContextCollector(emitContext: CoroutineContext): FlowCollector = when (this) { + // SendingCollector does not care about the context at all so can be used as it + is SendingCollector -> this + // Original collector is concurrent, so wrap into ConcurrentUndispatchedContextCollector (also concurrent) + is ConcurrentFlowCollector -> ConcurrentUndispatchedContextCollector(this, emitContext) + // Otherwise just wrap into UndispatchedContextCollector interface implementation + else -> UndispatchedContextCollector(this, emitContext) +} + +private open class UndispatchedContextCollector( + downstream: FlowCollector, + private val emitContext: CoroutineContext +) : FlowCollector { + private val countOrElement = threadContextElements(emitContext) // precompute for fast withContextUndispatched + private val emitRef: suspend (T) -> Unit = { downstream.emit(it) } // allocate suspend function ref once on creation + + override suspend fun emit(value: T): Unit = + withContextUndispatched(emitContext, countOrElement, emitRef, value) +} + +// named class for a combination of UndispatchedContextCollector & ConcurrentFlowCollector interface +private class ConcurrentUndispatchedContextCollector( + downstream: ConcurrentFlowCollector, + emitContext: CoroutineContext +) : UndispatchedContextCollector(downstream, emitContext), ConcurrentFlowCollector + +// Efficiently computes block(value) in the newContext +private suspend fun withContextUndispatched( + newContext: CoroutineContext, + countOrElement: Any = threadContextElements(newContext), // can be precomputed for speed + block: suspend (V) -> T, value: V +): T = + suspendCoroutineUninterceptedOrReturn sc@{ uCont -> + withCoroutineContext(newContext, countOrElement) { + block.startCoroutineUninterceptedOrReturn(value, Continuation(newContext) { + uCont.resumeWith(it) + }) + } + } diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Concurrent.kt b/kotlinx-coroutines-core/common/src/flow/internal/Concurrent.kt new file mode 100644 index 0000000000..6119d3dbc1 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/Concurrent.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.atomicfu.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.channels.ArrayChannel +import kotlinx.coroutines.flow.* + +internal fun FlowCollector.asConcurrentFlowCollector(): ConcurrentFlowCollector = + this as? ConcurrentFlowCollector ?: SerializingCollector(this) + +// Flow collector that supports concurrent emit calls. +// It is internal for now but may be public in the future. +// Two basic implementations are here: SendingCollector and ConcurrentFlowCollector +internal interface ConcurrentFlowCollector : FlowCollector + +// Concurrent collector because it sends to a channel +internal class SendingCollector( + private val channel: SendChannel +) : ConcurrentFlowCollector { + override suspend fun emit(value: T) = channel.send(value) +} + +// Effectively serializes access to downstream collector for merging +// This is basically a converted from FlowCollector interface to ConcurrentFlowCollector +private class SerializingCollector( + private val downstream: FlowCollector +) : ConcurrentFlowCollector { + // Let's try to leverage the fact that merge is never contended + // Should be Any, but KT-30796 + private val _channel = atomic?>(null) + private val inProgressLock = atomic(false) + + private val channel: ArrayChannel + get() = _channel.updateAndGet { value -> + if (value != null) return value + ArrayChannel(Channel.CHANNEL_DEFAULT_CAPACITY) + }!! + + public override suspend fun emit(value: T) { + if (!inProgressLock.tryAcquire()) { + channel.send(value ?: NULL) + if (inProgressLock.tryAcquire()) { + helpEmit() + } + return + } + downstream.emit(value) + helpEmit() + } + + @Suppress("UNCHECKED_CAST") + private suspend fun helpEmit() { + while (true) { + while (true) { + val element = _channel.value?.poll() ?: break // todo: pollOrClosed + downstream.emit(NULL.unbox(element)) + } + inProgressLock.release() + // Enforce liveness + if (_channel.value?.isEmpty != false || !inProgressLock.tryAcquire()) break + } + } +} + +@Suppress("NOTHING_TO_INLINE") +private inline fun AtomicBoolean.tryAcquire(): Boolean = compareAndSet(false, true) + +@Suppress("NOTHING_TO_INLINE") +private inline fun AtomicBoolean.release() { + value = false +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index c5a6a361a5..0a04493de4 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -9,16 +9,118 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.flow.internal.* import kotlin.coroutines.* import kotlin.jvm.* -import kotlinx.coroutines.flow.unsafeFlow as flow /** - * The operator that changes the context where this flow is executed to the given [flowContext]. + * Buffers flow emissions via channel of a specified capacity and runs collector in a separate coroutine. + * + * Normally, [flows][Flow] are _sequential_. It means that the code of all operators is executed in the + * same coroutine. For example, consider the following code using [onEach] and [collect] operators: + * + * ``` + * flowOf("A", "B", "C") + * .onEach { println("1$it") } + * .collect { println("2$it") } + * ``` + * + * It is going to be executed in the following order by the coroutine `Q` that calls this code: + * + * ``` + * Q : -->-- [1A] -- [2A] -- [1B] -- [2B] -- [1C] -- [2C] -->-- + * ``` + * + * So if the operator's code takes considerable time to execute, then the total execution time is going to be + * the sum of execution times for all operators. + * + * The `buffer` operator creates a separate coroutine during execution for the flow it applies to. + * Consider the following code: + * + * ``` + * flowOf("A", "B", "C") + * .onEach { println("1$it") } + * .buffer() // <--------------- buffer between onEach and collect + * .collect { println("2$it") } + * ``` + * + * It will use two coroutines for execution of the code. A coroutine `Q` that calls this code is + * going to execute `collect`, and the code before `buffer` will be executed in a separate + * new coroutine `P` concurrently with `Q`: + * + * ``` + * P : -->-- [1A] -- [1B] -- [1C] ---------->-- // flowOf(...).onEach { ... } + * + * | + * | channel // buffer() + * V + * + * Q : -->---------- [2A] -- [2B] -- [2C] -->-- // collect + * ``` + * + * When operator's code takes time to execute this decreases the total execution time of the flow. + * A [channel][Channel] is used between the coroutines to send elements emitted by the coroutine `P` to + * the coroutine `Q`. If the code before `buffer` operator (in the coroutine `P`) is faster than the code after + * `buffer` operator (in the coroutine `Q`), then this channel will become full at some point and will suspend + * the producer coroutine `P` until the consumer coroutine `Q` catches up. + * The [capacity] parameter defines the size of this buffer. + * + * ### Operator fusion + * + * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are + * always fused so that only one properly configured channel is used for execution. + * + * Explicitly specified buffer capacity takes precedence over `buffer()` or `buffer(Channel.BUFFERED)` calls, + * which effectively requests a buffer of any size. Multiple requests with a specified buffer + * size produce a buffer with the sum of the requested buffer sizes. + * + * ### Conceptual implementation + * + * The actual implementation of `buffer` is not trivial due to the fusing, but conceptually its + * implementation is equivalent to the following code that can be written using [produce] + * coroutine builder to produce a channel and [consumeEach][ReceiveChannel.consumeEach] extension to consume it: + * + * ``` + * fun Flow.buffer(capacity: Int = DEFAULT): Flow = flow { + * coroutineScope { // limit the scope of concurrent producer coroutine + * val channel = produce(capacity = capacity) { + * collect { send(it) } // send all to channel + * } + * // emit all received values + * channel.consumeEach { emit(it) } + * } + * } + * ``` + * + * @param capacity type/capacity of the buffer between coroutines. Allowed values are the same as in `Channel(...)` + * factory function: [BUFFERED][Channel.BUFFERED] (by default), [CONFLATED][Channel.CONFLATED], + * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating + * an explicitly requested size. + */ +@FlowPreview +public fun Flow.buffer(capacity: Int = BUFFERED): Flow { + require(capacity >= 0 || capacity == BUFFERED || capacity == CONFLATED) { + "Buffer size should be non-negative, BUFFERED, or CONFLATED, but was $capacity" + } + return if (this is ChannelFlow) + update(capacity = capacity) + else + ChannelFlowOperatorImpl(this, capacity = capacity) +} + +// todo: conflate would be a useful operator only when Channel.CONFLATE is changed to always deliver the last send value +//@FlowPreview +//public fun Flow.conflate(): Flow = buffer(CONFLATED) + +/** + * The operator that changes the context where this flow is executed to the given [context]. * This operator is composable and affects only preceding operators that do not have its own context. - * This operator is context preserving: [flowContext] **does not** leak into the downstream flow. + * This operator is context preserving: [context] **does not** leak into the downstream flow. * * For example: + * * ``` * withContext(Dispatchers.Main) { * val singleValue = intFlow // will be executed on IO if context wasn't specified before @@ -29,34 +131,40 @@ import kotlinx.coroutines.flow.unsafeFlow as flow * .single() // Will be executed in the Main * } * ``` + * * For more explanation of context preservation please refer to [Flow] documentation. * - * This operator uses a channel of the specific [bufferSize] in order to switch between contexts, - * but it is not guaranteed that the channel will be created, implementation is free to optimize it away in case of fusing. + * This operators retains a _sequential_ nature of flow if changing the context does not call for changing + * the [dispatcher][CoroutineDispatcher]. Otherwise, if changing dispatcher is required, it collects + * flow emissions in one coroutine that is run using a specified [context] and emits them from another coroutines + * with the original collector's context using a channel with a [default][Channel.BUFFERED] buffer size + * between two coroutines similarly to [buffer] operator, unless [buffer] operator is explicitly called + * before or after `flowOn`, which requests buffering behavior and specifies channel size. + * + * ### Operator fusion + * + * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are + * always fused so that only one properly configured channel is used for execution. + * + * Multiple `flowOn` operators fuse to a single `flowOn` with a combined context. The elements of the context of + * the first `flowOn` operator naturally take precedence over the elements of the second `flowOn` operator + * when they have the same context keys, for example: + * + * ``` + * flow.map { ... } // Will be executed in IO + * .flowOn(Dispatchers.IO) // This one takes precedence + * .flowOn(Dispatchers.Default) + * ``` * * @throws [IllegalArgumentException] if provided context contains [Job] instance. */ @FlowPreview -public fun Flow.flowOn(flowContext: CoroutineContext, bufferSize: Int = 16): Flow { - check(flowContext, bufferSize) - return flow { - // Fast-path, context is not changed, so we can just fallback to plain collect - val currentContext = coroutineContext.minusKey(Job) // Jobs are ignored - if (flowContext == currentContext) { - collect { value -> emit(value) } - return@flow - } - - coroutineScope { - val channel = produce(flowContext, capacity = bufferSize) { - collect { value -> - return@collect send(value) - } - } - channel.consumeEach { value -> - emit(value) - } - } +public fun Flow.flowOn(context: CoroutineContext): Flow { + checkFlowContext(context) + return when { + context == EmptyCoroutineContext -> this + this is ChannelFlow -> update(context = context) + else -> ChannelFlowOperatorImpl(this, context = context) } } @@ -65,6 +173,7 @@ public fun Flow.flowOn(flowContext: CoroutineContext, bufferSize: Int = 1 * This operator is context preserving and does not affect the context of the preceding and subsequent operations. * * Example: + * * ``` * flow // not affected * .map { ... } // Not affected @@ -74,12 +183,12 @@ public fun Flow.flowOn(flowContext: CoroutineContext, bufferSize: Int = 1 * } * .map { ... } // Not affected * ``` + * * For more explanation of context preservation please refer to [Flow] documentation. * - * This operator uses channel of the specific [bufferSize] in order to switch between contexts, - * but it is not guaranteed that channel will be created, implementation is free to optimize it away in case of fusing.* + * This operator is deprecated without replacement because it was discovered that it doesn't play well with coroutines + * and flow semantics: * - * This operator is deprecated without replacement because it was discovered that it doesn't play well with coroutines and flow semantics: * 1) It doesn't prevent context elements from the downstream to leak into its body * ``` * flowOf(1).flowWith(EmptyCoroutineContext) { @@ -96,31 +205,28 @@ public fun Flow.flowOn(flowContext: CoroutineContext, bufferSize: Int = 1 @Deprecated(message = "flowWith is deprecated without replacement, please refer to its KDoc for an explanation", level = DeprecationLevel.WARNING) // Error in beta release, removal in 1.4 public fun Flow.flowWith( flowContext: CoroutineContext, - bufferSize: Int = 16, + bufferSize: Int = BUFFERED, builder: Flow.() -> Flow ): Flow { - check(flowContext, bufferSize) + checkFlowContext(flowContext) val source = this - return flow { + return unsafeFlow { /** * Here we should remove a Job instance from the context. * All builders are written using scoping and no global coroutines are launched, so it is safe not to provide explicit Job. * It is also necessary not to mess with cancellation if multiple flowWith are used. */ val originalContext = coroutineContext.minusKey(Job) - val prepared = source.flowOn(originalContext, bufferSize) - builder(prepared).flowOn(flowContext, bufferSize).collect { value -> + val prepared = source.flowOn(originalContext).buffer(bufferSize) + builder(prepared).flowOn(flowContext).buffer(bufferSize).collect { value -> return@collect emit(value) } } } -private fun check(flowContext: CoroutineContext, bufferSize: Int) { - require(flowContext[Job] == null) { - "Flow context cannot contain job in it. Had $flowContext" - } - - require(bufferSize >= 0) { - "Buffer size should be positive, but was $bufferSize" +private fun checkFlowContext(context: CoroutineContext) { + require(context[Job] == null) { + "Flow context cannot contain job in it. Had $context" } } + diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index 6404d040d2..f7a644710f 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -8,67 +8,75 @@ package kotlinx.coroutines.flow -import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.channels.Channel.Factory.OPTIONAL_CHANNEL import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* import kotlin.jvm.* import kotlinx.coroutines.flow.unsafeFlow as flow /** - * Transforms elements emitted by the original flow by applying [transform], that returns another flow, and then concatenating and flattening these flows. - * This method is identical to `flatMapMerge(concurrency = 1, bufferSize = 1)` + * Name of the property that defines the value of [DEFAULT_CONCURRENCY]. + */ +@FlowPreview +public const val DEFAULT_CONCURRENCY_PROPERTY_NAME = "kotlinx.coroutines.flow.defaultConcurrency" + +/** + * Default concurrency limit that is used by [flattenMerge] and [flatMapMerge] operators. + * It is 16 by default and can be changed on JVM using [DEFAULT_CONCURRENCY_PROPERTY_NAME] property. + */ +@FlowPreview +public val DEFAULT_CONCURRENCY = systemProp(DEFAULT_CONCURRENCY_PROPERTY_NAME, + 16, 1, Int.MAX_VALUE +) + +/** + * Transforms elements emitted by the original flow by applying [transform], that returns another flow, + * and then concatenating and flattening these flows. + * + * This method is is a shortcut for `map(transform).flattenConcat()`. See [flattenConcat]. * * Note that even though this operator looks very familiar, we discourage its usage in a regular application-specific flows. * Most likely, suspending operation in [map] operator will be sufficient and linear transformations are much easier to reason about. */ @FlowPreview -public fun Flow.flatMapConcat(transform: suspend (value: T) -> Flow): Flow = flow { - collect { value -> - transform(value).collect { innerValue -> - emit(innerValue) - } - } -} +public fun Flow.flatMapConcat(transform: suspend (value: T) -> Flow): Flow = + map(transform).flattenConcat() /** - * Transforms elements emitted by the original flow by applying [transform], that returns another flow, and then merging and flattening these flows. + * Transforms elements emitted by the original flow by applying [transform], that returns another flow, + * and then merging and flattening these flows. + * + * This operator calls [transform] *sequentially* and then merges the resulting flows with a [concurrency] + * limit on the number of concurrently collected flows. + * It is a shortcut for `map(transform).flattenMerge(concurrency)`. + * See [flattenMerge] for details. * * Note that even though this operator looks very familiar, we discourage its usage in a regular application-specific flows. * Most likely, suspending operation in [map] operator will be sufficient and linear transformations are much easier to reason about. * - * [bufferSize] parameter controls the size of backpressure aka the amount of queued in-flight elements. - * [concurrency] parameter controls the size of in-flight flows, at most [concurrency] flows are collected at the same time. + * ### Operator fusion + * + * Applications of [flowOn], [buffer], [produceIn], and [broadcastIn] _after_ this operator are fused with + * its concurrent merging so that only one properly configured channel is used for execution of merging logic. + * + * @param concurrency controls the number of in-flight flows, at most [concurrency] flows are collected + * at the same time. By default it is equal to [DEFAULT_CONCURRENCY]. */ @FlowPreview -public fun Flow.flatMapMerge(concurrency: Int = 16, bufferSize: Int = 16, transform: suspend (value: T) -> Flow): Flow { - require(bufferSize >= 0) { "Expected non-negative buffer size, but had $bufferSize" } - require(concurrency >= 0) { "Expected non-negative concurrency level, but had $concurrency" } - return flow { - coroutineScope { - val semaphore = Channel(concurrency) - val flatMap = SerializingFlatMapCollector(this@flow, bufferSize) - collect { outerValue -> - // TODO real semaphore (#94) - semaphore.send(Unit) // Acquire concurrency permit - val inner = transform(outerValue) - launch { - try { - inner.collect { value -> - flatMap.emit(value) - } - } finally { - semaphore.receive() // Release concurrency permit - } - } - } - } - } -} +public fun Flow.flatMapMerge( + concurrency: Int = DEFAULT_CONCURRENCY, + transform: suspend (value: T) -> Flow +): Flow = + map(transform).flattenMerge(concurrency) /** * Flattens the given flow of flows into a single flow in a sequentially manner, without interleaving nested flows. - * This method is identical to `flattenMerge(concurrency = 1, bufferSize = 1) + * This method is conceptually identical to `flattenMerge(concurrency = 1)` but has faster implementation. + * + * Inner flows are collected by this operator *sequentially*. */ @FlowPreview public fun Flow>.flattenConcat(): Flow = flow { @@ -80,14 +88,25 @@ public fun Flow>.flattenConcat(): Flow = flow { } /** - * Flattens the given flow of flows into a single flow. - * This method is identical to `flatMapMerge(concurrency, bufferSize) { it }` + * Flattens the given flow of flows into a single flow with a [concurrency] limit on the number of + * concurrently collected flows. + * + * If [concurrency] is more than 1, then inner flows are be collected by this operator *concurrently*. + * With `concurrency == 1` this operator is identical to [flattenConcat]. + * + * ### Operator fusion * - * [bufferSize] parameter controls the size of backpressure aka the amount of queued in-flight elements. - * [concurrency] parameter controls the size of in-flight flows, at most [concurrency] flows are collected at the same time. + * Applications of [flowOn], [buffer], [produceIn], and [broadcastIn] _after_ this operator are fused with + * its concurrent merging so that only one properly configured channel is used for execution of merging logic. + * + * @param concurrency controls the number of in-flight flows, at most [concurrency] flows are collected + * at the same time. By default it is equal to [DEFAULT_CONCURRENCY]. */ @FlowPreview -public fun Flow>.flattenMerge(concurrency: Int = 16, bufferSize: Int = 16): Flow = flatMapMerge(concurrency, bufferSize) { it } +public fun Flow>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY): Flow { + require(concurrency > 0) { "Expected positive concurrency level, but had $concurrency" } + return if (concurrency == 1) flattenConcat() else ChannelFlowMerge(this, concurrency) +} /** * Returns a flow that switches to a new flow produced by [transform] function every time the original flow emits a value. @@ -126,49 +145,46 @@ public fun Flow.switchMap(transform: suspend (value: T) -> Flow): F } } -// Effectively serializes access to downstream collector from flatMap -private class SerializingFlatMapCollector( - private val downstream: FlowCollector, bufferSize: Int -) { - - // Let's try to leverage the fact that flatMapMerge is never contended - // TODO do not allocate channel - private val channel = Channel(bufferSize) // Should be any, but KT-30796 - private val inProgressLock = atomic(false) - - public suspend fun emit(value: T) { - if (!inProgressLock.tryAcquire()) { - channel.send(value ?: NULL) - if (inProgressLock.tryAcquire()) { - helpEmit() +private class ChannelFlowMerge( + flow: Flow>, + private val concurrency: Int, + context: CoroutineContext = EmptyCoroutineContext, + capacity: Int = OPTIONAL_CHANNEL +) : ChannelFlowOperator, T>(flow, context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelFlowMerge(flow, concurrency, context, capacity) + + // The actual merge implementation with concurrency limit + private suspend fun mergeImpl(scope: CoroutineScope, collector: ConcurrentFlowCollector) { + val semaphore = Channel(concurrency) + @Suppress("UNCHECKED_CAST") + flow.collect { inner -> + // TODO real semaphore (#94) + semaphore.send(Unit) // Acquire concurrency permit + scope.launch { + try { + inner.collect(collector) + } finally { + semaphore.receive() // Release concurrency permit + } } - return } - - downstream.emit(value) - helpEmit() } - @Suppress("UNCHECKED_CAST") - private suspend fun helpEmit() { - while (true) { - var element = channel.poll() - while (element != null) { // TODO receive or closed (#330) - downstream.emit(NULL.unbox(element)) - element = channel.poll() - } - - inProgressLock.release() - // Enforce liveness - if (channel.isEmpty || !inProgressLock.tryAcquire()) break + // Fast path in ChannelFlowOperator calls this function (channel was not created yet) + override suspend fun flowCollect(collector: FlowCollector) { + // this function should not have been invoked when channel was explicitly requested + check(capacity == OPTIONAL_CHANNEL) + coroutineScope { // todo: flowScope + mergeImpl(this, collector.asConcurrentFlowCollector()) } } -} -@Suppress("NOTHING_TO_INLINE") -private inline fun AtomicBoolean.tryAcquire(): Boolean = compareAndSet(false, true) + // Slow path when output channel is required (and was created) + override suspend fun collectTo(scope: ProducerScope) = + mergeImpl(scope, SendingCollector(scope)) -@Suppress("NOTHING_TO_INLINE") -private inline fun AtomicBoolean.release() { - value = false + override fun additionalToStringProps(): String = + "concurrency=$concurrency, " } + diff --git a/kotlinx-coroutines-core/common/test/channels/BroadcastChannelFactoryTest.kt b/kotlinx-coroutines-core/common/test/channels/BroadcastChannelFactoryTest.kt index 4f11f6cc1e..61e93fa8ea 100644 --- a/kotlinx-coroutines-core/common/test/channels/BroadcastChannelFactoryTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BroadcastChannelFactoryTest.kt @@ -33,6 +33,6 @@ class BroadcastChannelFactoryTest : TestBase() { @Test fun testInvalidCapacityNotSupported() { - assertFailsWith { BroadcastChannel(-2) } + assertFailsWith { BroadcastChannel(-3) } } } diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt index 825cbb37c3..72ba315450 100644 --- a/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt @@ -34,6 +34,6 @@ class ChannelFactoryTest : TestBase() { @Test fun testInvalidCapacityNotSupported() = runTest({ it is IllegalArgumentException }) { - Channel(-2) + Channel(-3) } } diff --git a/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt index e8754e1db4..0ae30e80ce 100644 --- a/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt @@ -21,22 +21,23 @@ class ChannelFlowTest : TestBase() { @Test fun testBuffer() = runTest { - val flow = channelFlow(bufferSize = 1) { + val flow = channelFlow { assertTrue(offer(1)) assertTrue(offer(2)) assertFalse(offer(3)) - } + }.buffer(1) assertEquals(listOf(1, 2), flow.toList()) } + // todo: this is pretty useless behavior @Test fun testConflated() = runTest { - val flow = channelFlow(bufferSize = Channel.CONFLATED) { + val flow = channelFlow { assertTrue(offer(1)) assertTrue(offer(2)) assertTrue(offer(3)) assertTrue(offer(4)) - } + }.buffer(Channel.CONFLATED) assertEquals(listOf(1, 4), flow.toList()) // two elements in the middle got conflated } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt new file mode 100644 index 0000000000..045afe99cf --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.math.* +import kotlin.test.* + +class BufferTest : TestBase() { + private val n = 50 // number of elements to emit for test + private val defaultBufferSize = 16 // expected default buffer size (per docs) + + // Use capacity == -1 to check case of "no buffer" + private fun checkBuffer(capacity: Int, op: suspend Flow.() -> Flow) = runTest { + expect(1) + val batchSize = capacity + 2 + flow { + repeat(n) { i -> + val batchNo = i / batchSize + val batchIdx = i % batchSize + expect(batchNo * batchSize * 2 + batchIdx + 2) + emit(i) + } + } + .op() // insert user-defined operator + .collect { i -> + val batchNo = i / batchSize + val batchIdx = i % batchSize + // last batch might have smaller size + val k = min((batchNo + 1) * batchSize, n) - batchNo * batchSize + expect(batchNo * batchSize * 2 + k + batchIdx + 2) + } + finish(2 * n + 2) + } + + @Test + // capacity == -1 to checkBuffer means "no buffer" -- emits / collects are sequentially ordered + fun testBaseline() = + checkBuffer(-1) { this } + + @Test + fun testBufferDefault() = + checkBuffer(defaultBufferSize) { + buffer() + } + + @Test + fun testBufferRendezvous() = + checkBuffer(0) { + buffer(0) + } + + @Test + fun testBuffer1() = + checkBuffer(1) { + buffer(1) + } + + @Test + fun testBuffer2() = + checkBuffer(2) { + buffer(2) + } + + @Test + fun testBuffer3() = + checkBuffer(3) { + buffer(3) + } + + @Test + fun testBuffer00Fused() = + checkBuffer(0) { + buffer(0).buffer(0) + } + + @Test + fun testBuffer01Fused() = + checkBuffer(1) { + buffer(0).buffer(1) + } + + @Test + fun testBuffer11Fused() = + checkBuffer(2) { + buffer(1).buffer(1) + } + + @Test + fun testBuffer111Fused() = + checkBuffer(3) { + buffer(1).buffer(1).buffer(1) + } + + @Test + fun testBuffer123Fused() = + checkBuffer(6) { + buffer(1).buffer(2).buffer(3) + } + + @Test // multiple calls to buffer() create one channel of default size + fun testBufferDefaultTwiceFused() = + checkBuffer(defaultBufferSize) { + buffer().buffer() + } + + @Test // explicit buffer takes precedence of default buffer on fuse + fun testBufferDefaultBufferFused() = + checkBuffer(7) { + buffer().buffer(7) + } + + @Test // explicit buffer takes precedence of default buffer on fuse + fun testBufferBufferDefaultFused() = + checkBuffer(8) { + buffer(8).buffer() + } + + @Test // flowOn operator does not use buffer when dispatches does not change + fun testFlowOnNameNoBuffer() = + checkBuffer(-1) { + flowOn(CoroutineName("Name")) + } + + @Test // flowOn operator uses default buffer size when dispatcher changes + fun testFlowOnDispatcherBufferDefault() = + checkBuffer(defaultBufferSize) { + flowOn(wrapperDispatcher()) + } + + @Test // flowOn(...).buffer(n) sets explicit buffer size to n + fun testFlowOnDispatcherBufferFused() = + checkBuffer(5) { + flowOn(wrapperDispatcher()).buffer(5) + } + + @Test // buffer(n).flowOn(...) sets explicit buffer size to n + fun testBufferFlowOnDispatcherFused() = + checkBuffer(6) { + buffer(6).flowOn(wrapperDispatcher()) + } + + @Test // flowOn(...).buffer(n) sets explicit buffer size to n + fun testFlowOnNameBufferFused() = + checkBuffer(7) { + flowOn(CoroutineName("Name")).buffer(7) + } + + @Test // buffer(n).flowOn(...) sets explicit buffer size to n + fun testBufferFlowOnNameFused() = + checkBuffer(8) { + buffer(8).flowOn(CoroutineName("Name")) + } + + @Test // multiple flowOn/buffer all fused together + fun testBufferFlowOnMultipleFused() = + checkBuffer(12) { + flowOn(wrapperDispatcher()).buffer(3) + .flowOn(CoroutineName("Name")).buffer(4) + .flowOn(wrapperDispatcher()).buffer(5) + } + + @Test + @Ignore // todo: conflated behavior is pretty useless right now, because closing channel overwrites last value + fun testConflate() = runTest { + expect(1) + // emit all and conflate / then collect first & last + flow { + repeat(n) { i -> + expect(i + 2) + emit(i) + } + } + .buffer(Channel.CONFLATED) + .collect { i -> + when (i) { + 0 -> expect(n + 2) // first value + n - 1 -> expect(n + 3) // last value + else -> error("Unexpected $i") + } + } + finish(n + 4) + } +} + diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowContextOptimizationsTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextOptimizationsTest.kt index 8c308c8f55..bf5297408f 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowContextOptimizationsTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextOptimizationsTest.kt @@ -7,23 +7,24 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlin.coroutines.* import kotlin.test.* +import kotlin.coroutines.coroutineContext as currentContext class FlowContextOptimizationsTest : TestBase() { - @Test fun testBaseline() = runTest { - val flowDispatcher = wrapperDispatcher(coroutineContext) - val collectContext = coroutineContext + val flowDispatcher = wrapperDispatcher(currentContext) + val collectContext = currentContext flow { - assertSame(flowDispatcher, kotlin.coroutines.coroutineContext[ContinuationInterceptor] as CoroutineContext) + assertSame(flowDispatcher, currentContext[ContinuationInterceptor] as CoroutineContext) expect(1) emit(1) expect(2) emit(2) expect(3) - }.flowOn(flowDispatcher) + } + .flowOn(flowDispatcher) .collect { value -> - assertEquals(collectContext, coroutineContext) + assertEquals(collectContext.minusKey(Job), currentContext.minusKey(Job)) if (value == 1) expect(4) else expect(5) } @@ -32,72 +33,84 @@ class FlowContextOptimizationsTest : TestBase() { } @Test - fun testFusable() = runTest { + fun testFusedSameContext() = runTest { flow { expect(1) emit(1) expect(3) emit(2) expect(5) - }.flowOn(coroutineContext.minusKey(Job)) + } + .flowOn(currentContext.minusKey(Job)) .collect { value -> if (value == 1) expect(2) else expect(4) } - finish(6) } @Test - fun testFusableWithIntermediateOperators() = runTest { + fun testFusedSameContextWithIntermediateOperators() = runTest { flow { expect(1) emit(1) expect(3) emit(2) expect(5) - }.flowOn(coroutineContext.minusKey(Job)) + } + .flowOn(currentContext.minusKey(Job)) .map { it } - .flowOn(coroutineContext.minusKey(Job)) + .flowOn(currentContext.minusKey(Job)) .collect { value -> if (value == 1) expect(2) else expect(4) } - finish(6) } @Test - fun testNotFusableWithContext() = runTest { + fun testFusedSameDispatcher() = runTest { flow { + assertEquals("Name", currentContext[CoroutineName]?.name) expect(1) emit(1) - expect(2) - emit(2) expect(3) - }.flowOn(coroutineContext.minusKey(Job) + CoroutineName("Name")) + emit(2) + expect(5) + } + .flowOn(CoroutineName("Name")) .collect { value -> - if (value == 1) expect(4) - else expect(5) + assertEquals(null, currentContext[CoroutineName]?.name) + if (value == 1) expect(2) + else expect(4) } - finish(6) } @Test - fun testFusableFlowWith() = runTest { + fun testFusedManySameDispatcher() = runTest { flow { + assertEquals("Name1", currentContext[CoroutineName]?.name) + assertEquals("OK", currentContext[CustomContextElement]?.str) expect(1) emit(1) - expect(4) - }.flowWith(coroutineContext.minusKey(Job)) { - onEach { value -> - expect(2) - } - }.collect { expect(3) + emit(2) + expect(5) } + .flowOn(CoroutineName("Name1")) // the first one works + .flowOn(CoroutineName("Name2")) + .flowOn(CoroutineName("Name3") + CustomContextElement("OK")) // but this is not lost + .collect { value -> + assertEquals(null, currentContext[CoroutineName]?.name) + assertEquals(null, currentContext[CustomContextElement]?.str) + if (value == 1) expect(2) + else expect(4) + } + finish(6) + } - finish(5) + data class CustomContextElement(val str: String) : AbstractCoroutineContextElement(Key) { + companion object Key : CoroutineContext.Key } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt index 004827d2e7..cd8af1d044 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowContextTest.kt @@ -150,8 +150,5 @@ class FlowContextTest : TestBase() { val flow = emptyFlow() assertFailsWith { flow.flowOn(Job()) } assertFailsWith { flow.flowWith(Job()) { this } } - assertFailsWith { flow.flowOn(EmptyCoroutineContext, bufferSize = -1) } - assertFailsWith { flow.flowWith(EmptyCoroutineContext, bufferSize = -1) { this } } - } } diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt index 2d16225317..ee41a0ac1a 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt @@ -107,7 +107,7 @@ public interface ActorScope : CoroutineScope, ReceiveChannel { @ObsoleteCoroutinesApi public fun CoroutineScope.actor( context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = 0, + capacity: Int = 0, // todo: Maybe Channel.DEFAULT here? start: CoroutineStart = CoroutineStart.DEFAULT, onCompletion: CompletionHandler? = null, block: suspend ActorScope.() -> Unit diff --git a/kotlinx-coroutines-core/jvm/test/flow/CallbackFlowTest.kt b/kotlinx-coroutines-core/jvm/test/flow/CallbackFlowTest.kt index 0a66ae66a2..e2b64a88a5 100644 --- a/kotlinx-coroutines-core/jvm/test/flow/CallbackFlowTest.kt +++ b/kotlinx-coroutines-core/jvm/test/flow/CallbackFlowTest.kt @@ -39,7 +39,7 @@ class CallbackFlowTest : TestBase() { runCatching { it.offer(++i) } } - val flow = channelFlow(16) { + val flow = channelFlow { api.start(channel) awaitClose { api.stop() diff --git a/kotlinx-coroutines-core/jvm/test/flow/FlatMapStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/FlatMapStressTest.kt index e2c709a35b..9092a18083 100644 --- a/kotlinx-coroutines-core/jvm/test/flow/FlatMapStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/flow/FlatMapStressTest.kt @@ -36,14 +36,14 @@ class FlatMapStressTest : TestBase() { withContext(Dispatchers.Default) { val inFlightElements = AtomicLong(0L) var result = 0L - (1..iterations step 4).asFlow().flatMapMerge(bufferSize = bufferSize) { value -> + (1..iterations step 4).asFlow().flatMapMerge { value -> unsafeFlow { repeat(4) { emit(value + it) inFlightElements.incrementAndGet() } } - }.collect { value -> + }.buffer(bufferSize).collect { value -> val inFlight = inFlightElements.get() assertTrue(inFlight <= bufferSize + 1, "Expected less in flight elements than ${bufferSize + 1}, but had $inFlight") From 3971df30db8cb10123641b2fb029fcc0075ac409 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 3 Jun 2019 18:15:42 +0300 Subject: [PATCH 45/56] Rename flow ChannelFlow.kt file to Channels.kt --- .../common/src/flow/{ChannelFlow.kt => Channels.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename kotlinx-coroutines-core/common/src/flow/{ChannelFlow.kt => Channels.kt} (100%) diff --git a/kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt similarity index 100% rename from kotlinx-coroutines-core/common/src/flow/ChannelFlow.kt rename to kotlinx-coroutines-core/common/src/flow/Channels.kt From db52e978ba64260396590537e4c710894b292b60 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 4 Jun 2019 22:02:37 +0300 Subject: [PATCH 46/56] Flow.conflate operator --- .../kotlinx-coroutines-core.txt | 1 + .../common/src/flow/operators/Context.kt | 42 +++++++++++++++++-- .../common/test/flow/operators/BufferTest.kt | 1 - .../test/flow/operators/ConflateTest.kt | 27 ++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/ConflateTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index aa0faba080..8ff82c508c 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -812,6 +812,7 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun combineLatest (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function5;)Lkotlinx/coroutines/flow/Flow; public static final fun combineLatest (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function6;)Lkotlinx/coroutines/flow/Flow; public static final fun combineLatest (Lkotlinx/coroutines/flow/Flow;[Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun conflate (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun debounce (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 0a04493de4..15b892e669 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -94,6 +94,11 @@ import kotlin.jvm.* * } * ``` * + * ### Conflation + * + * Usage of this function with [capacity] of [Channel.CONFLATED][Channel.CONFLATED] is provided as a shortcut via + * [conflate] operator. See its documentation for details. + * * @param capacity type/capacity of the buffer between coroutines. Allowed values are the same as in `Channel(...)` * factory function: [BUFFERED][Channel.BUFFERED] (by default), [CONFLATED][Channel.CONFLATED], * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating @@ -110,9 +115,40 @@ public fun Flow.buffer(capacity: Int = BUFFERED): Flow { ChannelFlowOperatorImpl(this, capacity = capacity) } -// todo: conflate would be a useful operator only when Channel.CONFLATE is changed to always deliver the last send value -//@FlowPreview -//public fun Flow.conflate(): Flow = buffer(CONFLATED) +/** + * Conflates flow emissions via conflated channel and runs collector in a separate coroutine. + * The effect of this is that emitter is never suspended due to a slow collector, but collector + * always gets the most recent value emitted. + * + * For example, consider the flow that emits integers from 1 to 30 with 100 ms delay between them: + * + * ``` + * val flow = flow { + * for (i in 1..30) { + * delay(100) + * emit(i) + * } + * } + * ``` + * + * Applying `conflate()` operator to it allows a collector that delays 1 second on each element to get + * integers 1, 10, 20, 30: + * + * ``` + * val result = flow.conflate().onEach { delay(1000) }.toList() + * assertEquals(listOf(1, 10, 20, 30), result) + * ``` + * + * Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED]. + * + * ### Operator fusion + * + * Adjacent applications of `conflate`/[buffer], [channelFlow], [flowOn], [produceIn], and [broadcastIn] are + * always fused so that only one properly configured channel is used for execution. + * **Conflation takes precedence over `buffer()` calls with any other capacity.** + */ +@FlowPreview +public fun Flow.conflate(): Flow = buffer(CONFLATED) /** * The operator that changes the context where this flow is executed to the given [context]. diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt index 045afe99cf..65fef02ca4 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt @@ -164,7 +164,6 @@ class BufferTest : TestBase() { } @Test - @Ignore // todo: conflated behavior is pretty useless right now, because closing channel overwrites last value fun testConflate() = runTest { expect(1) // emit all and conflate / then collect first & last diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ConflateTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ConflateTest.kt new file mode 100644 index 0000000000..264aba081c --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/ConflateTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.operators + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class ConflateTest : TestBase() { + @Test // from example + fun testExample() = withVirtualTime { + expect(1) + val flow = flow { + for (i in 1..30) { + delay(100) + emit(i) + } + } + val result = flow.conflate().onEach { + delay(1000) + }.toList() + assertEquals(listOf(1, 10, 20, 30), result) + finish(2) + } +} \ No newline at end of file From e2a5671636b4d7e5d5352ef821c6143f3ced4b82 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 5 Jun 2019 18:40:18 +0300 Subject: [PATCH 47/56] Flow scope (#1227) * Introducing flowScope, builder necessary for creating cancellation-transparent flow operators * Incorporate flow scope into flow operators Fixes #1218 Fixes #1128 --- .../kotlinx-coroutines-core.txt | 2 +- .../common/src/Exceptions.common.kt | 2 +- .../common/src/JobSupport.kt | 70 +++++++++----- kotlinx-coroutines-core/common/src/Timeout.kt | 4 +- .../common/src/channels/Produce.kt | 2 +- .../common/src/flow/Builders.kt | 2 +- .../common/src/flow/Migration.kt | 1 - .../common/src/flow/internal/ChannelFlow.kt | 4 +- .../common/src/flow/internal/FlowCoroutine.kt | 89 ++++++++++++++++++ ...ion.common.kt => FlowExceptions.common.kt} | 5 + .../common/src/flow/operators/Delay.kt | 92 +++++++++---------- .../common/src/flow/operators/Merge.kt | 23 +++-- .../common/src/internal/Scopes.kt | 5 +- .../common/test/SupervisorTest.kt | 18 ++++ .../test/flow/channels/ChannelFlowTest.kt | 59 ++++++++++++ .../test/flow/channels/FlowCallbackTest.kt | 1 - .../test/flow/internal/FlowScopeTest.kt | 77 ++++++++++++++++ .../test/flow/operators/CombineLatestTest.kt | 40 ++++++++ .../test/flow/operators/DebounceTest.kt | 11 ++- .../test/flow/operators/FlatMapMergeTest.kt | 37 ++++++++ .../common/test/flow/operators/FlowOnTest.kt | 27 ++++++ .../test/flow/operators/FlowWithTest.kt | 29 ++++++ .../common/test/flow/operators/SampleTest.kt | 12 ++- .../test/flow/operators/SwitchMapTest.kt | 6 ++ .../common/test/flow/operators/ZipTest.kt | 50 +++++++++- kotlinx-coroutines-core/js/src/Exceptions.kt | 2 - ...bortFlowException.kt => FlowExceptions.kt} | 1 + kotlinx-coroutines-core/jvm/src/Builders.kt | 3 +- kotlinx-coroutines-core/jvm/src/Exceptions.kt | 4 +- .../jvm/src/channels/Actor.kt | 1 - .../src/flow/internal/AbortFlowException.kt | 11 --- .../jvm/src/flow/internal/FlowExceptions.kt | 21 +++++ .../native/src/Builders.kt | 3 +- .../native/src/Exceptions.kt | 2 - ...bortFlowException.kt => FlowExceptions.kt} | 2 + 35 files changed, 589 insertions(+), 129 deletions(-) create mode 100644 kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt rename kotlinx-coroutines-core/common/src/flow/internal/{AbortFlowException.common.kt => FlowExceptions.common.kt} (71%) create mode 100644 kotlinx-coroutines-core/common/test/flow/internal/FlowScopeTest.kt rename kotlinx-coroutines-core/js/src/flow/internal/{AbortFlowException.kt => FlowExceptions.kt} (72%) delete mode 100644 kotlinx-coroutines-core/jvm/src/flow/internal/AbortFlowException.kt create mode 100644 kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt rename kotlinx-coroutines-core/native/src/flow/internal/{AbortFlowException.kt => FlowExceptions.kt} (72%) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 8ff82c508c..ab51a2dd72 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -381,7 +381,6 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public final fun getCancellationException ()Ljava/util/concurrent/CancellationException; - protected fun getCancelsParent ()Z public fun getChildJobCancellationCause ()Ljava/util/concurrent/CancellationException; public final fun getChildren ()Lkotlin/sequences/Sequence; protected final fun getCompletionCause ()Ljava/lang/Throwable; @@ -396,6 +395,7 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public final fun isCancelled ()Z public final fun isCompleted ()Z public final fun isCompletedExceptionally ()Z + protected fun isScopedCoroutine ()Z public final fun join (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; protected fun onCancelling (Ljava/lang/Throwable;)V diff --git a/kotlinx-coroutines-core/common/src/Exceptions.common.kt b/kotlinx-coroutines-core/common/src/Exceptions.common.kt index e8c2f5e1db..62b6ba4d51 100644 --- a/kotlinx-coroutines-core/common/src/Exceptions.common.kt +++ b/kotlinx-coroutines-core/common/src/Exceptions.common.kt @@ -23,7 +23,7 @@ internal expect class JobCancellationException( internal val job: Job } -internal expect class CoroutinesInternalError(message: String, cause: Throwable) : Error +internal class CoroutinesInternalError(message: String, cause: Throwable) : Error(message, cause) internal expect fun Throwable.addSuppressedThrowable(other: Throwable) // For use in tests diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index 4963c37bed..d8b6b92d62 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -319,6 +319,31 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren cancelParent(cause) // tentative cancellation -- does not matter if there is no parent } + /** + * The method that is invoked when the job is cancelled to possibly propagate cancellation to the parent. + * Returns `true` if the parent is responsible for handling the exception, `false` otherwise. + * + * Invariant: never returns `false` for instances of [CancellationException], otherwise such exception + * may leak to the [CoroutineExceptionHandler]. + */ + private fun cancelParent(cause: Throwable): Boolean { + /* CancellationException is considered "normal" and parent usually is not cancelled when child produces it. + * This allow parent to cancel its children (normally) without being cancelled itself, unless + * child crashes and produce some other exception during its completion. + */ + val isCancellation = cause is CancellationException + val parent = parentHandle + // No parent -- ignore CE, report other exceptions. + if (parent === null || parent === NonDisposableHandle) { + return isCancellation + } + + // Is scoped coroutine -- don't propagate, will be rethrown + if (isScopedCoroutine) return isCancellation + // Notify parent but don't forget to check cancellation + return parent.childCancelled(cause) || isCancellation + } + private fun NodeList.notifyCompletion(cause: Throwable?) = notifyHandlers>(this, cause) @@ -594,21 +619,29 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren cancelImpl(parentJob) } - // Child was cancelled with cause - // It is overridden in supervisor implementations to ignore child cancellation - public open fun childCancelled(cause: Throwable): Boolean = - cancelImpl(cause) && handlesException + /** + * Child was cancelled with a cause. + * In this method parent decides whether it cancels itself (e.g. on a critical failure) and whether it handles the exception of the child. + * It is overridden in supervisor implementations to completely ignore any child cancellation. + * Returns `true` if exception is handled, `false` otherwise (then caller is responsible for handling an exception) + * + * Invariant: never returns `false` for instances of [CancellationException], otherwise such exception + * may leak to the [CoroutineExceptionHandler]. + */ + public open fun childCancelled(cause: Throwable): Boolean { + if (cause is CancellationException) return true + return cancelImpl(cause) && handlesException + } /** * Makes this [Job] cancelled with a specified [cause]. * It is used in [AbstractCoroutine]-derived classes when there is an internal failure. */ - public fun cancelCoroutine(cause: Throwable?) = - cancelImpl(cause) + public fun cancelCoroutine(cause: Throwable?) = cancelImpl(cause) // cause is Throwable or ParentJob when cancelChild was invoked // returns true is exception was handled, false otherwise - private fun cancelImpl(cause: Any?): Boolean { + internal fun cancelImpl(cause: Any?): Boolean { if (onCancelComplete) { // make sure it is completing, if cancelMakeCompleting returns true it means it had make it // completing and had recorded exception @@ -912,14 +945,12 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren protected open fun onCancelling(cause: Throwable?) {} /** - * When this function returns `true` the parent is cancelled on cancellation of this job. - * Note that [CancellationException] is considered "normal" and parent is not cancelled when child produces it. - * This allows parent to cancel its children (normally) without being cancelled itself, unless - * child crashes and produce some other exception during its completion. - * - * @suppress **This is unstable API and it is subject to change.* + * Returns `true` for scoped coroutines. + * Scoped coroutine is a coroutine that is executed sequentially within the enclosing scope without any concurrency. + * Scoped coroutines always handle any exception happened within -- they just rethrow it to the enclosing scope. + * Examples of scoped coroutines are `coroutineScope`, `withTimeout` and `runBlocking`. */ - protected open val cancelsParent: Boolean get() = true + protected open val isScopedCoroutine: Boolean get() = false /** * Returns `true` for jobs that handle their exceptions or integrate them into the job's result via [onCompletionInternal]. @@ -939,20 +970,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren * * This method is invoked **exactly once** when the final exception of the job is determined * and before it becomes complete. At the moment of invocation the job and all its children are complete. - * - * @suppress **This is unstable API and it is subject to change.* */ protected open fun handleJobException(exception: Throwable): Boolean = false - private fun cancelParent(cause: Throwable): Boolean { - // CancellationException is considered "normal" and parent is not cancelled when child produces it. - // This allow parent to cancel its children (normally) without being cancelled itself, unless - // child crashes and produce some other exception during its completion. - if (cause is CancellationException) return true - if (!cancelsParent) return false - return parentHandle?.childCancelled(cause) == true - } - /** * Override for completion actions that need to update some external object depending on job's state, * right before all the waiters for coroutine's completion are notified. diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 3c902db9a8..8bfaf336fe 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -85,9 +85,7 @@ private open class TimeoutCoroutine( override val defaultResumeMode: Int get() = MODE_DIRECT override val callerFrame: CoroutineStackFrame? get() = (uCont as? CoroutineStackFrame) override fun getStackTraceElement(): StackTraceElement? = null - - override val cancelsParent: Boolean - get() = false // it throws exception to parent instead of cancelling it + override val isScopedCoroutine: Boolean get() = true @Suppress("LeakingThis", "Deprecation") override fun run() { diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index d7e01aba0b..9e34773c57 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -126,7 +126,7 @@ public fun CoroutineScope.produce( return coroutine } -private class ProducerCoroutine( +internal open class ProducerCoroutine( parentContext: CoroutineContext, channel: Channel ) : ChannelCoroutine(parentContext, channel, active = true), ProducerScope { override val isActive: Boolean diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 6147b65202..b4ff26de80 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -313,7 +313,7 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() public inline fun callbackFlow(@BuilderInference noinline block: suspend ProducerScope.() -> Unit): Flow = channelFlow(block) -// ChannelFlow implementation that is the first in the chain of flow operations and introduces (builds) a flow +// ChannelFlow implementation that is the first in the chain of flow operations and introduces (builds) a flow private class ChannelFlowBuilder( private val block: suspend ProducerScope.() -> Unit, context: CoroutineContext = EmptyCoroutineContext, diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 77beb3779c..bf20d2f2a2 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -118,7 +118,6 @@ public fun Flow.onErrorResume(fallback: Flow): Flow = error("Should @Deprecated(message = "withContext in flow body is deprecated, use flowOn instead", level = DeprecationLevel.ERROR) public fun FlowCollector.withContext(context: CoroutineContext, block: suspend () -> R): Unit = error("Should not be called") - /** * `subscribe` is Rx-specific API that has no direct match in flows. * One can use `launch` instead, for example the following: diff --git a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt index 30005afa5d..57a0132ff5 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt @@ -63,10 +63,10 @@ internal abstract class ChannelFlow( scope.broadcast(context, produceCapacity, start, block = collectToFun) fun produceImpl(scope: CoroutineScope): ReceiveChannel = - scope.produce(context, produceCapacity, block = collectToFun) + scope.flowProduce(context, produceCapacity, block = collectToFun) override suspend fun collect(collector: FlowCollector) = - coroutineScope { // todo: flowScope + coroutineScope { val channel = produceImpl(this) channel.consumeEach { collector.emit(it) } } diff --git a/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt b/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt new file mode 100644 index 0000000000..98f5cec597 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/FlowCoroutine.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.* +import kotlinx.coroutines.intrinsics.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* +import kotlinx.coroutines.flow.unsafeFlow as flow + +/** + * Creates a [CoroutineScope] and calls the specified suspend block with this scope. + * This builder is similar to [coroutineScope] with the only exception that it *ties* lifecycle of children + * and itself regarding the cancellation, thus being cancelled when one of the children becomes cancelled. + * + * For example: + * ``` + * flowScope { + * launch { + * throw CancellationException() + * } + * } // <- CE will be rethrown here + * ``` + */ +internal suspend fun flowScope(@BuilderInference block: suspend CoroutineScope.() -> R): R = + suspendCoroutineUninterceptedOrReturn { uCont -> + val coroutine = FlowCoroutine(uCont.context, uCont) + coroutine.startUndispatchedOrReturn(coroutine, block) + } + +/** + * Creates a flow that also provides a [CoroutineScope] for each collector + * Shorthand for: + * ``` + * flow { + * flowScope { + * ... + * } + * } + * ``` + * with additional constraint on cancellation. + * To cancel child without cancelling itself, `cancel(ChildCancelledException())` should be used. + */ +internal fun scopedFlow(@BuilderInference block: suspend CoroutineScope.(FlowCollector) -> Unit): Flow = + flow { + val collector = this + flowScope { block(collector) } + } + +/* + * Shortcut for produce { flowScope {block() } } + */ +internal fun CoroutineScope.flowProduce( + context: CoroutineContext, + capacity: Int = 0, @BuilderInference block: suspend ProducerScope.() -> Unit +): ReceiveChannel { + val channel = Channel(capacity) + val newContext = newCoroutineContext(context) + val coroutine = FlowProduceCoroutine(newContext, channel) + coroutine.start(CoroutineStart.DEFAULT, coroutine, block) + return coroutine +} + +private class FlowCoroutine( + context: CoroutineContext, + uCont: Continuation +) : ScopeCoroutine(context, uCont) { + + public override fun childCancelled(cause: Throwable): Boolean { + if (cause is ChildCancelledException) return true + return cancelImpl(cause) + } +} + +private class FlowProduceCoroutine( + parentContext: CoroutineContext, + channel: Channel +) : ProducerCoroutine(parentContext, channel) { + + public override fun childCancelled(cause: Throwable): Boolean { + if (cause is ChildCancelledException) return true + return cancelImpl(cause) + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbortFlowException.common.kt b/kotlinx-coroutines-core/common/src/flow/internal/FlowExceptions.common.kt similarity index 71% rename from kotlinx-coroutines-core/common/src/flow/internal/AbortFlowException.common.kt rename to kotlinx-coroutines-core/common/src/flow/internal/FlowExceptions.common.kt index 6d5a4b4d4d..6c675b33eb 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/AbortFlowException.common.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/FlowExceptions.common.kt @@ -11,3 +11,8 @@ import kotlinx.coroutines.* * This exception should never escape outside of operator's implementation. */ internal expect class AbortFlowException() : CancellationException + +/** + * Exception used to cancel child of [scopedFlow] without cancelling the whole scope. + */ +internal expect class ChildCancelledException() : CancellationException diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 32e9b3f3b7..4db30440f6 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -60,34 +60,33 @@ public fun Flow.delayEach(timeMillis: Long): Flow = flow { */ public fun Flow.debounce(timeoutMillis: Long): Flow { require(timeoutMillis > 0) { "Debounce timeout should be positive" } - return flow { - coroutineScope { - val values = Channel(Channel.CONFLATED) // Actually Any, KT-30796 - // Channel is not closed deliberately as there is no close with value - val collector = async { - collect { value -> values.send(value ?: NULL) } - } + return scopedFlow { downstream -> + val values = Channel(Channel.CONFLATED) // Actually Any, KT-30796 + // Channel is not closed deliberately as there is no close with value + val collector = async { + collect { value -> values.send(value ?: NULL) } + } - var isDone = false - var lastValue: Any? = null - while (!isDone) { - select { - values.onReceive { - lastValue = it - } + var isDone = false + var lastValue: Any? = null + while (!isDone) { + select { + values.onReceive { + lastValue = it + } - lastValue?.let { value -> // set timeout when lastValue != null - onTimeout(timeoutMillis) { - lastValue = null // Consume the value - emit(NULL.unbox(value)) - } + lastValue?.let { value -> + // set timeout when lastValue != null + onTimeout(timeoutMillis) { + lastValue = null // Consume the value + downstream.emit(NULL.unbox(value)) } + } - // Close with value 'idiom' - collector.onAwait { - if (lastValue != null) emit(NULL.unbox(lastValue)) - isDone = true - } + // Close with value 'idiom' + collector.onAwait { + if (lastValue != null) downstream.emit(NULL.unbox(lastValue)) + isDone = true } } } @@ -112,32 +111,31 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { */ public fun Flow.sample(periodMillis: Long): Flow { require(periodMillis > 0) { "Sample period should be positive" } - return flow { - coroutineScope { - val values = produce(capacity = Channel.CONFLATED) { // Actually Any, KT-30796 - collect { value -> send(value ?: NULL) } - } + return scopedFlow { downstream -> + val values = produce(capacity = Channel.CONFLATED) { + // Actually Any, KT-30796 + collect { value -> send(value ?: NULL) } + } - var isDone = false - var lastValue: Any? = null - val ticker = fixedPeriodTicker(periodMillis) - while (!isDone) { - select { - values.onReceiveOrNull { - if (it == null) { - ticker.cancel() - isDone = true - } else { - lastValue = it - } + var isDone = false + var lastValue: Any? = null + val ticker = fixedPeriodTicker(periodMillis) + while (!isDone) { + select { + values.onReceiveOrNull { + if (it == null) { + ticker.cancel(ChildCancelledException()) + isDone = true + } else { + lastValue = it } + } - // todo: shall be start sampling only when an element arrives or sample aways as here? - ticker.onReceive { - val value = lastValue ?: return@onReceive - lastValue = null // Consume the value - emit(NULL.unbox(value)) - } + // todo: shall be start sampling only when an element arrives or sample aways as here? + ticker.onReceive { + val value = lastValue ?: return@onReceive + lastValue = null // Consume the value + downstream.emit(NULL.unbox(value)) } } } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index f7a644710f..0fa6e8abd4 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -129,17 +129,16 @@ public fun Flow>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY * produces `aa bb b_last` */ @FlowPreview -public fun Flow.switchMap(transform: suspend (value: T) -> Flow): Flow = flow { - coroutineScope { - var previousFlow: Job? = null - collect { value -> - // Linearize calls to emit as alternative to the channel. Bonus points for never-overlapping channels. - previousFlow?.cancelAndJoin() - // Undispatched to have better user experience in case of synchronous flows - previousFlow = launch(start = CoroutineStart.UNDISPATCHED) { - transform(value).collect { innerValue -> - emit(innerValue) - } +public fun Flow.switchMap(transform: suspend (value: T) -> Flow): Flow = scopedFlow { downstream -> + var previousFlow: Job? = null + collect { value -> + // Linearize calls to emit as alternative to the channel. Bonus points for never-overlapping channels. + previousFlow?.cancel(ChildCancelledException()) + previousFlow?.join() + // Undispatched to have better user experience in case of synchronous flows + previousFlow = launch(start = CoroutineStart.UNDISPATCHED) { + transform(value).collect { innerValue -> + downstream.emit(innerValue) } } } @@ -175,7 +174,7 @@ private class ChannelFlowMerge( override suspend fun flowCollect(collector: FlowCollector) { // this function should not have been invoked when channel was explicitly requested check(capacity == OPTIONAL_CHANNEL) - coroutineScope { // todo: flowScope + flowScope { mergeImpl(this, collector.asConcurrentFlowCollector()) } } diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index 3361694481..9197ec83c0 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -17,13 +17,12 @@ internal open class ScopeCoroutine( ) : AbstractCoroutine(context, true), CoroutineStackFrame { final override val callerFrame: CoroutineStackFrame? get() = uCont as CoroutineStackFrame? final override fun getStackTraceElement(): StackTraceElement? = null + final override val isScopedCoroutine: Boolean get() = true + override val defaultResumeMode: Int get() = MODE_DIRECT internal val parent: Job? get() = parentContext[Job] - override val cancelsParent: Boolean - get() = false // it throws exception to parent instead of cancelling it - @Suppress("UNCHECKED_CAST") override fun afterCompletionInternal(state: Any?, mode: Int) { if (state is CompletedExceptionally) { diff --git a/kotlinx-coroutines-core/common/test/SupervisorTest.kt b/kotlinx-coroutines-core/common/test/SupervisorTest.kt index fae7091851..535073e046 100644 --- a/kotlinx-coroutines-core/common/test/SupervisorTest.kt +++ b/kotlinx-coroutines-core/common/test/SupervisorTest.kt @@ -219,4 +219,22 @@ class SupervisorTest : TestBase() { yield() // to coroutineScope finish(7) } + + @Test + fun testSupervisorJobCancellationException() = runTest { + val job = SupervisorJob() + val child = launch(job + CoroutineExceptionHandler { _, _ -> expectUnreached() }) { + expect(1) + hang { + expect(3) + } + } + + yield() + expect(2) + child.cancelAndJoin() + job.complete() + job.join() + finish(4) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt index 0ae30e80ce..a77f8fafe5 100644 --- a/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/channels/ChannelFlowTest.kt @@ -81,4 +81,63 @@ class ChannelFlowTest : TestBase() { assertFailsWith(flow) finish(4) } + + @Test + fun testMergeOneCoroutineWithCancellation() = runTest { + val flow = flowOf(1, 2, 3) + val f = flow.mergeOneCoroutine(flow).take(2) + assertEquals(listOf(1, 1), f.toList()) + } + + @Test + fun testMergeTwoCoroutinesWithCancellation() = runTest { + val flow = flowOf(1, 2, 3) + val f = flow.mergeTwoCoroutines(flow).take(2) + assertEquals(listOf(1, 1), f.toList()) + } + + private fun Flow.mergeTwoCoroutines(other: Flow): Flow = channelFlow { + launch { + collect { send(it); yield() } + } + launch { + other.collect { send(it) } + } + } + + private fun Flow.mergeOneCoroutine(other: Flow): Flow = channelFlow { + launch { + collect { send(it); yield() } + } + + other.collect { send(it); yield() } + } + + @Test + fun testBufferWithTimeout() = runTest { + fun Flow.bufferWithTimeout(): Flow = channelFlow { + expect(2) + launch { + expect(3) + hang { + expect(5) + } + } + launch { + expect(4) + collect { + withTimeout(-1) { + send(it) + } + expectUnreached() + } + expectUnreached() + } + } + + val flow = flowOf(1, 2, 3).bufferWithTimeout() + expect(1) + assertFailsWith(flow) + finish(6) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/channels/FlowCallbackTest.kt b/kotlinx-coroutines-core/common/test/flow/channels/FlowCallbackTest.kt index d992d06e48..a6b5340555 100644 --- a/kotlinx-coroutines-core/common/test/flow/channels/FlowCallbackTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/channels/FlowCallbackTest.kt @@ -45,4 +45,3 @@ class FlowCallbackTest : TestBase() { finish(3) } } - diff --git a/kotlinx-coroutines-core/common/test/flow/internal/FlowScopeTest.kt b/kotlinx-coroutines-core/common/test/flow/internal/FlowScopeTest.kt new file mode 100644 index 0000000000..d41ab8893f --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/internal/FlowScopeTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.coroutines.* +import kotlin.test.* + +class FlowScopeTest : TestBase() { + + @Test + fun testCancellation() = runTest { + assertFailsWith { + flowScope { + expect(1) + val child = launch { + expect(3) + hang { expect(5) } + } + expect(2) + yield() + expect(4) + child.cancel() + } + } + finish(6) + } + + @Test + fun testCancellationWithChildCancelled() = runTest { + flowScope { + expect(1) + val child = launch { + expect(3) + hang { expect(5) } + } + expect(2) + yield() + expect(4) + child.cancel(ChildCancelledException()) + } + finish(6) + } + + @Test + fun testCancellationWithSuspensionPoint() = runTest { + assertFailsWith { + flowScope { + expect(1) + val child = launch { + expect(3) + hang { expect(6) } + } + expect(2) + yield() + expect(4) + child.cancel() + hang { expect(5) } + } + } + finish(7) + } + + @Test + fun testNestedScopes() = runTest { + assertFailsWith { + flowScope { + flowScope { + launch { + throw CancellationException(null) + } + } + } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CombineLatestTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CombineLatestTest.kt index bda9927c79..54244f05db 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CombineLatestTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CombineLatestTest.kt @@ -197,6 +197,46 @@ abstract class CombineLatestTestBase : TestBase() { assertFailsWith(flow) finish(2) } + + @Test + fun testCancellationExceptionUpstream() = runTest { + val f1 = flow { + expect(1) + emit(1) + throw CancellationException("") + } + val f2 = flow { + emit(1) + hang { expect(3) } + } + + val flow = f1.combineLatest(f2, { _, _ -> 1 }).onEach { expect(2) } + assertFailsWith(flow) + finish(4) + } + + @Test + fun testCancellationExceptionDownstream() = runTest { + val f1 = flow { + emit(1) + expect(2) + hang { expect(5) } + } + val f2 = flow { + emit(1) + expect(3) + hang { expect(6) } + } + + val flow = f1.combineLatest(f2, { _, _ -> 1 }).onEach { + expect(1) + yield() + expect(4) + throw CancellationException("") + } + assertFailsWith(flow) + finish(7) + } } class CombineLatestTest : CombineLatestTestBase() { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index 607d4cd661..2a6e9c1238 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -95,20 +95,25 @@ class DebounceTest : TestBase() { } @Test - fun testUpstreamError() = runTest { + fun testUpstreamError()= testUpstreamError(TimeoutCancellationException("")) + + @Test + fun testUpstreamErrorCancellation() = testUpstreamError(TimeoutCancellationException("")) + + private inline fun testUpstreamError(cause: T) = runTest { val latch = Channel() val flow = flow { expect(1) emit(1) expect(2) latch.receive() - throw TestException() + throw cause }.debounce(1).map { latch.send(Unit) hang { expect(3) } } - assertFailsWith(flow) + assertFailsWith(flow) finish(4) } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt index 5d007c33a7..6069ae6d2a 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlatMapMergeTest.kt @@ -36,4 +36,41 @@ class FlatMapMergeTest : FlatMapMergeBaseTest() { consumer.cancelAndJoin() finish(3) } + + @Test + fun testCancellationExceptionDownstream() = runTest { + val flow = flow { + emit(1) + hang { expect(2) } + }.flatMapMerge { + flow { + emit(it) + expect(1) + throw CancellationException("") + } + } + + assertFailsWith(flow) + finish(3) + } + + @Test + fun testCancellationExceptionUpstream() = runTest { + val flow = flow { + expect(1) + emit(1) + expect(2) + yield() + throw CancellationException("") + }.flatMapMerge { + flow { + expect(3) + emit(it) + hang { expect(4) } + } + } + + assertFailsWith(flow) + finish(5) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt index 49df21d576..4adc35415e 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt @@ -234,6 +234,33 @@ class FlowOnTest : TestBase() { finish(6) } + @Test + fun testTimeoutExceptionUpstream() = runTest { + val flow = flow { + emit(1) + yield() + withTimeout(-1) {} + emit(42) + }.flowOn(NamedDispatchers("foo")).onEach { + expect(1) + } + assertFailsWith(flow) + finish(2) + } + + @Test + fun testTimeoutExceptionDownstream() = runTest { + val flow = flow { + emit(1) + hang { expect(2) } + }.flowOn(NamedDispatchers("foo")).onEach { + expect(1) + withTimeout(-1) {} + } + assertFailsWith(flow) + finish(3) + } + private inner class Source(private val value: Int) { public var contextName: String = "unknown" diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt index 055f84741c..a785814206 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowWithTest.kt @@ -199,4 +199,33 @@ class FlowWithTest : TestBase() { ensureActive() finish(5) } + + @Test + fun testTimeoutException() = runTest { + val flow = flow { + emit(1) + yield() + withTimeout(-1) {} + emit(42) + }.flowWith(NamedDispatchers("foo")) { + onEach { expect(1) } + } + assertFailsWith(flow) + finish(2) + } + + @Test + fun testTimeoutExceptionDownstream() = runTest { + val flow = flow { + emit(1) + hang { expect(2) } + }.flowWith(NamedDispatchers("foo")) { + onEach { + expect(1) + withTimeout(-1) {} + } + } + assertFailsWith(flow) + finish(3) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt index e77b128f76..9c96352df2 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/SampleTest.kt @@ -169,20 +169,25 @@ class SampleTest : TestBase() { } @Test - fun testUpstreamError() = runTest { + fun testUpstreamError() = testUpstreamError(TestException()) + + @Test + fun testUpstreamErrorCancellationException() = testUpstreamError(CancellationException("")) + + private inline fun testUpstreamError(cause: T) = runTest { val latch = Channel() val flow = flow { expect(1) emit(1) expect(2) latch.receive() - throw TestException() + throw cause }.sample(1).map { latch.send(Unit) hang { expect(3) } } - assertFailsWith(flow) + assertFailsWith(flow) finish(4) } @@ -219,7 +224,6 @@ class SampleTest : TestBase() { finish(3) } - @Test fun testUpstreamErrorSampleNotTriggeredInIsolatedContext() = runTest { val flow = flow { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/SwitchMapTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/SwitchMapTest.kt index 933bb1628e..fabca72c70 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/SwitchMapTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/SwitchMapTest.kt @@ -113,4 +113,10 @@ class SwitchMapTest : TestBase() { assertFailsWith(flow) finish(5) } + + @Test + fun testTake() = runTest { + val flow = flowOf(1, 2, 3, 4, 5).switchMap { flowOf(it) } + assertEquals(listOf(1), flow.take(1).toList()) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt index decd2307c6..b28320c391 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt @@ -93,7 +93,7 @@ class ZipTest : TestBase() { } @Test - fun testCancesWhenFlowIsDone2() = runTest { + fun testCancelWhenFlowIsDone2() = runTest { val f1 = flow { emit("1") emit("2") @@ -189,4 +189,52 @@ class ZipTest : TestBase() { assertFailsWith(flow) finish(2) } + + @Test + fun testCancellationUpstream() = runTest { + val f1 = flow { + expect(1) + emit(1) + yield() + expect(4) + throw CancellationException("") + } + + val f2 = flow { + expect(2) + emit(1) + expect(5) + hang { expect(6) } + } + + val flow = f1.zip(f2, { _, _ -> 1 }).onEach { expect(3) } + assertFailsWith(flow) + finish(7) + } + + @Test + fun testCancellationDownstream() = runTest { + val f1 = flow { + expect(1) + emit(1) + yield() + expect(4) + hang { expect(6) } + } + + val f2 = flow { + expect(2) + emit(1) + expect(5) + hang { expect(7) } + } + + val flow = f1.zip(f2, { _, _ -> 1 }).onEach { + expect(3) + yield() + throw CancellationException("") + } + assertFailsWith(flow) + finish(8) + } } diff --git a/kotlinx-coroutines-core/js/src/Exceptions.kt b/kotlinx-coroutines-core/js/src/Exceptions.kt index 83a0cdaf90..f42704107b 100644 --- a/kotlinx-coroutines-core/js/src/Exceptions.kt +++ b/kotlinx-coroutines-core/js/src/Exceptions.kt @@ -48,8 +48,6 @@ internal actual class JobCancellationException public actual constructor( (message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0) } -internal actual class CoroutinesInternalError actual constructor(message: String, cause: Throwable) : Error(message.withCause(cause)) - @Suppress("FunctionName") internal fun IllegalStateException(message: String, cause: Throwable?) = IllegalStateException(message.withCause(cause)) diff --git a/kotlinx-coroutines-core/js/src/flow/internal/AbortFlowException.kt b/kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt similarity index 72% rename from kotlinx-coroutines-core/js/src/flow/internal/AbortFlowException.kt rename to kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt index d6a9c31eaa..8422f2bf33 100644 --- a/kotlinx-coroutines-core/js/src/flow/internal/AbortFlowException.kt +++ b/kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt @@ -7,3 +7,4 @@ package kotlinx.coroutines.flow.internal import kotlinx.coroutines.* internal actual class AbortFlowException : CancellationException("Flow was aborted, no more elements needed") +internal actual class ChildCancelledException : CancellationException("Child of the scoped flow was cancelled") diff --git a/kotlinx-coroutines-core/jvm/src/Builders.kt b/kotlinx-coroutines-core/jvm/src/Builders.kt index d8f8ee33e1..52841cd2b2 100644 --- a/kotlinx-coroutines-core/jvm/src/Builders.kt +++ b/kotlinx-coroutines-core/jvm/src/Builders.kt @@ -59,8 +59,7 @@ private class BlockingCoroutine( private val blockedThread: Thread, private val eventLoop: EventLoop? ) : AbstractCoroutine(parentContext, true) { - override val cancelsParent: Boolean - get() = false // it throws exception to parent instead of cancelling it + override val isScopedCoroutine: Boolean get() = true override fun afterCompletionInternal(state: Any?, mode: Int) { // wake up blocked thread diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt index bc7e92cadf..7a8f385e64 100644 --- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt @@ -80,8 +80,6 @@ internal actual class JobCancellationException public actual constructor( (message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0) } -internal actual class CoroutinesInternalError actual constructor(message: String, cause: Throwable) : Error(message, cause) - @Suppress("NOTHING_TO_INLINE") internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) = - addSuppressed(other) + addSuppressed(other) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt index ee41a0ac1a..ffabb99dec 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt @@ -127,7 +127,6 @@ private open class ActorCoroutine( channel: Channel, active: Boolean ) : ChannelCoroutine(parentContext, channel, active), ActorScope { - override val cancelsParent: Boolean get() = true override fun onCancelling(cause: Throwable?) { _channel.cancel(cause?.let { diff --git a/kotlinx-coroutines-core/jvm/src/flow/internal/AbortFlowException.kt b/kotlinx-coroutines-core/jvm/src/flow/internal/AbortFlowException.kt deleted file mode 100644 index 7ff34e735b..0000000000 --- a/kotlinx-coroutines-core/jvm/src/flow/internal/AbortFlowException.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow.internal - -import kotlinx.coroutines.* - -internal actual class AbortFlowException : CancellationException("Flow was aborted, no more elements needed") { - override fun fillInStackTrace(): Throwable = this -} diff --git a/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt b/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt new file mode 100644 index 0000000000..d8d4d21e6f --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.coroutines.* + +internal actual class AbortFlowException : CancellationException("Flow was aborted, no more elements needed") { + override fun fillInStackTrace(): Throwable { + if (DEBUG) super.fillInStackTrace() + return this + } +} + +internal actual class ChildCancelledException : CancellationException("Child of the scoped flow was cancelled") { + override fun fillInStackTrace(): Throwable { + if (DEBUG) super.fillInStackTrace() + return this + } +} diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt index 82fd81a08f..0dc90d556a 100644 --- a/kotlinx-coroutines-core/native/src/Builders.kt +++ b/kotlinx-coroutines-core/native/src/Builders.kt @@ -54,8 +54,7 @@ private class BlockingCoroutine( parentContext: CoroutineContext, private val eventLoop: EventLoop? ) : AbstractCoroutine(parentContext, true) { - override val cancelsParent: Boolean - get() = false // it throws exception to parent instead of cancelling it + override val isScopedCoroutine: Boolean get() = true @Suppress("UNCHECKED_CAST") fun joinBlocking(): T = memScoped { diff --git a/kotlinx-coroutines-core/native/src/Exceptions.kt b/kotlinx-coroutines-core/native/src/Exceptions.kt index 29c3ce5135..109b9100cb 100644 --- a/kotlinx-coroutines-core/native/src/Exceptions.kt +++ b/kotlinx-coroutines-core/native/src/Exceptions.kt @@ -48,8 +48,6 @@ internal actual class JobCancellationException public actual constructor( (message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0) } -internal actual class CoroutinesInternalError actual constructor(message: String, cause: Throwable) : Error(message.withCause(cause)) - @Suppress("FunctionName") internal fun IllegalStateException(message: String, cause: Throwable?) = IllegalStateException(message.withCause(cause)) diff --git a/kotlinx-coroutines-core/native/src/flow/internal/AbortFlowException.kt b/kotlinx-coroutines-core/native/src/flow/internal/FlowExceptions.kt similarity index 72% rename from kotlinx-coroutines-core/native/src/flow/internal/AbortFlowException.kt rename to kotlinx-coroutines-core/native/src/flow/internal/FlowExceptions.kt index d6a9c31eaa..4a291ea27e 100644 --- a/kotlinx-coroutines-core/native/src/flow/internal/AbortFlowException.kt +++ b/kotlinx-coroutines-core/native/src/flow/internal/FlowExceptions.kt @@ -7,3 +7,5 @@ package kotlinx.coroutines.flow.internal import kotlinx.coroutines.* internal actual class AbortFlowException : CancellationException("Flow was aborted, no more elements needed") +internal actual class ChildCancelledException : CancellationException("Child of the scoped flow was cancelled") + From d15d8d677446cb5034b82086cbab2d521cdf10b0 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 6 Jun 2019 11:35:04 +0300 Subject: [PATCH 48/56] Make FastServiceLoader compatible with Java 1.6 --- .../jvm/src/internal/FastServiceLoader.kt | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt index ecb2213e4a..f512bb31bc 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt @@ -52,18 +52,37 @@ internal object FastServiceLoader { val pathToJar = path.substringAfter("jar:file:").substringBefore('!') val entry = path.substringAfter("!/") // mind the verify = false flag! - (JarFile(pathToJar, false) as Closeable).use { file -> - BufferedReader(InputStreamReader((file as JarFile).getInputStream(ZipEntry(entry)), "UTF-8")).use { r -> + (JarFile(pathToJar, false)).use { file -> + BufferedReader(InputStreamReader(file.getInputStream(ZipEntry(entry)), "UTF-8")).use { r -> return parseFile(r) } } } - // Regular path for everything elese + // Regular path for everything else return BufferedReader(InputStreamReader(url.openStream())).use { reader -> parseFile(reader) } } + // JarFile does no implement Closesable on Java 1.6 + private inline fun JarFile.use(block: (JarFile) -> R): R { + var cause: Throwable? = null + try { + return block(this) + } catch (e: Throwable) { + cause = e + throw e + } finally { + try { + close() + } catch (closeException: Throwable) { + if (cause === null) throw closeException + cause.addSuppressed(closeException) + throw cause + } + } + } + private fun parseFile(r: BufferedReader): List { val names = mutableSetOf() while (true) { From d5478b68bd7b22806b8246d3cbd6b863bb93d8a7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 6 Jun 2019 11:43:31 +0300 Subject: [PATCH 49/56] More operators (#1236) * Scan and emitAll operators * Flow.first operators family (without firstOrNull and firstOrDefault support) * More migrations Fixes #1094 Fixes #1078 Fixes #1244 --- .../kotlinx-coroutines-core.txt | 9 ++ .../common/src/flow/Migration.kt | 53 ++++++++++++ .../common/src/flow/operators/Errors.kt | 4 +- .../common/src/flow/operators/Merge.kt | 10 +-- .../common/src/flow/operators/Transform.kt | 45 ++++++++++ .../common/src/flow/terminal/Collect.kt | 6 ++ .../common/src/flow/terminal/Reduce.kt | 47 +++++++++- .../common/test/flow/operators/ScanTest.kt | 68 +++++++++++++++ .../common/test/flow/terminal/FirstTest.kt | 86 +++++++++++++++++++ 9 files changed, 315 insertions(+), 13 deletions(-) create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/ScanTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index ab51a2dd72..1dcad707b1 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -822,10 +822,13 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun distinctUntilChangedBy (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun drop (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun dropWhile (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun emitAll (Lkotlinx/coroutines/flow/FlowCollector;Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun emptyFlow ()Lkotlinx/coroutines/flow/Flow; public static final fun filter (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun filterNot (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun filterNotNull (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun first (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun first (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun flatMapConcat (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun flatMapMerge (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun flatMapMerge$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; @@ -853,6 +856,8 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun retry (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun sample (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow; + public static final fun scan (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; + public static final fun scanReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun single (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun singleOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun switchMap (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; @@ -872,13 +877,17 @@ public final class kotlinx/coroutines/flow/MigrationKt { public static final fun BehaviourSubject ()Ljava/lang/Object; public static final fun PublishSubject ()Ljava/lang/Object; public static final fun ReplaySubject ()Ljava/lang/Object; + public static final fun compose (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun concatMap (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun flatMap (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun flatten (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun forEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)V public static final fun merge (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun observeOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static final fun onErrorResume (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun publishOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; + public static final fun scanFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; + public static final fun skip (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index bf20d2f2a2..114a32e10d 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -193,3 +193,56 @@ public fun Flow>.merge(): Flow = error("Should not be called") replaceWith = ReplaceWith("flattenConcat()") ) public fun Flow>.flatten(): Flow = error("Should not be called") + +/** + * Kotlin has a built-in generic mechanism for making chained calls. + * If you wish to write something like + * ``` + * myFlow.compose(MyFlowExtensions.ignoreErrors()).collect { ... } + * ``` + * you can replace it with + * + * ``` + * myFlow.let(MyFlowExtensions.ignoreErrors()).collect { ... } + * ``` + * + * @suppress + */ +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Kotlin analogue of compose is 'let'", + replaceWith = ReplaceWith("let(transformer)") +) +public fun Flow.compose(transformer: Flow.() -> Flow): Flow = error("Should not be called") + +/** + * @suppress + */ +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Kotlin analogue of 'skip' is 'drop'", + replaceWith = ReplaceWith("drop(count)") +) +public fun Flow.skip(count: Int): Flow = error("Should not be called") + +/** + * Flow extension to iterate over elements is [collect]. + * Foreach wasn't introduced deliberately to avoid confusion. + * Flow is not a collection, iteration over it may be not idempotent + * and can *launch* computations with side-effects. + * This behaviour is not reflected in [forEach] name. + * @suppress + */ +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'forEach' is 'collect'", + replaceWith = ReplaceWith("collect(block)") +) +public fun Flow.forEach(action: suspend (value: T) -> Unit): Unit = error("Should not be called") + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow has less verbose 'scan' shortcut", + replaceWith = ReplaceWith("scan(initial, operation)") +) +public fun Flow.scanFold(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow = error("Should not be called") diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt index de964da6ef..29777b7a83 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt @@ -27,9 +27,7 @@ public fun Flow.onErrorCollect( predicate: ExceptionPredicate = ALWAYS_TRUE ): Flow = collectSafely { e -> if (!predicate(e)) throw e - fallback.collect { value -> - emit(value) - } + emitAll(fallback) } /** diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index 0fa6e8abd4..38b116a83f 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -80,11 +80,7 @@ public fun Flow.flatMapMerge( */ @FlowPreview public fun Flow>.flattenConcat(): Flow = flow { - collect { value -> - value.collect { innerValue -> - emit(innerValue) - } - } + collect { value -> emitAll(value) } } /** @@ -137,9 +133,7 @@ public fun Flow.switchMap(transform: suspend (value: T) -> Flow): F previousFlow?.join() // Undispatched to have better user experience in case of synchronous flows previousFlow = launch(start = CoroutineStart.UNDISPATCHED) { - transform(value).collect { innerValue -> - downstream.emit(innerValue) - } + downstream.emitAll(transform(value)) } } } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index aff523dd99..2ef4b97a9c 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -4,10 +4,12 @@ @file:JvmMultifileClass @file:JvmName("FlowKt") +@file:Suppress("UNCHECKED_CAST") package kotlinx.coroutines.flow import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.NULL import kotlin.jvm.* import kotlinx.coroutines.flow.unsafeFlow as flow @@ -97,3 +99,46 @@ public fun Flow.onEach(action: suspend (T) -> Unit): Flow = flow { emit(value) } } + +/** + * Folds the given flow with [operation], emitting every intermediate result, including [initial] value. + * Note that initial value should be immutable (or should not be mutated) as it is shared between different collectors. + * For example: + * ``` + * flowOf(1, 2, 3).accumulate(emptyList()) { acc, value -> acc + value }.toList() + * ``` + * will produce `[], [1], [1, 2], [1, 2, 3]]`. + */ +@FlowPreview +public fun Flow.scan(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow = flow { + var accumulator: R = initial + emit(accumulator) + collect { value -> + accumulator = operation(accumulator, value) + emit(accumulator) + } +} + +/** + * Reduces the given flow with [operation], emitting every intermediate result, including initial value. + * The first element is taken as initial value for operation accumulator. + * This operator has a sibling with initial value -- [scan]. + * + * For example: + * ``` + * flowOf(1, 2, 3, 4).scan { (v1, v2) -> v1 + v2 }.toList() + * ``` + * will produce `[1, 3, 6, 10]` + */ +@FlowPreview +public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = flow { + var accumulator: Any? = NULL + collect { value -> + accumulator = if (accumulator === NULL) { + value + } else { + operation(accumulator as T, value) + } + emit(accumulator as T) + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt index 624b51f683..a6a218cf46 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -32,3 +32,9 @@ public suspend inline fun Flow.collect(crossinline action: suspend (value collect(object : FlowCollector { override suspend fun emit(value: T) = action(value) }) + +/** + * Collects all the values from the given [flow] and emits them to the collector. + * Shortcut for `flow.collect { value -> emit(value) }`. + */ +public suspend inline fun FlowCollector.emitAll(flow: Flow) = flow.collect(this) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index 3a519e6514..4eca3efaf6 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -4,6 +4,7 @@ @file:JvmMultifileClass @file:JvmName("FlowKt") +@file:Suppress("UNCHECKED_CAST") package kotlinx.coroutines.flow @@ -50,7 +51,7 @@ public suspend inline fun Flow.fold( } /** - * Terminal operator, that awaits for one and only one value to be published. + * The terminal operator, that awaits for one and only one value to be published. * Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow * that contains more than one element. */ @@ -68,7 +69,7 @@ public suspend fun Flow.single(): T { } /** - * Terminal operator, that awaits for one and only one value to be published. + * The terminal operator, that awaits for one and only one value to be published. * Throws [IllegalStateException] for flow that contains more than one element. */ @FlowPreview @@ -81,3 +82,45 @@ public suspend fun Flow.singleOrNull(): T? { return result } + +/** + * The terminal operator that returns the first element emitted by the flow and then cancels flow's collection. + * Throws [NoSuchElementException] if the flow was empty. + */ +@FlowPreview +public suspend fun Flow.first(): T { + var result: Any? = NULL + try { + collect { value -> + result = value + throw AbortFlowException() + } + } catch (e: AbortFlowException) { + // Do nothing + } + + if (result === NULL) throw NoSuchElementException("Expected at least one element") + return result as T +} + +/** + * The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection. + * Throws [NoSuchElementException] if the flow has not contained elements matching the [predicate]. + */ +@FlowPreview +public suspend fun Flow.first(predicate: suspend (T) -> Boolean): T { + var result: Any? = NULL + try { + collect { value -> + if (predicate(value)) { + result = value + throw AbortFlowException() + } + } + } catch (e: AbortFlowException) { + // Do nothing + } + + if (result === NULL) throw NoSuchElementException("Expected at least one element matching the predicate $predicate") + return result as T +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ScanTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ScanTest.kt new file mode 100644 index 0000000000..d739f1a64f --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/ScanTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +class ScanTest : TestBase() { + @Test + fun testScan() = runTest { + val flow = flowOf(1, 2, 3, 4, 5) + val result = flow.scanReduce { acc, v -> acc + v }.toList() + assertEquals(listOf(1, 3, 6, 10, 15), result) + } + + @Test + fun testScanWithInitial() = runTest { + val flow = flowOf(1, 2, 3) + val result = flow.scan(emptyList()) { acc, value -> acc + value }.toList() + assertEquals(listOf(emptyList(), listOf(1), listOf(1, 2), listOf(1, 2, 3)), result) + } + + @Test + fun testNulls() = runTest { + val flow = flowOf(null, 2, null, null, null, 5) + val result = flow.scanReduce { acc, v -> if (v == null) acc else (if (acc == null) v else acc + v) }.toList() + assertEquals(listOf(null, 2, 2, 2, 2, 7), result) + } + + @Test + fun testEmptyFlow() = runTest { + val result = emptyFlow().scanReduce { _, _ -> 1 }.toList() + assertTrue(result.isEmpty()) + } + + @Test + fun testErrorCancelsUpstream() = runTest { + expect(1) + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang { expect(3) } + } + emit(1) + emit(2) + } + }.scanReduce { _, value -> + expect(value) // 2 + latch.receive() + throw TestException() + }.onErrorCollect(emptyFlow()) + + assertEquals(1, flow.single()) + finish(4) + } + + public operator fun Collection.plus(element: T): List { + val result = ArrayList(size + 1) + result.addAll(this) + result.add(element) + return result + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt new file mode 100644 index 0000000000..e84d4c7b77 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +class FirstTest : TestBase() { + @Test + fun testFirst() = runTest { + val flow = flowOf(1, 2, 3) + assertEquals(1, flow.first()) + } + + @Test + fun testNulls() = runTest { + val flow = flowOf(null, 1) + assertNull(flow.first()) + assertNull(flow.first { it == null }) + assertEquals(1, flow.first { it != null }) + } + + @Test + fun testFirstWithPredicate() = runTest { + val flow = flowOf(1, 2, 3) + assertEquals(1, flow.first { it > 0 }) + assertEquals(2, flow.first { it > 1 }) + assertFailsWith { flow.first { it > 3 } } + } + + @Test + fun testFirstCancellation() = runTest { + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang { expect(1) } + } + emit(1) + emit(2) + } + } + + + val result = flow.first { + latch.receive() + true + } + assertEquals(1, result) + finish(2) + } + + @Test + fun testEmptyFlow() = runTest { + assertFailsWith { emptyFlow().first() } + assertFailsWith { emptyFlow().first { true } } + } + + @Test + fun testErrorCancelsUpstream() = runTest { + val latch = Channel() + val flow = flow { + coroutineScope { + launch { + latch.send(Unit) + hang { expect(1) } + } + emit(1) + } + } + + assertFailsWith { + flow.first { + latch.receive() + throw TestException() + } + } + + assertEquals(1, flow.first()) + finish(2) + } +} From 15c7d0ff1dd01bd993141dd737c819ebb6e7c4b4 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 6 Jun 2019 11:47:19 +0300 Subject: [PATCH 50/56] Mark Flow declarations as experimental --- .../common/src/Annotations.kt | 4 ++- .../common/src/flow/Builders.kt | 28 +++++++++---------- .../common/src/flow/Flow.kt | 2 +- .../common/src/flow/FlowCollector.kt | 2 +- .../common/src/flow/operators/Context.kt | 6 ++-- .../common/src/flow/operators/Delay.kt | 6 ++-- .../common/src/flow/operators/Distinct.kt | 2 +- .../common/src/flow/operators/Limit.kt | 8 +++--- .../common/src/flow/operators/Merge.kt | 2 +- .../common/src/flow/operators/Transform.kt | 20 ++++++------- .../common/src/flow/operators/Zip.kt | 12 ++++---- .../common/src/flow/terminal/Collect.kt | 3 +- .../common/src/flow/terminal/Collection.kt | 6 ++-- .../common/src/flow/terminal/Count.kt | 4 +-- .../common/src/flow/terminal/Reduce.kt | 12 ++++---- .../src/flow/FlowAsPublisher.kt | 2 +- .../kotlinx-coroutines-rx2/src/RxConvert.kt | 4 +-- 17 files changed, 64 insertions(+), 59 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/Annotations.kt b/kotlinx-coroutines-core/common/src/Annotations.kt index e51e982f24..321db60e13 100644 --- a/kotlinx-coroutines-core/common/src/Annotations.kt +++ b/kotlinx-coroutines-core/common/src/Annotations.kt @@ -18,13 +18,15 @@ import kotlinx.coroutines.flow.Flow public annotation class ExperimentalCoroutinesApi /** - * Marks all [Flow] declarations as a feature preview to indicate that [Flow] is still experimental and has a 'preview' status. + * Marks [Flow]-related API as a feature preview. * * Flow preview has **no** backward compatibility guarantees, including both binary and source compatibility. * Its API and semantics can and will be changed in next releases. * * Feature preview can be used to evaluate its real-world strengths and weaknesses, gather and provide feedback. * According to the feedback, [Flow] will be refined on its road to stabilization and promotion to a stable API. + * + * The best way to speed up preview feature promotion is providing the feedback on the feature. */ @MustBeDocumented @Retention(value = AnnotationRetention.BINARY) diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index b4ff26de80..294044d875 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -44,7 +44,7 @@ import kotlin.jvm.* * ``` * If you want to switch the context of execution of a flow, use the [flowOn] operator. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun flow(@BuilderInference block: suspend FlowCollector.() -> Unit): Flow { return object : Flow { override suspend fun collect(collector: FlowCollector) { @@ -57,7 +57,6 @@ public fun flow(@BuilderInference block: suspend FlowCollector.() -> Unit * An analogue of the [flow] builder that does not check the context of execution of the resulting flow. * Used in our own operators where we trust the context of invocations. */ -@FlowPreview @PublishedApi internal inline fun unsafeFlow(@BuilderInference crossinline block: suspend FlowCollector.() -> Unit): Flow { return object : Flow { @@ -91,7 +90,7 @@ public fun (suspend () -> T).asFlow(): Flow = unsafeFlow { /** * Creates a flow that produces values from the given iterable. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Iterable.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) @@ -101,7 +100,7 @@ public fun Iterable.asFlow(): Flow = unsafeFlow { /** * Creates a flow that produces values from the given iterable. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Iterator.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) @@ -111,7 +110,7 @@ public fun Iterator.asFlow(): Flow = unsafeFlow { /** * Creates a flow that produces values from the given sequence. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Sequence.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) @@ -121,7 +120,7 @@ public fun Sequence.asFlow(): Flow = unsafeFlow { /** * Creates a flow that produces values from the given array of elements. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun flowOf(vararg elements: T): Flow = unsafeFlow { for (element in elements) { emit(element) @@ -131,7 +130,7 @@ public fun flowOf(vararg elements: T): Flow = unsafeFlow { /** * Creates flow that produces a given [value]. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun flowOf(value: T): Flow = unsafeFlow { /* * Implementation note: this is just an "optimized" overload of flowOf(vararg) @@ -143,7 +142,7 @@ public fun flowOf(value: T): Flow = unsafeFlow { /** * Returns an empty flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun emptyFlow(): Flow = EmptyFlow private object EmptyFlow : Flow { @@ -153,7 +152,7 @@ private object EmptyFlow : Flow { /** * Creates a flow that produces values from the given array. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Array.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) @@ -163,7 +162,7 @@ public fun Array.asFlow(): Flow = unsafeFlow { /** * Creates flow that produces values from the given array. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun IntArray.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) @@ -173,7 +172,7 @@ public fun IntArray.asFlow(): Flow = unsafeFlow { /** * Creates flow that produces values from the given array. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun LongArray.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) @@ -183,7 +182,7 @@ public fun LongArray.asFlow(): Flow = unsafeFlow { /** * Creates flow that produces values from the given range. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun IntRange.asFlow(): Flow = unsafeFlow { forEach { value -> emit(value) @@ -193,7 +192,7 @@ public fun IntRange.asFlow(): Flow = unsafeFlow { /** * Creates flow that produces values from the given range. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun LongRange.asFlow(): Flow = flow { forEach { value -> emit(value) @@ -262,7 +261,7 @@ public fun flowViaChannel( * } * ``` */ -@FlowPreview +@ExperimentalCoroutinesApi public fun channelFlow(@BuilderInference block: suspend ProducerScope.() -> Unit): Flow = ChannelFlowBuilder(block) @@ -310,6 +309,7 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() * ``` */ @Suppress("NOTHING_TO_INLINE") +@ExperimentalCoroutinesApi public inline fun callbackFlow(@BuilderInference noinline block: suspend ProducerScope.() -> Unit): Flow = channelFlow(block) diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index 0d7271d7b9..9e96780e88 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -109,7 +109,7 @@ import kotlinx.coroutines.* * Flow is [Reactive Streams](http://www.reactive-streams.org/) compliant, you can safely interop it with * reactive streams using [Flow.asPublisher] and [Publisher.asFlow] from kotlinx-coroutines-reactive module. */ -@FlowPreview +@ExperimentalCoroutinesApi public interface Flow { /** diff --git a/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt b/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt index 38d5954ce9..bb0d5b5d6a 100644 --- a/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt +++ b/kotlinx-coroutines-core/common/src/flow/FlowCollector.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.* * This interface should usually not be implemented directly, but rather used as a receiver in a [flow] builder when implementing a custom operator. * Implementations of this interface are not thread-safe. */ -@FlowPreview +@ExperimentalCoroutinesApi public interface FlowCollector { /** diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 15b892e669..8ccf7cf9dc 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -104,7 +104,7 @@ import kotlin.jvm.* * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating * an explicitly requested size. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.buffer(capacity: Int = BUFFERED): Flow { require(capacity >= 0 || capacity == BUFFERED || capacity == CONFLATED) { "Buffer size should be non-negative, BUFFERED, or CONFLATED, but was $capacity" @@ -147,7 +147,7 @@ public fun Flow.buffer(capacity: Int = BUFFERED): Flow { * always fused so that only one properly configured channel is used for execution. * **Conflation takes precedence over `buffer()` calls with any other capacity.** */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.conflate(): Flow = buffer(CONFLATED) /** @@ -194,7 +194,7 @@ public fun Flow.conflate(): Flow = buffer(CONFLATED) * * @throws [IllegalArgumentException] if provided context contains [Job] instance. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.flowOn(context: CoroutineContext): Flow { checkFlowContext(context) return when { diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 4db30440f6..2f01061d36 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.unsafeFlow as flow /** * Delays the emission of values from this flow for the given [timeMillis]. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.delayFlow(timeMillis: Long): Flow = flow { delay(timeMillis) collect(this@flow) @@ -26,7 +26,7 @@ public fun Flow.delayFlow(timeMillis: Long): Flow = flow { /** * Delays each element emitted by the given flow for the given [timeMillis]. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.delayEach(timeMillis: Long): Flow = flow { collect { value -> delay(timeMillis) @@ -58,6 +58,7 @@ public fun Flow.delayEach(timeMillis: Long): Flow = flow { * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. */ +@FlowPreview public fun Flow.debounce(timeoutMillis: Long): Flow { require(timeoutMillis > 0) { "Debounce timeout should be positive" } return scopedFlow { downstream -> @@ -109,6 +110,7 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * * Note that the latest element is not emitted if it does not fit into the sampling window. */ +@FlowPreview public fun Flow.sample(periodMillis: Long): Flow { require(periodMillis > 0) { "Sample period should be positive" } return scopedFlow { downstream -> diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt index 97e7463981..45e971e91e 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.unsafeFlow as flow /** * Returns flow where all subsequent repetitions of the same value are filtered out. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.distinctUntilChanged(): Flow = distinctUntilChangedBy { it } /** diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt index 61d4c590c6..f633612bd6 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.unsafeFlow as flow * Returns a flow that ignores first [count] elements. * Throws [IllegalArgumentException] if [count] is negative. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.drop(count: Int): Flow { require(count >= 0) { "Drop count should be non-negative, but had $count" } return flow { @@ -31,7 +31,7 @@ public fun Flow.drop(count: Int): Flow { /** * Returns a flow containing all elements except first elements that satisfy the given predicate. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.dropWhile(predicate: suspend (T) -> Boolean): Flow = flow { var matched = false collect { value -> @@ -49,7 +49,7 @@ public fun Flow.dropWhile(predicate: suspend (T) -> Boolean): Flow = f * When [count] elements are consumed, the original flow is cancelled. * Throws [IllegalArgumentException] if [count] is not positive. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.take(count: Int): Flow { require(count > 0) { "Requested element count $count should be positive" } return flow { @@ -70,7 +70,7 @@ public fun Flow.take(count: Int): Flow { /** * Returns a flow that contains first elements satisfying the given [predicate]. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.takeWhile(predicate: suspend (T) -> Boolean): Flow = flow { try { collect { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index 38b116a83f..1ca6c4b6c6 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -124,7 +124,7 @@ public fun Flow>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY * ``` * produces `aa bb b_last` */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.switchMap(transform: suspend (value: T) -> Flow): Flow = scopedFlow { downstream -> var previousFlow: Job? = null collect { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index 2ef4b97a9c..b10349e6d2 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.unsafeFlow as flow * } * ``` */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.transform(@BuilderInference crossinline transform: suspend FlowCollector.(value: T) -> Unit): Flow { return flow { collect { value -> @@ -40,7 +40,7 @@ public inline fun Flow.transform(@BuilderInference crossinline transfo /** * Returns a flow containing only values of the original flow that matches the given [predicate]. */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.filter(crossinline predicate: suspend (T) -> Boolean): Flow = flow { collect { value -> if (predicate(value)) return@collect emit(value) @@ -50,7 +50,7 @@ public inline fun Flow.filter(crossinline predicate: suspend (T) -> Boole /** * Returns a flow containing only values of the original flow that do not match the given [predicate]. */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.filterNot(crossinline predicate: suspend (T) -> Boolean): Flow = flow { collect { value -> if (!predicate(value)) return@collect emit(value) @@ -60,14 +60,14 @@ public inline fun Flow.filterNot(crossinline predicate: suspend (T) -> Bo /** * Returns a flow containing only values that are instances of specified type [R]. */ -@FlowPreview +@ExperimentalCoroutinesApi @Suppress("UNCHECKED_CAST") public inline fun Flow<*>.filterIsInstance(): Flow = filter { it is R } as Flow /** * Returns a flow containing only values of the original flow that are not null. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.filterNotNull(): Flow = flow { collect { value -> if (value != null) return@collect emit(value) } } @@ -75,7 +75,7 @@ public fun Flow.filterNotNull(): Flow = flow { /** * Returns a flow containing the results of applying the given [transform] function to each value of the original flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.map(crossinline transform: suspend (value: T) -> R): Flow = transform { value -> return@transform emit(transform(value)) } @@ -83,7 +83,7 @@ public inline fun Flow.map(crossinline transform: suspend (value: T) - /** * Returns a flow that contains only non-null results of applying the given [transform] function to each value of the original flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.mapNotNull(crossinline transform: suspend (value: T) -> R?): Flow = transform { value -> val transformed = transform(value) ?: return@transform return@transform emit(transformed) @@ -92,7 +92,7 @@ public inline fun Flow.mapNotNull(crossinline transform: suspend /** * Returns a flow which performs the given [action] on each value of the original flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.onEach(action: suspend (T) -> Unit): Flow = flow { collect { value -> action(value) @@ -109,7 +109,7 @@ public fun Flow.onEach(action: suspend (T) -> Unit): Flow = flow { * ``` * will produce `[], [1], [1, 2], [1, 2, 3]]`. */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.scan(initial: R, @BuilderInference operation: suspend (accumulator: R, value: T) -> R): Flow = flow { var accumulator: R = initial emit(accumulator) @@ -130,7 +130,7 @@ public fun Flow.scan(initial: R, @BuilderInference operation: suspend * ``` * will produce `[1, 3, 6, 10]` */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = flow { var accumulator: Any? = NULL collect { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt index 8ed89c07ed..e9d99d321a 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.unsafeFlow as flow * } * ``` */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.combineLatest(other: Flow, transform: suspend (T1, T2) -> R): Flow = flow { coroutineScope { val firstChannel = asFairChannel(this@combineLatest) @@ -80,7 +80,7 @@ public fun Flow.combineLatest(other: Flow, transform: suspen * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.combineLatest( other: Flow, other2: Flow, @@ -97,7 +97,7 @@ public inline fun Flow.combineLatest( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.combineLatest( other: Flow, other2: Flow, @@ -116,7 +116,7 @@ public inline fun Flow.combineLatest( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.combineLatest( other: Flow, other2: Flow, @@ -137,7 +137,7 @@ public inline fun Flow.combineLatest( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public inline fun Flow.combineLatest(vararg others: Flow, crossinline transform: suspend (Array) -> R): Flow = combineLatest(*others, arrayFactory = { arrayOfNulls(others.size + 1) }, transform = { transform(it) }) @@ -209,7 +209,7 @@ private fun CoroutineScope.asFairChannel(flow: Flow<*>): ReceiveChannel = p * } * ``` */ -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.zip(other: Flow, transform: suspend (T1, T2) -> R): Flow = flow { coroutineScope { val first = asChannel(this@zip) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt index a6a218cf46..c7f8f2eac0 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -27,7 +27,7 @@ import kotlin.jvm.* * } * ``` */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend inline fun Flow.collect(crossinline action: suspend (value: T) -> Unit): Unit = collect(object : FlowCollector { override suspend fun emit(value: T) = action(value) @@ -37,4 +37,5 @@ public suspend inline fun Flow.collect(crossinline action: suspend (value * Collects all the values from the given [flow] and emits them to the collector. * Shortcut for `flow.collect { value -> emit(value) }`. */ +@ExperimentalCoroutinesApi public suspend inline fun FlowCollector.emitAll(flow: Flow) = flow.collect(this) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collection.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collection.kt index ebeaee4dcd..836ea7e9a2 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collection.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collection.kt @@ -13,19 +13,19 @@ import kotlin.jvm.* /** * Collects given flow into a [destination] */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.toList(destination: MutableList = ArrayList()): List = toCollection(destination) /** * Collects given flow into a [destination] */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.toSet(destination: MutableSet = LinkedHashSet()): Set = toCollection(destination) /** * Collects given flow into a [destination] */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun > Flow.toCollection(destination: C): C { collect { value -> destination.add(value) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt index b3f75fa693..1d737002d0 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt @@ -13,7 +13,7 @@ import kotlin.jvm.* /** * Returns the number of elements in this flow. */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.count(): Int { var i = 0 collect { @@ -26,7 +26,7 @@ public suspend fun Flow.count(): Int { /** * Returns the number of elements matching the given predicate. */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.count(predicate: suspend (T) -> Boolean): Int { var i = 0 collect { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index 4eca3efaf6..8db762e12a 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -17,7 +17,7 @@ import kotlin.jvm.* * Accumulates value starting with the first element and applying [operation] to current accumulator value and each element. * Throws [UnsupportedOperationException] if flow was empty. */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.reduce(operation: suspend (accumulator: S, value: T) -> S): S { var accumulator: Any? = NULL @@ -38,7 +38,7 @@ public suspend fun Flow.reduce(operation: suspend (accumulator: S, /** * Accumulates value starting with [initial] value and applying [operation] current accumulator value and each element */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend inline fun Flow.fold( initial: R, crossinline operation: suspend (acc: R, value: T) -> R @@ -55,7 +55,7 @@ public suspend inline fun Flow.fold( * Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow * that contains more than one element. */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.single(): T { var result: Any? = NULL collect { value -> @@ -72,7 +72,7 @@ public suspend fun Flow.single(): T { * The terminal operator, that awaits for one and only one value to be published. * Throws [IllegalStateException] for flow that contains more than one element. */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.singleOrNull(): T? { var result: T? = null collect { value -> @@ -87,7 +87,7 @@ public suspend fun Flow.singleOrNull(): T? { * The terminal operator that returns the first element emitted by the flow and then cancels flow's collection. * Throws [NoSuchElementException] if the flow was empty. */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.first(): T { var result: Any? = NULL try { @@ -107,7 +107,7 @@ public suspend fun Flow.first(): T { * The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection. * Throws [NoSuchElementException] if the flow has not contained elements matching the [predicate]. */ -@FlowPreview +@ExperimentalCoroutinesApi public suspend fun Flow.first(predicate: suspend (T) -> Boolean): T { var result: Any? = NULL try { diff --git a/reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt b/reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt index 9267133dea..05f2391e36 100644 --- a/reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt +++ b/reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt @@ -14,7 +14,7 @@ import kotlin.coroutines.* * Transforms the given flow to a spec-compliant [Publisher]. */ @JvmName("from") -@FlowPreview +@ExperimentalCoroutinesApi public fun Flow.asPublisher(): Publisher = FlowAsPublisher(this) /** diff --git a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt index e6dc05e17b..dbf29f1ebb 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt @@ -84,8 +84,8 @@ public fun ReceiveChannel.asObservable(context: CoroutineContext): * Converts the given flow to a cold observable. * The original flow is cancelled if the observable subscriber was disposed. */ -@FlowPreview @JvmName("from") +@ExperimentalCoroutinesApi public fun Flow.asObservable() : Observable = Observable.create { emitter -> /* * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if @@ -109,6 +109,6 @@ public fun Flow.asObservable() : Observable = Observable.create { * Converts the given flow to a cold observable. * The original flow is cancelled if the flowable subscriber was disposed. */ -@FlowPreview @JvmName("from") +@ExperimentalCoroutinesApi public fun Flow.asFlowable(): Flowable = Flowable.fromPublisher(asPublisher()) From aa3d1ae0bffbf32fed55588fcea5ea6ac8b1d526 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 6 Jun 2019 12:11:41 +0300 Subject: [PATCH 51/56] Deprecate Channel operators --- .../common/src/channels/Channels.common.kt | 485 ++++++++++++++---- .../common/test/channels/ChannelsTest.kt | 2 + .../jvm/test/channels/ChannelsConsumeTest.kt | 2 + 3 files changed, 381 insertions(+), 108 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index c14929cae1..352c8c1aa1 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -3,6 +3,7 @@ */ @file:JvmMultifileClass @file:JvmName("ChannelsKt") +@file:Suppress("DEPRECATION") package kotlinx.coroutines.channels @@ -59,7 +60,10 @@ public suspend inline fun BroadcastChannel.consumeEach(action: (E) -> Uni * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel<*>.consumes(): CompletionHandler = { cause: Throwable? -> cancelConsumed(cause) } @@ -79,7 +83,10 @@ internal fun ReceiveChannel<*>.cancelConsumed(cause: Throwable?) { * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun consumesAll(vararg channels: ReceiveChannel<*>): CompletionHandler = { cause: Throwable? -> var exception: Throwable? = null @@ -138,7 +145,10 @@ public suspend inline fun ReceiveChannel.consumeEach(action: (E) -> Unit) * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.consumeEachIndexed(action: (IndexedValue) -> Unit) { var index = 0 consumeEach { @@ -155,7 +165,10 @@ public suspend inline fun ReceiveChannel.consumeEachIndexed(action: (Inde * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.elementAt(index: Int): E = elementAtOrElse(index) { throw IndexOutOfBoundsException("ReceiveChannel doesn't contain element at index $index.") } @@ -168,7 +181,10 @@ public suspend fun ReceiveChannel.elementAt(index: Int): E = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.elementAtOrElse(index: Int, defaultValue: (Int) -> E): E = consume { if (index < 0) @@ -190,7 +206,10 @@ public suspend inline fun ReceiveChannel.elementAtOrElse(index: Int, defa * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.elementAtOrNull(index: Int): E? = consume { if (index < 0) @@ -212,7 +231,10 @@ public suspend fun ReceiveChannel.elementAtOrNull(index: Int): E? = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.find(predicate: (E) -> Boolean): E? = firstOrNull(predicate) @@ -225,7 +247,10 @@ public suspend inline fun ReceiveChannel.find(predicate: (E) -> Boolean): * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.findLast(predicate: (E) -> Boolean): E? = lastOrNull(predicate) @@ -239,7 +264,10 @@ public suspend inline fun ReceiveChannel.findLast(predicate: (E) -> Boole * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.first(): E = consume { val iterator = iterator() @@ -258,7 +286,10 @@ public suspend fun ReceiveChannel.first(): E = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.first(predicate: (E) -> Boolean): E { consumeEach { if (predicate(it)) return it @@ -275,7 +306,10 @@ public suspend inline fun ReceiveChannel.first(predicate: (E) -> Boolean) * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.firstOrNull(): E? = consume { val iterator = iterator() @@ -293,7 +327,10 @@ public suspend fun ReceiveChannel.firstOrNull(): E? = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.firstOrNull(predicate: (E) -> Boolean): E? { consumeEach { if (predicate(it)) return it @@ -310,7 +347,10 @@ public suspend inline fun ReceiveChannel.firstOrNull(predicate: (E) -> Bo * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.indexOf(element: E): Int { var index = 0 consumeEach { @@ -330,7 +370,10 @@ public suspend fun ReceiveChannel.indexOf(element: E): Int { * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.indexOfFirst(predicate: (E) -> Boolean): Int { var index = 0 consumeEach { @@ -350,7 +393,10 @@ public suspend inline fun ReceiveChannel.indexOfFirst(predicate: (E) -> B * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.indexOfLast(predicate: (E) -> Boolean): Int { var lastIndex = -1 var index = 0 @@ -372,7 +418,10 @@ public suspend inline fun ReceiveChannel.indexOfLast(predicate: (E) -> Bo * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.last(): E = consume { val iterator = iterator() @@ -394,7 +443,10 @@ public suspend fun ReceiveChannel.last(): E = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.last(predicate: (E) -> Boolean): E { var last: E? = null var found = false @@ -418,7 +470,10 @@ public suspend inline fun ReceiveChannel.last(predicate: (E) -> Boolean): * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.lastIndexOf(element: E): Int { var lastIndex = -1 var index = 0 @@ -439,7 +494,10 @@ public suspend fun ReceiveChannel.lastIndexOf(element: E): Int { * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.lastOrNull(): E? = consume { val iterator = iterator() @@ -460,7 +518,10 @@ public suspend fun ReceiveChannel.lastOrNull(): E? = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.lastOrNull(predicate: (E) -> Boolean): E? { var last: E? = null consumeEach { @@ -480,7 +541,10 @@ public suspend inline fun ReceiveChannel.lastOrNull(predicate: (E) -> Boo * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.single(): E = consume { val iterator = iterator() @@ -501,7 +565,10 @@ public suspend fun ReceiveChannel.single(): E = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.single(predicate: (E) -> Boolean): E { var single: E? = null var found = false @@ -526,7 +593,10 @@ public suspend inline fun ReceiveChannel.single(predicate: (E) -> Boolean * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.singleOrNull(): E? = consume { val iterator = iterator() @@ -547,7 +617,10 @@ public suspend fun ReceiveChannel.singleOrNull(): E? = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.singleOrNull(predicate: (E) -> Boolean): E? { var single: E? = null var found = false @@ -571,7 +644,10 @@ public suspend inline fun ReceiveChannel.singleOrNull(predicate: (E) -> B * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.drop(n: Int, context: CoroutineContext = Dispatchers.Unconfined): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { require(n >= 0) { "Requested element count $n is less than zero." } @@ -596,8 +672,10 @@ public fun ReceiveChannel.drop(n: Int, context: CoroutineContext = Dispat * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.dropWhile(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { for (e in this@dropWhile) { @@ -620,8 +698,10 @@ public fun ReceiveChannel.dropWhile(context: CoroutineContext = Dispatche * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.filter(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { for (e in this@filter) { @@ -640,8 +720,10 @@ public fun ReceiveChannel.filter(context: CoroutineContext = Dispatchers. * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.filterIndexed(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (index: Int, E) -> Boolean): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { var index = 0 @@ -661,7 +743,10 @@ public fun ReceiveChannel.filterIndexed(context: CoroutineContext = Dispa * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.filterIndexedTo(destination: C, predicate: (index: Int, E) -> Boolean): C { consumeEachIndexed { (index, element) -> if (predicate(index, element)) destination.add(element) @@ -680,7 +765,10 @@ public suspend inline fun > ReceiveChannel.fil * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.filterIndexedTo(destination: C, predicate: (index: Int, E) -> Boolean): C { consumeEachIndexed { (index, element) -> if (predicate(index, element)) destination.send(element) @@ -697,8 +785,10 @@ public suspend inline fun > ReceiveChannel.filterIndexe * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.filterNot(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = filter(context) { !predicate(it) } @@ -711,7 +801,10 @@ public fun ReceiveChannel.filterNot(context: CoroutineContext = Dispatche * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) @Suppress("UNCHECKED_CAST") public fun ReceiveChannel.filterNotNull(): ReceiveChannel = filter { it != null } as ReceiveChannel @@ -725,7 +818,10 @@ public fun ReceiveChannel.filterNotNull(): ReceiveChannel = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun > ReceiveChannel.filterNotNullTo(destination: C): C { consumeEach { if (it != null) destination.add(it) @@ -742,7 +838,10 @@ public suspend fun > ReceiveChannel.fil * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun > ReceiveChannel.filterNotNullTo(destination: C): C { consumeEach { if (it != null) destination.send(it) @@ -759,7 +858,10 @@ public suspend fun > ReceiveChannel.filterNotNul * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.filterNotTo(destination: C, predicate: (E) -> Boolean): C { consumeEach { if (!predicate(it)) destination.add(it) @@ -776,7 +878,10 @@ public suspend inline fun > ReceiveChannel.fil * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.filterNotTo(destination: C, predicate: (E) -> Boolean): C { consumeEach { if (!predicate(it)) destination.send(it) @@ -793,7 +898,10 @@ public suspend inline fun > ReceiveChannel.filterNotTo( * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.filterTo(destination: C, predicate: (E) -> Boolean): C { consumeEach { if (predicate(it)) destination.add(it) @@ -810,7 +918,10 @@ public suspend inline fun > ReceiveChannel.fil * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.filterTo(destination: C, predicate: (E) -> Boolean): C { consumeEach { if (predicate(it)) destination.send(it) @@ -827,7 +938,10 @@ public suspend inline fun > ReceiveChannel.filterTo(des * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.take(n: Int, context: CoroutineContext = Dispatchers.Unconfined): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { if (n == 0) return@produce @@ -850,8 +964,10 @@ public fun ReceiveChannel.take(n: Int, context: CoroutineContext = Dispat * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.takeWhile(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { for (e in this@takeWhile) { @@ -874,7 +990,10 @@ public fun ReceiveChannel.takeWhile(context: CoroutineContext = Dispatche * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.associate(transform: (E) -> Pair): Map = associateTo(LinkedHashMap(), transform) @@ -892,7 +1011,10 @@ public suspend inline fun ReceiveChannel.associate(transform: (E) - * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.associateBy(keySelector: (E) -> K): Map = associateByTo(LinkedHashMap(), keySelector) @@ -909,7 +1031,10 @@ public suspend inline fun ReceiveChannel.associateBy(keySelector: (E) * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.associateBy(keySelector: (E) -> K, valueTransform: (E) -> V): Map = associateByTo(LinkedHashMap(), keySelector, valueTransform) @@ -926,7 +1051,10 @@ public suspend inline fun ReceiveChannel.associateBy(keySelector: ( * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.associateByTo(destination: M, keySelector: (E) -> K): M { consumeEach { destination.put(keySelector(it), it) @@ -947,7 +1075,10 @@ public suspend inline fun > ReceiveChannel.a * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.associateByTo(destination: M, keySelector: (E) -> K, valueTransform: (E) -> V): M { consumeEach { destination.put(keySelector(it), valueTransform(it)) @@ -967,7 +1098,10 @@ public suspend inline fun > ReceiveChannel> ReceiveChannel.associateTo(destination: M, transform: (E) -> Pair): M { consumeEach { destination += transform(it) @@ -985,7 +1119,10 @@ public suspend inline fun > ReceiveChannel> ReceiveChannel.toChannel(destination: C): C { consumeEach { destination.send(it) @@ -1002,7 +1139,10 @@ public suspend fun > ReceiveChannel.toChannel(destinati * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun > ReceiveChannel.toCollection(destination: C): C { consumeEach { destination.add(it) @@ -1028,7 +1168,10 @@ public suspend fun ReceiveChannel.toList(): List = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel>.toMap(): Map = toMap(LinkedHashMap()) @@ -1041,7 +1184,10 @@ public suspend fun ReceiveChannel>.toMap(): Map = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun > ReceiveChannel>.toMap(destination: M): M { consumeEach { destination += it @@ -1058,7 +1204,10 @@ public suspend fun > ReceiveChannel> * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.toMutableList(): MutableList = toCollection(ArrayList()) @@ -1073,7 +1222,10 @@ public suspend fun ReceiveChannel.toMutableList(): MutableList = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.toSet(): Set = this.toMutableSet() @@ -1086,8 +1238,10 @@ public suspend fun ReceiveChannel.toSet(): Set = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.flatMap(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (E) -> ReceiveChannel): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { for (e in this@flatMap) { @@ -1107,7 +1261,10 @@ public fun ReceiveChannel.flatMap(context: CoroutineContext = Dispatch * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.groupBy(keySelector: (E) -> K): Map> = groupByTo(LinkedHashMap(), keySelector) @@ -1124,7 +1281,10 @@ public suspend inline fun ReceiveChannel.groupBy(keySelector: (E) -> K * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.groupBy(keySelector: (E) -> K, valueTransform: (E) -> V): Map> = groupByTo(LinkedHashMap(), keySelector, valueTransform) @@ -1140,7 +1300,10 @@ public suspend inline fun ReceiveChannel.groupBy(keySelector: (E) - * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun >> ReceiveChannel.groupByTo(destination: M, keySelector: (E) -> K): M { consumeEach { val key = keySelector(it) @@ -1163,7 +1326,10 @@ public suspend inline fun >> ReceiveCh * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun >> ReceiveChannel.groupByTo(destination: M, keySelector: (E) -> K, valueTransform: (E) -> V): M { consumeEach { val key = keySelector(it) @@ -1180,7 +1346,10 @@ public suspend inline fun >> Receiv * The operation is _intermediate_ and _stateless_. * This function [consumes][ReceiveChannel.consume] all elements of the original [ReceiveChannel]. */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.map(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (E) -> R): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { consumeEach { @@ -1200,8 +1369,10 @@ public fun ReceiveChannel.map(context: CoroutineContext = Dispatchers. * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.mapIndexed(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (index: Int, E) -> R): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { var index = 0 @@ -1222,8 +1393,10 @@ public fun ReceiveChannel.mapIndexed(context: CoroutineContext = Dispa * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.mapIndexedNotNull(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (index: Int, E) -> R?): ReceiveChannel = mapIndexed(context, transform).filterNotNull() @@ -1239,7 +1412,10 @@ public fun ReceiveChannel.mapIndexedNotNull(context: CoroutineCo * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.mapIndexedNotNullTo(destination: C, transform: (index: Int, E) -> R?): C { consumeEachIndexed { (index, element) -> transform(index, element)?.let { destination.add(it) } @@ -1259,7 +1435,10 @@ public suspend inline fun > ReceiveChann * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.mapIndexedNotNullTo(destination: C, transform: (index: Int, E) -> R?): C { consumeEachIndexed { (index, element) -> transform(index, element)?.let { destination.send(it) } @@ -1279,7 +1458,10 @@ public suspend inline fun > ReceiveChannel.map * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.mapIndexedTo(destination: C, transform: (index: Int, E) -> R): C { var index = 0 consumeEach { @@ -1300,7 +1482,10 @@ public suspend inline fun > ReceiveChannel. * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.mapIndexedTo(destination: C, transform: (index: Int, E) -> R): C { var index = 0 consumeEach { @@ -1319,8 +1504,10 @@ public suspend inline fun > ReceiveChannel.mapIndexe * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.mapNotNull(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (E) -> R?): ReceiveChannel = map(context, transform).filterNotNull() @@ -1334,7 +1521,10 @@ public fun ReceiveChannel.mapNotNull(context: CoroutineContext = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.mapNotNullTo(destination: C, transform: (E) -> R?): C { consumeEach { transform(it)?.let { destination.add(it) } @@ -1352,7 +1542,10 @@ public suspend inline fun > ReceiveChann * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.mapNotNullTo(destination: C, transform: (E) -> R?): C { consumeEach { transform(it)?.let { destination.send(it) } @@ -1370,7 +1563,10 @@ public suspend inline fun > ReceiveChannel.map * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.mapTo(destination: C, transform: (E) -> R): C { consumeEach { destination.add(transform(it)) @@ -1388,7 +1584,10 @@ public suspend inline fun > ReceiveChannel. * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.mapTo(destination: C, transform: (E) -> R): C { consumeEach { destination.send(transform(it)) @@ -1405,7 +1604,10 @@ public suspend inline fun > ReceiveChannel.mapTo(des * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.withIndex(context: CoroutineContext = Dispatchers.Unconfined): ReceiveChannel> = GlobalScope.produce(context, onCompletion = consumes()) { var index = 0 @@ -1425,7 +1627,10 @@ public fun ReceiveChannel.withIndex(context: CoroutineContext = Dispatche * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.distinct(): ReceiveChannel = this.distinctBy { it } @@ -1441,8 +1646,10 @@ public fun ReceiveChannel.distinct(): ReceiveChannel = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark predicate with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.distinctBy(context: CoroutineContext = Dispatchers.Unconfined, selector: suspend (E) -> K): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { val keys = HashSet() @@ -1466,7 +1673,10 @@ public fun ReceiveChannel.distinctBy(context: CoroutineContext = Dispa * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.toMutableSet(): MutableSet = toCollection(LinkedHashSet()) @@ -1479,7 +1689,10 @@ public suspend fun ReceiveChannel.toMutableSet(): MutableSet = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.all(predicate: (E) -> Boolean): Boolean { consumeEach { if (!predicate(it)) return false @@ -1496,7 +1709,10 @@ public suspend inline fun ReceiveChannel.all(predicate: (E) -> Boolean): * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.any(): Boolean = consume { return iterator().hasNext() @@ -1511,7 +1727,10 @@ public suspend fun ReceiveChannel.any(): Boolean = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.any(predicate: (E) -> Boolean): Boolean { consumeEach { if (predicate(it)) return true @@ -1528,7 +1747,10 @@ public suspend inline fun ReceiveChannel.any(predicate: (E) -> Boolean): * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.count(): Int { var count = 0 consumeEach { count++ } @@ -1544,7 +1766,10 @@ public suspend fun ReceiveChannel.count(): Int { * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.count(predicate: (E) -> Boolean): Int { var count = 0 consumeEach { @@ -1562,7 +1787,10 @@ public suspend inline fun ReceiveChannel.count(predicate: (E) -> Boolean) * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.fold(initial: R, operation: (acc: R, E) -> R): R { var accumulator = initial consumeEach { @@ -1583,7 +1811,10 @@ public suspend inline fun ReceiveChannel.fold(initial: R, operation: ( * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.foldIndexed(initial: R, operation: (index: Int, acc: R, E) -> R): R { var index = 0 var accumulator = initial @@ -1602,7 +1833,10 @@ public suspend inline fun ReceiveChannel.foldIndexed(initial: R, opera * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.maxBy(selector: (E) -> R): E? = consume { val iterator = iterator() @@ -1629,7 +1863,10 @@ public suspend inline fun > ReceiveChannel.maxBy(selecto * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.maxWith(comparator: Comparator): E? = consume { val iterator = iterator() @@ -1651,7 +1888,10 @@ public suspend fun ReceiveChannel.maxWith(comparator: Comparator): * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun > ReceiveChannel.minBy(selector: (E) -> R): E? = consume { val iterator = iterator() @@ -1678,7 +1918,10 @@ public suspend inline fun > ReceiveChannel.minBy(selecto * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.minWith(comparator: Comparator): E? = consume { val iterator = iterator() @@ -1700,7 +1943,10 @@ public suspend fun ReceiveChannel.minWith(comparator: Comparator): * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend fun ReceiveChannel.none(): Boolean = consume { return !iterator().hasNext() @@ -1715,7 +1961,10 @@ public suspend fun ReceiveChannel.none(): Boolean = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.none(predicate: (E) -> Boolean): Boolean { consumeEach { if (predicate(it)) return false @@ -1732,7 +1981,10 @@ public suspend inline fun ReceiveChannel.none(predicate: (E) -> Boolean): * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.reduce(operation: (acc: S, E) -> S): S = consume { val iterator = this.iterator() @@ -1756,8 +2008,10 @@ public suspend inline fun ReceiveChannel.reduce(operation: (acc: S * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark operation with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.reduceIndexed(operation: (index: Int, acc: S, E) -> S): S = consume { val iterator = this.iterator() @@ -1779,7 +2033,10 @@ public suspend inline fun ReceiveChannel.reduceIndexed(operation: * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.sumBy(selector: (E) -> Int): Int { var sum = 0 consumeEach { @@ -1797,7 +2054,10 @@ public suspend inline fun ReceiveChannel.sumBy(selector: (E) -> Int): Int * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.sumByDouble(selector: (E) -> Double): Double { var sum = 0.0 consumeEach { @@ -1815,7 +2075,10 @@ public suspend inline fun ReceiveChannel.sumByDouble(selector: (E) -> Dou * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.requireNoNulls(): ReceiveChannel = map { it ?: throw IllegalArgumentException("null element found in $this.") } @@ -1830,7 +2093,10 @@ public fun ReceiveChannel.requireNoNulls(): ReceiveChannel = * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public suspend inline fun ReceiveChannel.partition(predicate: (E) -> Boolean): Pair, List> { val first = ArrayList() val second = ArrayList() @@ -1854,7 +2120,10 @@ public suspend inline fun ReceiveChannel.partition(predicate: (E) -> Bool * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public infix fun ReceiveChannel.zip(other: ReceiveChannel): ReceiveChannel> = zip(other) { t1, t2 -> t1 to t2 } @@ -1867,8 +2136,10 @@ public infix fun ReceiveChannel.zip(other: ReceiveChannel): Receive * **Note: This API will become obsolete in future updates with introduction of lazy asynchronous streams.** * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ -@ObsoleteCoroutinesApi -// todo: mark transform with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159 +@Deprecated( + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", + level = DeprecationLevel.WARNING +) public fun ReceiveChannel.zip(other: ReceiveChannel, context: CoroutineContext = Dispatchers.Unconfined, transform: (a: E, b: R) -> V): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumesAll(this, other)) { val otherIterator = other.iterator() @@ -1878,5 +2149,3 @@ public fun ReceiveChannel.zip(other: ReceiveChannel, context: Co send(transform(element1, element2)) } } - - diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt index 5ef040c5ce..983f353f07 100644 --- a/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt @@ -2,6 +2,8 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("DEPRECATION") + package kotlinx.coroutines.channels import kotlinx.coroutines.* diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelsConsumeTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelsConsumeTest.kt index 2e299c9a7c..d9ef22b10e 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelsConsumeTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelsConsumeTest.kt @@ -2,6 +2,8 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("DEPRECATION") + package kotlinx.coroutines.channels import kotlinx.coroutines.* From 253e8ebb8a5c649bdceadbf86278130e7762a3d1 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Tue, 4 Jun 2019 22:39:51 +0200 Subject: [PATCH 52/56] Add fast `Semaphore`. In addition, the `SegmentQueue` data structure, which emulates an infinite array with fast removing from the middle, is introduced for storing suspended acquirers in semaphore/mutex/channel algorithms. Fixes #1088 --- .../kotlin/benchmarks/SemaphoreBenchmark.kt | 97 ++++++++ .../kotlinx-coroutines-core.txt | 13 ++ .../common/src/internal/SegmentQueue.kt | 176 +++++++++++++++ .../common/src/sync/Semaphore.kt | 213 ++++++++++++++++++ .../common/test/sync/SemaphoreTest.kt | 119 ++++++++++ .../jvm/test/internal/SegmentBasedQueue.kt | 72 ++++++ .../jvm/test/internal/SegmentQueueLFTest.kt | 26 +++ .../jvm/test/internal/SegmentQueueTest.kt | 99 ++++++++ .../jvm/test/sync/SemaphoreStressTest.kt | 60 +++++ 9 files changed, 875 insertions(+) create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt create mode 100644 kotlinx-coroutines-core/common/src/internal/SegmentQueue.kt create mode 100644 kotlinx-coroutines-core/common/src/sync/Semaphore.kt create mode 100644 kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt create mode 100644 kotlinx-coroutines-core/jvm/test/internal/SegmentQueueLFTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt diff --git a/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt new file mode 100644 index 0000000000..0fc563a89e --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/SemaphoreBenchmark.kt @@ -0,0 +1,97 @@ +package benchmarks + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import org.openjdk.jmh.annotations.* +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.ThreadLocalRandom +import java.util.concurrent.TimeUnit + +@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MICROSECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MICROSECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +open class SemaphoreBenchmark { + @Param + private var _1_dispatcher: SemaphoreBenchDispatcherCreator = SemaphoreBenchDispatcherCreator.FORK_JOIN + + @Param("0", "1000") + private var _2_coroutines: Int = 0 + + @Param("1", "2", "4", "8", "32", "128", "100000") + private var _3_maxPermits: Int = 0 + + @Param("1", "2", "4") // local machine +// @Param("1", "2", "4", "8", "16", "32", "64", "128", "144") // dasquad +// @Param("1", "2", "4", "8", "16", "32", "64", "96") // Google Cloud + private var _4_parallelism: Int = 0 + + private lateinit var dispatcher: CoroutineDispatcher + private var coroutines = 0 + + @InternalCoroutinesApi + @Setup + fun setup() { + dispatcher = _1_dispatcher.create(_4_parallelism) + coroutines = if (_2_coroutines == 0) _4_parallelism else _2_coroutines + } + + @Benchmark + fun semaphore() = runBlocking { + val n = BATCH_SIZE / coroutines + val semaphore = Semaphore(_3_maxPermits) + val jobs = ArrayList(coroutines) + repeat(coroutines) { + jobs += GlobalScope.launch { + repeat(n) { + semaphore.withPermit { + doWork(WORK_INSIDE) + } + doWork(WORK_OUTSIDE) + } + } + } + jobs.forEach { it.join() } + } + + @Benchmark + fun channelAsSemaphore() = runBlocking { + val n = BATCH_SIZE / coroutines + val semaphore = Channel(_3_maxPermits) + val jobs = ArrayList(coroutines) + repeat(coroutines) { + jobs += GlobalScope.launch { + repeat(n) { + semaphore.send(Unit) // acquire + doWork(WORK_INSIDE) + semaphore.receive() // release + doWork(WORK_OUTSIDE) + } + } + } + jobs.forEach { it.join() } + } +} + +enum class SemaphoreBenchDispatcherCreator(val create: (parallelism: Int) -> CoroutineDispatcher) { + FORK_JOIN({ parallelism -> ForkJoinPool(parallelism).asCoroutineDispatcher() }), + EXPERIMENTAL({ parallelism -> ExperimentalCoroutineDispatcher(corePoolSize = parallelism, maxPoolSize = parallelism) }) +} + +private fun doWork(work: Int) { + // We use geometric distribution here + val p = 1.0 / work + val r = ThreadLocalRandom.current() + while (true) { + if (r.nextDouble() < p) break + } +} + +private const val WORK_INSIDE = 80 +private const val WORK_OUTSIDE = 40 +private const val BATCH_SIZE = 1000000 \ No newline at end of file diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 1dcad707b1..86a2203aba 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -1008,6 +1008,19 @@ public final class kotlinx/coroutines/sync/MutexKt { public static synthetic fun withLock$default (Lkotlinx/coroutines/sync/Mutex;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } +public abstract interface class kotlinx/coroutines/sync/Semaphore { + public abstract fun acquire (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getAvailablePermits ()I + public abstract fun release ()V + public abstract fun tryAcquire ()Z +} + +public final class kotlinx/coroutines/sync/SemaphoreKt { + public static final fun Semaphore (II)Lkotlinx/coroutines/sync/Semaphore; + public static synthetic fun Semaphore$default (IIILjava/lang/Object;)Lkotlinx/coroutines/sync/Semaphore; + public static final fun withPermit (Lkotlinx/coroutines/sync/Semaphore;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class kotlinx/coroutines/test/TestCoroutineContext : kotlin/coroutines/CoroutineContext { public fun ()V public fun (Ljava/lang/String;)V diff --git a/kotlinx-coroutines-core/common/src/internal/SegmentQueue.kt b/kotlinx-coroutines-core/common/src/internal/SegmentQueue.kt new file mode 100644 index 0000000000..4ad554fd38 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/SegmentQueue.kt @@ -0,0 +1,176 @@ +package kotlinx.coroutines.internal + +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop + +/** + * Essentially, this segment queue is an infinite array of segments, which is represented as + * a Michael-Scott queue of them. All segments are instances of [Segment] class and + * follow in natural order (see [Segment.id]) in the queue. + */ +internal abstract class SegmentQueue>() { + private val _head: AtomicRef + /** + * Returns the first segment in the queue. + */ + protected val head: S get() = _head.value + + private val _tail: AtomicRef + /** + * Returns the last segment in the queue. + */ + protected val tail: S get() = _tail.value + + init { + val initialSegment = newSegment(0) + _head = atomic(initialSegment) + _tail = atomic(initialSegment) + } + + /** + * The implementation should create an instance of segment [S] with the specified id + * and initial reference to the previous one. + */ + abstract fun newSegment(id: Long, prev: S? = null): S + + /** + * Finds a segment with the specified [id] following by next references from the + * [startFrom] segment. The typical use-case is reading [tail] or [head], doing some + * synchronization, and invoking [getSegment] or [getSegmentAndMoveHead] correspondingly + * to find the required segment. + */ + protected fun getSegment(startFrom: S, id: Long): S? { + // Go through `next` references and add new segments if needed, + // similarly to the `push` in the Michael-Scott queue algorithm. + // The only difference is that `CAS failure` means that the + // required segment has already been added, so the algorithm just + // uses it. This way, only one segment with each id can be in the queue. + var cur: S = startFrom + while (cur.id < id) { + var curNext = cur.next + if (curNext == null) { + // Add a new segment. + val newTail = newSegment(cur.id + 1, cur) + curNext = if (cur.casNext(null, newTail)) { + if (cur.removed) { + cur.remove() + } + moveTailForward(newTail) + newTail + } else { + cur.next!! + } + } + cur = curNext + } + if (cur.id != id) return null + return cur + } + + /** + * Invokes [getSegment] and replaces [head] with the result if its [id] is greater. + */ + protected fun getSegmentAndMoveHead(startFrom: S, id: Long): S? { + @Suppress("LeakingThis") + if (startFrom.id == id) return startFrom + val s = getSegment(startFrom, id) ?: return null + moveHeadForward(s) + return s + } + + /** + * Updates [head] to the specified segment + * if its `id` is greater. + */ + private fun moveHeadForward(new: S) { + _head.loop { curHead -> + if (curHead.id > new.id) return + if (_head.compareAndSet(curHead, new)) { + new.prev.value = null + return + } + } + } + + /** + * Updates [tail] to the specified segment + * if its `id` is greater. + */ + private fun moveTailForward(new: S) { + _tail.loop { curTail -> + if (curTail.id > new.id) return + if (_tail.compareAndSet(curTail, new)) return + } + } +} + +/** + * Each segment in [SegmentQueue] has a unique id and is created by [SegmentQueue.newSegment]. + * Essentially, this is a node in the Michael-Scott queue algorithm, but with + * maintaining [prev] pointer for efficient [remove] implementation. + */ +internal abstract class Segment>(val id: Long, prev: S?) { + // Pointer to the next segment, updates similarly to the Michael-Scott queue algorithm. + private val _next = atomic(null) + val next: S? get() = _next.value + fun casNext(expected: S?, value: S?): Boolean = _next.compareAndSet(expected, value) + // Pointer to the previous segment, updates in [remove] function. + val prev = atomic(null) + + /** + * Returns `true` if this segment is logically removed from the queue. + * The [remove] function should be called right after it becomes logically removed. + */ + abstract val removed: Boolean + + init { + this.prev.value = prev + } + + /** + * Removes this segment physically from the segment queue. The segment should be + * logically removed (so [removed] returns `true`) at the point of invocation. + */ + fun remove() { + check(removed) { " The segment should be logically removed at first "} + // Read `next` and `prev` pointers. + var next = this._next.value ?: return // tail cannot be removed + var prev = prev.value ?: return // head cannot be removed + // Link `next` and `prev`. + prev.moveNextToRight(next) + while (prev.removed) { + prev = prev.prev.value ?: break + prev.moveNextToRight(next) + } + next.movePrevToLeft(prev) + while (next.removed) { + next = next.next ?: break + next.movePrevToLeft(prev) + } + } + + /** + * Updates [next] pointer to the specified segment if + * the [id] of the specified segment is greater. + */ + private fun moveNextToRight(next: S) { + while (true) { + val curNext = this._next.value as S + if (next.id <= curNext.id) return + if (this._next.compareAndSet(curNext, next)) return + } + } + + /** + * Updates [prev] pointer to the specified segment if + * the [id] of the specified segment is lower. + */ + private fun movePrevToLeft(prev: S) { + while (true) { + val curPrev = this.prev.value ?: return + if (curPrev.id <= prev.id) return + if (this.prev.compareAndSet(curPrev, prev)) return + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt new file mode 100644 index 0000000000..0ffb99006b --- /dev/null +++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt @@ -0,0 +1,213 @@ +package kotlinx.coroutines.sync + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.atomicArrayOfNulls +import kotlinx.atomicfu.getAndUpdate +import kotlinx.atomicfu.loop +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.resume +import kotlin.math.max + +/** + * A counting semaphore for coroutines. It maintains a number of available permits. + * Each [acquire] suspends if necessary until a permit is available, and then takes it. + * Each [release] adds a permit, potentially releasing a suspended acquirer. + * + * Semaphore with `permits = 1` is essentially a [Mutex]. + **/ +public interface Semaphore { + /** + * Returns the current number of permits available in this semaphore. + */ + public val availablePermits: Int + + /** + * Acquires a permit from this semaphore, suspending until one is available. + * All suspending acquirers are processed in first-in-first-out (FIFO) order. + * + * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this + * function is suspended, this function immediately resumes with [CancellationException]. + * + * *Cancellation of suspended semaphore acquisition` is atomic* -- when this function + * throws [CancellationException] it means that the semaphore was not acquired. + * + * Note, that this function does not check for cancellation when it is not suspended. + * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. + * + * Use [tryAcquire] to try acquire a permit of this semaphore without suspension. + */ + public suspend fun acquire() + + /** + * Tries to acquire a permit from this semaphore without suspension. + * + * @return `true` if a permit was acquired, `false` otherwise. + */ + public fun tryAcquire(): Boolean + + /** + * Releases a permit, returning it into this semaphore. Resumes the first + * suspending acquirer if there is one at the point of invocation. + * Throws [IllegalStateException] if there is no acquired permit + * at the point of invocation. + */ + public fun release() +} + +/** + * Creates new [Semaphore] instance. + * @param permits the number of permits available in this semaphore. + * @param acquiredPermits the number of already acquired permits, + * should be between `0` and `permits` (inclusively). + */ +@Suppress("FunctionName") +public fun Semaphore(permits: Int, acquiredPermits: Int = 0): Semaphore = SemaphoreImpl(permits, acquiredPermits) + +/** + * Executes the given [action], acquiring a permit from this semaphore at the beginning + * and releasing it after the [action] is completed. + * + * @return the return value of the [action]. + */ +public suspend inline fun Semaphore.withPermit(action: () -> T): T { + acquire() + try { + return action() + } finally { + release() + } +} + +private class SemaphoreImpl( + private val permits: Int, acquiredPermits: Int +) : Semaphore, SegmentQueue() { + init { + require(permits > 0) { "Semaphore should have at least 1 permit" } + require(acquiredPermits in 0..permits) { "The number of acquired permits should be in 0..permits" } + } + + override fun newSegment(id: Long, prev: SemaphoreSegment?) = SemaphoreSegment(id, prev) + + /** + * This counter indicates a number of available permits if it is non-negative, + * or the size with minus sign otherwise. Note, that 32-bit counter is enough here + * since the maximal number of available permits is [permits] which is [Int], + * and the maximum number of waiting acquirers cannot be greater than 2^31 in any + * real application. + */ + private val _availablePermits = atomic(permits) + override val availablePermits: Int get() = max(_availablePermits.value, 0) + + // The queue of waiting acquirers is essentially an infinite array based on `SegmentQueue`; + // each segment contains a fixed number of slots. To determine a slot for each enqueue + // and dequeue operation, we increment the corresponding counter at the beginning of the operation + // and use the value before the increment as a slot number. This way, each enqueue-dequeue pair + // works with an individual cell. + private val enqIdx = atomic(0L) + private val deqIdx = atomic(0L) + + override fun tryAcquire(): Boolean { + _availablePermits.loop { p -> + if (p <= 0) return false + if (_availablePermits.compareAndSet(p, p - 1)) return true + } + } + + override suspend fun acquire() { + val p = _availablePermits.getAndDecrement() + if (p > 0) return // permit acquired + addToQueueAndSuspend() + } + + override fun release() { + val p = _availablePermits.getAndUpdate { cur -> + check(cur < permits) { "The number of acquired permits cannot be greater than `permits`" } + cur + 1 + } + if (p >= 0) return // no waiters + resumeNextFromQueue() + } + + private suspend fun addToQueueAndSuspend() = suspendAtomicCancellableCoroutine sc@ { cont -> + val last = this.tail + val enqIdx = enqIdx.getAndIncrement() + val segment = getSegment(last, enqIdx / SEGMENT_SIZE) + val i = (enqIdx % SEGMENT_SIZE).toInt() + if (segment === null || segment.get(i) === RESUMED || !segment.cas(i, null, cont)) { + // already resumed + cont.resume(Unit) + return@sc + } + cont.invokeOnCancellation(CancelSemaphoreAcquisitionHandler(this, segment, i).asHandler) + } + + @Suppress("UNCHECKED_CAST") + private fun resumeNextFromQueue() { + val first = this.head + val deqIdx = deqIdx.getAndIncrement() + val segment = getSegmentAndMoveHead(first, deqIdx / SEGMENT_SIZE) ?: return + val i = (deqIdx % SEGMENT_SIZE).toInt() + val cont = segment.getAndUpdate(i) { + // Cancelled continuation invokes `release` + // and resumes next suspended acquirer if needed. + if (it === CANCELLED) return + RESUMED + } + if (cont === null) return // just resumed + (cont as CancellableContinuation).resume(Unit) + } +} + +private class CancelSemaphoreAcquisitionHandler( + private val semaphore: Semaphore, + private val segment: SemaphoreSegment, + private val index: Int +) : CancelHandler() { + override fun invoke(cause: Throwable?) { + segment.cancel(index) + semaphore.release() + } + + override fun toString() = "CancelSemaphoreAcquisitionHandler[$semaphore, $segment, $index]" +} + +private class SemaphoreSegment(id: Long, prev: SemaphoreSegment?): Segment(id, prev) { + private val acquirers = atomicArrayOfNulls(SEGMENT_SIZE) + + @Suppress("NOTHING_TO_INLINE") + inline fun get(index: Int): Any? = acquirers[index].value + + @Suppress("NOTHING_TO_INLINE") + inline fun cas(index: Int, expected: Any?, value: Any?): Boolean = acquirers[index].compareAndSet(expected, value) + + inline fun getAndUpdate(index: Int, function: (Any?) -> Any?): Any? { + while (true) { + val cur = acquirers[index].value + val upd = function(cur) + if (cas(index, cur, upd)) return cur + } + } + + private val cancelledSlots = atomic(0) + override val removed get() = cancelledSlots.value == SEGMENT_SIZE + + // Cleans the acquirer slot located by the specified index + // and removes this segment physically if all slots are cleaned. + fun cancel(index: Int) { + // Clean the specified waiter + acquirers[index].value = CANCELLED + // Remove this segment if needed + if (cancelledSlots.incrementAndGet() == SEGMENT_SIZE) + remove() + } + + override fun toString() = "SemaphoreSegment[id=$id, hashCode=${hashCode()}]" +} + +@SharedImmutable +private val RESUMED = Symbol("RESUMED") +@SharedImmutable +private val CANCELLED = Symbol("CANCELLED") +@SharedImmutable +private val SEGMENT_SIZE = systemProp("kotlinx.coroutines.semaphore.segmentSize", 16) \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt b/kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt new file mode 100644 index 0000000000..a6aaf24cb3 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt @@ -0,0 +1,119 @@ +package kotlinx.coroutines.sync + +import kotlinx.coroutines.TestBase +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SemaphoreTest : TestBase() { + + @Test + fun testSimple() = runTest { + val semaphore = Semaphore(2) + launch { + expect(3) + semaphore.release() + expect(4) + } + expect(1) + semaphore.acquire() + semaphore.acquire() + expect(2) + semaphore.acquire() + finish(5) + } + + @Test + fun testSimpleAsMutex() = runTest { + val semaphore = Semaphore(1) + expect(1) + launch { + expect(4) + semaphore.acquire() // suspends + expect(7) // now got lock + semaphore.release() + expect(8) + } + expect(2) + semaphore.acquire() // locked + expect(3) + yield() // yield to child + expect(5) + semaphore.release() + expect(6) + yield() // now child has lock + finish(9) + } + + @Test + fun tryAcquireTest() = runTest { + val semaphore = Semaphore(2) + assertTrue(semaphore.tryAcquire()) + assertTrue(semaphore.tryAcquire()) + assertFalse(semaphore.tryAcquire()) + assertEquals(0, semaphore.availablePermits) + semaphore.release() + assertEquals(1, semaphore.availablePermits) + assertTrue(semaphore.tryAcquire()) + assertEquals(0, semaphore.availablePermits) + } + + @Test + fun withSemaphoreTest() = runTest { + val semaphore = Semaphore(1) + assertEquals(1, semaphore.availablePermits) + semaphore.withPermit { + assertEquals(0, semaphore.availablePermits) + } + assertEquals(1, semaphore.availablePermits) + } + + @Test + fun fairnessTest() = runTest { + val semaphore = Semaphore(1) + semaphore.acquire() + launch(coroutineContext) { + // first to acquire + expect(2) + semaphore.acquire() // suspend + expect(6) + } + launch(coroutineContext) { + // second to acquire + expect(3) + semaphore.acquire() // suspend + expect(9) + } + expect(1) + yield() + expect(4) + semaphore.release() + expect(5) + yield() + expect(7) + semaphore.release() + expect(8) + yield() + finish(10) + } + + @Test + fun testCancellationReleasesSemaphore() = runTest { + val semaphore = Semaphore(1) + semaphore.acquire() + assertEquals(0, semaphore.availablePermits) + val job = launch { + assertFalse(semaphore.tryAcquire()) + semaphore.acquire() + } + yield() + job.cancelAndJoin() + assertEquals(0, semaphore.availablePermits) + semaphore.release() + assertEquals(1, semaphore.availablePermits) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt new file mode 100644 index 0000000000..293be7a59e --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt @@ -0,0 +1,72 @@ +package kotlinx.coroutines.internal + +import kotlinx.atomicfu.atomic + +/** + * This queue implementation is based on [SegmentQueue] for testing purposes and is organized as follows. Essentially, + * the [SegmentBasedQueue] is represented as an infinite array of segments, each stores one element (see [OneElementSegment]). + * Both [enqueue] and [dequeue] operations increment the corresponding global index ([enqIdx] for [enqueue] and + * [deqIdx] for [dequeue]) and work with the indexed by this counter cell. Since both operations increment the indices + * at first, there could be a race: [enqueue] increments [enqIdx], then [dequeue] checks that the queue is not empty + * (that's true) and increments [deqIdx], looking into the corresponding cell after that; however, the cell is empty + * because the [enqIdx] operation has not been put its element yet. To make the queue non-blocking, [dequeue] can mark + * the cell with [BROKEN] token and retry the operation, [enqueue] at the same time should restart as well; this way, + * the queue is obstruction-free. + */ +internal class SegmentBasedQueue : SegmentQueue>() { + override fun newSegment(id: Long, prev: OneElementSegment?): OneElementSegment = OneElementSegment(id, prev) + + private val enqIdx = atomic(0L) + private val deqIdx = atomic(0L) + + // Returns the segments associated with the enqueued element. + fun enqueue(element: T): OneElementSegment { + while (true) { + var tail = this.tail + val enqIdx = this.enqIdx.getAndIncrement() + tail = getSegment(tail, enqIdx) ?: continue + if (tail.element.value === BROKEN) continue + if (tail.element.compareAndSet(null, element)) return tail + } + } + + fun dequeue(): T? { + while (true) { + if (this.deqIdx.value >= this.enqIdx.value) return null + var firstSegment = this.head + val deqIdx = this.deqIdx.getAndIncrement() + firstSegment = getSegmentAndMoveHead(firstSegment, deqIdx) ?: continue + var el = firstSegment.element.value + if (el === null) { + if (firstSegment.element.compareAndSet(null, BROKEN)) continue + else el = firstSegment.element.value + } + if (el === REMOVED) continue + return el as T + } + } + + val numberOfSegments: Int get() { + var s: OneElementSegment? = head + var i = 0 + while (s != null) { + s = s.next + i++ + } + return i + } +} + +internal class OneElementSegment(id: Long, prev: OneElementSegment?) : Segment>(id, prev) { + val element = atomic(null) + + override val removed get() = element.value === REMOVED + + fun removeSegment() { + element.value = REMOVED + remove() + } +} + +private val BROKEN = Symbol("BROKEN") +private val REMOVED = Symbol("REMOVED") \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueLFTest.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueLFTest.kt new file mode 100644 index 0000000000..b6faf683fb --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueLFTest.kt @@ -0,0 +1,26 @@ +package kotlinx.coroutines.internal + +import com.devexperts.dxlab.lincheck.LinChecker +import com.devexperts.dxlab.lincheck.annotations.Operation +import com.devexperts.dxlab.lincheck.annotations.Param +import com.devexperts.dxlab.lincheck.paramgen.IntGen +import com.devexperts.dxlab.lincheck.strategy.stress.StressCTest +import org.junit.Test + +@StressCTest +class SegmentQueueLFTest { + private val q = SegmentBasedQueue() + + @Operation + fun add(@Param(gen = IntGen::class) x: Int) { + q.enqueue(x) + } + + @Operation + fun poll(): Int? = q.dequeue() + + @Test + fun test() { + LinChecker.check(SegmentQueueLFTest::class.java) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt new file mode 100644 index 0000000000..9a6ee42aa0 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt @@ -0,0 +1,99 @@ +package kotlinx.coroutines.internal + +import kotlinx.coroutines.TestBase +import org.junit.Test +import java.util.* +import java.util.concurrent.CyclicBarrier +import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread +import kotlin.random.Random +import kotlin.test.assertEquals + +class SegmentQueueTest : TestBase() { + + @Test + fun simpleTest() { + val q = SegmentBasedQueue() + assertEquals( 1, q.numberOfSegments) + assertEquals(null, q.dequeue()) + q.enqueue(1) + assertEquals(1, q.numberOfSegments) + q.enqueue(2) + assertEquals(2, q.numberOfSegments) + assertEquals(1, q.dequeue()) + assertEquals(2, q.numberOfSegments) + assertEquals(2, q.dequeue()) + assertEquals(1, q.numberOfSegments) + assertEquals(null, q.dequeue()) + } + + @Test + fun testSegmentRemoving() { + val q = SegmentBasedQueue() + q.enqueue(1) + val s = q.enqueue(2) + q.enqueue(3) + assertEquals(3, q.numberOfSegments) + s.removeSegment() + assertEquals(2, q.numberOfSegments) + assertEquals(1, q.dequeue()) + assertEquals(3, q.dequeue()) + assertEquals(null, q.dequeue()) + } + + @Test + fun testRemoveHeadSegment() { + val q = SegmentBasedQueue() + q.enqueue(1) + val s = q.enqueue(2) + assertEquals(1, q.dequeue()) + q.enqueue(3) + s.removeSegment() + assertEquals(3, q.dequeue()) + assertEquals(null, q.dequeue()) + } + + @Test + fun stressTest() { + val q = SegmentBasedQueue() + val expectedQueue = ArrayDeque() + val r = Random(0) + repeat(1_000_000 * stressTestMultiplier) { + if (r.nextBoolean()) { // add + val el = r.nextInt() + q.enqueue(el) + expectedQueue.add(el) + } else { // remove + assertEquals(expectedQueue.poll(), q.dequeue()) + } + } + } + + @Test + fun stressTestRemoveSegmentsSerial() = stressTestRemoveSegments(false) + + @Test + fun stressTestRemoveSegmentsRandom() = stressTestRemoveSegments(true) + + private fun stressTestRemoveSegments(random: Boolean) { + val N = 100_000 * stressTestMultiplier + val T = 10 + val q = SegmentBasedQueue() + val segments = (1..N).map { q.enqueue(it) }.toMutableList() + if (random) segments.shuffle() + assertEquals(N, q.numberOfSegments) + val nextSegmentIndex = AtomicInteger() + val barrier = CyclicBarrier(T) + (1..T).map { + thread { + barrier.await() + while (true) { + val i = nextSegmentIndex.getAndIncrement() + if (i >= N) break + segments[i].removeSegment() + } + } + }.forEach { it.join() } + assertEquals(2, q.numberOfSegments) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt new file mode 100644 index 0000000000..cdfcc6bded --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt @@ -0,0 +1,60 @@ +package kotlinx.coroutines.sync + +import kotlinx.coroutines.* +import org.junit.Test +import kotlin.test.assertEquals + +class SemaphoreStressTest : TestBase() { + + @Test + fun stressTestAsMutex() = runTest { + val n = 10_000 * stressTestMultiplier + val k = 100 + var shared = 0 + val semaphore = Semaphore(1) + val jobs = List(n) { + launch { + repeat(k) { + semaphore.acquire() + shared++ + semaphore.release() + } + } + } + jobs.forEach { it.join() } + assertEquals(n * k, shared) + } + + @Test + fun stressTest() = runTest { + val n = 10_000 * stressTestMultiplier + val k = 100 + val semaphore = Semaphore(10) + val jobs = List(n) { + launch { + repeat(k) { + semaphore.acquire() + semaphore.release() + } + } + } + jobs.forEach { it.join() } + } + + @Test + fun stressCancellation() = runTest { + val n = 10_000 * stressTestMultiplier + val semaphore = Semaphore(1) + semaphore.acquire() + repeat(n) { + val job = launch { + semaphore.acquire() + } + yield() + job.cancelAndJoin() + } + assertEquals(0, semaphore.availablePermits) + semaphore.release() + assertEquals(1, semaphore.availablePermits) + } +} \ No newline at end of file From d90eb26a1dd525cf177110cd215665eb3cdfb06f Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 6 Jun 2019 16:44:33 +0300 Subject: [PATCH 53/56] atomicfu version 0.12.8 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 13b510dcef..387d56e166 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ kotlin_version=1.3.31 # Dependencies junit_version=4.12 -atomicfu_version=0.12.7 +atomicfu_version=0.12.8 html_version=0.6.8 lincheck_version=2.0 dokka_version=0.9.16-rdev-2-mpp-hacks From 3216825bb25345982833e1b1f412108d5f3f2ba0 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 6 Jun 2019 20:16:28 +0300 Subject: [PATCH 54/56] Use real semaphore in flatten/flatMapMerge --- .../common/src/flow/operators/Merge.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index 1ca6c4b6c6..3ed2c0bf63 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.* import kotlinx.coroutines.channels.Channel.Factory.OPTIONAL_CHANNEL import kotlinx.coroutines.flow.internal.* import kotlinx.coroutines.internal.* +import kotlinx.coroutines.sync.* import kotlin.coroutines.* import kotlin.jvm.* import kotlinx.coroutines.flow.unsafeFlow as flow @@ -149,16 +150,15 @@ private class ChannelFlowMerge( // The actual merge implementation with concurrency limit private suspend fun mergeImpl(scope: CoroutineScope, collector: ConcurrentFlowCollector) { - val semaphore = Channel(concurrency) + val semaphore = Semaphore(concurrency) @Suppress("UNCHECKED_CAST") flow.collect { inner -> - // TODO real semaphore (#94) - semaphore.send(Unit) // Acquire concurrency permit + semaphore.acquire() // Acquire concurrency permit scope.launch { try { inner.collect(collector) } finally { - semaphore.receive() // Release concurrency permit + semaphore.release() // Release concurrency permit } } } From 18e3a4a9328acda42db22fca3046dbb143fe2499 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 6 Jun 2019 23:25:51 +0300 Subject: [PATCH 55/56] Mark Flow.collect as internal to prevent its direct implementation and provide AbstractFlow instead that enforces context preservation guarantees --- .../kotlinx-coroutines-core.txt | 6 ++ .../common/src/flow/Flow.kt | 50 +++++++++++++++- .../common/test/flow/FlowInvariantsTest.kt | 59 +++++++++++-------- 3 files changed, 89 insertions(+), 26 deletions(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 86a2203aba..d48cb51800 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -780,6 +780,12 @@ public final class kotlinx/coroutines/channels/TickerMode : java/lang/Enum { public static fun values ()[Lkotlinx/coroutines/channels/TickerMode; } +public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/Flow { + public fun ()V + public final fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun collectSafely (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public abstract interface class kotlinx/coroutines/flow/Flow { public abstract fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index 9e96780e88..a60598c003 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -5,6 +5,8 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.SafeCollector +import kotlin.coroutines.* /** * A cold asynchronous data stream that sequentially emits values @@ -112,6 +114,47 @@ import kotlinx.coroutines.* @ExperimentalCoroutinesApi public interface Flow { + /** + * Accepts the given [collector] and [emits][FlowCollector.emit] values into it. + * This method should never be implemented or used directly. + * + * The only way to implement flow interface directly is to extend [AbstractFlow]. + * To collect it into the specific collector, either `collector.emitAll(flow)` or `collect { }` extension should be used. + * Such limitation ensures that context preservation property is not violated and prevents most of the developer mistakes + * related to concurrency, inconsistent flow dispatchers and cancellation. + */ + @InternalCoroutinesApi + public suspend fun collect(collector: FlowCollector) +} + +/** + * Base class to extend to have a stateful implementation of the flow. + * It tracks all the properties required for context preservation and throws [IllegalStateException] if any of the properties are violated. + * Example of the implementation: + * ``` + * // list.asFlow() + collect counter + * class CountingListFlow(private val values: List) : AbstractFlow() { + * private val collectedCounter = AtomicInteger(0) + * + * override suspend fun collectSafely(collector: FlowCollector) { + * collectedCounter.incrementAndGet() // Increment collected counter + * values.forEach { // Emit all the values + * collector.emit(it) + * } + * } + * + * fun toDiagnosticString(): String = "Flow with values $values was collected ${collectedCounter.value} times" + * } + * ``` + */ +@FlowPreview +public abstract class AbstractFlow : Flow { + + @InternalCoroutinesApi + public final override suspend fun collect(collector: FlowCollector) { + collectSafely(SafeCollector(collector, collectContext = coroutineContext)) + } + /** * Accepts the given [collector] and [emits][FlowCollector.emit] values into it. * @@ -119,10 +162,11 @@ public interface Flow { * 1) It should not change the coroutine context (e.g. with `withContext(Dispatchers.IO)`) when emitting values. * The emission should happen in the context of the [collect] call. * Please refer to the top-level [Flow] documentation for more details. - * * 2) It should serialize calls to [emit][FlowCollector.emit] as [FlowCollector] implementations are not - * thread safe by default. + * thread-safe by default. * To automatically serialize emissions [channelFlow] builder can be used instead of [flow] + * + * @throws IllegalStateException if any of the invariants are violated. */ - public suspend fun collect(collector: FlowCollector) + public abstract suspend fun collectSafely(collector: FlowCollector) } diff --git a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt index 0659166557..98406869e5 100644 --- a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt @@ -7,12 +7,37 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlin.coroutines.* +import kotlin.reflect.* import kotlin.test.* class FlowInvariantsTest : TestBase() { + private fun runParametrizedTest( + expectedException: KClass? = null, + testBody: suspend (flowFactory: (suspend FlowCollector.() -> Unit) -> Flow) -> Unit + ) = runTest { + val r1 = runCatching { testBody { flow(it) } }.exceptionOrNull() + check(r1, expectedException) + reset() + + val r2 = runCatching { testBody { abstractFlow(it) } }.exceptionOrNull() + check(r2, expectedException) + } + + private fun abstractFlow(block: suspend FlowCollector.() -> Unit): Flow = object : AbstractFlow() { + override suspend fun collectSafely(collector: FlowCollector) { + collector.block() + } + } + + private fun check(exception: Throwable?, expectedException: KClass?) { + if (expectedException != null && exception == null) fail("Expected $expectedException, but test completed successfully") + if (expectedException != null && exception != null) assertTrue(expectedException.isInstance(exception)) + if (expectedException == null && exception != null) throw exception + } + @Test - fun testWithContextContract() = runTest({ it is IllegalStateException }) { + fun testWithContextContract() = runParametrizedTest(IllegalStateException::class) { flow -> flow { kotlinx.coroutines.withContext(NonCancellable) { emit(1) @@ -23,7 +48,7 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testWithDispatcherContractViolated() = runTest({ it is IllegalStateException }) { + fun testWithDispatcherContractViolated() = runParametrizedTest(IllegalStateException::class) { flow -> flow { kotlinx.coroutines.withContext(NamedDispatchers("foo")) { emit(1) @@ -34,7 +59,7 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testCachedInvariantCheckResult() = runTest { + fun testCachedInvariantCheckResult() = runParametrizedTest { flow -> flow { emit(1) @@ -55,7 +80,7 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testWithNameContractViolated() = runTest({ it is IllegalStateException }) { + fun testWithNameContractViolated() = runParametrizedTest(IllegalStateException::class) { flow -> flow { kotlinx.coroutines.withContext(CoroutineName("foo")) { emit(1) @@ -86,8 +111,8 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testScopedJob() = runTest({ it is IllegalStateException }) { - flow { emit(1) }.buffer(EmptyCoroutineContext).collect { + fun testScopedJob() = runParametrizedTest(IllegalStateException::class) { flow -> + flow { emit(1) }.buffer(EmptyCoroutineContext, flow).collect { expect(1) } @@ -95,8 +120,8 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testScopedJobWithViolation() = runTest({ it is IllegalStateException }) { - flow { emit(1) }.buffer(Dispatchers.Unconfined).collect { + fun testScopedJobWithViolation() = runParametrizedTest(IllegalStateException::class) { flow -> + flow { emit(1) }.buffer(Dispatchers.Unconfined, flow).collect { expect(1) } @@ -104,7 +129,7 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testMergeViolation() = runTest { + fun testMergeViolation() = runParametrizedTest { flow -> fun Flow.merge(other: Flow): Flow = flow { coroutineScope { launch { @@ -130,17 +155,6 @@ class FlowInvariantsTest : TestBase() { assertFailsWith { flow.trickyMerge(flow).toList() } } - // TODO merge artifact - private fun channelFlow(bufferSize: Int = 16, @BuilderInference block: suspend ProducerScope.() -> Unit): Flow = - flow { - coroutineScope { - val channel = produce(capacity = bufferSize, block = block) - channel.consumeEach { value -> - emit(value) - } - } - } - @Test fun testNoMergeViolation() = runTest { fun Flow.merge(other: Flow): Flow = channelFlow { @@ -167,7 +181,7 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testScopedCoroutineNoViolation() = runTest { + fun testScopedCoroutineNoViolation() = runParametrizedTest { flow -> fun Flow.buffer(): Flow = flow { coroutineScope { val channel = produce { @@ -180,11 +194,10 @@ class FlowInvariantsTest : TestBase() { } } } - assertEquals(listOf(1, 1), flowOf(1, 1).buffer().toList()) } - private fun Flow.buffer(coroutineContext: CoroutineContext): Flow = flow { + private fun Flow.buffer(coroutineContext: CoroutineContext, flow: (suspend FlowCollector.() -> Unit) -> Flow): Flow = flow { coroutineScope { val channel = Channel() launch { From 6139ed387a1cebb3cb8af2985a58e8968f9643f0 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 6 Jun 2019 23:29:16 +0300 Subject: [PATCH 56/56] Version 1.3.0-M1 --- CHANGES.md | 38 +++++++++++++++++++ README.md | 16 ++++---- gradle.properties | 2 +- kotlinx-coroutines-debug/README.md | 4 +- kotlinx-coroutines-test/README.md | 2 +- ui/coroutines-guide-ui.md | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 8 files changed, 53 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 61aabfebd1..c894b8b30e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,43 @@ # Change log for kotlinx.coroutines +## Version 1.3.0-M1 + +Flow: + * Core `Flow` interfaces and operators are graduated from preview status to experimental. + * Context preservation invariant rework (#1210). + * `channelFlow` and `callbackFlow` replacements for `flowViaChannel` for concurrent flows or callback-based APIs. + * `flow` prohibits emissions from non-scoped coroutines by default and recommends to use `channelFlow` instead to avoid most of the concurrency-related bugs. + * Flow cannot be implemented directly + * `AbstractFlow` is introduced for extension (e.g. for managing state) and ensures all context preservation invariants. + * Buffer size is decoupled from all operators that imply channel usage (#1233) + * `buffer` operator can be used to adjust buffer size of any buffer-dependent operator (e.g. `channelFlow`, `flowOn` and `flatMapMerge`). + * `conflate` operator is introduced. + * Flow performance is significantly improved. + * New operators: `scan`, `scanReduce`, `first`, `emitAll`. + * `flowWith` and `flowViaChannel` are deprecated. + * `retry` ignores cancellation exceptions from upstream when the flow was externally cancelled (#1122). + * `combineLatest` overloads for multiple flows (#1193). + * Fixed numerical overflow in `drop` operator. + +Channels: + * `consumeEach` is promoted to experimental API (#1080). + * Conflated channels always deliver the latest value after closing (#332, #1235). + * Non-suspending `ChannelIterator.next` to improve iteration performance (#1162). + * Channel exception types are consistent with `produce` and are no longer swallowed as cancellation exceptions in case of programmatic errors (#957, #1128). + * All operators on channels (that were prone to coroutine leaks) are deprecated in the favor of `Flow`. + +General changes: + * Kotlin updated to 1.3.31 + * `Semaphore` implementation (#1088) + * Loading of `Dispatchers.Main` is tweaked so the latest version of R8 can completely remove I/O when loading it (#1231). + * Performace of all JS dispatchers is significantly improved (#820). + * `withContext` checks cancellation status on exit to make reasoning about sequential concurrent code easier (#1177). + * Consistent exception handling mechanism for complex hierarchies (#689). + * Convenient overload for `CoroutinesTimeout.seconds` (#1184). + * Fix cancellation bug in onJoin (#1130). + * Prevent internal names clash that caused errors for ProGuard (#1159). + * POSIX's `nanosleep` as `delay` in `runBlocking ` in K/N (#1225). + ## Version 1.2.1 Major: diff --git a/README.md b/README.md index e8e37a8c41..a31c31c4ba 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.2.1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.2.1) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.0-M1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.0-M1) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. This is a companion version for Kotlin `1.3.31` release. @@ -81,7 +81,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.2.1 + 1.3.0-M1 ``` @@ -99,7 +99,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1' } ``` @@ -125,7 +125,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1") } ``` @@ -144,7 +144,7 @@ Make sure that you have either `jcenter()` or `mavenCentral()` in the list of re Core modules of `kotlinx.coroutines` are also available for [Kotlin/JS](#js) and [Kotlin/Native](#native). In common code that should get compiled for different platforms, add dependency to -[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.2.1/jar) +[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.0-M1/jar) (follow the link to get the dependency declaration snippet). ### Android @@ -153,7 +153,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-M1' ``` This gives you access to Android [Dispatchers.Main](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-android/kotlinx.coroutines.android/kotlinx.coroutines.-dispatchers/index.html) @@ -172,7 +172,7 @@ R8 is a replacement for ProGuard in Android ecosystem, it is enabled by default ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.2.1/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.0-M1/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -180,7 +180,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.2.1/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.0-M1/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. diff --git a/gradle.properties b/gradle.properties index 387d56e166..643d36a682 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kotlin -version=1.2.1-SNAPSHOT +version=1.3.0-M1-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.3.31 diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index e3fdcf52ff..ba7df50939 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -18,7 +18,7 @@ of coroutines hierarchy referenced by a [Job] or [CoroutineScope] instances usin Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.2.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.0-M1' } ``` @@ -57,7 +57,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent It is possible to use this module as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.2.1.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.0-M1.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 8b9061efee..77ed6e298d 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0-M1' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 26144fac43..7dcf907447 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -165,7 +165,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-M1" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 342b103ab2..ab28563c68 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -19,5 +19,5 @@ org.gradle.jvmargs=-Xmx1536m kotlin.coroutines=enable kotlin_version=1.3.31 -coroutines_version=1.2.1 +coroutines_version=1.3.0-M1 diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 342b103ab2..ab28563c68 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -19,5 +19,5 @@ org.gradle.jvmargs=-Xmx1536m kotlin.coroutines=enable kotlin_version=1.3.31 -coroutines_version=1.2.1 +coroutines_version=1.3.0-M1