Skip to content

FastServiceLoader performs I/O on calling thread and prevents R8 optimization #1231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wojtek-kalicinski opened this issue May 30, 2019 · 6 comments
Assignees

Comments

@wojtek-kalicinski
Copy link
Contributor

wojtek-kalicinski commented May 30, 2019

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().
wojtek-kalicinski added a commit to wojtek-kalicinski/kotlinx.coroutines that referenced this issue May 30, 2019
The developer still has to disable the FastServiceLoader manually
if they're using R8.

R8 is then able to optimize away the ServiceLoader and reflection
entirely, resulting in direct class instantiation and no extra I/O
on calling thread.

Fixes Kotlin#1231
@qwwdfsad qwwdfsad self-assigned this May 31, 2019
@aaronweihe
Copy link

aaronweihe commented Jun 5, 2019

So if I understand this correctly, I'll need both a version of coroutine with #1232 and R8 with https://issuetracker.google.com/issues/120436373 fix to mitigate the issue, right?

@wojtek-kalicinski
Copy link
Contributor Author

wojtek-kalicinski commented Jun 5, 2019

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.

@JakeWharton
Copy link
Contributor

JakeWharton commented Jun 6, 2019

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!

@Tolriq
Copy link

Tolriq commented Aug 29, 2019

@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 (#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

@wojtek-kalicinski
Copy link
Contributor Author

wojtek-kalicinski commented Aug 29, 2019

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

@Tolriq
Copy link

Tolriq commented Aug 29, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants