Norbi
07/25/2023, 1:15 PMkotlinx-io
... why choose an API as the base of the new library where I/O operations are not? 🤔suspend
> ... I believe that the overhead of coroutines was unacceptable for low-level io operations.I still think that non-suspend I/O operations would be unnatural in Kotlin... (Further details in comment.)
Norbi
07/25/2023, 1:16 PMI claim:
• suspend function calls are fast
• non-suspending function calls are very fast
• I/O libraries like JSON parsers, protobuf encoders, HTTP processors etc. are quite sensitive to the cost of making function calls
• Doing oneand then doing a bunch of blockingwithContext(<http://Dispatchers.IO|Dispatchers.IO>)
• calls is likely to be meaningfully faster than doing many suspending calls
Retrofit’s suspend feature works this way. I think both its API and its
performance are great. Doing a suspend call for every byte processed
would harm performance without improving the developer experience.See https://kotlinlang.slack.com/archives/C0922A726/p1690214615519589?thread_ts=1689261431.918149&cid=C0922A726 and the follow-up replies.
franztesca
07/25/2023, 2:16 PMwhile (true) {
readOneByteSuspendThanksToDispatchersIO()
}
can be very inefficient due to the context switch (note: suspending functions are not guaranteed to be invoked from the optimized Dispatchers.Default only.
At the same time, doing a blocking a IO on Android ends up being an ANR very easily, and on other platforms there may not be blocking methods (OPFS on browser, I'm looking at you).
A possible solution is to have low-level blocking APIs and higher-level (in a separate module) suspending APIs:
// kotlinx.io.lowlevel
fun readOneByteFromStream() <- blocking
// kotlinx.io.highlevel
fun readFile(): Flow<BytesChunk> <- suspend
Another option would be to add a RequireOptIn annotation for blocking methods, making the user super aware of the effect of that function and let the user decide when to wrap in a separate dispatcher
// kotlinx.io
@Blocking
fun readOneByteFromStream()
@Blocking
fun readFile(processData: (BytesChunk) -> Unit)
// user side
@OptIn(Blocking)
fun myReadFile(): Flow<BytesChunk> = flow {
readFile { emit(it) }
}.flowOn(<http://Dispatchers.IO|Dispatchers.IO>)
Or even better, have a blocking API with typed coeffect thanks to context receivers
// <http://kotlinx.io|kotlinx.io>
context(BlockingIO)
fun readOneByteFromStream()
context(BlockingIO)
fun readFile(processData: (BytesChunk) -> Unit)
fun <R> withBlockingIO(block: BlockingIO.() -> R): R = BlockingIOSingleton.block()
suspend fun <R> withSuspendingIO(block: BlockingIO.() -> R): R = withContext(<http://Dispatchers.IO|Dispatchers.IO>) { BlockingIOSingleon.block() }
// user side
fun myReadFile(): Flow<BytesChunk> = channelFlow {
withSuspendingIO {
readFile { channel.send(it) }
}
}
This would maybe make happier both parties: a typed coeffect library where the handling (blocking or suspending through a separate dispatcher) can be decided by the user...
🤔CLOVIS
07/25/2023, 2:46 PMJeff Lockhart
07/25/2023, 3:01 PMLoney Chou
07/31/2023, 5:27 PM