Status Update
Comments
sg...@google.com <sg...@google.com> #2
Also crashes in R8 version 8.5.35, 8.6.17, 8.7.2-dev
to...@gmail.com <to...@gmail.com> #3
You cannot apply keep rules for classes synthesized by R8, but if you keep the implemented interfaces, the merging should not happen.
That said, checking the implementing class of a lambda might not be a good idea. The specification for java.lang.invoke.LambdaMetafactory
Capture may involve allocation of a new function object, or may return a suitable existing function object. The identity of a function object produced by capture is unpredictable, and therefore identity-sensitive operations (such as reference equality, object locking, and System.identityHashCode()) may produce different results in different implementations, or even upon different invocations in the same implementation.
If I understand this correctly the only guarantee is that the function object implements the interface(s) specified.
to...@gmail.com <to...@gmail.com> #4
The actual problem is that R8 deletes FunInterface1 and FunInterface2 classes, which are used for runtime di:
fun hello() {
val instance1 = FunInterface1 { println("Doing business") }
val instance2 = FunInterface2 { println("Starting the show") }
val map = mapOf(
FunInterface1::class.java to instance1,
FunInterface2::class.java to instance2,
)
check(map.size == 2) { "Map size is ${map.size}" }
}
So do I have to list all fun interfaces in my sdk using -keep? I might do that for ~100 of them that exist right now, but this won't save future developers from adding another fun interface and forgetting to list them in .pro file.
ri...@google.com <ri...@google.com> #5
Not sure what the use case is here. However, if you want the interfaces to stay as is you will have to have a -keep
rule for them. One option to handle this without adding a rule for each interface could be to annotate them and add a single rule for all interfaces annotated by that annotation.
to...@gmail.com <to...@gmail.com> #6
Our use case is to create a static in-memory Di object, to put and get instances from all over the multi-module application. It kinda looks like
class DiContainer(bindings: Map<Class<*>, SingletonBinding<*>>) {
private val bindings: Map<Class<*>, SingletonBinding<*>> = HashMap(bindings)
fun <T : Any> instance(typeSpec: Class<*>): T {
@Suppress("UNCHECKED_CAST")
val result: T = (bindings[typeSpec]?.lazy?.value) as? T ?: error("No binding for $typeSpec found.")
return result
}
}
class SingletonBinding<out T : Any>(private val singleton: () -> T) {
val lazy = lazy { singleton() }
}
class DiScope {
val bindings: MutableMap<Class<*>, SingletonBinding<*>> = HashMap()
fun addBinding(typeSpec: Class<*>, binding: SingletonBinding<*>) {
check(typeSpec !in bindings) { "$typeSpec already registered." }
bindings[typeSpec] = binding
}
}
open class DiHolder {
@Volatile
@PublishedApi
internal var instance: DiContainer? = null
protected fun build(init: DiScope.() -> Unit) {
check(instance == null) { "Di already initialized" }
instance = DiContainer(DiScope().apply(init).bindings)
}
inline fun <reified T : Any> instance(): T = instance(T::class.java)
fun <T : Any> instance(typeSpec: Class<*>): T = instance!!.instance(typeSpec)
}
object Di : DiHolder() {
operator fun invoke(init: DiScope.() -> Unit) = build(init)
}
Now, when we use this, for example:
fun hello() {
Di {
addBinding(FunInterface1::class.java, SingletonBinding { FunInterface1 { println("Doing business") } })
addBinding(FunInterface2::class.java, SingletonBinding { FunInterface2 { println("Doing business") } })
}
// In some other place in code, but in the same process
Di.instance<FunInterface1>().doBusiness()
Di.instance<FunInterface2>().startTheShow()
}
Now we can get an interface instance from anywhere in the app, because Di is singleton. However, when running release with R8 full mode, I get this exception:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.r8/com.example.r8.MainActivity}: java.lang.IllegalStateException: class com.example.r8.MainActivityKt$$ExternalSyntheticLambda2 already registered.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3782)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8176)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: java.lang.IllegalStateException: class com.example.r8.MainActivityKt$$ExternalSyntheticLambda2 already registered.
at com.example.r8.DiScope.addBinding(Unknown Source:37)
at com.example.r8.MainActivity.onCreate(SourceFile:3)
at android.app.Activity.performCreate(Activity.java:8595)
at android.app.Activity.performCreate(Activity.java:8573)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8176)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
sg...@google.com <sg...@google.com> #7
I also tried koin, and somehow their code arrangement dodges the type check, and everything works.
Oh, I see now, by default their
// These are reduced InsertKoinIO/koin sources
class InstanceRegistry {
val instances = ConcurrentHashMap<String, SingleInstanceFactory<*>>()
fun saveMapping(mapping: String, factory: SingleInstanceFactory<*>) {
if (instances.containsKey(mapping)) {
throw Exception("Already existing definition for ${factory.beanDefinition} at $mapping")
}
instances[mapping] = factory
}
inline fun <reified T> instance(): T {
return instances[T::class.getFullName()]?.get() as T
}
}
class SingleInstanceFactory<T>(val beanDefinition: () -> T) {
private var value: T? = null
private fun getValue(): T = value ?: error("Single instance created couldn't return value")
fun get(): T {
synchronized(this) {
if (value == null) {
value = beanDefinition.invoke()
}
}
return getValue()
}
}
fun KClass<*>.getFullName(): String = this.java.name
/// --------------
fun helloKoin() {
val koin = InstanceRegistry()
val shouldR8CrashTheApp = true
if (shouldR8CrashTheApp) {
koin.saveMapping(FunInterface1::class.getFullName(), SingleInstanceFactory { FunInterface1 { println("Do business") } })
koin.saveMapping(FunInterface2::class.getFullName(), SingleInstanceFactory{ FunInterface2 { println("Start the show") } })
} else {
// because in runtime FunInterfaces are clamped into one class, only one saveMapping is called, and everything works
val mappings = hashMapOf<String, SingleInstanceFactory<*>>()
mappings[FunInterface1::class.getFullName()] = SingleInstanceFactory { FunInterface1 { println("Do business") } }
mappings[FunInterface2::class.getFullName()] = SingleInstanceFactory { FunInterface2 { println("Start the show") } }
mappings.forEach { (mapping, factory) ->
koin.saveMapping(mapping, factory)
}
}
koin.instance<FunInterface1>().doBusiness()
koin.instance<FunInterface2>().startTheShow()
}
to...@gmail.com <to...@gmail.com> #8
It would also be nice, if FunInterface1::class.java == FunInterface2::class.java
and setOf(FunInterface1::class.java, FunInterface2::class.java).size == 2
ri...@google.com <ri...@google.com> #9
Also, is there a way to disable this optimization only for my classes? Or can I only remove all optimizations via -keep, allowobfuscation, allowshrinking class_names
?
sg...@google.com <sg...@google.com> #10
Branch: main
commit ccfe5ff0d44034c39187c5c2656503549a614934
Author: Christoffer Adamsen <christofferqa@google.com>
Date: Wed Aug 07 08:41:16 2024
Test Map with class const keys
Bug:
Change-Id: Icb32bf21030c7800c409ba5e4279dc2bdee05cb4
A src/test/java/com/android/tools/r8/classmerging/horizontal/ConstClassAfterVerticalClassMergingTest.java
to...@gmail.com <to...@gmail.com> #11
Branch: main
commit f5c7414f6891c90f375215f6e3e84db42435f0f6
Author: Christoffer Adamsen <christofferqa@google.com>
Date: Wed Aug 07 10:47:18 2024
Disable synthetic sharing of synthetics with vertically merged sources
Bug:
Change-Id: I9cc17b10095c5a74f3f142217b5f4c6a4de37f09
M src/main/java/com/android/tools/r8/shaking/Enqueuer.java
M src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
M src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
A src/main/java/com/android/tools/r8/shaking/SyntheticKeepClassInfo.java
M src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
M src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
M src/test/java/com/android/tools/r8/classmerging/horizontal/ConstClassAfterVerticalClassMergingTest.java
to...@gmail.com <to...@gmail.com> #12
Turned out the issue was an interplay between the vertical class merging and sharing of synthesized lambda classes. This should be fixed and you can try a build from HEAD with the fix by merging the following into your settings.gradle
or settings.gradle.kts
:
pluginManagement {
buildscript {
repositories {
mavenCentral()
maven {
url = uri("https://storage.googleapis.com/r8-releases/raw/main")
}
}
dependencies {
classpath("com.android.tools:r8:f5c7414f6891c90f375215f6e3e84db42435f0f6")
}
}
}
jo...@aetna.com <jo...@aetna.com> #13
I've been getting wildly inconsistent R8 runtimes for quite a while now. I think its been since at least AGP 8.6 or sooner. I'll have to go gather data for the last year to see when things went weird. I think I should have build scans going back to at least AGP 8.3.
Attaching some outputs from a job earlier today. Config of those two tasks is almost identical so I find the variance between the two highly suspect. Only substantial difference is the pentest variant has a handful of non-prod environment configurations (via a few kotlin classes typically only in a debug srcset) to enable usage of our QA environments. This particular build was with 8.9.0
All anecdotal evidence I know but I think something isn't quite working how it was intended in the R8 task.
Description
I've seen some other issues reported here regarding CI and memory issues but I think this is a little different as in my case this does not happen in CI and is 100% reproducible.
This only started to happen recently but unfortunately not sure if it's 100% related to R8 8.10-dev and not sure what to provide since the task never ends to generate a dump (Or at least I did not wait long enough).
My build command for releases is:
Before it always worked normally, now the first run after code change will have the R8 task hang. Pressing ctrl+C to kill it and restart the exact same script just after will always then work and the R8 tasks run in 2 minutes as usual.
This is 100% reproducible I suppose there's a way to debug this.
Note: The command in CI is using --no-daemon and -no-configuration-cache but in theory since the script have the stop it should behave mostly the same.