Skip to content

Commit abd1ef6

Browse files
committed
Optimize the size of the coroutines library in Android projects
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.
1 parent 12a0318 commit abd1ef6

File tree

17 files changed

+138
-87
lines changed

17 files changed

+138
-87
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,11 @@ 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 best results it is recommended to use a recent version of R8.
169+
R8 is a replacement for ProGuard in Android ecosystem, it is enabled by default since Android gradle plugin 3.4.0
170+
(3.3.0-beta also had it enabled). The upcoming AGP 4.0.0 has never R8 and additional rules enable
171+
more optimizations, producing smaller binary size.
172172

173173
### JS
174174

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ allprojects {
165165
kotlinOptions.freeCompilerArgs += "-XXLanguage:+InlineClasses"
166166
// Binary compatibility support
167167
kotlinOptions.freeCompilerArgs += ["-Xdump-declarations-to=${buildDir}/visibilities.json"]
168+
// Remove null assertions to get smaller bytecode on Android
169+
kotlinOptions.freeCompilerArgs += ["-Xno-param-assertions", "-Xno-receiver-assertions", "-Xno-call-assertions"]
168170
}
169171
}
170172

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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 4 additions & 2 deletions
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

Lines changed: 26 additions & 8 deletions
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+
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

Lines changed: 8 additions & 4 deletions
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

Lines changed: 11 additions & 11 deletions
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
@@ -387,7 +387,7 @@ internal class CoroutineScheduler(
387387
}
388388
}
389389
// Checking 'task' instead of 'notAdded' is completely okay
390-
if (task.mode == TaskMode.NON_BLOCKING) {
390+
if (task.mode == TASK_NON_BLOCKING) {
391391
signalCpuWork()
392392
} else {
393393
signalBlockingWork()
@@ -489,7 +489,7 @@ internal class CoroutineScheduler(
489489
*/
490490
if (worker.state === WorkerState.TERMINATED) return task
491491
// Do not add CPU tasks in local queue if we are not able to execute it
492-
if (task.mode === TaskMode.NON_BLOCKING && worker.state === WorkerState.BLOCKING) {
492+
if (task.mode == TASK_NON_BLOCKING && worker.state === WorkerState.BLOCKING) {
493493
return task
494494
}
495495
worker.mayHaveLocalTasks = true
@@ -728,16 +728,16 @@ internal class CoroutineScheduler(
728728
afterTask(taskMode)
729729
}
730730

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

739-
private fun afterTask(taskMode: TaskMode) {
740-
if (taskMode == TaskMode.NON_BLOCKING) return
739+
private fun afterTask(taskMode: Int) {
740+
if (taskMode == TASK_NON_BLOCKING) return
741741
decrementBlockingTasks()
742742
val currentState = state
743743
// Shutdown sequence of blocking dispatcher
@@ -835,10 +835,10 @@ internal class CoroutineScheduler(
835835
}
836836

837837
// It is invoked by this worker when it finds a task
838-
private fun idleReset(mode: TaskMode) {
838+
private fun idleReset(mode: Int) {
839839
terminationDeadline = 0L // reset deadline for termination
840840
if (state == WorkerState.PARKING) {
841-
assert { mode == TaskMode.PROBABLY_BLOCKING }
841+
assert { mode == TASK_PROBABLY_BLOCKING }
842842
state = WorkerState.BLOCKING
843843
}
844844
}
@@ -915,12 +915,12 @@ internal class CoroutineScheduler(
915915

916916
enum class WorkerState {
917917
/**
918-
* Has CPU token and either executes [TaskMode.NON_BLOCKING] task or tries to find one.
918+
* Has CPU token and either executes [TASK_NON_BLOCKING] task or tries to find one.
919919
*/
920920
CPU_ACQUIRED,
921921

922922
/**
923-
* Executing task with [TaskMode.PROBABLY_BLOCKING].
923+
* Executing task with [TASK_PROBABLY_BLOCKING].
924924
*/
925925
BLOCKING,
926926

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

Lines changed: 4 additions & 4 deletions
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, fair: 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

Lines changed: 13 additions & 16 deletions
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(

kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import kotlin.coroutines.*
1212
import kotlin.test.*
1313

1414
class CoroutineSchedulerTest : TestBase() {
15+
private val taskModes = listOf(TASK_NON_BLOCKING, TASK_PROBABLY_BLOCKING)
1516

1617
@Test
1718
fun testModesExternalSubmission() { // Smoke
1819
CoroutineScheduler(1, 1).use {
19-
for (mode in TaskMode.values()) {
20+
for (mode in taskModes) {
2021
val latch = CountDownLatch(1)
2122
it.dispatch(Runnable {
2223
latch.countDown()
@@ -30,9 +31,9 @@ class CoroutineSchedulerTest : TestBase() {
3031
@Test
3132
fun testModesInternalSubmission() { // Smoke
3233
CoroutineScheduler(2, 2).use {
33-
val latch = CountDownLatch(TaskMode.values().size)
34+
val latch = CountDownLatch(taskModes.size)
3435
it.dispatch(Runnable {
35-
for (mode in TaskMode.values()) {
36+
for (mode in taskModes) {
3637
it.dispatch(Runnable {
3738
latch.countDown()
3839
}, TaskContextImpl(mode))
@@ -167,7 +168,7 @@ class CoroutineSchedulerTest : TestBase() {
167168
}
168169
}
169170

170-
private class TaskContextImpl(override val taskMode: TaskMode) : TaskContext {
171+
private class TaskContextImpl(override val taskMode: Int) : TaskContext {
171172
override fun afterTask() {}
172173
}
173174
}

0 commit comments

Comments
 (0)