Skip to content

Commit bf9509d

Browse files
authored
Optimize the size of the coroutines library in Android projects (#1282)
* 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
1 parent 1ac3dc2 commit bf9509d

File tree

19 files changed

+189
-108
lines changed

19 files changed

+189
-108
lines changed

README.md

+2-5
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,8 @@ threads are handled by Android runtime.
164164

165165
#### R8 and ProGuard
166166

167-
For R8 no actions required, it will take obfuscation rules from the jar.
168-
169-
For Proguard you need to add options from [coroutines.pro](kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro) to your rules manually.
170-
171-
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).
167+
R8 and ProGuard rules are bundled into the [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module.
168+
For more details see ["Optimization" section for Android](ui/kotlinx-coroutines-android/README.md#optimization).
172169

173170
### JS
174171

build.gradle

+14-12
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,9 @@ apiValidation {
138138
ignoredPackages += "kotlinx.coroutines.internal"
139139
}
140140

141-
141+
// Configure repositories
142142
allprojects {
143-
apply plugin: 'kotlinx-atomicfu' // it also adds all the necessary dependencies
144-
def projectName = it.name
143+
String projectName = it.name
145144
repositories {
146145
/*
147146
* google should be first in the repository list because some of the play services
@@ -159,30 +158,33 @@ allprojects {
159158
maven { url "https://kotlin.bintray.com/kotlin-eap" }
160159
maven { url "https://kotlin.bintray.com/kotlinx" }
161160
}
161+
}
162162

163-
if (projectName == rootModule || projectName == coreModule) return
164-
165-
// Add dependency to core source sets. Core is configured in kx-core/build.gradle
163+
// Add dependency to core source sets. Core is configured in kx-core/build.gradle
164+
configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != coreModule }) {
166165
evaluationDependsOn(":$coreModule")
167-
if (sourceless.contains(projectName)) return
168-
169166
def platform = platformOf(it)
170167
apply from: rootProject.file("gradle/compile-${platform}.gradle")
171-
172168
dependencies {
173169
// See comment below for rationale, it will be replaced with "project" dependency
174170
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
175-
176171
// the only way IDEA can resolve test classes
177172
testCompile project(":$coreModule").kotlin.targets.jvm.compilations.test.output.allOutputs
178173
}
174+
}
175+
176+
// Configure subprojects with Kotlin sources
177+
configure(subprojects.findAll { !sourceless.contains(it.name) }) {
178+
// Use atomicfu plugin, it also adds all the necessary dependencies
179+
apply plugin: 'kotlinx-atomicfu'
179180

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

docs/debugging.md

+10
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
* [Stacktrace recovery machinery](#stacktrace-recovery-machinery)
99
* [Debug agent](#debug-agent)
1010
* [Debug agent and Android](#debug-agent-and-android)
11+
* [Android optimization](#android-optimization)
1112

1213
<!--- END -->
1314

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

91+
## Android optimization
92+
93+
In optimized (release) builds with R8 version 1.6.0 or later both
94+
[Debugging mode](../../docs/debugging.md#debug-mode) and
95+
[Stacktrace recovery](../../docs/debugging.md#stacktrace-recovery)
96+
are permanently turned off.
97+
For more details see ["Optimization" section for Android](../ui/kotlinx-coroutines-android/README.md#optimization).
98+
8999
<!--- MODULE kotlinx-coroutines-core -->
90100
<!--- INDEX kotlinx.coroutines -->
91101
[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

kotlinx-coroutines-core/common/src/selects/Select.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,10 @@ internal class SelectBuilderImpl<in R>(
264264
assert { isSelected } // "Must be selected first"
265265
_result.loop { result ->
266266
when {
267-
result === UNDECIDED -> if (_result.compareAndSet(UNDECIDED, value())) return
267+
result === UNDECIDED -> {
268+
val update = value()
269+
if (_result.compareAndSet(UNDECIDED, update)) return
270+
}
268271
result === COROUTINE_SUSPENDED -> if (_result.compareAndSet(COROUTINE_SUSPENDED, RESUMED)) {
269272
block()
270273
return

kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package kotlinx.coroutines.internal
@@ -16,7 +16,9 @@ internal actual typealias ReentrantLock = java.util.concurrent.locks.ReentrantLo
1616

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

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

2123
private val REMOVE_FUTURE_ON_CANCEL: Method? = try {
2224
ScheduledThreadPoolExecutor::class.java.getMethod("setRemoveOnCancelPolicy", Boolean::class.java)

kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/*
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
15
package kotlinx.coroutines.internal
26

37
import kotlinx.coroutines.*
@@ -30,11 +34,12 @@ internal object MainDispatcherLoader {
3034
MainDispatcherFactory::class.java.classLoader
3135
).iterator().asSequence().toList()
3236
}
37+
@Suppress("ConstantConditionIf")
3338
factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
34-
?: MissingMainCoroutineDispatcher(null)
39+
?: createMissingDispatcher()
3540
} catch (e: Throwable) {
3641
// Service loader can throw an exception as well
37-
MissingMainCoroutineDispatcher(e)
42+
createMissingDispatcher(e)
3843
}
3944
}
4045
}
@@ -51,13 +56,30 @@ public fun MainDispatcherFactory.tryCreateDispatcher(factories: List<MainDispatc
5156
try {
5257
createDispatcher(factories)
5358
} catch (cause: Throwable) {
54-
MissingMainCoroutineDispatcher(cause, hintOnError())
59+
createMissingDispatcher(cause, hintOnError())
5560
}
5661

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

66+
// R8 optimization hook, not const on purpose to enable R8 optimizations via "assumenosideeffects"
67+
@Suppress("MayBeConstant")
68+
private val SUPPORT_MISSING = true
69+
70+
@Suppress("ConstantConditionIf")
71+
private fun createMissingDispatcher(cause: Throwable? = null, errorHint: String? = null) =
72+
if (SUPPORT_MISSING) MissingMainCoroutineDispatcher(cause, errorHint) else
73+
cause?.let { throw it } ?: throwMissingMainDispatcherException()
74+
75+
internal fun throwMissingMainDispatcherException(): Nothing {
76+
throw IllegalStateException(
77+
"Module with the Main dispatcher is missing. " +
78+
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' " +
79+
"and ensure it has the same version as 'kotlinx-coroutines-core'"
80+
)
81+
}
82+
6183
private class MissingMainCoroutineDispatcher(
6284
private val cause: Throwable?,
6385
private val errorHint: String? = null
@@ -85,11 +107,7 @@ private class MissingMainCoroutineDispatcher(
85107

86108
private fun missing(): Nothing {
87109
if (cause == null) {
88-
throw IllegalStateException(
89-
"Module with the Main dispatcher is missing. " +
90-
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' " +
91-
"and ensure it has the same version as 'kotlinx-coroutines-core'"
92-
)
110+
throwMissingMainDispatcherException()
93111
} else {
94112
val message = "Module with the Main dispatcher had failed to initialize" + (errorHint?.let { ". $it" } ?: "")
95113
throw IllegalStateException(message, cause)

kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
@file:Suppress("UNCHECKED_CAST")
@@ -52,7 +52,8 @@ private fun <E : Throwable> E.sanitizeStackTrace(): E {
5252
return this
5353
}
5454

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

158-
internal actual fun <E : Throwable> unwrap(exception: E): E {
159-
if (!RECOVER_STACK_TRACES) return exception
159+
@Suppress("NOTHING_TO_INLINE") // Inline for better R8 optimizations
160+
internal actual inline fun <E : Throwable> unwrap(exception: E): E =
161+
if (!RECOVER_STACK_TRACES) exception else unwrapImpl(exception)
162+
163+
internal fun <E : Throwable> unwrapImpl(exception: E): E {
160164
val cause = exception.cause
161165
// Fast-path to avoid array cloning
162166
if (cause == null || cause.javaClass != exception.javaClass) {

kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt

+11-11
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import kotlin.random.*
7373
* Only [corePoolSize] workers can be created for regular CPU tasks)
7474
*
7575
* ### Support for blocking tasks
76-
* The scheduler also supports the notion of [blocking][TaskMode.PROBABLY_BLOCKING] tasks.
76+
* The scheduler also supports the notion of [blocking][TASK_PROBABLY_BLOCKING] tasks.
7777
* When executing or enqueuing blocking tasks, the scheduler notifies or creates one more worker in
7878
* addition to core pool size, so at any given moment, it has [corePoolSize] threads (potentially not yet created)
7979
* to serve CPU-bound tasks. To properly guarantee liveness, the scheduler maintains
@@ -394,7 +394,7 @@ internal class CoroutineScheduler(
394394
}
395395
val skipUnpark = tailDispatch && currentWorker != null
396396
// Checking 'task' instead of 'notAdded' is completely okay
397-
if (task.mode == TaskMode.NON_BLOCKING) {
397+
if (task.mode == TASK_NON_BLOCKING) {
398398
if (skipUnpark) return
399399
signalCpuWork()
400400
} else {
@@ -499,7 +499,7 @@ internal class CoroutineScheduler(
499499
*/
500500
if (state === WorkerState.TERMINATED) return task
501501
// Do not add CPU tasks in local queue if we are not able to execute it
502-
if (task.mode === TaskMode.NON_BLOCKING && state === WorkerState.BLOCKING) {
502+
if (task.mode == TASK_NON_BLOCKING && state === WorkerState.BLOCKING) {
503503
return task
504504
}
505505
mayHaveLocalTasks = true
@@ -739,16 +739,16 @@ internal class CoroutineScheduler(
739739
afterTask(taskMode)
740740
}
741741

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

750-
private fun afterTask(taskMode: TaskMode) {
751-
if (taskMode == TaskMode.NON_BLOCKING) return
750+
private fun afterTask(taskMode: Int) {
751+
if (taskMode == TASK_NON_BLOCKING) return
752752
decrementBlockingTasks()
753753
val currentState = state
754754
// Shutdown sequence of blocking dispatcher
@@ -846,10 +846,10 @@ internal class CoroutineScheduler(
846846
}
847847

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

927927
enum class WorkerState {
928928
/**
929-
* Has CPU token and either executes [TaskMode.NON_BLOCKING] task or tries to find one.
929+
* Has CPU token and either executes [TASK_NON_BLOCKING] task or tries to find one.
930930
*/
931931
CPU_ACQUIRED,
932932

933933
/**
934-
* Executing task with [TaskMode.PROBABLY_BLOCKING].
934+
* Executing task with [TASK_PROBABLY_BLOCKING].
935935
*/
936936
BLOCKING,
937937

kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package kotlinx.coroutines.scheduling
@@ -85,7 +85,7 @@ open class ExperimentalCoroutineDispatcher(
8585
*/
8686
public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
8787
require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
88-
return LimitingDispatcher(this, parallelism, TaskMode.PROBABLY_BLOCKING)
88+
return LimitingDispatcher(this, parallelism, TASK_PROBABLY_BLOCKING)
8989
}
9090

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

104104
internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
@@ -132,7 +132,7 @@ open class ExperimentalCoroutineDispatcher(
132132
private class LimitingDispatcher(
133133
val dispatcher: ExperimentalCoroutineDispatcher,
134134
val parallelism: Int,
135-
override val taskMode: TaskMode
135+
override val taskMode: Int
136136
) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
137137

138138
private val queue = ConcurrentLinkedQueue<Runnable>()

kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt

+13-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package kotlinx.coroutines.scheduling
@@ -51,26 +51,23 @@ internal val IDLE_WORKER_KEEP_ALIVE_NS = TimeUnit.SECONDS.toNanos(
5151
@JvmField
5252
internal var schedulerTimeSource: TimeSource = NanoTimeSource
5353

54-
internal enum class TaskMode {
55-
56-
/**
57-
* Marker indicating that task is CPU-bound and will not block
58-
*/
59-
NON_BLOCKING,
54+
/**
55+
* Marker indicating that task is CPU-bound and will not block
56+
*/
57+
internal const val TASK_NON_BLOCKING = 0
6058

61-
/**
62-
* Marker indicating that task may potentially block, thus giving scheduler a hint that additional thread may be required
63-
*/
64-
PROBABLY_BLOCKING,
65-
}
59+
/**
60+
* Marker indicating that task may potentially block, thus giving scheduler a hint that additional thread may be required
61+
*/
62+
internal const val TASK_PROBABLY_BLOCKING = 1
6663

6764
internal interface TaskContext {
68-
val taskMode: TaskMode
65+
val taskMode: Int // TASK_XXX
6966
fun afterTask()
7067
}
7168

7269
internal object NonBlockingContext : TaskContext {
73-
override val taskMode: TaskMode = TaskMode.NON_BLOCKING
70+
override val taskMode: Int = TASK_NON_BLOCKING
7471

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

88-
internal inline val Task.isBlocking get() = taskContext.taskMode == TaskMode.PROBABLY_BLOCKING
85+
internal inline val Task.isBlocking get() = taskContext.taskMode == TASK_PROBABLY_BLOCKING
8986

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

0 commit comments

Comments
 (0)