JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.24k stars 1.18k forks source link

FR: Add support for running R8 on JVM Desktop #1604

Closed ScottPierce closed 1 month ago

ScottPierce commented 2 years ago

The performance benefits for R8 and Compose on Android is significant (supposedly because of its optimizations of Lambdas). I recently found out that R8 can just be run on Java class files. It'd be nice to be able to add support to use R8 directly from the compose Gradle plugin on Compose Desktop projects, so that people can easily take advantage of the performance improvements on Compose Desktop.

Thomas-Vos commented 2 years ago

Would love to see R8 support for code obfuscation. This is currently blocking me from publishing my app.

ScottPierce commented 2 years ago

@Thomas-Vos look at the link I posted where a gradle project has a task to run r8 on it.

You should be able to do it yourself in 30 minutes or so:

  1. Use the Shadow Jar Gradle plugin to create an Uber / Fat Jar. This is a jar that contains all your other Jar files.
  2. Run R8 on it similar to my above link
  3. Swap out your jar files in your packaged application with the newly optimized / obfuscated single jar file. There is also a text file that lives alongside the jar files that define the running classpath. Edit it to remove all the jar files from the classpath, and ensure your optimized fat jar is the only jar in that file now.
mcpiroman commented 2 years ago

@ScottPierce Is there a way to run R8 selectively on jars e.g. not to obfuscate (certain) third-parties instead of user-jar way?

This has to be somewhat easily hackable to achieve that, just the 30 minutes was not enough for me with my gradle-fu. Generally we would all appreciate if one would share sample gist gradle.kts file for CfD app so that we don't have to arrive to the same solution independently 🙏 .

ScottPierce commented 2 years ago

R8 has similar functionality and accepts the same configs as Proguard. You can tell it to ignore certain packages.

mcpiroman commented 2 years ago

I finally managed to do that with ProGuard (example setup), without uberjar though. It's actually not that hard. I guess it should be similar for R8.

ScottPierce commented 2 years ago

No uber jar has benefits imo. While doing updates, that would allow you to only swap out the file that's been changed. Uber jars with compose can be 50+ MB

ScottPierce commented 1 year ago

Another point that was brought up about R8 - R8 has the ability to read rules directly from jars, meaning that its a significantly better user experience to build a library ecosystem with.

mipastgt commented 1 year ago

Taking into account all the recent problems with ProGuard, e.g., https://github.com/JetBrains/compose-multiplatform/issues/3387 https://github.com/JetBrains/compose-multiplatform/pull/3408#discussion_r1274012736 https://github.com/square/okio/issues/1298 it may be worthwile to reconsider supporting R8 on desktops again.

YektaDev commented 1 year ago

Dropping this tweet here. It might be useful.

gogopro-dev commented 1 year ago

bump

JakeWharton commented 9 months ago

Compose Desktop seems to be speed-running the problems of Android from a decade past by not using R8.

https://github.com/Kotlin/kotlinx.coroutines/issues/4025

mikedawson commented 9 months ago

I think right now the biggest problem is that whilst the Compose/Desktop documentation states that proguard is supported it is not really being fully used even on official-ish examples. Even if rules are not auto-bundled, just enabling proguard (including obfuscation) for release jvm builds would uncover many (if not most) issues.

EchoEllet commented 5 months ago

Another point that was brought up about R8 - R8 has the ability to read rules directly from jars, meaning that its a significantly better user experience to build a library ecosystem with.

This feature is usually easy to implement. Use JarFile to read the fat JAR file. You will usually find the rules of the supported libraries in META-INF/proguard of the JAR file

Which will be included by libraries even if you don't use Proguard.

This approach is less flexible and could produce issues later. For most use cases and projects, it works as expected.

Unless I'm mistaken, R8 uses the rules from META-INF/com.android.tools/r8 and fallback to META-INF/proguard

Examples:

JarFile(shadowJarFile.get().asFile).use { jarFile ->
            val generatedProguardFile =
                project.layout.buildDirectory.file("proguard/generated-proguard-libraries-rules.pro").get()
                    .asFile
            if (!generatedProguardFile.exists()) {
                generatedProguardFile.parentFile.mkdir()
            }
            val generatedRulesFiles =
                jarFile.entries().asSequence()
                    .filter { it.name.startsWith("META-INF/proguard") && !it.isDirectory }
                    .map { entry ->
                        jarFile.getInputStream(entry).bufferedReader().use { reader ->
                            Pair(reader.readText(), entry)
                        }
                    }
                    .toList()
            generatedProguardFile.bufferedWriter().use { bufferedWriter ->
                bufferedWriter.appendLine("# GENERATED FILE - manual changes will be overwritten")
                bufferedWriter.appendLine()
                generatedRulesFiles.forEach { (rulesContent, rulesFileEntry) ->
                    bufferedWriter.appendLine("# START of ($rulesFileEntry)")
                    bufferedWriter.appendLine()
                    bufferedWriter.appendLine(rulesContent)
                    bufferedWriter.appendLine("# END of ($rulesFileEntry)")
                    bufferedWriter.appendLine()
                }
            }
            configuration(generatedProguardFile)
        }

Which will have all rules from all libraries in a single file.

Or separate the rules for each library:

JarFile(shadowJarFile.get().asFile).use { jarFile ->
            val generatedRulesFiles =
                jarFile.entries().asSequence()
                    .filter { it.name.startsWith("META-INF/proguard") && !it.isDirectory }
                    .map { entry ->
                        jarFile.getInputStream(entry).bufferedReader().use { reader ->
                            Pair(reader.readText(), entry)
                        }
                    }
                    .toList()

            val buildProguardDirectory = project.layout.buildDirectory.dir("proguard").get().asFile
            if (!buildProguardDirectory.exists()) {
                buildProguardDirectory.mkdir()
            }
            generatedRulesFiles.forEach { (rulesContent, rulesFileEntry) ->
                val rulesFileNameWithExtension = rulesFileEntry.name.substringAfterLast("/")
                val generatedProguardFile = File(buildProguardDirectory, "generated-$rulesFileNameWithExtension")
                if (!generatedProguardFile.exists()) {
                    generatedProguardFile.createNewFile()
                }
                generatedProguardFile.bufferedWriter().use { bufferedWriter ->
                    bufferedWriter.appendLine("# Generated file from ($rulesFileEntry) - manual changes will be overwritten")
                    bufferedWriter.appendLine()

                    bufferedWriter.appendLine(rulesContent)
                }

                configuration(generatedProguardFile)
            }
        }
okushnikov commented 3 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

okushnikov commented 3 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

okushnikov commented 3 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.