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

Guide to Improving Compose Performance

Guide to Improving Compose Performance

Mohit S

May 26, 2024
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Guide to Improving Compose Performance • Recomposition performance • Compiler

    Plugin Internals • Compiler Plugin Tests • Experimental features
  2. Stability @Composable fun UsersScreen(modifier: Modifier, users: List<UserModel>) { Column {

    users.forEach { model -> UserRow(model) } } } @Composable fun UserRow(model: UserModel)
  3. Stability Frame 1 Frame 2 Frame 3 Frame 4 Recompose

    UsersScreen Recomposition count increase
  4. Stability Under the Hood @Composable fun UsersScreen(…) { … }

    @Composable fun UserRow() { … } data class UserModel( var name: String, val friends: List<User>, val lastLoggedIn: LocalDateTime ) Compose Compiler Plugin Transformed IR
  5. Stability Under the Hood @Composable fun UsersScreen(modifier: Modifier, users: List<UserModel>)

    { Column { users.forEach { model -> UserRow(model) } } } @Composable fun UserRow(model: UserModel)
  6. @Composable fun UsersScreen(users, %composer: Composer?, %changed: Int) { } if

    (%changed) { %dirty = %dirty or if (%composer.changedInstance(users)) } %composer = %composer.startRestartGroup()
  7. @Composable fun UsersScreen(users, %composer: Composer?, %changed: Int) { … }

    … if (dirty and !%composer.skipping) { items.forEach { model: UserModel -> … } } %composer.endRestartGroup() ?. updateScope { %composer: Composer? -> UsersScreen(users, composer,…) } Recomposition scope
  8. @Composable fun UsersScreen(users, %composer: Composer?, %changed: Int) { } if

    (%changed and 0b0110 == 0) { %dirty = %dirty or if (%composer.changedInstance(users)) } if (dirty and !%composer.skipping) { … } %composer = %composer.startRestartGroup( <> ) Bit masking
  9. @Composable fun UsersScreen(users, %composer: Composer?, %changed: Int) { } if

    (%changed and 0b0110 == 0) { %dirty = %dirty or if (%composer.changedInstance(users)) } if (dirty and !%composer.skipping) { … } %composer = %composer.startRestartGroup( <> ) enum class StabilityBits(val bits: Int) { UNSTABLE(0b100), STABLE(0b000); fun bitsForSlot(slot: Int): Int }
  10. @Composable fun UsersScreen(users, %composer: Composer?, %changed: Int) { } if

    (%changed and 0b0110 == 0) { %dirty = %dirty or if (%composer.changedInstance(users)) } if (dirty and !%composer.skipping) { … } %composer = %composer.startRestartGroup( <> ) Instance Equality for unstable types
  11. Stable Param @Composable fun example(state, %composer) { %composer = %composer.startRestartGroup()

    … if (%changed) { %dirty = %dirty or if (%composer.changed(state)) 0b0100 else 0b0010 } … %composer.endRestartGroup() }
  12. Stable Param @Composable fun example(state, %composer) { %composer = %composer.startRestartGroup(

    <> ) … if (%changed) { %dirty = %dirty or if (%composer.changed(state)) 0b0100 else 0b0010 } … %composer.endRestartGroup() } Equality check
  13. Unskippable Conditions • Stable parameters have differences from last execution

    • If the composer.skipping call returns false • Provided parameters to the function were unstable
  14. Compiler Report @Composable fun UsersScreen(…) { … } @Composable fun

    UserRow() { … } data class UserModel( var name: String, val friends: List<User>, val lastLoggedIn: LocalDateTime ) Compose Compiler Plugin Metrics
  15. Compiler Report Module Metrics Class Stability Transformer Composer Lamba Memoization

    Composable Function Body Transformer Composable Param Transformer
  16. Compiler Report interface ModuleMetrics { fun recordFunction( ... ) fun

    recordClass( ... ) fun recordLambda( ... ) fun saveMetricsTo( ... ) }
  17. Compiler Report class ModuleMetricsImpl(var name: String) : ModuleMetrics { var

    skippableComposables = 0 var restartableComposables = 0 var readonlyComposables = 0 var markedStableClasses = 0 var inferredStableClasses = 0 var inferredUnstableClasses = 0 ... }
  18. Compiler Report Inferred Stable Classes Marked Stable Classes Inferred Unstable

    Classes 70 0 Total Classes 61 9 Memoized Lambdas 76 Inferred Uncertain Classes 15
  19. Stability Under the Hood @Composable fun Profile(modifier: Modifier, user: UserModel)

    { ... } data class UserModel( var name: String, val friends: List<User>, val lastLoggedIn: LocalDateTime )
  20. Stability Under the Hood data class UserModel( var name: String,

    val friends: List<User>, val lastLoggedIn: LocalDateTime ) @StabilityInferred(parameters = 0) unstable class User { unstable name: String unstable friends: List<String> unstable lastLoggedIn: LocalDateTime } Class Stability Transformer
  21. Stability Under the Hood @StabilityInferred(parameters = 0) unstable class User

    { unstable name: String unstable friends: List<String> unstable lastLoggedIn: LocalDateTime } data class UserModel( var name: String, val friends: List<User>, val lastLoggedIn: LocalDateTime ) Synthesized Annotation
  22. Stability Under the Hood sealed class Stability { class Certain(…)

    : Stability() class Parameter(…) : Stability() class Combined(…) : Stability() class Runtime(…) : Stability() class Unknown(…) : Stability() … } Internal to Compiler
  23. Stability Under the Hood sealed class Stability { class Certain(…)

    : Stability() class Parameter(…) : Stability() class Combined(…) : Stability() class Runtime(…) : Stability() class Unknown(…) : Stability() … }
  24. Stability Under the Hood sealed class Stability { class Certain(…)

    : Stability() class Parameter(…) : Stability() class Combined(…) : Stability() class Runtime(…) : Stability() class Unknown(…) : Stability() … }
  25. Stability Under the Hood unstable class User { unstable name:

    String unstable friends: List<String> unstable lastLoggedIn: LocalDateTime } Stability.Certain(false)
  26. Stability Under the Hood unstable class User { unstable name:

    String unstable friends: List<String> unstable lastLoggedIn: LocalDateTime } Stability.Runtime
  27. Compiler Report Inferred Stable Classes Marked Stable Classes Inferred Unstable

    Classes 70 0 Total Classes 61 9 Memoized Lambdas 76 Inferred Uncertain Classes 15
  28. Stability Under the Hood class StabilityInferencer( ... ) { fun

    stabilityOf( declaration: IrClass ): Stability { ... } } What are the rules for stability?
  29. Stability Under the Hood fun stabilityOf( declaration: IrClass ): Stability

    { val fqName = declaration.fqNameWhenAvailable if (KnownStableConstructs.stableTypes.contains(fqName)) { stability = Stability.Stable } }
  30. Stability Under the Hood fun stabilityOf( declaration: IrClass ): Stability

    { val fqName = declaration.fqNameWhenAvailable if (KnownStableConstructs.stableTypes.contains(fqName)) { stability = Stability.Stable } } Known Stable Types
  31. Stability Under the Hood message User name: String location: String

    lastLoggedIn: LocalDataTime } @Composable fun Profile(modifier: Modifier, user: User) { ... } Protobuff
  32. Stability Under the Hood @Composable fun Profile(modifier: Modifier, user: User)

    { ... } message User name: String location: String lastLoggedIn: LocalDataTime } import com.google.protobuf.GeneratedMessageLite
  33. Stability Under the Hood @Composable restartable fun Profile(modifier: Modifier, unstable

    user: User) { ... } message User name: String location: String lastLoggedIn: LocalDataTime } import com.google.protobuf.GeneratedMessageLite
  34. Stability Under the Hood fun IrClass.isProtobufType(): Boolean { val directParentClassName

    = superTypes.lastOrNull { … }.fqNameWhenAvailable return directParentClassName == "com.google.protobuf.GeneratedMessageLite" || directParentClassName == "com.google.protobuf.GeneratedMessage" }
  35. Stability Under the Hood fun IrClass.isProtobufType(): Boolean { val directParentClassName

    = superTypes.lastOrNull { … }.fqNameWhenAvailable return directParentClassName == "com.google.protobuf.GeneratedMessageLite" || directParentClassName == "com.google.protobuf.GeneratedMessage" }
  36. Stability Under the Hood fun stabilityOf( declaration: IrClass ): Stability

    { ... if (declaration.isInterface) { return Stability.Unknown(declaration) } }
  37. Stable Markers unstable class User { unstable name: String unstable

    friends: List<String> unstable lastLoggedIn: LocalDateTime } data class UserModel( var name: String, val friends: List<User>, val lastLoggedIn: LocalDateTime )
  38. Stability Markers fun stabilityOf( declaration: IrClass ): Stability { if

    (declaration.hasStableMarkedDescendant()) { return Stability.Stable } … }
  39. Stability Markers fun stabilityOf( declaration: IrClass ): Stability { if

    (declaration.hasStableMarkedDescendant()) { return Stability.Stable } … } Stability Marker Check
  40. Stability Markers @Stable data class UserModel( val name: String, val

    friends: ImmutableList<User>, val lastLoggedIn: LocalDateTime )
  41. Stability Markers @StabilityInferred(parameters = 3) stable class UserModel { stable

    name: String stable friends: ImmutableList<String> stable lastLoggedIn: LocalDateTime }
  42. Stability Markers @Composable restartable skippable fun Profile(…, stable user: UserModel)

    @Composable fun Profile(modifier: Modifier, user: UserModel) { ... }
  43. Stability Under the Hood data class UserModel( var name: String,

    val friends: List<User>, val lastLoggedIn: LocalDateTime ) @StabilityInferred(parameters = 0) unstable class User { unstable name: String unstable friends: List<String> unstable lastLoggedIn: LocalDateTime } Stability Inferencer
  44. Testing Stability data class UserModel( var name: String, val friends:

    List<User>, val lastLoggedIn: LocalDateTime ) Compose Compiler Plugin @Composable fun Profile( ... ) IR
  45. Testing Stability data class UserModel( var name: String, val friends:

    List<User>, val lastLoggedIn: LocalDateTime ) @Composable fun Profile( ... ) K1Compiler Facade K2Compiler Facade
  46. Testing Stability val irModule = compileToIr(source) val stabilityInferencer = StabilityInferencer(irModule.descriptor)

    val classStability = stabilityInferencer.stabilityOf(irClass) // Stability.Stable
  47. Testing Stability val irModule = compileToIr(source) val stabilityInferencer = StabilityInferencer(irModule.descriptor)

    val classStability = stabilityInferencer.stabilityOf(irClass) // Stability.Stable
  48. Testing Stability val irModule = compileToIr(source) val stabilityInferencer = StabilityInferencer(irModule.descriptor)

    val classStability = stabilityInferencer.stabilityOf(irClass) // Stability.Stable
  49. Strategies • Use stable types • Use @Stable or @Immutable

    markers • Mark NonRestartableComposable for “root”
  50. Non Restartable Composable @Composable fun example(model: Unstable) { remember(model) {

    1 } } restartable fun example(unstable model: Unstable)
  51. Non Restartable Composable @Composable fun example(model: Unstable, %composer: Composer?, %changed:

    Int) { %composer.startRestartGroup() val tmp0_group = %composer.cache(%composer.changed(model)) { 1 } ... %composer.endRestartGroup().updateScope({…}) }
  52. Non Restartable Composable @Composable fun example(model: Unstable, %composer: Composer?, %changed:

    Int) { %composer.startRestartGroup() val tmp0_group = %composer.cache(%composer.changed(model)) { 1 } ... %composer.endRestartGroup().updateScope({…}) }
  53. Non Restartable Composable @Composable @NonRestartableComposable fun example(model: Unstable) { remember(model)

    { 1 } } fun example(unstable model: Unstable) Not restartable nor skippable
  54. Non Restartable Composable @Composable fun example(model: Unstable, %composer: Composer?, %changed:

    Int) { val tmp0_group = %composer.cache(%composer.changedInstance(model)) { 1 } ... } No restart group created
  55. Without Strong Skipping @Composable fun Profile(modifier: Modifier, user: User) {

    ... } restartable fun Profile( stable modifier: Modifier? = @static Companion unstable user: User ) Only restartable
  56. With Strong Skipping @Composable fun Profile(modifier: Modifier, user: User) {

    ... } restartable skippable fun Profile( stable modifier: Modifier? = @static Companion unstable user: User ) All restartable functions are skippable
  57. @Composable fun Profile(user, %composer: Composer?, %changed: Int) { } if

    (%changed and 0b0110 == 0) { %dirty = %dirty or if (%composer.changedInstance(users)) } if (dirty and !%composer.skipping) { … } %composer = %composer.startRestartGroup( <> ) enum class StabilityBits(val bits: Int) { UNSTABLE(0b100), STABLE(0b000); fun bitsForSlot(slot: Int): Int }
  58. Strong Skipping Lambdas class Unstable(var value: Int = 0) @Composable

    fun example() { val unstableObject = Unstable(0) val lambda = { unstableObject } }
  59. Strong Skipping Lambdas @Composable fun example(%composer: Composer?, %changed: Int) {

    … if (%changed) { val foo = Unstable(0) val lambda = %composer.cache( %composer.changedInstance(unstableObject)) { { unstableObject } } … } Memoize Lambda
  60. Strong Skipping Lambdas (Opt out) class Unstable(var value: Int =

    0) @Composable fun example() { val unstableObject = Unstable(0) val lambda = @DontMemoize { unstableObject } }
  61. Strong Skipping Lambdas (Opt out) @Composable fun example(%composer: Composer?, %changed:

    Int) { ... if (%changed) { val unstableObject = Unstable(0) val lambda = { { unstableObject } } } ... } Not Memoized
  62. @Composable @NonRestartableComposable fun example(model, %composer: Composer?) { } %composer =

    %composer.startReplaceGroup() gets a replace group placed around the body
  63. @Composable @NonRestartableComposable fun example(model, %composer: Composer?) { } %composer =

    %composer.startReplaceGroup() if (%changed) { %dirty = %dirty or if (%composer.changedInstance(…)) } Never calls `$composer.changed( ... )` with its parameters
  64. @Composable @NonRestartableComposable fun example(model, %composer: Composer?) { } %composer =

    %composer.startReplaceGroup() if (show) { %composer.startMoveableGroup() … %composer.endMovableGroup() } Proper groups around control flow
  65. @Composable @NonRestartableComposable fun example(model, %composer: Composer?) { } %composer =

    %composer.startReplaceGroup() if (show) { %composer.startMoveableGroup() … %composer.endMovableGroup() } Not necessary
  66. @Composable @NonRestartableComposable fun example(model, %composer: Composer?) { … } Never

    omit if (show) { %composer.startMoveableGroup() … %composer = %composer.endMovableGroup() }
  67. moveableContentOf val content = remember { content } if (showVertical)

    { Column { content() } } else { Row { content() } }
  68. moveableContentOf val content = remember { content } if (portrait)

    { Column { content() } } else { Row { content() } }
  69. Inside Compiler Plugin Compose Compiler Plugin Test Recompositions val content

    = remember { content } if (showVertical) { Column { content() } } else { Row { content() } }
  70. Inside Compiler Plugin val content = movableContentOf { val privateState

    = remember { mutableStateOf(0) } Text("Heading") Text("Content") }
  71. Inside Compiler Plugin var portrait by mutableStateOf(false) val content =

    movableContentOf { val privateState = remember { mutableStateOf(0) } Text("Heading") Text(“Content") }
  72. Inside Compiler Plugin @Composable fun example() { if (portrait) {

    Column { content() } } else { Row { content() } } }
  73. Inside Compiler Plugin var lastPrivateState: State<Int> = mutableStateOf(0) var portrait

    by mutableStateOf(false) val content = movableContentOf { val privateState = remember { mutableStateOf(0) } lastPrivateState = privateState Text("Heading") Text(“Content") }
  74. Inside Compiler Plugin interface CompositionTestScope : CoroutineScope { ... fun

    compose(block: @Composable () -> Unit) fun revalidate() ... }
  75. Inside Compiler Plugin compose { val firstPrivateState = lastPrivateState Content()

    portrait = true Snapshot.sendApplyNotifications() revalidate() assertSame(firstPrivateState, lastPrivateState) }
  76. Inside Compiler Plugin compose { val firstPrivateState = lastPrivateState example()

    portrait = true Snapshot.sendApplyNotifications() revalidate() assertSame(firstPrivateState, lastPrivateState) }
  77. Inside Compiler Plugin compose { val firstPrivateState = lastPrivateState example()

    portrait = true Snapshot.sendApplyNotifications() revalidate() assertSame(firstPrivateState, lastPrivateState) }
  78. Inside Compiler Plugin compose { val firstPrivateState = lastPrivateState example()

    portrait = true Snapshot.sendApplyNotifications() revalidate() assertSame(firstPrivateState, lastPrivateState) }
  79. Inside Compiler Plugin compose { val firstPrivateState = lastPrivateState example()

    portrait = true Snapshot.sendApplyNotifications() revalidate() assertSame(firstPrivateState, lastPrivateState) }
  80. Inside Compiler Plugin compose { val firstPrivateState = lastPrivateState example()

    portrait = true Snapshot.sendApplyNotifications() revalidate() assertSame(firstPrivateState, lastPrivateState) }
  81. Inside Compiler Plugin var portrait by mutableStateOf(false) val content =

    movableContentOf { val privateState = remember { mutableStateOf(0) } lastPrivateState = privateState Text("Heading") Text(“Content") }
  82. derivedStateOf @Composable fun Profile() { var shouldShowFab = remember(lazyListState.firstVisibleItemIndex) {

    lazyListState.firstVisibleItemIndex > 10 } if(shouldShowFab){ /* Display the floating action button */ } }
  83. derivedStateOf @Composable fun Profile() { var shouldShowFab = remember(lazyListState.firstVisibleItemIndex) {

    lazyListState.firstVisibleItemIndex > 10 } if(shouldShowFab){ /* Display the floating action button */ } }
  84. derivedStateOf @Composable fun Profile() { var shouldShowFab = remember(lazyListState.firstVisibleItemIndex) {

    lazyListState.firstVisibleItemIndex > 10 } if(shouldShowFab){ /* Display the floating action button */ } }
  85. derivedStateOf Profile Frame 1 Frame 2 Frame 3 Frame 4

    Recompose Recomposition count increase Pro .. Pro. Pro. Pro. Pro. Pro. Pro. Pro. Pro.
  86. derivedStateOf @Composable fun Profile() { var shouldShowFab = remember(lazyListState.firstVisibleItemIndex) {

    lazyListState.firstVisibleItemIndex > 10 } if(shouldShowFab){ /* Display the floating action button */ } } Value changes very frequently
  87. Inside Compiler Plugin @Composable fun UsersScreen(…) { … } @Composable

    fun UserRow() { … } data class UserModel( var name: String, val friends: List<User>, val lastLoggedIn: LocalDateTime ) Compose Compiler Plugin Test Snapshot changes
  88. Inside Compiler Plugin fun runSimpleTest( block: (…) -> Unit )

    { val stateObserver = SnapshotStateObserver { it() } try { stateObserver.start() block(stateObserver,…) Snapshot.sendApplyNotifications() } finally { stateObserver.stop() } }
  89. Inside Compiler Plugin fun runSimpleTest( block: (…) -> Unit )

    { val stateObserver = SnapshotStateObserver { it() } try { stateObserver.start() block(stateObserver,…) Snapshot.sendApplyNotifications() } finally { stateObserver.stop() } }
  90. Referential Equality Policy runSimpleTest { stateObserver, _ -> val state

    = mutableStateOf(mutableListOf(1)) val derivedState = derivedStateOf(referentialEqualityPolicy()) { state.value } }
  91. Referential Equality Policy runSimpleTest { stateObserver, _ -> val state

    = mutableStateOf(mutableListOf(1)) val derivedState = derivedStateOf(referentialEqualityPolicy()) { state.value } }
  92. Referential Equality Policy runSimpleTest { stateObserver, _ -> val state

    = mutableStateOf(mutableListOf(1)) val derivedState = derivedStateOf(referentialEqualityPolicy()) { state.value } }
  93. Referential Equality Policy runSimpleTest { stateObserver, _ -> … var

    changes = 0 stateObserver.observeReads(…, { changes ++ }) { derivedState.value } }
  94. Referential Equality Policy runSimpleTest { stateObserver, _ -> val state

    = mutableStateOf(mutableListOf(1)) var changes = 0 stateObserver.observeReads(…, { changes ++ }) { derivedState.value } state.value = mutableListOf(1) } Update State
  95. Referential Equality Policy runSimpleTest { stateObserver, _ -> val state

    = mutableStateOf(mutableListOf(1)) val derivedState = derivedStateOf(referentialEqualityPolicy()) { state.value } state.value = mutableListOf(1) } State will observe read due to ref check
  96. Referential Equality Policy runSimpleTest { stateObserver, _ -> var changes

    = 0 stateObserver.observeReads(…, { changes ++ }) { derivedState.value } state.value = mutableListOf(1) assertEquals(1, changes) } Changes is updated
  97. Referential Equality Policy runSimpleTest { stateObserver, _ -> var changes

    = 0 stateObserver.observeReads(…, { changes ++ }) { derivedState.value } state.value = mutableListOf(1) assertEquals(1, changes) }
  98. Structural Equality Policy runSimpleTest { stateObserver, _ -> val state

    = mutableStateOf(mutableListOf(1)) val derivedState = derivedStateOf(structureEqualityPolicy()) { state.value } }
  99. Structural Equality Policy runSimpleTest { stateObserver, _ -> var changes

    = 0 stateObserver.observeReads(…, { changes ++ }) { derivedState.value } state.value = mutableListOf(1) assertEquals(0, changes) // changes = 0 } Value is not emitted by derived state
  100. Structural Equality Policy runSimpleTest { stateObserver, _ -> var changes

    = 0 stateObserver.observeReads(…, { changes ++ }) { derivedState.value } state.value = mutableListOf(1) assertEquals(0, changes) // changes = 0 }
  101. Guide to Improving Compose Performance • Recomposition performance • Compiler

    Plugin Internals • Compiler Plugin Tests • Experimental features