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
diff --git a/README.md b/README.md
index 9ff957b..42b2b3a 100644
--- a/README.md
+++ b/README.md
@@ -164,11 +164,8 @@
#### 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
diff --git a/build.gradle b/build.gradle
index 273fd00..67e1e27 100644
--- a/build.gradle
+++ b/build.gradle
@@ -138,10 +138,9 @@
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
@@ -159,30 +158,33 @@
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"]
}
}
diff --git a/docs/debugging.md b/docs/debugging.md
index d3caa9a..6c846f2 100644
--- a/docs/debugging.md
+++ b/docs/debugging.md
@@ -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.
@@ -86,6 +88,14 @@
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
diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt
index 98ead14..b741a1a 100644
--- a/kotlinx-coroutines-core/common/src/selects/Select.kt
+++ b/kotlinx-coroutines-core/common/src/selects/Select.kt
@@ -264,7 +264,10 @@
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
diff --git a/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt b/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt
index e835d34..94e3818 100644
--- a/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt
+++ b/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
@@ -16,7 +16,9 @@
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)
diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
index 6f11cdf..fe21a25 100644
--- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
+++ b/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.*
@@ -30,11 +34,12 @@
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)
}
}
}
@@ -51,13 +56,30 @@
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
@@ -85,11 +107,7 @@
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)
diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt
index b512815..9c9931c 100644
--- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt
+++ b/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")
@@ -52,7 +52,8 @@
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)
}
@@ -155,8 +156,11 @@
}
}
-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) {
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
index 7a52d34..03604ce 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
+++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt
@@ -73,7 +73,7 @@
* 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
@@ -394,7 +394,7 @@
}
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 {
@@ -499,7 +499,7 @@
*/
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
@@ -739,16 +739,16 @@
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
@@ -846,10 +846,10 @@
}
// 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
}
}
@@ -926,12 +926,12 @@
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,
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
index bbc2b35..bbbb706 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt
+++ b/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
@@ -85,7 +85,7 @@
*/
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)
}
/**
@@ -98,7 +98,7 @@
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) {
@@ -132,7 +132,7 @@
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>()
diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
index c0a3e64..ca0b2de 100644
--- a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt
+++ b/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
@@ -51,26 +51,23 @@
@JvmField
internal var schedulerTimeSource: TimeSource = NanoTimeSource
-internal enum class TaskMode {
+/**
+ * Marker indicating that task is CPU-bound and will not block
+ */
+internal const val TASK_NON_BLOCKING = 0
- /**
- * Marker indicating that task is CPU-bound and will not block
- */
- NON_BLOCKING,
-
- /**
- * 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
@@ -82,10 +79,10 @@
@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(
diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
index 38145af..b0a5954 100644
--- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt
@@ -12,11 +12,12 @@
import kotlin.test.*
class CoroutineSchedulerTest : TestBase() {
+ private val taskModes = listOf(TASK_NON_BLOCKING, TASK_PROBABLY_BLOCKING)
@Test
fun testModesExternalSubmission() { // Smoke
CoroutineScheduler(1, 1).use {
- for (mode in TaskMode.values()) {
+ for (mode in taskModes) {
val latch = CountDownLatch(1)
it.dispatch(Runnable {
latch.countDown()
@@ -30,9 +31,9 @@
@Test
fun testModesInternalSubmission() { // Smoke
CoroutineScheduler(2, 2).use {
- val latch = CountDownLatch(TaskMode.values().size)
+ val latch = CountDownLatch(taskModes.size)
it.dispatch(Runnable {
- for (mode in TaskMode.values()) {
+ for (mode in taskModes) {
it.dispatch(Runnable {
latch.countDown()
}, TaskContextImpl(mode))
@@ -167,7 +168,7 @@
}
}
- private class TaskContextImpl(override val taskMode: TaskMode) : TaskContext {
+ private class TaskContextImpl(override val taskMode: Int) : TaskContext {
override fun afterTask() {}
}
}
\ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/README.md b/ui/kotlinx-coroutines-android/README.md
index 77bd2af..5be286c 100644
--- a/ui/kotlinx-coroutines-android/README.md
+++ b/ui/kotlinx-coroutines-android/README.md
@@ -5,6 +5,23 @@
Read [Guide to UI programming with coroutines](https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md)
for tutorial on this module.
+# Optimization
+
+R8 and ProGuard rules are bundled into this module.
+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).
+For best results it is recommended to use a recent version of R8, which produces a smaller binary.
+
+When optimizations are enabled with R8 version 1.6.0 or later
+the following debugging features are permanently turned off to reduce the size of the resulting binary:
+
+* [Debugging mode](../../docs/debugging.md#debug-mode)
+* [Stacktrace recovery](../../docs/debugging.md#stacktrace-recovery)
+* The internal assertions in the library are also permanently removed.
+
+You can examine the corresponding rules in this
+[`coroutines.pro`](resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro) file.
+
# Package kotlinx.coroutines.android
Provides `Dispatchers.Main` context for Android applications.
diff --git a/ui/kotlinx-coroutines-android/build.gradle b/ui/kotlinx-coroutines-android/build.gradle
index c05881e..68e2232 100644
--- a/ui/kotlinx-coroutines-android/build.gradle
+++ b/ui/kotlinx-coroutines-android/build.gradle
@@ -4,13 +4,6 @@
repositories {
google()
- // TODO Remove once R8 is updated to a 1.6.x version.
- maven {
- url "http://storage.googleapis.com/r8-releases/raw/master"
- metadataSources {
- artifact()
- }
- }
}
configurations {
@@ -25,8 +18,7 @@
testImplementation "org.robolectric:robolectric:$robolectric_version"
testImplementation "org.smali:baksmali:$baksmali_version"
- // TODO Replace with a 1.6.x version once released to maven.google.com.
- r8 'com.android.tools:r8:a7ce65837bec81c62261bf0adac73d9c09d32af2'
+ r8 'com.android.tools.build:builder:4.0.0-alpha06' // Contains r8-2.0.4-dev
}
class RunR8Task extends JavaExec {
@@ -38,7 +30,7 @@
File inputConfig
@InputFile
- final File inputConfigCommon = new File('r8-test-common.pro')
+ final File inputConfigCommon = new File('testdata/r8-test-common.pro')
@InputFiles
final File jarFile = project.jar.archivePath
@@ -74,30 +66,36 @@
}
}
-def optimizedDex = new File(buildDir, "dex-optim/")
-def unOptimizedDex = new File(buildDir, "dex-unoptim/")
+def optimizedDexDir = new File(buildDir, "dex-optim/")
+def unOptimizedDexDir = new File(buildDir, "dex-unoptim/")
+
+def optimizedDexFile = new File(optimizedDexDir, "classes.dex")
+def unOptimizedDexFile = new File(unOptimizedDexDir, "classes.dex")
task runR8(type: RunR8Task, dependsOn: 'jar'){
- outputDex = optimizedDex
- inputConfig = file('r8-test-rules.pro')
+ outputDex = optimizedDexDir
+ inputConfig = file('testdata/r8-test-rules.pro')
}
task runR8NoOptim(type: RunR8Task, dependsOn: 'jar'){
- outputDex = unOptimizedDex
- inputConfig = file('r8-test-rules-no-optim.pro')
+ outputDex = unOptimizedDexDir
+ inputConfig = file('testdata/r8-test-rules-no-optim.pro')
}
test {
// Ensure the R8-processed dex is built and supply its path as a property to the test.
dependsOn(runR8)
dependsOn(runR8NoOptim)
- def dex1 = new File(optimizedDex, "classes.dex")
- def dex2 = new File(unOptimizedDex, "classes.dex")
- inputs.files(dex1, dex2)
+ inputs.files(optimizedDexFile, unOptimizedDexFile)
- systemProperty 'dexPath', dex1.absolutePath
- systemProperty 'noOptimDexPath', dex2.absolutePath
+ systemProperty 'dexPath', optimizedDexFile.absolutePath
+ systemProperty 'noOptimDexPath', unOptimizedDexFile.absolutePath
+
+ // Output custom metric with the size of the optimized dex
+ doLast {
+ println("##teamcity[buildStatisticValue key='optimizedDexSize' value='${optimizedDexFile.length()}']")
+ }
}
tasks.withType(dokka.getClass()) {
diff --git a/ui/kotlinx-coroutines-android/r8-test-rules-no-optim.pro b/ui/kotlinx-coroutines-android/r8-test-rules-no-optim.pro
deleted file mode 100644
index d6bd4a4..0000000
--- a/ui/kotlinx-coroutines-android/r8-test-rules-no-optim.pro
+++ /dev/null
@@ -1,4 +0,0 @@
--include r8-test-common.pro
-
-# Include the shrinker config used by legacy versions of AGP and ProGuard
--include resources/META-INF/com.android.tools/proguard/coroutines.pro
diff --git a/ui/kotlinx-coroutines-android/r8-test-rules.pro b/ui/kotlinx-coroutines-android/r8-test-rules.pro
deleted file mode 100644
index 2e7fdd8..0000000
--- a/ui/kotlinx-coroutines-android/r8-test-rules.pro
+++ /dev/null
@@ -1,7 +0,0 @@
--include r8-test-common.pro
-
-# Ensure the custom, fast service loader implementation is removed. In the case of fast service
-# loader encountering an exception it falls back to regular ServiceLoader in a way that cannot be
-# optimized out by R8.
--include resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
--checkdiscard class kotlinx.coroutines.internal.FastServiceLoader
\ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
index b57b077..fd25b21 100644
--- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
+++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
@@ -5,6 +5,18 @@
boolean FAST_SERVICE_LOADER_ENABLED return false;
}
--assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader {
+-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoaderKt {
boolean ANDROID_DETECTED return true;
+}
+
+# Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher
+-assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt {
+ boolean SUPPORT_MISSING return false;
+}
+
+# Statically turn off all debugging facilities and assertions
+-assumenosideeffects class kotlinx.coroutines.DebugKt {
+ boolean getASSERTIONS_ENABLED() return false;
+ boolean getDEBUG() return false;
+ boolean getRECOVER_STACK_TRACES() return false;
}
\ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt
index 0944326..198fe07 100644
--- a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt
+++ b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.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.android
@@ -12,17 +12,24 @@
@Keep
internal class AndroidExceptionPreHandler :
- AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, Function0<Method?> {
+ AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler
+{
+ @Volatile
+ private var _preHandler: Any? = this // uninitialized marker
- private val preHandler by lazy(this)
-
- // Reflectively lookup pre-handler. Implement Function0 to avoid generating second class for lambda
- override fun invoke(): Method? = try {
- Thread::class.java.getDeclaredMethod("getUncaughtExceptionPreHandler").takeIf {
- Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers)
+ // Reflectively lookup pre-handler.
+ private fun preHandler(): Method? {
+ val current = _preHandler
+ if (current !== this) return current as Method?
+ val declared = try {
+ Thread::class.java.getDeclaredMethod("getUncaughtExceptionPreHandler").takeIf {
+ Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers)
+ }
+ } catch (e: Throwable) {
+ null /* not found */
}
- } catch (e: Throwable) {
- null /* not found */
+ _preHandler = declared
+ return declared
}
override fun handleException(context: CoroutineContext, exception: Throwable) {
@@ -39,7 +46,7 @@
if (Build.VERSION.SDK_INT >= 28) {
thread.uncaughtExceptionHandler.uncaughtException(thread, exception)
} else {
- (preHandler?.invoke(null) as? Thread.UncaughtExceptionHandler)
+ (preHandler()?.invoke(null) as? Thread.UncaughtExceptionHandler)
?.uncaughtException(thread, exception)
}
}
diff --git a/ui/kotlinx-coroutines-android/r8-test-common.pro b/ui/kotlinx-coroutines-android/testdata/r8-test-common.pro
similarity index 76%
rename from ui/kotlinx-coroutines-android/r8-test-common.pro
rename to ui/kotlinx-coroutines-android/testdata/r8-test-common.pro
index 03f36a8..d29377e 100644
--- a/ui/kotlinx-coroutines-android/r8-test-common.pro
+++ b/ui/kotlinx-coroutines-android/testdata/r8-test-common.pro
@@ -8,5 +8,11 @@
void handleCoroutineException(...);
}
+# Entry point for the rest of coroutines machinery
+-keep class kotlinx.coroutines.BuildersKt {
+ ** runBlocking(...);
+ ** launch(...);
+}
+
# We are cheating a bit by not having android.jar on R8's library classpath. Ignore those warnings.
-ignorewarnings
\ No newline at end of file
diff --git a/ui/kotlinx-coroutines-android/testdata/r8-test-rules-no-optim.pro b/ui/kotlinx-coroutines-android/testdata/r8-test-rules-no-optim.pro
new file mode 100644
index 0000000..61afeed
--- /dev/null
+++ b/ui/kotlinx-coroutines-android/testdata/r8-test-rules-no-optim.pro
@@ -0,0 +1,4 @@
+-include r8-test-common.pro
+
+# Include the shrinker config used by legacy versions of AGP and ProGuard
+-include ../resources/META-INF/com.android.tools/proguard/coroutines.pro
diff --git a/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro b/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro
new file mode 100644
index 0000000..dde8600
--- /dev/null
+++ b/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro
@@ -0,0 +1,14 @@
+-include r8-test-common.pro
+
+-include ../resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro
+
+# Validate that service-loader & debugger classes are discarded
+-checkdiscard class kotlinx.coroutines.internal.FastServiceLoader
+-checkdiscard class kotlinx.coroutines.DebugKt
+-checkdiscard class kotlinx.coroutines.internal.StackTraceRecoveryKt
+
+# Real android projects do not keep this class, but somehow it is kept in this test (R8 bug)
+# -checkdiscard class kotlinx.coroutines.internal.MissingMainCoroutineDispatcher
+
+# Should not keep this class, but it is still there (R8 bug)
+#-checkdiscard class kotlinx.coroutines.CoroutineId
\ No newline at end of file