Skip to content

Commit

Permalink
Optimize the size of the coroutines library in Android projects (#1282)
Browse files Browse the repository at this point in the history
* Includes additional R8 rules to disable debugging & stack-trace recovery in optimized
Android builds. Additional savings with AGP 4.0.0-alpha06 (r8-2.0.4-dev) are ~16kb
in uncompressed DEX size.
* Tests are modified to verify that the classes that are supposed to be removed are
indeed removed.
* Cleaner build logic without error-prone "return" in the middle
* Report the size of optimized Android Dex as teamcity metric
  • Loading branch information
elizarov committed Feb 14, 2020
1 parent 1ac3dc2 commit bf9509d
Show file tree
Hide file tree
Showing 19 changed files with 189 additions and 108 deletions.
7 changes: 2 additions & 5 deletions README.md
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
Expand Up @@ -138,10 +138,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 @@ -159,30 +158,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
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
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
@@ -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
@@ -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
@@ -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
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
@@ -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
@@ -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

0 comments on commit bf9509d

Please sign in to comment.