Status Update
Comments
th...@gmail.com <th...@gmail.com> #2
th...@gmail.com <th...@gmail.com> #3
Ideally there would be two thresholds we could customize:
- Pixel difference threshold, i.e how much any given pixel can change before it's considered different from the target pixel.
- Total difference threshold, i.e how much of the total screenshot can be different from the target
In my situation, the ideal settings (and what I currently use on other tools/platforms) is ~1% pixel difference threshold to ignore text rendering and anti-aliasing differences that occur across hardware and a 0% total difference threshold so that any change more significant than that is flagged immediately.
Using a single total difference threshold to ignore aliasing is unideal, because on screenshots with text this value can end up being very large to account for every possible pixel that could receive different aliasing. So large that other actually important differences may sneak by undetected, e.g. the height or color of a divider line.
js...@google.com <js...@google.com> #4
I like the idea of having multiple thresholds. Could you please explain option 2 (total diff) in more detail?
th...@gmail.com <th...@gmail.com> #5
Option 2 (total difference) is the percentage of the image that is allowed to be 'different' from the reference image. For example, if the reference image is 10x10 pixels, a 5% threshold would allow the test to pass even if up to 5 pixels do not match the reference. In my opinion, this is the least useful threshold option, even though most other screenshot testing tools have it, as it's the most straightforward threshold to implement. I avoid using this kind of threshold because it doesn't scale well with large screenshots. If I'm taking a screenshot of an entire screen of my app, say 1080x1920, even a small threshold of 0.5% allows for over 10,000 pixels to not match the reference. This means that it's possible the test won't catch important changes like the color of a border, a change of font, or potentially even a small icon disappearing.
What I find to be a more useful option is configuring when any given pixel is considered different from the reference pixel. For example, if I have a screenshot of an black circle on a white background, my laptop (an Apple Silicon MacBook) will anti-alias the image slightly differently than a colleague's Windows desktop or the Linux instance running my tests for CI. Even though to the human eye the image is identical, it will fail due to the minor differences in the gray pixels used to anti-alias the edge of the circle. In this situation, I don't care if a pixel that was #9A9A9A is now #9B9B9B; they're essentially identical, and I'd want the test to ignore that difference.
Trying to work around this kind of issue with option 2 isn't ideal. Larger screenshots have more anti-aliasing, which necessitates a larger total difference threshold to ignore the larger number of imperceptible differences, which in turn just increases the potential for a meaningful change to not be detected.
du...@gmail.com <du...@gmail.com> #6
huge shoutout to jrodbx for amazing writeup of the problem and suggested solution
maybe the same can be applied here, or shared with the papparazi lib
th...@gmail.com <th...@gmail.com> #8
but the problem with rendering of colors is still there, not sure will it be dealt with in this ticket via smart diffing, or the other
meaning if you render the previews on different machines and you get the color space problem, you will need almost ~0,5f (50% ?) threshold to let the tests pass
see attached images of a button as example
in another example of color mismatch
composable code color hex:
0xFF003EB0
reference image hex from (CI ubuntu x86_64 GNU/Linux machine)
FF2B2D30
actual image hex(mbpro M1 Max)
FF003EB0
(please note this does not mean macbook is always the correct one, as we have multiple identical mac machines, some of the devs get different colors rendered, I am not sure where the differences stem from, maybe some internal dependency of the library)
js...@google.com <js...@google.com> #9
Diff threshold feature is enabled since alpha06 version. This does not solve the platform related rendering differences. Tracking that as a separate issue and closing this
th...@gmail.com <th...@gmail.com> #10
thank you for looking into this. From what I noticed in the reported crashes, only non-major brands are affected from this bug. Normally, I would see Samsung flagships, Pixels, or Huawei at the top. So, this is strange! All models have either Android 7 or 7.1.1.
The name of the class in the exception message, like 'ek9', is not deobfuscated by Firebase properly; this happens on all Firebase crashes. So, no worries there.
I have been unable to reproduce this error myself and do not know where it goes wrong in the app (Kotlin coroutines all over the app). Difficult to simplify the version this way.
I attached some screenshots. (different app versions, so no ek9, but still same crash)
th...@gmail.com <th...@gmail.com> #11
kotlinx.coroutines.channels.ProducerCoroutine -> dr9:
So the issue is still exactly the same. It looks like the crash is happening for every update of my app, so although I cannot reproduce it myself, other users do get the crash in every build.
js...@google.com <js...@google.com> #12
Perhaps too early to tell, but this could be a vendor-specific issue. I searched for spec of those devices, and here are some findings:
* Xperia XA F311:
* BV8000 pro:
* Xperia L1:
* k11as_a:
* ZenPad 10 (Z300M):
* vernee_m5:
Alas, we don't have any testing devices that use one of those chipsets. Will discuss with teammates.
------
Meanwhile, could you do us a favor? You can build your release apk with Proguard, not R8 (by disabling it), and see if it doesn't trigger the same error. If so, investigating that apk could help us find a workaround too.
js...@google.com <js...@google.com> #13
ze...@google.com <ze...@google.com> #14
th...@gmail.com <th...@gmail.com> #15
ze...@google.com <ze...@google.com> #16
adb install app-googlePlay-release-universal.apk
adb shell cmd package compile -m speed -f com.superthomaslab.hueessentials
start the app
hit next until the "connect" screen
hit "philips hue" button
hit allow location
==> application crashes.
After this the app crashes on start until uninstalled and reinstalled.
Next step is looking at the compiled code to find the issue.
ze...@google.com <ze...@google.com> #17
th...@gmail.com <th...@gmail.com> #18
The app uses kotlinx.coroutines.flow.callbackFlow after permissions (allow location) have been granted. This could be the cause. A callbackFlow uses ProducerCoroutine.
The pattern that the app crashes on start and should be un-/reinstalled matches the user's indication. Clear data/cache was not sufficient.
ze...@google.com <ze...@google.com> #19
As noted previously by Jinseong, the allocation fa9 leads to the failing cast and is only allocated after on-device compilation. The stack trace at the point of compilation is:
at fa9.<init>(SourceFile:1)
at lw8.a(SourceFile:398)
at lw8.c(SourceFile:7)
at gg9.a(SourceFile:14)
at af9.a(SourceFile:4)
at lw8.a(SourceFile:343)
at lw8.a(SourceFile:362)
at an9.c(SourceFile:8)
at an9.a(SourceFile)
at lw8.a(SourceFile:328)
at lw8.a(SourceFile:468)
at lw8.a(SourceFile:208)
at o84.a(SourceFile:4)
at p84.c(SourceFile:18)
at p84.a(SourceFile:2)
at rm9.c(SourceFile:7)
at rm9.a(SourceFile)
at gg9.a(SourceFile:5)
at lw8.b(SourceFile:63)
at lw8.b(SourceFile:56)
at sm9.a(SourceFile:8)
at q84.a(SourceFile:9)
at lw8.a(SourceFile:45)
at zk9.a(SourceFile:1)
at r84.a(SourceFile:2)
at tm9.c(SourceFile:7)
at tm9.a(SourceFile)
at lw8.a(SourceFile:328)
at lw8.b(SourceFile:28)
at um9.b(SourceFile:5)
at om9.a(SourceFile:1)
at fm9.c(SourceFile:2)
at ga9.a(SourceFile:2)
at sg9.run(SourceFile:19)
at wr9.a(SourceFile:26)
at ur9.run(SourceFile:46)
retraced:
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$IntrinsicsKt__IntrinsicsJvmKt$4.<init>(IntrinsicsKt__IntrinsicsJvmKt.java:180)
at lw8.a(lw8.java:398)
at lw8.c(lw8.java:7)
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.java:109)
at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.java:154)
at lw8.a(lw8.java:343)
at lw8.a(lw8.java:362)
at kotlinx.coroutines.flow.internal.CombineKt$combineInternal$2.invokeSuspend(CombineKt.java:56)
at kotlinx.coroutines.flow.internal.CombineKt$combineInternal$2.create(CombineKt.java)
invoke(CombineKt.java)
at lw8.a(lw8.java:328)
at lw8.a(lw8.java:468)
at lw8.a(lw8.java:208)
at o84.a(o84.java:4)
at p84.c(p84.java:18)
at p84.a(p84.java:2)
at kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3$invokeSuspend$$inlined$collect$1$lambda$1.invokeSuspend(ChannelFlowTransformLatest.java:33)
at kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3$invokeSuspend$$inlined$collect$1$lambda$1.create(ChannelFlowTransformLatest.java)
invoke(ChannelFlowTransformLatest.java)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(UndispatchedKt.java:55)
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.java:111)
at lw8.b(lw8.java:63)
at lw8.b(lw8.java:56)
at kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3$invokeSuspend$$inlined$collect$1.emit(ChannelFlowTransformLatest.java:141)
at q84.a(q84.java:9)
at lw8.a(lw8.java:45)
at kotlinx.coroutines.flow.FlowKt__ChannelsKt$asFlow$$inlined$unsafeFlow$1.collect(FlowKt__ChannelsKt.java:121)
at r84.a(r84.java:2)
at kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3.invokeSuspend(ChannelFlowTransformLatest.java:88)
at kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest$flowCollect$3.create(ChannelFlowTransformLatest.java)
invoke(ChannelFlowTransformLatest.java)
at lw8.a(lw8.java:328)
at lw8.b(lw8.java:28)
at kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest.flowCollect(ChannelFlowTransformLatest.java:24)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlowOperator.java:102)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(ChannelFlowOperator.java:0)
at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.java:62)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(BaseContinuationImpl.java:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.java:241)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.java:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.java:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.java:740)
If this gives you or Jinseong any ideas let me know, and otherwise, I'll continue tracing the difference back tomorrow.
ze...@google.com <ze...@google.com> #20
ze...@google.com <ze...@google.com> #21
The addition of three nop instructions to:
lw8.a(Leg9;Ly99;ILeb9;I)Lhk9;
causes the issue to disappear.
I've tracked that to the callsite in:
.method public static final synthetic a(Leg9;Lsk9;)Lhk9;
.locals 3
.line 362
new-instance v0, Lxm9;
const/4 v1, 0x0
invoke-direct {v0, p1, v1}, Lxm9;-><init>(Lsk9;Ls99;)V
const/4 p1, 0x0
const/4 v2, 0x3
# Looks like compiler is inlining the call below.
# Here v0 <- xm9 : type eb9 == Function2
# but the contents of v0 appears to have been trashed with some other value.
invoke-static {p0, v1, p1, v0, v2}, Llw8;->a(Leg9;Ly99;ILeb9;I)Lhk9;
move-result-object p0
return-object p0
.end method
th...@gmail.com <th...@gmail.com> #22
th...@gmail.com <th...@gmail.com> #23
ze...@google.com <ze...@google.com> #24
I'd start by adding a keep rule for (the original source of) lw8.a(Leg9;Ly99;ILeb9;I)Lhk9 and also for the call-site method. Having the keep rule might be sufficient to ensure that the method is too large or otherwise avoids the invalid inlining of the host VM that we are hypothesizing is the culprit. I'd be very interested to hear if you get a local workaround, and I'll keep looking at how the compiler might implement a general workaround.
th...@gmail.com <th...@gmail.com> #25
Hi, I was looking into this issue again and it appears to have been fixed after updating Kotlin Coroutines to version 1.3.5-native-mt
. Note that this coroutines version is based on this WIP pull request (a preview release):
The normal "stable" coroutines version 1.3.5
still crashes. That means that something changed in the native-mt branch which fixes this issue.
The native-mt branch (see PR) has some changes to how the classes/functions CoroutineStart
, startCoroutineUndispatched
, etc are called in the library. Those changes appear to have fixed/workaround this ClassCastException issue.
ze...@google.com <ze...@google.com> #26
[Deleted User] <[Deleted User]> #27
We have a similar crash but with a different class (kotlinx.coroutines.channels.ProducerCoroutine being cast to kotlin.jvm.functions.Function2)
I have updated to Coroutines 1.3.6 and will release to production soon. I will report back if this issue is solved then.
Android Version: 100% Android 7
Devices:
- TCL 5049S
- Asus ZenFone 3 Max (ZC520TL)
- Hisense F24
- TECNO i5-Pro
- Sony Xperia XA
- STF DUO
- OPPO CPH1717
Stack Trace:
Fatal Exception: java.lang.ClassCastException: k.a.l2.u cannot be cast to kotlin.jvm.functions.Function2
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$IntrinsicsKt__IntrinsicsJvmKt$4.invokeSuspend(IntrinsicsKt__IntrinsicsJvmKt.java:199)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(BaseContinuationImpl.java:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.java:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.java:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.java:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.java:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.java:665)
ze...@google.com <ze...@google.com> #28
[Deleted User] <[Deleted User]> #29
I still see the issue in 1.3.6 unfortunately.
th...@gmail.com <th...@gmail.com> #30
The Coroutines native-mt branch has not been merged into stable yet (in your case version 1.3.6). That will probably not happen anytime soon, unfortunately. See:
b9...@gmail.com <b9...@gmail.com> #31
Does AGP 4.0 stable fix this?
ze...@google.com <ze...@google.com> #32
b9...@gmail.com <b9...@gmail.com> #33
I saw lots of this type of crash in OPPO Android 7.
ey...@gmail.com <ey...@gmail.com> #35
Unsure if this will help with discovering the VM issue, but I started getting a new crash from the manufacturer (Tecno) on Android 7:
Fatal Exception: java.lang.AbstractMethodError: abstract method "int kotlin.coroutines.jvm.internal.SuspendLambda.getArity()"
at kotlin.jvm.internal.TypeIntrinsics.getFunctionArity(TypeIntrinsics.java:259)
at kotlin.jvm.internal.TypeIntrinsics.isFunctionOfArity(TypeIntrinsics.java:336)
at kotlin.jvm.internal.TypeIntrinsics.beforeCheckcastToFunctionOfArity(TypeIntrinsics.java:341)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(UndispatchedKt.java:91)
at kotlinx.coroutines.flow.internal.FlowCoroutineKt.flowScope(FlowCoroutineKt.java:33)
at kotlinx.coroutines.flow.internal.ChannelFlowTransformLatest.flowCollect(ChannelFlowTransformLatest.java:24)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlowOperator.java:128)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(ChannelFlowOperator.java:1)
at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.java:53)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(BaseContinuationImpl.java:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.java:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.java:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.java:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.java:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.java:665)
sg...@google.com <sg...@google.com> #36
eygraber, the crash reported in
lo...@gmail.com <lo...@gmail.com> #37
I've seen a similar issue affect this device:
Brand: Blackview
Model: BV8000 Pro
Android version: 7.0
Here's the stacktrace that I got:
Fatal Exception: java.lang.ClassCastException: a0.a.t2.s cannot be cast to kotlin.jvm.functions.Function2
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$IntrinsicsKt__IntrinsicsJvmKt$4.invokeSuspend(IntrinsicsKt__IntrinsicsJvmKt.java:205)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(BaseContinuationImpl.java:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.java:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.java:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.java:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.java:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.java:665)
lo...@gmail.com <lo...@gmail.com> #38
I think if you get that device (can be found for cheap on Ali Express I guess), and you run the apk provided in
th...@gmail.com <th...@gmail.com> #39
Just wanted to give an update on this old issue. For the past few years I have been using the native-mt versions of coroutines, where the issue was not reproducible, so that worked fine for me. However, recently support for the native-mt versions has ended and everyone is supposed to use the normal coroutines releases now. So I have changed my coroutines version to "1.6.1".
Tried reproducing the crash with "1.6.1" and was not able to get my app to crash. I used the steps in
I am no longer able to reproduce the issue in my current project, but I am not sure if it is because of my code changes or coroutines code changes. Or maybe even because I am using a newer R8 version now (3.3.26-dev).
In case the crash would appear again in the future, I will keep you updated and hope to find some keep rules that work.
Description
Fatal Exception: java.lang.ClassCastException: ek9 cannot be cast to kotlin.jvm.functions.Function2
at kotlin.jvm.internal.TypeIntrinsics.throwCce + 26(TypeIntrinsics.java:26)
at kotlin.jvm.internal.TypeIntrinsics.throwCce + 22(TypeIntrinsics.java:22)
at kotlin.jvm.internal.TypeIntrinsics.beforeCheckcastToFunctionOfArity + 342(TypeIntrinsics.java:342)
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$IntrinsicsKt__IntrinsicsJvmKt$4.invokeSuspend + 199(IntrinsicsKt__IntrinsicsJvmKt.java:199)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith + 33(BaseContinuationImpl.java:33)
at kotlinx.coroutines.DispatchedTask.run + 241(DispatchedTask.java:241)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely + 594(CoroutineScheduler.java:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely + 60(CoroutineScheduler.java:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run + 740(CoroutineScheduler.java:740)
Android Studio 3.5 stable. The mapping shows R8 version 1.5.64 and R8 fullMode is enabled.
I attached the APK in this post. I modified the attached to only include the kotlin* classes. Let me know if you need other parts of the mapping.