Skip to content

Optimize the size of the coroutines library in Android projects #1282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,8 @@ threads are handled by Android runtime.

#### R8 and ProGuard

For R8 no actions required, it will take obfuscation rules from the jar.

For Proguard you need to add options from [coroutines.pro](kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro) to your rules manually.

R8 is a replacement for ProGuard in Android ecosystem, it is enabled by default since Android gradle plugin 3.4.0 (3.3.0-beta also had it enabled).
R8 and ProGuard rules are bundled into the [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module.
For more details see ["Optimization" section for Android](ui/kotlinx-coroutines-android/README.md#optimization).

### JS

Expand Down
26 changes: 14 additions & 12 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,9 @@ apiValidation {
ignoredPackages += "kotlinx.coroutines.internal"
}


// Configure repositories
allprojects {
apply plugin: 'kotlinx-atomicfu' // it also adds all the necessary dependencies
def projectName = it.name
String projectName = it.name
repositories {
/*
* google should be first in the repository list because some of the play services
Expand All @@ -158,30 +157,33 @@ allprojects {
maven { url "https://kotlin.bintray.com/kotlin-eap" }
maven { url "https://kotlin.bintray.com/kotlinx" }
}
}

if (projectName == rootModule || projectName == coreModule) return

// Add dependency to core source sets. Core is configured in kx-core/build.gradle
// Add dependency to core source sets. Core is configured in kx-core/build.gradle
configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != coreModule }) {
evaluationDependsOn(":$coreModule")
if (sourceless.contains(projectName)) return

def platform = platformOf(it)
apply from: rootProject.file("gradle/compile-${platform}.gradle")

dependencies {
// See comment below for rationale, it will be replaced with "project" dependency
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"

// the only way IDEA can resolve test classes
testCompile project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
}
}

// Configure subprojects with Kotlin sources
configure(subprojects.findAll { !sourceless.contains(it.name) }) {
// Use atomicfu plugin, it also adds all the necessary dependencies
apply plugin: 'kotlinx-atomicfu'

// Configure options for all Kotlin compilation tasks
tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
kotlinOptions.freeCompilerArgs += experimentalAnnotations.collect { "-Xuse-experimental=" + it }
kotlinOptions.freeCompilerArgs += "-progressive"
kotlinOptions.freeCompilerArgs += "-XXLanguage:+InlineClasses"
// Binary compatibility support
kotlinOptions.freeCompilerArgs += ["-Xdump-declarations-to=${buildDir}/visibilities.json"]
// Remove null assertions to get smaller bytecode on Android
kotlinOptions.freeCompilerArgs += ["-Xno-param-assertions", "-Xno-receiver-assertions", "-Xno-call-assertions"]
}
}

Expand Down
10 changes: 10 additions & 0 deletions docs/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
* [Stacktrace recovery machinery](#stacktrace-recovery-machinery)
* [Debug agent](#debug-agent)
* [Debug agent and Android](#debug-agent-and-android)
* [Android optimization](#android-optimization)

<!--- END -->

## Debugging coroutines

Debugging asynchronous programs is challenging, because multiple concurrent coroutines are typically working at the same time.
To help with that, `kotlinx.coroutines` comes with additional features for debugging: debug mode, stacktrace recovery
and debug agent.
Expand Down Expand Up @@ -86,6 +88,14 @@ java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/Mana
at kotlinx.coroutines.debug.DebugProbes.install(DebugProbes.kt:49)
-->

## Android optimization

In optimized (release) builds with R8 version 1.6.0 or later both
[Debugging mode](../../docs/debugging.md#debug-mode) and
[Stacktrace recovery](../../docs/debugging.md#stacktrace-recovery)
are permanently turned off.
For more details see ["Optimization" section for Android](../ui/kotlinx-coroutines-android/README.md#optimization).

<!--- MODULE kotlinx-coroutines-core -->
<!--- INDEX kotlinx.coroutines -->
[DEBUG_PROPERTY_NAME]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html
Expand Down
5 changes: 4 additions & 1 deletion kotlinx-coroutines-core/common/src/selects/Select.kt
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,10 @@ internal class SelectBuilderImpl<in R>(
assert { isSelected } // "Must be selected first"
_result.loop { result ->
when {
result === UNDECIDED -> if (_result.compareAndSet(UNDECIDED, value())) return
result === UNDECIDED -> {
val update = value()
if (_result.compareAndSet(UNDECIDED, update)) return
}
result === COROUTINE_SUSPENDED -> if (_result.compareAndSet(COROUTINE_SUSPENDED, RESUMED)) {
block()
return
Expand Down
6 changes: 4 additions & 2 deletions kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2018 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 kotlinx.coroutines.internal
Expand All @@ -16,7 +16,9 @@ internal actual typealias ReentrantLock = java.util.concurrent.locks.ReentrantLo

internal actual inline fun <T> ReentrantLock.withLock(action: () -> T) = this.withLockJvm(action)

internal actual fun <E> identitySet(expectedSize: Int): MutableSet<E> = Collections.newSetFromMap(IdentityHashMap(expectedSize))
@Suppress("NOTHING_TO_INLINE") // So that R8 can completely remove ConcurrentKt class
internal actual inline fun <E> identitySet(expectedSize: Int): MutableSet<E> =
Collections.newSetFromMap(IdentityHashMap(expectedSize))

private val REMOVE_FUTURE_ON_CANCEL: Method? = try {
ScheduledThreadPoolExecutor::class.java.getMethod("setRemoveOnCancelPolicy", Boolean::class.java)
Expand Down
34 changes: 26 additions & 8 deletions kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.coroutines.internal

import kotlinx.coroutines.*
Expand Down Expand Up @@ -30,11 +34,12 @@ internal object MainDispatcherLoader {
MainDispatcherFactory::class.java.classLoader
).iterator().asSequence().toList()
}
@Suppress("ConstantConditionIf")
factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
?: MissingMainCoroutineDispatcher(null)
?: createMissingDispatcher()
} catch (e: Throwable) {
// Service loader can throw an exception as well
MissingMainCoroutineDispatcher(e)
createMissingDispatcher(e)
}
}
}
Expand All @@ -51,13 +56,30 @@ public fun MainDispatcherFactory.tryCreateDispatcher(factories: List<MainDispatc
try {
createDispatcher(factories)
} catch (cause: Throwable) {
MissingMainCoroutineDispatcher(cause, hintOnError())
createMissingDispatcher(cause, hintOnError())
}

/** @suppress */
@InternalCoroutinesApi
public fun MainCoroutineDispatcher.isMissing(): Boolean = this is MissingMainCoroutineDispatcher

// R8 optimization hook, not const on purpose to enable R8 optimizations via "assumenosideeffects"
@Suppress("MayBeConstant")
private val SUPPORT_MISSING = true

@Suppress("ConstantConditionIf")
private fun createMissingDispatcher(cause: Throwable? = null, errorHint: String? = null) =
if (SUPPORT_MISSING) MissingMainCoroutineDispatcher(cause, errorHint) else
cause?.let { throw it } ?: throwMissingMainDispatcherException()

internal fun throwMissingMainDispatcherException(): Nothing {
throw IllegalStateException(
"Module with the Main dispatcher is missing. " +
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' " +
"and ensure it has the same version as 'kotlinx-coroutines-core'"
)
}

private class MissingMainCoroutineDispatcher(
private val cause: Throwable?,
private val errorHint: String? = null
Expand Down Expand Up @@ -85,11 +107,7 @@ private class MissingMainCoroutineDispatcher(

private fun missing(): Nothing {
if (cause == null) {
throw IllegalStateException(
"Module with the Main dispatcher is missing. " +
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' " +
"and ensure it has the same version as 'kotlinx-coroutines-core'"
)
throwMissingMainDispatcherException()
} else {
val message = "Module with the Main dispatcher had failed to initialize" + (errorHint?.let { ". $it" } ?: "")
throw IllegalStateException(message, cause)
Expand Down
12 changes: 8 additions & 4 deletions kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2018 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.
*/

@file:Suppress("UNCHECKED_CAST")
Expand Down Expand Up @@ -52,7 +52,8 @@ private fun <E : Throwable> E.sanitizeStackTrace(): E {
return this
}

internal actual fun <E : Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E {
@Suppress("NOTHING_TO_INLINE") // Inline for better R8 optimization
internal actual inline fun <E : Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E {
if (!RECOVER_STACK_TRACES || continuation !is CoroutineStackFrame) return exception
return recoverFromStackFrame(exception, continuation)
}
Expand Down Expand Up @@ -155,8 +156,11 @@ internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothin
}
}

internal actual fun <E : Throwable> unwrap(exception: E): E {
if (!RECOVER_STACK_TRACES) return exception
@Suppress("NOTHING_TO_INLINE") // Inline for better R8 optimizations
internal actual inline fun <E : Throwable> unwrap(exception: E): E =
if (!RECOVER_STACK_TRACES) exception else unwrapImpl(exception)

internal fun <E : Throwable> unwrapImpl(exception: E): E {
val cause = exception.cause
// Fast-path to avoid array cloning
if (cause == null || cause.javaClass != exception.javaClass) {
Expand Down
22 changes: 11 additions & 11 deletions kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import kotlin.random.*
* Only [corePoolSize] workers can be created for regular CPU tasks)
*
* ### Support for blocking tasks
* The scheduler also supports the notion of [blocking][TaskMode.PROBABLY_BLOCKING] tasks.
* The scheduler also supports the notion of [blocking][TASK_PROBABLY_BLOCKING] tasks.
* When executing or enqueuing blocking tasks, the scheduler notifies or creates one more worker in
* addition to core pool size, so at any given moment, it has [corePoolSize] threads (potentially not yet created)
* to serve CPU-bound tasks. To properly guarantee liveness, the scheduler maintains
Expand Down Expand Up @@ -394,7 +394,7 @@ internal class CoroutineScheduler(
}
val skipUnpark = tailDispatch && currentWorker != null
// Checking 'task' instead of 'notAdded' is completely okay
if (task.mode == TaskMode.NON_BLOCKING) {
if (task.mode == TASK_NON_BLOCKING) {
if (skipUnpark) return
signalCpuWork()
} else {
Expand Down Expand Up @@ -499,7 +499,7 @@ internal class CoroutineScheduler(
*/
if (state === WorkerState.TERMINATED) return task
// Do not add CPU tasks in local queue if we are not able to execute it
if (task.mode === TaskMode.NON_BLOCKING && state === WorkerState.BLOCKING) {
if (task.mode == TASK_NON_BLOCKING && state === WorkerState.BLOCKING) {
return task
}
mayHaveLocalTasks = true
Expand Down Expand Up @@ -739,16 +739,16 @@ internal class CoroutineScheduler(
afterTask(taskMode)
}

private fun beforeTask(taskMode: TaskMode) {
if (taskMode == TaskMode.NON_BLOCKING) return
private fun beforeTask(taskMode: Int) {
if (taskMode == TASK_NON_BLOCKING) return
// Always notify about new work when releasing CPU-permit to execute some blocking task
if (tryReleaseCpu(WorkerState.BLOCKING)) {
signalCpuWork()
}
}

private fun afterTask(taskMode: TaskMode) {
if (taskMode == TaskMode.NON_BLOCKING) return
private fun afterTask(taskMode: Int) {
if (taskMode == TASK_NON_BLOCKING) return
decrementBlockingTasks()
val currentState = state
// Shutdown sequence of blocking dispatcher
Expand Down Expand Up @@ -846,10 +846,10 @@ internal class CoroutineScheduler(
}

// It is invoked by this worker when it finds a task
private fun idleReset(mode: TaskMode) {
private fun idleReset(mode: Int) {
terminationDeadline = 0L // reset deadline for termination
if (state == WorkerState.PARKING) {
assert { mode == TaskMode.PROBABLY_BLOCKING }
assert { mode == TASK_PROBABLY_BLOCKING }
state = WorkerState.BLOCKING
}
}
Expand Down Expand Up @@ -926,12 +926,12 @@ internal class CoroutineScheduler(

enum class WorkerState {
/**
* Has CPU token and either executes [TaskMode.NON_BLOCKING] task or tries to find one.
* Has CPU token and either executes [TASK_NON_BLOCKING] task or tries to find one.
*/
CPU_ACQUIRED,

/**
* Executing task with [TaskMode.PROBABLY_BLOCKING].
* Executing task with [TASK_PROBABLY_BLOCKING].
*/
BLOCKING,

Expand Down
8 changes: 4 additions & 4 deletions kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2018 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 kotlinx.coroutines.scheduling
Expand Down Expand Up @@ -85,7 +85,7 @@ open class ExperimentalCoroutineDispatcher(
*/
public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
return LimitingDispatcher(this, parallelism, TaskMode.PROBABLY_BLOCKING)
return LimitingDispatcher(this, parallelism, TASK_PROBABLY_BLOCKING)
}

/**
Expand All @@ -98,7 +98,7 @@ open class ExperimentalCoroutineDispatcher(
public fun limited(parallelism: Int): CoroutineDispatcher {
require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" }
return LimitingDispatcher(this, parallelism, TaskMode.NON_BLOCKING)
return LimitingDispatcher(this, parallelism, TASK_NON_BLOCKING)
}

internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
Expand Down Expand Up @@ -132,7 +132,7 @@ open class ExperimentalCoroutineDispatcher(
private class LimitingDispatcher(
val dispatcher: ExperimentalCoroutineDispatcher,
val parallelism: Int,
override val taskMode: TaskMode
override val taskMode: Int
) : ExecutorCoroutineDispatcher(), TaskContext, Executor {

private val queue = ConcurrentLinkedQueue<Runnable>()
Expand Down
29 changes: 13 additions & 16 deletions kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2018 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 kotlinx.coroutines.scheduling
Expand Down Expand Up @@ -51,26 +51,23 @@ internal val IDLE_WORKER_KEEP_ALIVE_NS = TimeUnit.SECONDS.toNanos(
@JvmField
internal var schedulerTimeSource: TimeSource = NanoTimeSource

internal enum class TaskMode {

/**
* Marker indicating that task is CPU-bound and will not block
*/
NON_BLOCKING,
/**
* Marker indicating that task is CPU-bound and will not block
*/
internal const val TASK_NON_BLOCKING = 0

/**
* Marker indicating that task may potentially block, thus giving scheduler a hint that additional thread may be required
*/
PROBABLY_BLOCKING,
}
/**
* Marker indicating that task may potentially block, thus giving scheduler a hint that additional thread may be required
*/
internal const val TASK_PROBABLY_BLOCKING = 1

internal interface TaskContext {
val taskMode: TaskMode
val taskMode: Int // TASK_XXX
fun afterTask()
}

internal object NonBlockingContext : TaskContext {
override val taskMode: TaskMode = TaskMode.NON_BLOCKING
override val taskMode: Int = TASK_NON_BLOCKING

override fun afterTask() {
// Nothing for non-blocking context
Expand All @@ -82,10 +79,10 @@ internal abstract class Task(
@JvmField var taskContext: TaskContext
) : Runnable {
constructor() : this(0, NonBlockingContext)
inline val mode: TaskMode get() = taskContext.taskMode
inline val mode: Int get() = taskContext.taskMode // TASK_XXX
}

internal inline val Task.isBlocking get() = taskContext.taskMode == TaskMode.PROBABLY_BLOCKING
internal inline val Task.isBlocking get() = taskContext.taskMode == TASK_PROBABLY_BLOCKING

// Non-reusable Task implementation to wrap Runnable instances that do not otherwise implement task
internal class TaskImpl(
Expand Down
Loading