The fix to #878 that introduces a FastServiceLoader to mitigate the fact that ServiceLoader implementation on Android is slow is still inadequate for Android apps and moreover, the solution prevents the R8 optimization from working.
For Android apps to avoid doing I/O on the main thread, we should
1) let the user disable the FastServiceLoader (this can be done now via setting a systemProp now, but is quite hidden and requires action from the developer)
2) Enable the R8 optimization by making sure that any calls to ServiceLoader.load are of the form:
ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()
Everything matters here: 2 argument constructor, class constants as arguments, and the only call to the returned ServiceLoader has to be .iterator().
So if I understand this correctly, I'll need both a version of coroutine with https://github.com/Kotlin/kotlinx.coroutines/pull/1232 and R8 with https://issuetracker.google.com/issues/120436373 fix to mitigate the issue, right?
Yes, precisely. Once you have both the patched coroutines version and R8 from 1.5.x branch enabled, you will also need to disable the FastServiceLoader before any accesses to Dispathers.Main:
System.setProperty("kotlinx.coroutines.fast.service.loader", false)
I am working on making this automatic, but we need a new mechanism for reading R8-specific rules, so it won't come until Android Studio 3.6 at least, possibly later. For everyone on current Android Studio versions, you'll have to set the property manually.
You can also add
# Ensure the custom, fast service loader implementation is removed.
-assumevalues class kotlinx.coroutines.internal.MainDispatcherLoader {
boolean FAST_SERVICE_LOADER_ENABLED return false;
}
-checkdiscard class kotlinx.coroutines.internal.FastServiceLoader
to do this at compile-time with R8 instead of at runtime with a property. Your APK also gets a teeeeeeny bit smaller!
@JakeWharton / @wojtek-kalicinski Sorry to bother you but I'm having an hard time checking the resulting byte code after those optimisations.
I do not really understand what result should R8 generate, but I still see normal ServiceLoader calls (And correctly not the fast one so the assume values part did work) is this normal or is the R8 ServiceLoader optimisation not applied?
Using R8 1.5.68 that is supposed to have the fix according to this thread.
Would love a confirmation to finally drop my custom code after the initial report I made about this (https://github.com/Kotlin/kotlinx.coroutines/issues/878)
.method public static constructor <clinit>()V
.registers 8
const-string v0, "kotlinx.coroutines.fast.service.loader"
.line 1
invoke-static {v0}, Lw3/a/r2/v;->a(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
if-eqz v0, :cond_b
.line 2
invoke-static {v0}, Ljava/lang/Boolean;->parseBoolean(Ljava/lang/String;)Z
.line 3
:cond_b
const-class v0, Lkotlinx/coroutines/android/AndroidDispatcherFactory;
const/4 v1, 0x2
const/4 v2, 0x0
.line 4
:try_start_f
invoke-virtual {v0}, Ljava/lang/Class;->getClassLoader()Ljava/lang/ClassLoader;
move-result-object v3
.line 5
invoke-static {v0, v3}, Ljava/util/ServiceLoader;->load(Ljava/lang/Class;Ljava/lang/ClassLoader;)Ljava/util/ServiceLoader;
move-result-object v0
.line 6
invoke-virtual {v0}, Ljava/util/ServiceLoader;->iterator()Ljava/util/Iterator;
move-result-object v0
invoke-static {v0}, Ls3/z/r0;->a(Ljava/util/Iterator;)Lv3/b0/h;
move-result-object v0
invoke-static {v0}, Ls3/z/r0;->e(Lv3/b0/h;)Ljava/util/List;
move-result-object v0
.line 7
invoke-interface {v0}, Ljava/lang/Iterable;->iterator()Ljava/util/Iterator;
move-result-object v3
.line 8
invoke-interface {v3}, Ljava/util/Iterator;->hasNext()Z
move-result v4
if-nez v4, :cond_2f
move-object v4, v2
goto :goto_56
.line 9
:cond_2f
invoke-interface {v3}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v4
.line 10
invoke-interface {v3}, Ljava/util/Iterator;->hasNext()Z
move-result v5
if-nez v5, :cond_3a
goto :goto_56
.line 11
:cond_3a
move-object v5, v4
check-cast v5, Lkotlinx/coroutines/android/AndroidDispatcherFactory;
.line 12
invoke-virtual {v5}, Lkotlinx/coroutines/android/AndroidDispatcherFactory;->getLoadPriority()I
move-result v5
.line 13
:cond_41
invoke-interface {v3}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v6
.line 14
move-object v7, v6
check-cast v7, Lkotlinx/coroutines/android/AndroidDispatcherFactory;
.line 15
invoke-virtual {v7}, Lkotlinx/coroutines/android/AndroidDispatcherFactory;->getLoadPriority()I
move-result v7
if-ge v5, v7, :cond_50
move-object v4, v6
move v5, v7
.line 16
:cond_50
invoke-interface {v3}, Ljava/util/Iterator;->hasNext()Z
move-result v6
if-nez v6, :cond_41
.line 17
:goto_56
check-cast v4, Lkotlinx/coroutines/android/AndroidDispatcherFactory;
:try_end_58
.catchall {:try_start_f .. :try_end_58} :catchall_73
if-eqz v4, :cond_6d
.line 18
:try_start_5a
invoke-virtual {v4, v0}, Lkotlinx/coroutines/android/AndroidDispatcherFactory;->createDispatcher(Ljava/util/List;)Lw3/a/b2;
move-result-object v0
:try_end_5e
.catchall {:try_start_5a .. :try_end_5e} :catchall_5f
goto :goto_6a
:catchall_5f
move-exception v0
.line 19
:try_start_60
new-instance v3, Lw3/a/r2/o;
invoke-virtual {v4}, Lkotlinx/coroutines/android/AndroidDispatcherFactory;->hintOnError()Ljava/lang/String;
move-result-object v4
invoke-direct {v3, v0, v4}, Lw3/a/r2/o;-><init>(Ljava/lang/Throwable;Ljava/lang/String;)V
move-object v0, v3
:goto_6a
if-eqz v0, :cond_6d
goto :goto_7a
.line 20
:cond_6d
new-instance v0, Lw3/a/r2/o;
invoke-direct {v0, v2, v2, v1}, Lw3/a/r2/o;-><init>(Ljava/lang/Throwable;Ljava/lang/String;I)V
:try_end_72
.catchall {:try_start_60 .. :try_end_72} :catchall_73
goto :goto_7a
:catchall_73
move-exception v0
.line 21
new-instance v3, Lw3/a/r2/o;
invoke-direct {v3, v0, v2, v1}, Lw3/a/r2/o;-><init>(Ljava/lang/Throwable;Ljava/lang/String;I)V
move-object v0, v3
.line 22
:goto_7a
sput-object v0, Lw3/a/r2/n;->a:Lw3/a/b2;
return-void
.end method
Hey, the fix should work with AGP 3.6.0-alpha08 and coroutines 1.3.0.
Unfortunately even though R8 1.5.x branch should support the optimization, there is a keep rule in coroutines that prevents it from working. (at least that's what I think is happening)
A workaround in AGP 3.6.0-alpha08 makes sure that rule doesn't apply and hence R8 can optimize the ServiceLoader (on release builds).
Please let me know if it works for you
Thanks, I can confirm that latest R8 master seems to produce a proper bytecode.
Unfortunately I'm not ready for R8 1.6 yet and will do my usual bug reports with Jinseong if any when AS 3.6 reach beta. After dozen of issues reported for 1.4/1.5 I'm playing safe now :)
It may worth to add a note somewhere about the fact that it won't work with 1.5 as without checking bytecode they could think it works.
.method public static constructor <clinit>()V
.registers 8
const-string v0, "kotlinx.coroutines.fast.service.loader"
.line 1
invoke-static {v0}, Lu3/a/r2/v;->a(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
if-eqz v0, :cond_b
.line 2
invoke-static {v0}, Ljava/lang/Boolean;->parseBoolean(Ljava/lang/String;)Z
:cond_b
const/4 v0, 0x1
const/4 v1, 0x2
const/4 v2, 0x0
:try_start_e
new-array v0, v0, [Lkotlinx/coroutines/internal/MainDispatcherFactory;
const/4 v3, 0x0
.line 3
new-instance v4, Lkotlinx/coroutines/android/AndroidDispatcherFactory;
invoke-direct {v4}, Lkotlinx/coroutines/android/AndroidDispatcherFactory;-><init>()V
aput-object v4, v0, v3
invoke-static {v0}, Ljava/util/Arrays;->asList([Ljava/lang/Object;)Ljava/util/List;
move-result-object v0
.line 4
invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator;
move-result-object v0
invoke-static {v0}, Lq3/z/s0;->a(Ljava/util/Iterator;)Lt3/z/h;
move-result-object v0
invoke-static {v0}, Lq3/z/s0;->b(Lt3/z/h;)Ljava/util/List;
move-result-object v0
.line 5
invoke-interface {v0}, Ljava/lang/Iterable;->iterator()Ljava/util/Iterator;
move-result-object v3
.line 6
invoke-interface {v3}, Ljava/util/Iterator;->hasNext()Z
move-result v4
if-nez v4, :cond_34
move-object v4, v2
goto :goto_5b
.line 7
:cond_34
invoke-interface {v3}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v4
.line 8
invoke-interface {v3}, Ljava/util/Iterator;->hasNext()Z
move-result v5
if-nez v5, :cond_3f
goto :goto_5b
.line 9
:cond_3f
move-object v5, v4
check-cast v5, Lkotlinx/coroutines/internal/MainDispatcherFactory;
.line 10
invoke-interface {v5}, Lkotlinx/coroutines/internal/MainDispatcherFactory;->getLoadPriority()I
move-result v5
.line 11
:cond_46
invoke-interface {v3}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v6
.line 12
move-object v7, v6
check-cast v7, Lkotlinx/coroutines/internal/MainDispatcherFactory;
.line 13
invoke-interface {v7}, Lkotlinx/coroutines/internal/MainDispatcherFactory;->getLoadPriority()I
move-result v7
if-ge v5, v7, :cond_55
move-object v4, v6
move v5, v7
.line 14
:cond_55
invoke-interface {v3}, Ljava/util/Iterator;->hasNext()Z
move-result v6
if-nez v6, :cond_46
.line 15
:goto_5b
check-cast v4, Lkotlinx/coroutines/internal/MainDispatcherFactory;
:try_end_5d
.catchall {:try_start_e .. :try_end_5d} :catchall_78
if-eqz v4, :cond_72
.line 16
:try_start_5f
invoke-interface {v4, v0}, Lkotlinx/coroutines/internal/MainDispatcherFactory;->createDispatcher(Ljava/util/List;)Lu3/a/b2;
move-result-object v0
:try_end_63
.catchall {:try_start_5f .. :try_end_63} :catchall_64
goto :goto_6f
:catchall_64
move-exception v0
.line 17
:try_start_65
new-instance v3, Lu3/a/r2/o;
invoke-interface {v4}, Lkotlinx/coroutines/internal/MainDispatcherFactory;->hintOnError()Ljava/lang/String;
move-result-object v4
invoke-direct {v3, v0, v4}, Lu3/a/r2/o;-><init>(Ljava/lang/Throwable;Ljava/lang/String;)V
move-object v0, v3
:goto_6f
if-eqz v0, :cond_72
goto :goto_7f
.line 18
:cond_72
new-instance v0, Lu3/a/r2/o;
invoke-direct {v0, v2, v2, v1}, Lu3/a/r2/o;-><init>(Ljava/lang/Throwable;Ljava/lang/String;I)V
:try_end_77
.catchall {:try_start_65 .. :try_end_77} :catchall_78
goto :goto_7f
:catchall_78
move-exception v0
.line 19
new-instance v3, Lu3/a/r2/o;
invoke-direct {v3, v0, v2, v1}, Lu3/a/r2/o;-><init>(Ljava/lang/Throwable;Ljava/lang/String;I)V
move-object v0, v3
.line 20
:goto_7f
sput-object v0, Lu3/a/r2/n;->a:Lu3/a/b2;
return-void
.end method
Most helpful comment
You can also add
to do this at compile-time with R8 instead of at runtime with a property. Your APK also gets a teeeeeeny bit smaller!