Status Update
Comments
ma...@google.com <ma...@google.com>
to...@gmail.com <to...@gmail.com> #2
First of all thanks for this detailed issue.
This issue had been investigated thoroughly when it was first reported internally. The surprising detail in this report is that the issue is not reproducible before 1.7
. I will look into this.
The main problem with POBox is the fact that it is deprecated. Since 2021 Sony has been shipping new Xperia devices with Gboard pre-installed. Although we are aware that there is still a considerable amount of users still using POBox, the described behavior is caused by POBox's noncompliant behavior with InputConnection
and InputMethodManager
documentation. However, this is understandable since TextView
implementation was also not respecting the behavior that is expected from Editors.
Ultimately we have decided to enforce the documented behavior with specifically regards to when editors should call InputMethodManager.updateSelection
. Also, although unconfirmed, there were traces of possible custom code being included in Sony OEM images that changed how InputMethodManager was notified from TextView. If POBox also depended on something like this, it would be impossible for Compose code to replicate the same unknown behavior.
an...@google.com <an...@google.com> #3
Or is that option not available?
Even if the root cause is POBox, from the perspective of the app's customers, it looks like an app bug, so this issue is a blocker against updating Jetpack Compose.
to...@gmail.com <to...@gmail.com> #4
Just to be sure, it is dangerous to replace Compose TextField with Android View EditText as a workaround for this issue.
Compose 1.7 has a bug that causes ANR when the focus is on EditText.
Another View-related bug in Compose 1.7 is that an Android View is focused by calling FocusManager.clearFocus().
Perhaps there is a lack of testing of Compose 1.7 in combination with Android View. There is also a possibility that there are other fatal bugs related to View.
In other words, the only options for apps targeting the Japanese market that require POBox support are to continue using Compose 1.6 or to use EditText in combination with various workarounds.
an...@google.com <an...@google.com> #5
Project: platform/frameworks/support
Branch: androidx-main
Author: Halil Ozercan <
Link:
Fix POBox keyboard issue
Expand for full commit details
Fix POBox keyboard issue
Fix: 373743376
Fix: 329209241
Test: NullableInputConnectionWrapperTest
Change-Id: I94e0e598274fb88b255f977f9fbd50dfbbb1ecb1
Files:
- M
compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapperTest.kt
- M
compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/NullableInputConnectionWrapper.android.kt
Hash: 57f58c4b80d5d8470b2aca325dfdcd55f235231e
Date: Thu Oct 24 01:25:20 2024
to...@gmail.com <to...@gmail.com> #6
Many thanks again for this report. Especially for giving us a huge clue in terms of what could be going wrong. The fix is now merged and I will ask for a cherry-pick into a stable release.
to...@gmail.com <to...@gmail.com> #7
Do you have any concrete plan to cherry-pick the fix into current stable version (1.7.x)? We are currently waiting it.
to...@gmail.com <to...@gmail.com> #8
Yes, this fix is planned to be included in a future 1.7.x
release.
an...@google.com <an...@google.com> #9
Thanks for the fix. Sorry to follow up on this. is it possible for you to share specific release version/date for the stable version? We are waiting on this to decide on our direction.
to...@gmail.com <to...@gmail.com> #10
Hum did not thought I always see you answer late, I'm at the point where I would give you access to the code but it's a 100k lloc + tons of deps only available on a private gitlab server this would be as a pain than rebuilding a full repro.
I guess this will make my day tomorrow :(
About the snapshot version and the lag we are talking about 1 - 1.5 second complete lock of UI during scroll and loading the next item, item that is a text and a lazyrow of 15 item not even large.
to...@gmail.com <to...@gmail.com> #11
Ok my wife is not happy but found the culprit in case it's enough and I can avoid a repro.
val contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
LazyRow(
state = lazyListState,
contentPadding = contentPadding,
flingBehavior = rememberSnapperFlingBehavior(lazyListState, snapOffsetForItem = SnapOffsets.Start, endContentPadding = contentPadding.calculateEndPadding(LayoutDirection.Ltr)),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
The lazyrows use Chris Banes Snapper 0.2.0
If it's not enough leave a message else I'll focus on the mem leak, removing snapping as temporary workaround is not a major issue.
an...@google.com <an...@google.com> #12
LazyColumn {
items(1000) { x ->
val contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
val lazyListState = rememberLazyListState()
LazyRow(
state = lazyListState,
contentPadding = contentPadding,
flingBehavior = rememberSnapperFlingBehavior(
lazyListState,
snapOffsetForItem = SnapOffsets.Start,
endContentPadding = contentPadding.calculateEndPadding(
LayoutDirection.Ltr
)
),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(100) { y ->
Text("Item x=$x y=$y", Modifier.height(300.dp))
}
}
}
}
Tried this code, and didn't notice any difference if I comment out the flingBehavior. not sure why it affects you
to...@gmail.com <to...@gmail.com> #13
Ok so I'll try to do both.
Seeing all the issues do you accept a large repro that I'll keep as a base matching most of my code and not directly trying to do small repros ?
to...@gmail.com <to...@gmail.com> #14
So I'm far from having a proper repro but:
- Wrapping the LazyList in a Box with nestedScroll have high performance impact and triggers the below issue way faster. Even an empty one like
.nestedScroll(remember<NestedScrollConnection> {
object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return Offset.Zero
}
}
})
- The real underlying issue seems to be that LazyColums makes a tons of copy of the whole backed data, even in the non leak scenario if the backed data is large it relatively quickly generate memory fragmentation and prevent the further allocations. See attached screenshot of when it happens, the GC starts to panic and the app quickly crash after that.
For the snapper / perf regression this seems to be a Android Studio issue when it happens clear cache, restart, rebuild fix it.
For the nestedScroll I'm using it to move a top floating bar with and applying .offset { IntOffset(x = 0, y = topButtonBarState.offsetState.value.roundToInt()) },
@Stable
data class TopButtonBarState(val offsetState: State<Float>, val nestedScrollConnection: NestedScrollConnection, private val topBarScrollDistancePx: Float) {
fun onManualScrollDirection(direction: Int) {
nestedScrollConnection.onPostScroll(Offset(0f, topBarScrollDistancePx * -direction), Offset(0f, 0f), NestedScrollSource.Fling)
}
}
@Composable
fun rememberTopButtonBarState(): TopButtonBarState {
val statusBarPadding = WindowInsets.statusBars.asPaddingValues()
val topBarScrollDistance = 56.dp + statusBarPadding.calculateTopPadding()
val topBarScrollDistancePx = with(LocalDensity.current) { topBarScrollDistance.roundToPx().toFloat() }
val topBarOffsetPx = rememberSaveable { mutableStateOf(0f) }
val nestedScrollConnectionOffset = remember<NestedScrollConnection> {
object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
val delta = consumed.y
val newOffset = topBarOffsetPx.value + delta
topBarOffsetPx.value = newOffset.coerceIn(-topBarScrollDistancePx, 0f)
return Offset.Zero
}
}
}
return remember {
TopButtonBarState(topBarOffsetPx, nestedScrollConnectionOffset, topBarScrollDistancePx)
}
}
Is there another way to achieve that from the lazyliststate without the nested scroll?
Edit: found
to...@gmail.com <to...@gmail.com> #15
So got some kind of repro based on a previous one I made for you.
Just start the app and it will crash after some time.
The scroll/refresh simulation is made via
var reload by remember { mutableStateOf(0) }
LaunchedEffect(key1 = Unit) {
while (true) {
reload = Random.nextInt()
delay(100)
}
}
val items = remember(lazyPagingItems.size, stepSize, reload) {
derivedStateOf {
List(lazyPagingItems.size) { it }.windowed(stepSize, stepSize, true)
}
}
to...@gmail.com <to...@gmail.com> #16
And last comment to confirm the underlying issue with the full data copy.
Switching from:
items(lazyPagingItems) { item ->
to
items(lazyPagingItems.itemCount) { index ->
val item = lazyPagingItems[index]
vastly reduce the memory constraints and the issue on the main app.
an...@google.com <an...@google.com> #17
Can confirm your sample project is crashing for me, thanks. I will investigate it
to...@gmail.com <to...@gmail.com> #18
Nice.
Tell me if you need more data from the real app, the repro is a bit hackish but leads to same result so I suppose it's still valid enough about the root cause.
an...@google.com <an...@google.com> #19
Chuck, I need your help with investigating this. The smallest repro
var reload by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
reload++
delay(100)
}
}
val items = remember(reload) {
derivedStateOf {
List(1000000) { it }
}
}
Text("List of size ${items.value.size}")
After a few seconds the app will crash with out of memory exception. Seems like there is some kind of a memory leak in the derivedstate implementation.
to...@gmail.com <to...@gmail.com> #20
For the record the main app crash when scrolling the lazy list without any refresh of any derivedState. So if no derived inside Lazy it's maybe 2 different issues and I repro the wrong one.
an...@google.com <an...@google.com> #21
We use DerivedState as an implementation detail in lazy lists as well, so most likely it is the same issue. Otherwise we first try to fix DerivedState and then test with your app again
ch...@google.com <ch...@google.com> #22
The issue is that there is an assumption that between the start of a function and the end that the Snapshot.current.id
does not change. However, reading a derivedStateOf
value will change the id of a snapshot so the derived state of value can know when to correctly recalculate the value if the values to calculate it change in the same snapshot.
This causes a memory leak because the RecomposeScope
tries to only forget about object that it is no longer observing if it the changes are actually applied for that scope. It does this by recording a lambda in the changes
list of composition. It further checks if the change that is being applied is from the last composition, if not, the next composition will clean the scope so it can ignore the notification. The recompose scope uses the Snapshot.id
of the composition snapshot as a token to identify which composition it is in. This is not valid to do as the snapshot id
can change during the composition. Because it changed the RecomposeScope
thinks it is receiving an out-of-date notification to forget its unused objects and ignores the notification.
The fix for this is uncertain yet. We can either copy the snapshot id at the beginning of composition and use that instead of snapshot.id
or always assume that start()
and end()
for the recompose scope are paired and trust that when end()
of the RecomposeScope is called it is for the same composition for which start()
was called. The check might also not be needed as the case it is checking probably never occurs.
ch...@google.com <ch...@google.com> #23
I have a fix for this pending that takes the "copy the snapshot id at the beginning of composition" option from above.
to...@gmail.com <to...@gmail.com> #24
Nice, is this 1.2 material or beta is already branched and it's 1.3?
ch...@google.com <ch...@google.com> #25
This should make it into 1.2 automatically. If not, i will start a cherry-pick to get it into 1.2.
ap...@google.com <ap...@google.com> #26
Branch: androidx-main
commit 78dc85f30cb72ba38d36bee4f85509a975f19d06
Author: Chuck Jazdzewski <chuckj@google.com>
Date: Wed May 04 12:01:08 2022
Fix memory leak when observing derived state objects
When observing derived state objects, if the recompose snapshot changed
id it caused the derived state objects no longer observed by composition
to be retained by the recompose scope, causing them to leak. The
snapshot id would change if the read is the first time a derived state
object is read in the snapshot.
To detect when a derived state object is no longer necessary a token
value is used to record when the derived state was added into the
composition. This token was the current snapshot id. However, since
the id is not static through-out composition, this could cause the
notification to be skipped.
This change fixes the token to be the snapshot id at the beginning of
composition and, after that, ignores the snapshot id. This means the
token is consistent through-out the same composition allowing unused
derived object to be detected.
Fixes 230168389
Test: new test, ./gradlew :compose:r:r:tDUT
Change-Id: I877c826764592a7e424667d3e2b677d385cf0b7f
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
M compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/mock/CompositionTest.kt
M compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
to...@gmail.com <to...@gmail.com> #27
So just tested with Snapshot 8544662 that contains that fix.
And the root issue is still present. The application attached earlier still crash after a few seconds. (And my main app during simple scrolls).
Can you please reopen?
an...@google.com <an...@google.com>
to...@gmail.com <to...@gmail.com> #28
Thanks,
For the record on release builds there's a huge win I can scroll a lot longer than without this so this was also related I guess.
an...@google.com <an...@google.com> #29
```
var reload by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
reload++
delay(100)
}
}
val items = remember(reload) {
derivedStateOf {
List(1000000) { it }
}
}
LazyColumn {
items(items.value) { item ->
Text("item $item")
}
}
```
Wasn't able to decouple it even more yet. Probably what is important is the fact that the lambda passed into LazyColumn is wrapped into another derived state and then this derived state is executed in composition, but when I tried to emulate it it wasn't crashing
ch...@google.com <ch...@google.com> #30
The previous fix was necessary but not sufficient to fix this issue.
The issue demonstrated by the above is when derivedStateOf
object changes what objects it reads the composition still remembers the old objects in addition to the new objects. A composition maintains a map of the objects that a dependency object reads so that, when one of those objects changes, it knows which dependency object to invalidate. If this list changes the the only the current dependencies should be retained and any previous value no longer observed should be forgotten.
I am investigating ways to ensure the inverted map is valid without causing undo additional overhead.
to...@gmail.com <to...@gmail.com> #31
Small bump on this one, the app is now in prod and I start to get OOM crash reports.
Is this something that could be targeted for 1.2 or should I try to figure out a way to reduce even more the data used in those derivedstate for the moment?
ap...@google.com <ap...@google.com> #32
Branch: androidx-main
commit ad7c95cd58204ef7be4cec4f999d602619c51a8a
Author: Andrei Shikov <ashikov@google.com>
Date: Wed Jun 01 14:11:05 2022
Clean up derived state dependencies in composition
Fixes two cases when derived state dependencies were retained:
1) Derived state instance is no longer observed in composition.
2) Derived state instance is still observed in composition, but its dependencies has been changed since last observation.
As composition uses `dependency -> derived state` mapping, this fix relies on iterating over all map values with `.removeValueIf`. In the future, we can optimize the lookup by using reverse index to lookup dependencies for derived state.
Fixes: 230168389
Test: CompositionAndDerivedStateTests#changingTheDerivedStateInstanceShouldClearDependencies, CompositionAndDerivedStateTests#changingDerivedStateDependenciesShouldClearThem
Change-Id: Id175952fedae53d669d028e460c65d6d2d5d4174
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityScopeMap.kt
M compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
to...@gmail.com <to...@gmail.com> #33
Ok so the reproduction posted here is now fixed, but the main app still OOM when scrolling in large lazy list can this be reopened and moved back to lazylist?
Starting to wonder if now it's not back to be tied to
to...@gmail.com <to...@gmail.com> #34
The heapdump looks pretty much the same see attached. Will need some hints to build the new repro.
ad...@google.com <ad...@google.com> #35
Cases in
an...@google.com <an...@google.com>
an...@google.com <an...@google.com> #36
The best new repro I found so far is:
var reload by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
reload++
delay(200)
}
}
val items = remember {
derivedStateOf {
reload
ShortArray(10000000) { -5 }
}
}
LazyColumn {
items(500) { item ->
Text("item index=$item ${items.value[0]}")
}
}
But now it will not crash straightaway because of the previous fix and now you have to scroll until it crashes. Note: I used ShortArray so it is easier to find it in the heap as short[]
to...@gmail.com <to...@gmail.com> #37
Thanks for the repro it made me think about testing a change on my code and reverting the previous derived state indexer to avoid passing the full items list to items() and it does reduce the memory constraint (it still crash but requires more scroll).
So going back from
val indexer by remember {
derivedStateOf {
List(lazyPagingItems.itemCount) { it }
}
}
items(indexer) { index ->
val item = lazyPagingItems[index]
to the normal
items(lazyPagingItems) { item ->
Does reduce the issue when before the 2 derivedstate fixes it was helping a lot.
to...@gmail.com <to...@gmail.com> #38
Sorry to post here as not really related but I'm trying to update the snapshot version to a more recent version to see if your
But I'm getting java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/compose/foundation/lazy/LazyItemScope$DefaultImpls;
errors from any libraries compiled with compose 1.2 beta 2 probably because of
Is there any trick to have this working or do I need to wait for the libs to update and won't be able to test possible fixes for this issue here? (I tested adding -Xjvm-default=all
on my side side but no effect)
an...@google.com <an...@google.com> #39
Does reduce the issue when before the 2 derivedstate fixes it was helping a lot. Yes, I would recommend you to rewrite your code without derivedstate in the meantime
see if your
https://github.com/androidx/androidx/commit/a1ba9c954b9cc2e36bf897e4c649f52442a07a0b fix the rarecrash I get. No, I do not expect this change to fix anything for you https://issuetracker.google.com/issues/233929916
About your new crash I will let the responsible folks know, thanks
to...@gmail.com <to...@gmail.com> #40
Yes, I would recommend you to rewrite your code without derivedstate in the meantime
Is there any performance difference between LazyVerticalGrid with 1 column and LazyColumn ?
I still use a derivedstate to manually window and implement the grid via a LazyColumn as the app allows switching between both and I need to listen to the state for other things related to first visible items.
Since LazyGrid and LazyColumn state are not equivalent and there's no common interface for the common functions it's a pain to deal with both and I'd like to stay with only one.
an...@google.com <an...@google.com> #41
If your state is not frequently changing (like when user tap on switch mode button, not on every frame) you don't really need derived state.
```
val mode by remember { mutableStateOf(true) }
val derivedValue = remember(mode) {
... calculate something based on mode
}
```
to...@gmail.com <to...@gmail.com> #42
By derived data I meant the data to feed the LazyColumn to emulate the LazyGrid.
val items = remember(lazyPagingItems.itemCount, stepSize) {
derivedStateOf {
List(lazyPagingItems.itemCount) { it }.windowed(stepSize, stepSize, true)
}
}
But yes I suppose the preventive derivedStateOf is not mandatory in this case.
The problem about the state being different classes, is for the fastscroll for example, or the quick return or any other visible items observing to react. The state is hoisted and used elsewhere, but this does not work if the inner component can change between both, this is the main pain point of the 2 different states, either have 2 different implementations of the observers or build another state and wrap what's needed.
Anyway this is not the place and since the LazyGrid api is no more experimental and I believe you about perf, I'll migrate all to LazyGrid to simplify my future life.
Thanks again.
to...@gmail.com <to...@gmail.com> #44
Forget to post but yes this fix the compilation.
But since I digressed a lot, just wanted to be sure the original issue is still being investigated? I see it's already 1.3 now.
th...@gmail.com <th...@gmail.com> #45
th...@gmail.com <th...@gmail.com> #46
ap...@google.com <ap...@google.com> #47
Branch: androidx-main
commit 11dbe1f459907ec1c23e730c77e33de39fc894cf
Author: Chuck Jazdzewski <chuckj@google.com>
Date: Wed Jun 22 14:17:20 2022
Remove tracking information from release recompose scopes
Recompose scopes can be tracked by the recomposer past when they
are removed from composition. Since it is expensive to accurately
track the lifetime of scopes they are cleaned up lazily. This
lazy cleanup might not trigger in some cases causing the references
tracked by the scope to leak.
This change first removes tracked references from recmpose scopes
that are removed from composition and, second, reduces the chance
that such scopes are tracked longer then necessary by cleaning up
the conditionally invalid scopes table when scopes are removed.
Fixes: 230168389
Test: ./gradlew :compose:r:r:tDUT
Change-Id: Idfe3e3d5d6d65bac2cf06ee19fd00a5d9253d8a1
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
M compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
to...@gmail.com <to...@gmail.com> #48
So unfortunately it's not 100% fixed, can this be reopened again?
The repro app still crash after scrolling but requires more than before. (Same for prod app)
Furthermore last snapshot completely breaks LazyX overscroll (a LazyColumn will now overscroll on left / right too when 100% unwanted see attached video).
an...@google.com <an...@google.com> #49
Thanks for the report. I filed a separate bug for this issue with overscroll -
to...@gmail.com <to...@gmail.com> #50
Thanks I lack time before the holidays but I hope you can go to the bottom of it. (Other bug is private I can't follow it)
Since the leak is not tied to the actual composable even leaving the screen completely won't return the memory so in the end if the user stays enough in the app it will reach OOM, and even if not seems on Pixels the memory killer don't like it at all and kill the app even with a foreground service active for low memory when going background. (And well that's a bummer to have a music player killed during playback it kinda defeat it's purpose).
an...@google.com <an...@google.com> #51
Sorry, made
an...@google.com <an...@google.com> #52
I tried to run the demo app again after all the recent changes and unfortunately can't reproduce what you are seeing. It is not crashing for me after scroll and I don't see anything leaking. And regardless of the leaks, I really do not recommend you writing code like this
val items = remember(lazyPagingItems.size, stepSize, reload) {
derivedStateOf {
List(lazyPagingItems.size) { it }.windowed(stepSize, stepSize, true)
}
}
Allocating such lists only to produce list of indexes is very inefficient. You can just instead write some function which will be mapping indexes from the main data set to the new windowed one and it will require no allocations. But realistically as we already discussed you better just use LazyVerticalGrid, it is an api exactly for this use case. If after applying those recommendations you will think you still have some memory leaks feel free to file a separate bug. Thanks
to...@gmail.com <to...@gmail.com> #53
The issue also occurs on normal lazylist without using those derived state.
The demo requires a lot more scroll but will trigger the issue at some point.
The GC will say 75% free then 45% free then 75% free then at some point will go down to 15% then 45% then ... Until the OOM.
This is just no more automatic and quick as previously. Scroll more up down to at least the middle to trigger it.
an...@google.com <an...@google.com> #54
For you it is crashing exactly because you are creating huge lists inside derivedStateOf. And I recommend you to not do that in your code as you are really don't have to
to...@gmail.com <to...@gmail.com> #55
Again I'm not when in list mode I directly use the lazy items and it behave the same.
And even when I do I create a list of 22000 Int this is not exactly what you can call a huge list.
Anyway I'll try to build another repro when back in 2 weeks. Will link here for proper attribution.
to...@gmail.com <to...@gmail.com> #56
Just tested again with one of the last snapshot from last hours and there's changes.
It's now quite hard to reproduce with the repro from here but still randomly can trigger.
But the very nice change is that removing the lazycolumn from composition does free the memory now. So on the main app the impact should be minimal instead of ineluctable. Just need the overscroll fix.
Was the video enough or need something before I leave ?
to...@gmail.com <to...@gmail.com> #57
Did a quick heap dump since the memory is freed on composable removal from the screen and the data is different see attached screenshots.
Maybe this will lead some light until I can build a repro in at least 2 weeks.
to...@gmail.com <to...@gmail.com> #58
One last for Adam
java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/compose/foundation/lazy/grid/LazyGridItemScope$DefaultImpls;
Seems that one passed the 2 JvmDefaultWithCompatibility fixes.
And now sea and sun.
to...@gmail.com <to...@gmail.com> #59
So for the record my leaks are now fully fixed with snapshot 8859449 (Maybe thanks to this
But I'm now facing another strange Compose bug that I have an hard time debugging :(
ma...@gmail.com <ma...@gmail.com> #60
This issue should be reopened as it still valid in compose 1.3.0-beta01
.
How to reproduce:
1 - Scroll the LazyList
a bit
2 - Press the big red rectangle to remove the LazyList
from the composition
3 - Press the big red rectangle again
4 - Go to 1
The memory usage will grow up endlessly.
to...@gmail.com <to...@gmail.com> #61
I can't repro with my previous code and have not found a proper repro (not tested #60)
But I've pushed a beta of my app with 1.3.0 beta 1 and I've now got a return of OOM crashes, so there's definitively something back here :(
ch...@google.com <ch...@google.com>
ch...@google.com <ch...@google.com> #62
I reopen the bug. I reproduced the memory leak demonstrated by the above linked MainActivity.kt supplied by
SnapshotStateObserver
created for AndroidComposeView
are not be removed when the LazyColumn
is removed from composition.
This appears to be a different issue that was originally reported but will use this bug to track this as it is still a memory leak involving a LazyColumn
or LazyRow
and might be the issues the followers of this bug are running into instead of the original problem.
Note that a number of fixes regarding this went into 1.2 and 1.3. However, if you just update the version of androidx.compose.ui to 1.3.0-beta01 (which is what you get if you update compose_ui_version
in the default Studio template to 1.3.0-beta01) you will not many of these fixes as they are part of the 1.3 runtime. Ensure you update to an explicit reference to androidx.compose.runtime:1.3.0-beta01 as 1.3.0-beta01 only requires you update to 1.2.0+ of the runtime and will not automatically update to the latest runtime.
to...@gmail.com <to...@gmail.com> #63
For the record the mails are hidden to normal users you should not disclose them in case it's unwanted and to avoid spam.
I'm full B1 and no issue with alpha3 all previous issues where correctly fixed.
I proposed to Andrey to bisec between those versions is this still necessary?
an...@google.com <an...@google.com> #64
ch...@google.com <ch...@google.com>
mo...@google.com <mo...@google.com> #65
Ralston is making a large change for focus that will fix this problem. He's agreed to take this on.
da...@sonos.com <da...@sonos.com> #66
I had the same type of issues, where using a LazyRow and scrolling a lot would see performance degradation to the point where the app was unusable. I can say that the fixes from this bug have definitely fixed my issue, so thank you very much! However, now I'm seeing that my derivedStateOf calculations are getting called all the time.
I have a derivedStateOf which calculates layout information based off changes to the scroll state. For example:
class LayoutProvider(
val layoutState: State<MyLayout>
)
@Composable
fun rememberLayoutProvider(): LayoutProvider {
val layoutState = remember {
derivedStateOf {
// run some big calculations to calculate a layout
calculateLayout()
}
}
return remember { LayoutProvider(layoutState) }
}
And then many different composables use my derived state to create their own derivedStateOf to get their layout information. For example:
@Composable
fun SomeTile() {
val myLayout = remember {
derivedStateOf {
layoutProvider.getMyLayout()
}
}
do something with myLayout
}
In compose 1.2.0, this worked really well, and when the scroll state would change, the layout provider's calculation would run once, and only composables whose specific layout changed (myLayout
) would get recomposed.
Now in compose 1.3.0-beta01, the calculation in the LayoutProvider gets called a large number of times, and the composoables who have their own derivedState get recomposed even when their specific layout hasn't changed.
I noticed the change to the derivedStateOf
, with the addition of a policy, but I tried writing my own and still couldn't get the results I wanted. Can you please explain how I can go about getting these calculations to get called as I'd expect and only have the composables that have their derived state changed recompose?
Thank you
da...@sonos.com <da...@sonos.com> #67
To give some more information, when when calling derivedStateOf(structuralEqualityPolicy())
in both those places in the code above, it still behaves incorrectly. Basically, the composable SomeTile
always gets recomposed any time the scroll state changes, and only at the call site where myLayout
gets used does the derivedState calculation run (i.e. layoutProvider.getMyLayout()
. Seeing the whole point of derivedState is so that we can avoid recomposing composables on state changes that can happen frequently, this now feels broken to me. Please let me know if I'm missing something.
Thank you
da...@sonos.com <da...@sonos.com> #68
I was able to make it even more simple:
@Composable
fun SomeComposable(
scrollState: LazyListState
) {
val red by remember {
derivedStateOf(structuralEqualityPolicy()) {
// cause an observation on the scroll state
scrollState.firstVisibleItemScrollOffset != 0
}
}
Log.e("Test", "composing SomeComposable")
if (red) {
Log.e("Test", "composing a red box")
Box(modifier = Modifier.background(Color.Red).fillMaxSize())
}
else {
Log.e("Test", "composing a blue box")
Box(modifier = Modifier.background(Color.Blue).fillMaxSize())
}
}
With this code, red
should always be true except when the scroll offset is at 0, and thus the composable should only ever recompose when the scroll offset comes to zero or leaves zero. However, it recomposes on every single scroll change.
to...@gmail.com <to...@gmail.com> #69
Your last example sounds normal IMO. The scrollstate change, the SomeComposable is recomposed the state is calculated, if is called and your log is ran.
To see any benefit here you need to extract the second part to another composable that is skippable.
@Composable
fun SomeComposable(
scrollState: LazyListState
) {
val red by remember {
derivedStateOf(structuralEqualityPolicy()) {
// cause an observation on the scroll state
scrollState.firstVisibleItemScrollOffset != 0
}
}
Log.e("Test", "composing SomeComposable")
SomeComposable2(red)
}
@Composable
fun SomeComposable2(
red: Boolean
) {
if (red) {
Log.e("Test", "composing a red box")
Box(modifier = Modifier.background(Color.Red).fillMaxSize())
}
else {
Log.e("Test", "composing a blue box")
Box(modifier = Modifier.background(Color.Blue).fillMaxSize())
}
}
This will display the first composing log each time the scrollstate change, but the box composing will only happen when the derived state change as the SomeComposable2 is skippable.
da...@sonos.com <da...@sonos.com> #70
Thanks for the response, but unfortunately, that still isn't correct IMO. I understand the concept of using a composable that is skippable, but what that would accomplish is that when red
changes, SomeComposable
would not recompose, while SomeComposable2
would. That is not what I'm trying to accomplish. The issue here is that red
is not supposed to change unless the scrollable state crosses the zero boundary. So in your example, SomeComposable2
still recomposes every single time the scroll state changes, but it shouldn't.
The issue is that the red
state only gets evaluated/calculated when it is accessed, which is in the recomposition, but at this point, it's too late, as the composable is already recomposing. So DerivedState is no longer useful. In Compose 1.2.0, the red
calculation would run at the time that the scroll state changes outside the composition scope, and then would appropriately cause the composable to recompose only if the red
state actually changed.
to...@gmail.com <to...@gmail.com> #71
No in my example SomeComposable2 does not recompose because it's skippable and red state does not change.
SomeComposable will always recompose because the scrollstate change. It's up to you that your composable is made of skippable sub component so that they are not recomposed.
In the case of #68 the if is not a composable function so is not skippable. So if SomeComposable is recomposed the if will always be ran and so your log will run. If you had something different before then it was something else and not how compose is supposed to work.
In all case in my example the red state is read as it's a parameter to SomeComposable2 so it's up to date thanks to the derivedstate and so will properly change.
I use this pattern a lot in prod and it works like that since 1.0 and it still works the same in 1.3 alpha3 (and beta 1 but I'm not in prod with it due to the return of the mem leak).
In all cases your issue seems unrelated to this one so you might want to open another one if you want more traction from Google.
da...@sonos.com <da...@sonos.com> #72
da...@sonos.com <da...@sonos.com> #73
da...@sonos.com <da...@sonos.com> #74
Here is sample code that describes exactly the issue:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
OuterComposable()
}
}
}
}
@Suppress("EqualsOrHashCode")
class ZeroMap(
val zeroMap: Map<Int, Boolean>
) {
override fun equals(other: Any?): Boolean {
if (this === other) {
Log.e("StateTest", "ZeroMap is the same reference")
return true
}
if (javaClass != other?.javaClass) {
Log.e("StateTest", "ZeroMap is not the same because of javaClass")
return false
}
other as ZeroMap
if (zeroMap.size != other.zeroMap.size) {
Log.e("StateTest", "ZeroMap is not the same because trackLayouts.size (${zeroMap.size}) != other.trackLayouts.size (${other.zeroMap.size})")
return false
}
val otherEntries = other.zeroMap.entries
zeroMap.entries.forEachIndexed { index, entry ->
val otherEntry = otherEntries.elementAt(index)
if (entry.value != otherEntry.value) {
Log.e("StateTest", "ZeroMap is not the same because entry.value (${entry.value}) != otherEntry.value (${otherEntry.value}) for index $index")
return false
}
}
Log.e("StateTest", "ZeroMap is the same value")
return true
}
}
class ZeroStateProvider(
val scrollState: LazyListState,
private val zeroState: State<ZeroMap>
) {
fun isAtZero(index: Int) = zeroState.value.zeroMap[index]
}
@Composable
fun rememberZeroStateProvider(): ZeroStateProvider {
val scrollState = rememberLazyListState()
val zeroState = remember {
derivedStateOf(structuralEqualityPolicy()) {
Log.e("StateTest", "calculating zero state for scroll offset: ${scrollState.firstVisibleItemScrollOffset}")
val zeroMap = mutableMapOf<Int, Boolean>()
scrollState.layoutInfo.visibleItemsInfo.forEach {
zeroMap[it.index] = it.offset == 0
}
ZeroMap(zeroMap)
}
}
return remember {
ZeroStateProvider(scrollState, zeroState)
}
}
@Composable
fun OuterComposable(
modifier: Modifier = Modifier,
) {
Log.e("StateTest", "Composing OuterComposable")
val zeroStateProvider = rememberZeroStateProvider()
LazyRow(
state = zeroStateProvider.scrollState,
horizontalArrangement = Arrangement.spacedBy(60.dp),
verticalAlignment = Alignment.Top,
modifier = modifier
.fillMaxWidth()
) {
items(
count = 100,
) { index ->
InnerComposable(index, zeroStateProvider)
}
}
}
@Composable
private fun InnerComposable(
index: Int,
zeroStateProvider: ZeroStateProvider,
modifier: Modifier = Modifier
) {
Log.e("StateTest", "Composing InnerComposable (index=$index)")
val color by remember {
derivedStateOf(structuralEqualityPolicy()) {
Log.e("StateTest", "InnerComposable: calculating color")
when (zeroStateProvider.isAtZero(index)) {
true -> Color.Red
false,
null -> Color.Blue
}
}
}
Box(
modifier = modifier
.background(color)
.size(200.dp)
)
}
The output in Compose 1.2.0 gives:
E/StateTest: InnerComposable: calculating color
E/StateTest: calculating zero state for scroll offset: 1
E/StateTest: Composing InnerComposable (index=0)
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: Composing InnerComposable (index=4)
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: calculating zero state for scroll offset: 3
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: calculating zero state for scroll offset: 5
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: calculating zero state for scroll offset: 6
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
and in Compose 1.3.0-beta01 gives:
E/StateTest: calculating zero state for scroll offset: 2
E/StateTest: ZeroMap is not the same because entry.value (false) != otherEntry.value (true) for index 0
E/StateTest: InnerComposable: calculating color
E/StateTest: calculating zero state for scroll offset: 2
E/StateTest: ZeroMap is not the same because entry.value (false) != otherEntry.value (true) for index 0
E/StateTest: Composing InnerComposable (index=0)
E/StateTest: calculating zero state for scroll offset: 2
E/StateTest: ZeroMap is not the same because entry.value (false) != otherEntry.value (true) for index 0
E/StateTest: InnerComposable: calculating color
E/StateTest: calculating zero state for scroll offset: 2
E/StateTest: ZeroMap is not the same because entry.value (false) != otherEntry.value (true) for index 0
E/StateTest: calculating zero state for scroll offset: 2
E/StateTest: ZeroMap is not the same because entry.value (false) != otherEntry.value (true) for index 0
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: InnerComposable: calculating color
E/StateTest: calculating zero state for scroll offset: 4
E/StateTest: ZeroMap is the same value
E/StateTest: calculating zero state for scroll offset: 5
E/StateTest: ZeroMap is the same value
As you can see from this output, in Compose 1.2.0, calculating zero state for scroll offset
only appears once every time the scroll state changes, and then the inner compositions recalculate accordingly. But in Compose 1.3.0-beta01, calculating zero state for scroll offset
shows up many times every time the scroll state changes. The number of times it runs increases with the number of composables that use its state.
From the documentation of derivedStateOf, it says "The result of calculation will be cached in such a way that calling [State.value] repeatedly will not cause[calculation] to be executed multiple times". This was true prior to Compose 1.3.0-beta01, but no longer is.
Please let me know what you think. Thanks!
to...@gmail.com <to...@gmail.com> #75
As said I'm not a Googler and your issue is unrelated to the original issue. You really should open another issue.
I'd say that your issue might not be related to derivedstate at all but to the compiler generation of your ZeroStateProvider class.
I'd use
da...@sonos.com <da...@sonos.com> #76
Yes, I'm ok with creating a new issue (which I did here:
to...@gmail.com <to...@gmail.com> #77
You can easily bisect the version that introduced the issue. All the fixes for this issue are in 1.2.0. AFAIR the only missing to fix all was
Try 1.3 alpha 3 that I use in prod with no performance regression. Then alpha 2 then 1 ;)
da...@sonos.com <da...@sonos.com> #78
It was introduced by
to...@gmail.com <to...@gmail.com> #79
Small bump on this to follow from #65
Is this planned to be backported to 1.3?
With Material 3 pinned to 1.3 we can't keep 1.3 A3 and update material. And not sure they will start an 1.1 alpha pinned to 1.4 before a final 1.3 release. (Maybe you have some info about that)
to...@gmail.com <to...@gmail.com> #80
So 1.3 RC1 is cut, is it possible to have some insight on this please? On my app with tons of large lazylist this leads to very fast OOM (even faster than previous issues).
Not having a solution for 1.3 may have high impact on updating M3 / Glance and the sooner we know the easier to anticipate.
ad...@google.com <ad...@google.com> #81
1.3-rc01 hasn't been cut yet, though the commit to bump the version to rc is in the system to ready for that build cut
ap...@google.com <ap...@google.com> #82
Branch: androidx-main
commit 3bd044a0a148b16ce17eaa71ceb5b8aed1bde70a
Author: Ralston Da Silva <ralu@google.com>
Date: Thu Sep 15 13:22:36 2022
Fix for issue where ModifierLocals were not being updated when modifiers were reused
With the introduction of Modifier.Node in aosp/2108724 onModifierLocalsUpdated was
not being called with the default value when modifiers were reused. This broke
LazyListFocusMoveTest.
Verified that this CL fixes the test, and added new tests to ModifierLocalSameLayoutNodeTest.kt
Bug: 245512265
Bug: 245131728
Bug: 245363143
Bug: 230168389
Fixes: 245512265
Fixes: 245131728
Test: ./gradlew compose:f:f:cC -P android.testInstrumentationRunnerArguments.class=androidx.compose.foundation.lazy.list.LazyListFocusMoveTest
Test: ./gradlew compose:ui:ui:cC -P android.testInstrumentationRunnerArguments.class=androidx.compose.ui.modifier.ModifierLocalSameLayoutNodeTest
Change-Id: I90d10bf341c4b19a979440886f3d002f5bc8320a
M compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierLocalSameLayoutNodeTest.kt
M compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
to...@gmail.com <to...@gmail.com> #83
So tested snapshot 9082024 that include this and it seems to be fixed.
And I also see nice scrolling performance on large lists, maybe due to other commits.
The last question is. Are there any commits in 9082024 that won't be part of the final 1.3? Or can I expect the same performance gain in the coming 1.3 releases ?
an...@google.com <an...@google.com> #84
ra...@google.com <ra...@google.com>
ju...@gmail.com <ju...@gmail.com> #85
na...@google.com <na...@google.com> #86
This bug was linked in a change in the following release(s):
androidx.compose.ui:ui:1.3.0-rc01
na...@google.com <na...@google.com> #87
The following release(s) address this bug:
androidx.compose.runtime:runtime:1.3.0
Description
Jetpack Compose version: 1.2.0 alpha 07 Jetpack Compose component used: LazyColumn Android Studio Build: AGP 7.3.0-alpha09 Kotlin version: 1.6.20 / 1.6.21
I have not yet a repro but will need some input about where to look for to build it.
Starting recently so I guess Alpha 06 or 07 I started to have OOM issues in my app. Doing some heap dump I see the following see attached images.
The first dump is after a few scroll and normal usage of the application. The second dump is after a lot more scrolling in a single LazyColumn.
As you can see the retained size is insane for SnapshotMutableStateImpl and the app quickly OOM.
I'd need some input about what is stored in those to be able to build a repro