Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Compose 성능 끌어올리기

Compose 성능 끌어올리기

I/O Extended Incheon 2024 발표자료
- Stability in Compose
- 성능 측정으로 문제 찾기

강경완

July 27, 2024
Tweet

More Decks by 강경완

Other Decks in Programming

Transcript

  1. Woowaw Bros. Android Developer GDG Korea Android Organizer Linkedin :

    https://www.linkedin.com/in/kyeongwan-lucas-kang/ Kyeongwan Kang
  2. @Composable fun PublishedText(published: Instant, modifier: Modifier = Modifier) { DisposableEffect(Unit)

    { val receiver = object : BroadcastReceiver() {...} context.registerReceiver(receiver, IntentFilter(...)) onDispose { context.unregisterReceiver(receiver) } } Text(...) } Compose
  3. • Faster time to first draw 17% up • String

    skipping mode • Lamda memoization • Indication API • Slot table rewrite Compose performance h tt ps://developer.android.com/develop/ui/compose/pe rf ormance/stability/strongskipping h tt ps://developer.android.com/develop/ui/compose/touch-input/user-interactions/migrate-indication-ripple Compose
  4. Co Ru Source Code Compose Compiler Compose Phases @Composable private

    fun TestListScreen(userList: List<User>) { var favorite by remember { mutableStateOf(false) } Column { FavoriteSwitch(checked = favorite, onCheckedChange = { favorite = !favorite }) UserListColumn(userList) } } restartable scheme("[androidx.compose.ui.UiComposable]") fun UserListColumn( unstable user: List<User> ) restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun FavoriteSwitch( stable isFavorite: Boolean stable onToggle: Function0<Unit> )
  5. Co Ru Source Code Compose Compiler Compose Phases @Composable private

    fun TestListScreen(userList: List<User>) { var favorite by remember { mutableStateOf(false) } Column { FavoriteSwitch(checked = favorite, onCheckedChange = { favorite = !favorite }) UserListColumn(userList) } } State Change 가장 가까운 restartable function Recomposition 범위 Read
  6. Co Ru Source Code Compose Compiler Compose Phases @Composable private

    fun TestListScreen(userList: List<User>) { var favorite by remember { mutableStateOf(false) } Column { FavoriteSwitch(checked = favorite, onCheckedChange = { favorite = !favorite }) UserListColumn(userList) } } Recomposition State Change Recomposition 범위 Recomposition Not Skippable restartable scheme("[androidx.compose.ui.UiComposable]") fun UserListColumn( unstable user: List<User> ) restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun FavoriteSwitch( stable isFavorite: Boolean stable onToggle: Function0<Unit> )
  7. Co Ru Source Code Compose Compiler Compose Phases @Composable private

    fun TestListScreen(userList: List<User>) { var favorite by remember { mutableStateOf(false) } Column { Switch(checked = favorite, onCheckedChange = { favorite = !favorite }) UserListColumn(userList) } } Recomposition State Change Recomposition 범위 Recomposition Not Skippable restartable scheme("[androidx.compose.ui.UiComposable]") fun UserListColumn( unstable user: List<User> ) restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun FavoriteButton( stable isFavorite: Boolean stable onToggle: Function0<Unit> )
  8. Co Ru Source Code Compose Compiler Compose Phases @Composable private

    fun TestListScreen(userList: List<User>) { var favorite by remember { mutableStateOf(false) } Column { Button(onClick = { favorite = !favorite }) { Text(“$favorite") } UserListColumn(userList) } } Read 가장 가까운 restartable lambda content: @Composable RowScope.() -> Unit Recomposition 범위
  9. Co Ru Source Code Compose Compiler Compose Phases @Composable private

    fun TestListScreen(userList: List<User>) { var favorite by remember { mutableStateOf(false) } Column { Button(onClick = { favorite = !favorite }) { Text(“$favorite") } UserListColumn(userList) } } Read 가장 가까운 restartable lambda content: @Composable RowScope.() -> Unit Recomposition 범위 żŦ Composable function 을 나누는 것으로 recomposition 범위를 제약할 수는 있음 다만, 성급한 최적화는 유지보수를 어렵게 한다. 어차피 컴파일러가 recomposition 에 대한 최적화를 한번 더 진행해준다.
  10. Co Ru Source Code Compose Compiler Compose Phases @Composable private

    fun TestListScreen(userList: List<User>) { … Column { … UserListColumn(userList) } } @Composable private fun TestListScreen(userList: ImmutableList<User>) { … Column { … UserListColumn(userList) } } restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun UserListColumn( unstable user: ImmutableList<User> ) restartable scheme("[androidx.compose.ui.UiComposable]") fun UserListColumn( unstable user: List<User> )
  11. Compose Phases subprojects { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { if (project.findProperty("composeCompilerReports")

    == "true") { freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.absolutePath}/ compose_compiler" ) } if (project.findProperty("composeCompilerMetrics") == "true") { freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir.absolutePath}/ compose_compiler" ) } } } } ./gradlew assembleRelease -PcomposeCompilerReports=true Compose compiler reports h tt ps://developer.android.com/develop/ui/compose/pe rf ormance/stability/diagnose
  12. Compose Phases Compose compiler reports • Primitive types. • (data)

    class ੄ ݽٚ public ೐۽ಌ౭о val ੋ ҃਋ • (data) class ী @Stable ߂ @Immutable ਸ
 ݺद੸ਵ۽ ૑੿ೞৈ ஹ౵ੌ۞৬ ডࣘೠ ҃਋ • ಴ળ ஸ۩࣌ ௿ېझ (List, Set, Map) - val set: Set<String> = mutableSetOf(“foo") 
 ୊ۢ ҳഅ਷ mutable ೡ ࣻ ੓ਵ޲۽ ஹ౵ੌ۞о ౸ױೡ ࣻহ਺ • (data) class ੄ public ೐۽ಌ౭ ઺ ೞա ੉࢚ unstable ೠ ҃਋ • Java class, functions Stable Unstable
  13. Compose Phases @Immutable @Immutable data class User( val number: Int,

    val name: String, val images: List<String> ) stable class User { stable val number: Int stable val name: String unstable val images: List<String> } • 컴파일러와 강한 약속 • 초기화 된 이후 무조건 불변임을 보장 • 변경가능한 필드나 객체에 대한 참조가 있을 경우 사용하면 안됨
  14. Compose Phases @Stable • 컴파일러와 강력한 약속 • Recomposition 이

    발생했을 때 이전 호출 값과 equals 하다면 skip • 객체가 안정적으로 상태가 예측 가능함을 나타냄 @Stable class UserPreferences { var darkModeEnabled by mutableStateOf(false) var notificationEnabled by mutableStateOf(true) }
  15. Compose Compose compiler reports য়൤۰ җೠ Compose উ੿ࢿ ѐࢶ਷ ա઺ী

    ਬ૑ ҙܻী য۰਑ਸ ѻਸ ࣻ ੓णפ׮. ࢿәೠ ୭੸ചח Әޛ! উ੿ࢿҗ ҙ۲ػ ࢿמޙઁо ߊࢤೞݶ compiler report ܳ ഝਊ೧ࠁ੗ Stable җ Unstable ী ؀ೠ ର੉ܳ ੉೧ೞҊ, Composable function੉ ০਷ recomposition scope ղীࢲ যڌѱ recomposition ੉ ੌযաҊ ੓ח૑ ഛੋೞӝ.
  16. Compose Strong skipping mode composeCompiler { enableStrongSkippingMode = false }

    class User( val number: Int, var name: String, val images: List<String> ) // normal mode restartable scheme("[androidx.compose.ui.UiComposable]") fun UserListColumn( unstable user: User ) // strong skipping mode restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun UserListColumn( unstable user: User ) // @Stable User, normal mode restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun UserListColumn( stable user: User )
  17. ୡӝ ஏ੿ೞӝ അ੤ ࢚ടਸ ஏ੿ೞҊ ӝળࢶਸ ੿೤פ׮. ޙઁ ౵ঈ ௏٘

    ࣻ੿ ࢿҗ ஏ੿ ߂ ࠺Ү ߮஖݃௼ܳ ࠁҊ ޙઁܳ ౵ঈ೤פ׮. ౵ঈػ ޙઁܳ ௏٘۽ ࣻ੿೤פ׮. ੤ ஏ੿ೞҊ पઁ۽ ѐࢶ੉ غ঻ח૑ ୡӝࢿמҗ ࠺Ү೤פ׮. ߈ࠂ
  18. @RunWith(AndroidJUnit4::class) class AccelerateHeavyScreenBenchmark { @get:Rule val rule = MacrobenchmarkRule() @Test

    fun accelerateHeavyScreenCompilationFull() = benchmark() fun benchmark() { rule.measureRepeated( packageName = "com.compose.performance", metrics = listOf( FrameTimingMetric(), TraceSectionMetric("ImagePlaceholder", TraceSectionMetric.Mode.Sum), TraceSectionMetric("PublishDate.registerReceiver", TraceSectionMetric.Mode.Sum), TraceSectionMetric("ItemTag", TraceSectionMetric.Mode.Sum) ), compilationMode = CompilationMode.Full(), startupMode = StartupMode.COLD, iterations = 10, setupBlock = { }, measureBlock = { measureBlock() } ) } fun MacrobenchmarkScope.measureBlock() { pressHome()
  19. @RunWith(AndroidJUnit4::class) class AccelerateHeavyScreenBenchmark { @get:Rule val rule = MacrobenchmarkRule() @Test

    fun accelerateHeavyScreenCompilationFull() = benchmark() fun benchmark() { rule.measureRepeated( packageName = "com.compose.performance", metrics = listOf( FrameTimingMetric(), TraceSectionMetric("ImagePlaceholder", TraceSectionMetric.Mode.Sum), TraceSectionMetric("PublishDate.registerReceiver", TraceSectionMetric.Mode.Sum), TraceSectionMetric("ItemTag", TraceSectionMetric.Mode.Sum) ), compilationMode = CompilationMode.Full(), startupMode = StartupMode.COLD, iterations = 10, setupBlock = { }, measureBlock = { measureBlock() } ) } fun MacrobenchmarkScope.measureBlock() { pressHome()
  20. @RunWith(AndroidJUnit4::class) class AccelerateHeavyScreenBenchmark { @get:Rule val rule = MacrobenchmarkRule() @Test

    fun accelerateHeavyScreenCompilationFull() = benchmark() fun benchmark() { rule.measureRepeated( packageName = "com.compose.performance", metrics = listOf( FrameTimingMetric(), TraceSectionMetric("ImagePlaceholder", TraceSectionMetric.Mode.Sum), TraceSectionMetric("PublishDate.registerReceiver", TraceSectionMetric.Mode.Sum), TraceSectionMetric("ItemTag", TraceSectionMetric.Mode.Sum) ), compilationMode = CompilationMode.Full(), startupMode = StartupMode.COLD, iterations = 10, setupBlock = { }, measureBlock = { measureBlock() } ) } fun MacrobenchmarkScope.measureBlock() { pressHome()
  21. @RunWith(AndroidJUnit4::class) class AccelerateHeavyScreenBenchmark { @get:Rule val rule = MacrobenchmarkRule() @Test

    fun accelerateHeavyScreenCompilationFull() = benchmark() fun benchmark() { rule.measureRepeated( packageName = "com.compose.performance", metrics = listOf( FrameTimingMetric(), TraceSectionMetric("ImagePlaceholder", TraceSectionMetric.Mode.Sum), TraceSectionMetric("PublishDate.registerReceiver", TraceSectionMetric.Mode.Sum), TraceSectionMetric("ItemTag", TraceSectionMetric.Mode.Sum) ), compilationMode = CompilationMode.Full(), startupMode = StartupMode.COLD, iterations = 10, setupBlock = { }, measureBlock = { measureBlock() } ) } fun MacrobenchmarkScope.measureBlock() { pressHome()
  22. @RunWith(AndroidJUnit4::class) class AccelerateHeavyScreenBenchmark { @get:Rule val rule = MacrobenchmarkRule() @Test

    fun accelerateHeavyScreenCompilationFull() = benchmark() fun benchmark() { rule.measureRepeated( packageName = "com.compose.performance", metrics = listOf( FrameTimingMetric(), TraceSectionMetric("ImagePlaceholder", TraceSectionMetric.Mode.Sum), TraceSectionMetric("PublishDate.registerReceiver", TraceSectionMetric.Mode.Sum), TraceSectionMetric("ItemTag", TraceSectionMetric.Mode.Sum) ), compilationMode = CompilationMode.Full(), startupMode = StartupMode.COLD, iterations = 10, setupBlock = { }, measureBlock = { measureBlock() } ) } fun MacrobenchmarkScope.measureBlock() { pressHome() ஏ੿ೡ metric ܳ ૑੿
  23. TraceSectionMetric("ImagePlaceholder", TraceSectionMetric.Mode.Sum), TraceSectionMetric("PublishDate.registerReceiver", TraceSectionMetric.Mode.Sum), TraceSectionMetric("ItemTag", TraceSectionMetric.Mode.Sum) ), compilationMode = CompilationMode.Full(),

    startupMode = StartupMode.COLD, iterations = 10, setupBlock = { }, measureBlock = { measureBlock() } ) } fun MacrobenchmarkScope.measureBlock() { pressHome() startTaskActivity("accelerate_heavy") device.wait(Until.hasObject(By.res("list_of_items")), 5_000) val feed = device.findObject(By.res("list_of_items")) feed.setGestureMargin(device.displayWidth / 5) repeat(2) { feed.drag(Point(feed.visibleCenter.x, feed.visibleBounds.top)) Thread.sleep(500) } } } @Composable fun ScreenContent(items: List<HeavyItem>) { LazyVerticalGrid( modifier = Modifier .fillMaxSize() .testTag("list_of_items"), ... ) { items(items) { item -> HeavyItem(item) } } } ۨ੉ইਓ੉ ے؊݂ ؼ ٸ ө૑ ӝ׮ܻ੗! ۨ੉ইਓী testTag ૑੿
  24. TraceSectionMetric("ImagePlaceholder", TraceSectionMetric.Mode.Sum), TraceSectionMetric("PublishDate.registerReceiver", TraceSectionMetric.Mode.Sum), TraceSectionMetric("ItemTag", TraceSectionMetric.Mode.Sum) ), compilationMode = CompilationMode.Full(),

    startupMode = StartupMode.COLD, iterations = 10, setupBlock = { }, measureBlock = { measureBlock() } ) } fun MacrobenchmarkScope.measureBlock() { pressHome() startTaskActivity("accelerate_heavy") device.wait(Until.hasObject(By.res("list_of_items")), 5_000) val feed = device.findObject(By.res("list_of_items")) feed.setGestureMargin(device.displayWidth / 5) repeat(2) { feed.drag(Point(feed.visibleCenter.x, feed.visibleBounds.top)) Thread.sleep(500) } } }
  25. @Composable fun PhasesComposeLogo() = trace("PhasesComposeLogo") { val logo = painterResource(id

    = R.drawable.compose_logo) var size by remember { mutableStateOf(IntSize.Zero) } val logoPosition by logoPosition(size = size, logoSize = logo.intrinsicSize) Box( modifier = Modifier .fillMaxSize() .onPlaced { size = it.size } ) { with(LocalDensity.current) { Image( painter = logo, contentDescription = "logo", modifier = Modifier.offset(logoPosition.x.toDp(), logoPosition.y.toDp()) ) } } }
  26. @Composable fun PhasesComposeLogo() = trace("PhasesComposeLogo") { val logo = painterResource(id

    = R.drawable.compose_logo) var size by remember { mutableStateOf(IntSize.Zero) } val logoPosition by logoPosition(size = size, logoSize = logo.intrinsicSize) Box( modifier = Modifier .fillMaxSize() .onPlaced { size = it.size } ) { with(LocalDensity.current) { Image( painter = logo, contentDescription = "logo", modifier = Modifier.offset(logoPosition.x.toDp(), logoPosition.y.toDp()) ) } } }
  27. @Composable fun PhasesComposeLogo() = trace("PhasesComposeLogo") { val logo = painterResource(id

    = R.drawable.compose_logo) var size by remember { mutableStateOf(IntSize.Zero) } val logoPosition by logoPosition(size = size, logoSize = logo.intrinsicSize) Box( modifier = Modifier .fillMaxSize() .onPlaced { size = it.size } ) { Image( painter = logo, contentDescription = "logo", modifier = Modifier.offset { IntOffset(logoPosition.x, logoPosition.y) } ) } }
  28. .background(color) .drawBehind { drawRect(color) } .offset(x, y) .offset { IntOffset(x,

    y) } .alpha(a), .rotate(r), .scale(s) .graphicsLayer { alpha = a; rotationZ = r; scaleX = s; scaleY = s } Modifier Deferred TABLE ELEMENTS