Status Update
Comments
so...@google.com <so...@google.com>
rh...@gmail.com <rh...@gmail.com> #2
Some notes from digging into this a bit:
- unexpected wrapping doesn't start until a TextView that line wraps runs it's line breaking logic.
- style doesn't seem to matter, so long as the line breaker is ran.
- Moving the
TextView
below the compose view so that it runs first makes the first draw not wrap in the compose text. Subsequent re-measures will start wrapping. - This may require a
StaticLayout
to run inTextView
beforeStaticLayout
runs inText
(BoringLayout
doesn't seem to cause this, but that doesn't have line breaking by definition), but I'm not certain.
- Inputs to
LineBreaker.computeLineBreaks
seem to have consistent arguments for repro and non-repro use cases. - Couldn't run in demo app, so the layouts/views may also be necessary to repro.
- Used a API 35 Pixel 9 Pro XL emulator to repro. Verified that using API 34 does not repro.
- Moving the
TextView
from the layout to anAndroidView
in ourComposeView
still repros.
rh...@gmail.com <rh...@gmail.com> #3
Able to repro from a blank project with only activity-compose, compose foundation, and the font files.
- Create new blank compose project. (should be target api 35 already)
- Replace
dependencies
inapp/build.gradle.kts
with the below and sync the dependencies. - Delete the
ui
source dirs (all the material related stuff). - Copy the font files from the
reprod.zip
in the description of this bug into the new project. - Replace the
MainActivity
file with the below code. - Run the app on a Pixel 9 Pro XL - API 35 emulator.
dependencies {
implementation("androidx.activity:activity-compose:1.10.0")
implementation("androidx.compose.foundation:foundation:1.7.6")
}
import android.os.Bundle
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent { Content() }
}
}
private val ReproFontFamily =
FontFamily(
Font(R.font.noto_ikea_latin_regular, FontWeight.Normal),
Font(R.font.noto_ikea_latin_bold, FontWeight.Bold)
)
@Composable
private fun Content() {
Column(
modifier = Modifier
.safeContentPadding()
.fillMaxWidth()
.padding(32.dp)
) {
AndroidView(factory = { ctx -> TextView(ctx).apply { text = "Line1\nLine2" } })
BasicText(
text = "ALEX",
style = TextStyle(
fontWeight = FontWeight.Bold,
color = Color.Black,
fontFamily = ReproFontFamily,
fontSize = 14.sp,
lineHeight = 22.sp,
),
modifier = Modifier.background(Color.Magenta),
)
}
}
rh...@gmail.com <rh...@gmail.com> #4
This actually does work in the demo app, I just forgot to change the target api. See
an...@google.com <an...@google.com>
se...@gmail.com <se...@gmail.com> #5
Maybe related, but not convinced:
[Deleted User] <[Deleted User]> #6
Nona, can you take a look?
se...@gmail.com <se...@gmail.com> #7
fontSize = 12.sp
fontWeight = SemiBold
letterSpacing = 0
lineHeights = 16.sp
se...@gmail.com <se...@gmail.com> #8
lu...@perrystreet.com <lu...@perrystreet.com> #9
I don't recommend this, but if you really need a workaround right now, here it is. This is probably quite brittle, slow, and I have not tested it other than checking that it does fix the bug. Use at your own risk. The actual fix for this is expected in 1.8.0-beta02
.
A very hacky workaround would be adding this modifier to the end of your Text
's modifier chain:
/** Intercept pre-layout to manually recycle `StaticLayout.Builder.useBoundsForWidth`. */
private fun Modifier.unexpectedTextWrappingWorkaround(): Modifier =
layout { measurable, constraints ->
if (Build.VERSION.SDK_INT >= 35) {
Api35Helper.resetStaticLayoutBuilderUseBoundsForWidth()
}
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) { placeable.place(0, 0) }
}
@RequiresApi(35)
private object Api35Helper {
@JvmStatic
fun resetStaticLayoutBuilderUseBoundsForWidth() {
StaticLayout.Builder.obtain("a", 0, 1, TextPaint(), 1024)
.setUseBoundsForWidth(false)
.build() // recycles the StaticLayout.
}
}
zh...@gmail.com <zh...@gmail.com> #10
Project: platform/frameworks/support
Branch: androidx-main
Author: Seigo Nonaka <
Link:
Fix unexpected enabling of useBoundsForWidth
Expand for full commit details
Fix unexpected enabling of useBoundsForWidth
This fixes a bug where a text may wrap to a second line where it is unnecessary.
Bug: 391378120
Test: Manual tests to ensure the unexpected wrapping stops after the fix is applied
Test: StaticLayoutFactoryTest#create_useBoundsForWidth_disabled
Test: BasicTextUnexpectedWrappingRegressionTest
Change-Id: I1b40c5816f2b4c1e787de05d5332db6fc0efad14
Files:
- A
compose/foundation/foundation/src/androidInstrumentedTest/assets/font/overshoot_test.ttx
- A
compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextUnexpectedWrappingRegressionTest.kt
- A
compose/foundation/foundation/src/androidInstrumentedTest/res/font/overshoot_test.ttf
- M
compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/android/StaticLayoutFactoryTest.kt
- M
compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt
Hash: 7d19be04e9c2af237f0605a148605bf52f607497
Date: Thu Jan 23 17:40:54 2025
jo...@muzz.com <jo...@muzz.com> #11
lu...@gmail.com <lu...@gmail.com> #12
lu...@gmail.com <lu...@gmail.com> #13
Separate note, an upstream fix in the android platform is expected in api level 36, so this issue should only occur when API level is 35 and compose version is below 1.8.
lu...@gmail.com <lu...@gmail.com> #14
Saw that beta02
is out but there is no mention of this issue being fixed in the release notes. Were you able to land the fix in beta02 or it got pushed to a future version?
rh...@gmail.com <rh...@gmail.com> #15
Looks like it actually went out in beta01
.
lu...@gmail.com <lu...@gmail.com> #16
This is my sample: A Simple MainTimer1Activity and a couple of seconds go to MainTimer2Activity
private const val TAG = "MAIN_TIMER_1"
class MainTimer1Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
MainTimer1Screen()
}
}
}
}
@Composable
fun MainTimer1Screen() {
Log.e(TAG, "MainTimer1Screen ...")
val ctx = LocalContext.current
LaunchedEffect(key1 = Unit, block = {
try {
startTimer1(5000L) { // start a timer for 5 secs
Log.e(TAG, "Timer ended")
toTimer2Screen(ctx)
}
} catch(ex: Exception) {
Log.e(TAG,"timer cancelled")
}
})
}
suspend fun startTimer1(time: Long, onTimerEnd: () -> Unit) {
delay(timeMillis = time)
onTimerEnd()
}
fun toTimer2Screen(ctx: Context){
val act = ctx.findActivity() as MainTimer1Activity
val intent = Intent(ctx, MainTimer2Activity::class.java)
ctx.startActivity(intent)
act.finish()
}
---------
private const val TAG = "MAIN_TIMER_2"
class MainTimer2Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
MainTimer2Screen()
}
}
}
}
@Composable
fun MainTimer2Screen() {
Log.e(TAG, "MainTimer2Screen ...")
val ctx = LocalContext.current
}
---------
The log ourput as espected
MainTimer1Screen ...
Timer ended
MainTimer2Screen ...
So if i look to profiler. and force GC the activitie MainTimer1Activity is removed but still have info about MainTimer1Screen Composable. i dont know if that belong to Composable tree taht never released
in first Capture you see MainTimer1 and Maintimer2 Activities
in Second Capture After force GC many time in 7 minutes. Maintimer2Activity is released. but still there this objects:
ComposableSingletons$Maintiner1Activitykt$lambda-1$1
ComposableSingletons$Maintiner1Activitykt$lambda-2$1
I think this composable are leaked, is strange or there is always info about compsables tree that android never releases
lu...@gmail.com <lu...@gmail.com> #17
fl...@polarsteps.com <fl...@polarsteps.com> #18
So this essentially means that every app out there, which is in any of its activities using ComposeView(s)
will leak those activities. Seems like a P1 to me.
ve...@gmail.com <ve...@gmail.com> #19
Could be related to
There is a small demo project simulating any number of leaks (using fragment and further activities). This is a major issue, which blocking us from releasing many features using compose. They have reproducibility 100%, and have potentially huge impact on memory and performance. Could you please bump this issues to P1?
mo...@gmail.com <mo...@gmail.com> #20
ve...@gmail.com <ve...@gmail.com> #21
From
on 14/02/2023: issue 264389670
Please note that ComposeView is not the prerequisite for this leak, but the changing of the mutable state during hiding of composition in first activity. Although, the root cause could be the same, but this issue doesn't assume any root cause, but provides the setup for replication. Fragment is also not required. I will attach new setup, which does use only just 3 activities. And that's enough for memory leak.
Replication
- From MainActivity go to IntermediateActivity
- From IntermediateActivity go to ComposeActivity
- From ComposeActivity go back
- Checking profiler. Have memory leak
se...@journiapp.com <se...@journiapp.com> #22
ve...@gmail.com <ve...@gmail.com> #23
Any mutable state change during composition hiding is enough for replication. The impact is drastic: all subsequent compose activities are stucking in the memory, and just piling up.
wu...@gmail.com <wu...@gmail.com> #24
lu...@gmail.com <lu...@gmail.com> #25
See $lambda-1$1 in the profiler
ComposableSingletons$Maintiner1Activitykt$lambda-1$1
ComposableSingletons$Maintiner1Activitykt$lambda-2$1
mg...@a-bly.com <mg...@a-bly.com> #26
as...@google.com <as...@google.com> #27
The memory leak is caused by an edge case, when the activity is stopped while Recomposer
is holding some pending state for composition. By default, MonotonicClock
used in Recomposer
is Recomposer
has already queued an update before activity was stopped, and it continues to accumulate states from other activities until the first activity is restarted or destroyed. The issue is quite deeply rooted, as the fix is likely to change how compositions record states in background, so we are targeting next release for the fix.
If you are affected by this issue in the meantime, consider specifying a custom WindowRecomposerFactory
in your app that doesn't pause MonotonicClock
(or unpauses it periodically). The side effect of such custom Recomposer
is that compositions will continue running animations when activity is in background.
ap...@google.com <ap...@google.com> #28
Branch: androidx-main
commit 66fef38b1d11e0c48b11137e6c3d007909f4a2d1
Author: Chuck Jazdzewski <chuckj@google.com>
Date: Wed Mar 29 13:51:01 2023
ON_STOP should pause the frame clock broadcasts instead of composition
When an Android window receives an ON_STOP noficiation it would pause
the pausable clock it created for its recomposer. This has the effect
of pausing all compositions and causes the recomposer to collect change
nofications from the snapshot sustem in an unbounded collection.
With this change, windows that receive ON_STOP will now only pause the
frame clock broadcasts which has the effect of blocking withFrameNanos
that is used by animations. This means animations stop advancing but
composition is allowed to continue allowing the recomposer to drain the
notifications from the snapshot system.
Relnote: """The recomposer created for an Android window will now
only block calls to `withFrameNanos` instead of all composition when it
receives an ON_STOP notification. This means windows associated with
stopped activites will continue to recompose for data changes but the
animations, or any other caller of withFrameNanos, will block."""
Fixes: 240975572
Test: ./gradlew :compose:r:r:tDUT
Change-Id: Id9e7fe262710544a48c2e4fc5fcbf1d27bfaa1ba
M compose/runtime/runtime/api/current.txt
M compose/runtime/runtime/api/public_plus_experimental_current.txt
M compose/runtime/runtime/api/restricted_current.txt
M compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
M compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
M compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
ma...@xapo.com <ma...@xapo.com> #29
This leak is happening for us on every screen migrated to Compose and would like to check any alpha version for the fix.
ch...@google.com <ch...@google.com> #30
This is in android-main
which is now the 1.5 branch. It should be out with the next alpha of 1.5 but not stable until 1.5 is stable.
pr...@google.com <pr...@google.com> #31
The following release(s) address this bug.It is possible this bug has only been partially addressed:
androidx.compose.runtime:runtime:1.5.0-alpha03
androidx.compose.ui:ui:1.5.0-alpha03
lu...@gmail.com <lu...@gmail.com> #32
androidx.compose.runtime:runtime:1.5.0-alpha03
androidx.compose.ui:ui:1.5.0-alpha03
But the problems still exists
ComposableSingletons$Activitykt$lambda-1$1. never are released . The Activity is released. but the composables are always keep it in memory, so its seems to be a kind of memory leak
lu...@gmail.com <lu...@gmail.com> #33
aa...@gmail.com <aa...@gmail.com> #34
How does version 1.4.x handle this bug?
xu...@gmail.com <xu...@gmail.com> #35
[Deleted User] <[Deleted User]> #37
ch...@google.com <ch...@google.com> #38
Lambdas like ComposableSingletons$Maintiner1Activitykt$lambda-1$1
, as the name implies, are lazy allocated singleton instances that are stored globally. They will only be allocated once. This is not addressed by the above fix because it is not a leak but a singlton.
lu...@gmail.com <lu...@gmail.com> #39
lu...@gmail.com <lu...@gmail.com> #40
lu...@gmail.com <lu...@gmail.com> #41
ComposableSingletons$Activitykt$lambda-1$1. that keep in memory
I think is part of the composable framework
Description
Jetpack Compose version: 1.2.0
Android Studio Build: Android Studio Chipmunk | 2021.2.1 Patch 1
Kotlin version: 1.7.0
Steps to Reproduce or Code Sample to Reproduce:
Opening and closing an Activity that uses
ComposeView
causes memory leak. Here's sample code used:LeakCanary points to
Recomposer.snapshotInvalidations
.Stack trace: