paparazzi: Getting OutOfMemoryErrors after ~500 screenshots
Description
In our project we’re using a combination of Showkase and Paparazzi to automatically generate screenshot tests for our components. Everything was working fine until quite recently, when we noticed at around 500 screenshots the record and verification tests were failing with OOMs.
Steps to Reproduce
You can use this branch in our repo experiment/screenshots-oom
, then run either recordPaparazziDebug
or verifyPaparazziDebug
.
If you run it with logs, you’ll be able to see some logs like:
io.element.android.tests.uitests.ScreenshotTest > preview_tests[io.element.android.libraries.designsystem.theme.components_null_TextFields_TextFieldDarkPreview_0_null,NEXUS_5,1.0,en] STANDARD_OUT
maxMemory : 536870912
totalMemory: 330301440
...
io.element.android.tests.uitests.ScreenshotTest > preview_tests_screens_light[preview] STANDARD_OUT
maxMemory : 536870912
totalMemory: 536870912
io.element.android.tests.uitests.ScreenshotTest > preview_tests_screens_light[preview] STANDARD_ERROR
Jun 01, 2023 9:48:06 AM app.cash.paparazzi.internal.PaparazziLogger logAndroidFramework
INFO: Settings [3]: Can't get key animator_duration_scale from content://settings/global
Jun 01, 2023 9:48:07 AM app.cash.paparazzi.internal.PaparazziLogger error
SEVERE: broken: Failed executing Choreographer#doFrame
java.lang.OutOfMemoryError: Java heap space
at java.desktop/java.awt.image.DataBufferInt.<init>(DataBufferInt.java:75)
at java.desktop/java.awt.image.Raster.createPackedRaster(Raster.java:539)
at java.desktop/java.awt.image.DirectColorModel.createCompatibleWritableRaster(DirectColorModel.java:1032)
at java.desktop/java.awt.image.BufferedImage.<init>(BufferedImage.java:333)
at app.cash.paparazzi.internal.ImageUtils.scale(ImageUtils.kt:238)
at app.cash.paparazzi.Paparazzi.scaleImage(Paparazzi.kt:412)
at app.cash.paparazzi.Paparazzi.access$scaleImage(Paparazzi.kt:83)
at app.cash.paparazzi.Paparazzi$takeSnapshots$1$2.invoke(Paparazzi.kt:325)
at app.cash.paparazzi.Paparazzi$takeSnapshots$1$2.invoke(Paparazzi.kt:312)
at app.cash.paparazzi.Paparazzi.withTime(Paparazzi.kt:360)
at app.cash.paparazzi.Paparazzi.takeSnapshots(Paparazzi.kt:312)
at app.cash.paparazzi.Paparazzi.snapshot(Paparazzi.kt:206)
at app.cash.paparazzi.Paparazzi.snapshot$default(Paparazzi.kt:205)
at app.cash.paparazzi.Paparazzi.snapshot(Paparazzi.kt:201)
at app.cash.paparazzi.Paparazzi.snapshot$default(Paparazzi.kt:197)
at io.element.android.tests.uitests.ScreenshotTestKt.screenshotTest(ScreenshotTest.kt:176)
at io.element.android.tests.uitests.ScreenshotTestKt.access$screenshotTest(ScreenshotTest.kt:1)
at io.element.android.tests.uitests.ScreenshotTest.preview_tests_screens_light(ScreenshotTest.kt:121)
at jdk.internal.reflect.GeneratedMethodAccessor30.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at app.cash.paparazzi.Paparazzi$apply$statement$1.evaluate(Paparazzi.kt:125)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
io.element.android.tests.uitests.ScreenshotTest > preview_tests_screens_light[preview] FAILED
java.lang.OutOfMemoryError: Java heap space: failed reallocation of scalar replaced objects
And every test adds a couple of MBs to the totalMemory value until the maxMemory is reached.
We tried removing everything that’s not ‘standard’ from the tests, even tried moving from TestParameterInjector
to Parameterized
runners in case this was the source of the issue, but the issue persisted.
I’d upload a heap dump, but that’s quite big, so it’s better to just run the tests to generate one locally by running either paparazzi task.
Expected behavior Tests should run with no memory being retained in each run.
Additional information:
- Paparazzi Version: 1.3.0
- OS: MacOS 13.4, but it was reproduced in a Linux machine too.
- Compile SDK: 33
- Gradle Version: 8.1
- Android Gradle Plugin Version: 8.0.1
About this issue
- Original URL
- State: closed
- Created a year ago
- Reactions: 7
- Comments: 18 (2 by maintainers)
@jmartinesp After conducting some investigations, I identified it is related to the memory leak of context in BridgeContentResolver from animationScale map in WindowRecomposer.android.kt.
In your instance where lots of test cases are executed within same
Paparazzi
instance, an excessive accumulation ofAndroidComposeView
occurs, leading to the occurrence of memory leak.An issue has been filed to the layoutlib team.
cc @jrodbx
This fix works around the memory leak issue, tested on kotlin 1.9.10 This involves a couple of steps:
Paparazzi
in your own rule, and use aRunRules
chain to runPaparazziCleanupRule
at the end.PaparazziCleanupRule
using a reflection hack to retrieve the privatebridgeRenderSession
object.RenderSession
object.TestRule wrapper
Rendersession Hack
I still can’t see what MainTestWatcher is @KatieBarnett thoughts on that? Desperately trying to get this to work 😕
Thanks for any help in this.
Working on this more, you meant to say you didn’t need the Watcher thing? I am trying that with no change, not sure why but 1.3.2 and 1.3.3 hang gradle with this fix as I’ve attempted it from the description 😕
This issue is still happening with version 1.3.3. The above hack needs to be altered to comment out
as
ComposeViewAdapter
is now internal.The hack still appears to work with this commented out.
Got it working now, thanks @agrosner!