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

Visualizing Compose IR Transformations

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for kitakkun kitakkun
November 29, 2025

Visualizing Compose IR Transformations

「Visualizing Compose IR Transformations プラグイン実装から解き明かすComposable関数の正体」

DroidKaigi.collect { #28@Fukuoka } の発表資料です。

Avatar for kitakkun

kitakkun

November 29, 2025
Tweet

More Decks by kitakkun

Other Decks in Programming

Transcript

  1. @Composable function → UI Component 3 @Composable fun A() {

    Column { Text("Hello") Text("World!") } }
  2. @Composable function → UI Component 4 @Composable fun A() {

    var count by remember { mutableIntStateOf(0) } Button( onClick = { count++ } ) { Text("$count") } }
  3. @Composable fun A( x: Int, $composer: Composer?, $changed: Int, )

    Adding extra value parameters by ComposerParamTransformer 9
  4. @Composable fun A( x: Int = 10, $composer: Composer?, $changed:

    Int, ) Adding extra value parameters by ComposerParamTransformer 10
  5. Adding extra value parameters by ComposerParamTransformer 11 @Composable fun A(

    x: Int, $composer: Composer?, $changed: Int, $default: Int, )
  6. Adding extra value parameters by ComposerParamTransformer 12 @Composable fun A(

    x: Int, $composer: Composer?, $changed: Int, $default: Int, )
  7. Bitmask allocation for $default 15 @Composable fun A( x: Int

    = 10, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 high l ow x 1 :de f a ul t va lu e p r esent 0 :no de f a ul t va lu e @Composable fun A( x: Int, $default: Int, )
  8. Bitmask allocation for $default 16 0 0 0 0 0

    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 high l ow x 1 :de f a ul t va lu e p r esent 0 :no de f a ul t va lu e y @Composable fun A( x: Int = 10, y: Int, ) @Composable fun A( x: Int, y: Int, $default: Int, )
  9. Bitmask allocation for $default 17 0 0 0 0 0

    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 high l ow x 1 :de f a ul t va lu e p r esent 0 :no de f a ul t va lu e y z @Composable fun A( x: Int = 10, y: Int, z: Int = 20, ) @Composable fun A( x: Int, y: Int, z: Int, $default: Int, )
  10. Bitmask allocation for $changed 19 @Composable fun A( x: Int,

    $changed: Int, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 x Rese r ved bit (1: f o r ce r ecomposition) high l ow
  11. Bitmask allocation for $changed 20 @Composable fun A( x: Int,

    y: Int, $changed: Int, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y x high l ow Rese r ved bit (1: f o r ce r ecomposition)
  12. Bitmask allocation for $changed 21 @Composable fun B.A( x: Int,

    y: Int, $changed: Int, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y x high l ow <this> ※B Rese r ved bit (1: f o r ce r ecomposition)
  13. Bitmask allocation for $changed 22 context(z: Z) @Composable fun B.A(

    x: Int, y: Int, $changed: Int, ) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y x high l ow <this> ※B z Rese r ved bit (1: f o r ce r ecomposition)
  14. Bitmask allocation for $changed 23 class C { context(z: Z)

    @Composable fun B.A( x: Int, y: Int, $changed: Int, ) } high l ow <this> ※C Rese r ved bit (1: f o r ce r ecomposition) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 y x <this> ※B z
  15. Parameter-state bits for $changed ParamState in ComposableFunctionBobyTransformer.kt 24 0 0

    0 Unce r tain 0 0 1 Same 0 1 0 Di ff e r ent 0 1 1 Static 1 0 0 Un k nown 1 1 1 Mas k
  16. $dirty bitmask calculation 27 @Composable fun A( x: Int, //

    default: 10 $composer: Composer?, $changed: Int, $default: Int, ) { f(x) // Composable }
  17. $dirty bitmask calculation 29 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed // f(x) }
  18. $dirty bitmask calculation 30 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed if ($default and 0b0001 != 0) { // default value presents } // f(x) }
  19. $dirty bitmask calculation 31 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed if ($default and 0b0001 != 0) { // default value presents $dirty = $dirty and 0b0110 // Static } // f(x) } 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown
  20. $dirty bitmask calculation 32 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed if ($default and 0b0001 != 0) { // default value presents $dirty = $dirty and 0b0110 // Static } else if ($changed and 0b0110 == 0) { // Uncertain or Unknown } // f(x) } 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown
  21. $dirty bitmask calculation 33 @Composable fun A(x: Int, $composer: Composer?,

    $changed: Int, $default: Int) { var $dirty = $changed if ($default and 0b0001 != 0) { // default value presents $dirty = $dirty and 0b0110 // Static } else if ($changed and 0b0110 == 0) { // Uncertain or Unknown $dirty = $dirty or if ($composer.changed(x)) 0b0100 // Different else 0b0010 // Same } // f(x) } 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown
  22. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default: Int)

    { var $dirty = } var $dirty = … // calculate $dirty bitmask… if ( $composer.shouldExecute( $dirty and 0b0011 != 0b0010, // state of x was changed $dirty and 0b0001 // force recomposition ) ) { } } Recompose with $dirty 34 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown
  23. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default: Int)

    { var $dirty = … // calculate $dirty bitmask… if ( $composer.shouldExecute( $dirty and 0b0011 != 0b0010, // state of x was changed $dirty and 0b0001 // force recomposition ) ) { } } Recompose with $dirty 35 0 0 0 Unce r tain 0 1 0 Di ff e r ent 0 0 1 Same 0 1 1 Static 1 0 0 Un k nown @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default: Int) { var $dirty = … // calculate $dirty bitmask… if ( $composer.shouldExecute( $dirty and 0b0011 != 0b0010, // state of x was changed $dirty and 0b0001 // force recomposition ) ) { if ($default and 0b0001 != 0) x = 10 f(x, $composer, 0b0110) // x is Static from here } }
  24. What is Group? Replace Group 38 @Composable fun A(flag: Boolean)

    { if (flag) { Text("true") } else { Text("false") } } ReplaceGroup Text("true") ReplaceGroup Text("false") RestartGroup A
  25. What is Group? Movable Group 39 @Composable fun A(items: List<Int>)

    { Column { for (x in items) { key(x) { Text("$x") } } } } MovableGroup key(x) ReplaceGroup Column(lamba) RestartGroup A
  26. Types of Group • Resta r tab l e G

    r o u p • ؔ਺ͷ࠶࣮ߦڥքɺεΩοϓՄೳͳάϧʔϓ • Rep l aceab l e G r o u p • ஔ׵Մೳͳ୯Ґʢi f -e l se, whenͳͲ੍ޚϑϩʔʹඥͮ͘άϧʔϓʣ • Movab l e G r o u pʢ k e y ͳͲͰੜ੒ʣ • ݺͼग़͠ॱʹґଘͤͣɺS l otͷҐஔΛॊೈʹҠಈͰ͖Δάϧʔϓ 40
  27. 43 @Composable fun A( x: Int, // default: 10 $composer:

    Composer?, $changed: Int, $default: Int, ) { f(x) // Composable }
  28. 44 @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default:

    Int) { var $dirty = … // calculate $dirty bitmask… }
  29. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, $default: Int)

    { var $dirty = … // calculate $dirty bitmask… if ( $composer.shouldExecute( $dirty and 0b0011 != 0b0010, // state of x was changed $dirty and 0b0001 // force recomposition ) ) { if ($default and 0b0001 != 0) x = 10 f(x, $composer, 0b0110) // x is Static from here } } 45
  30. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    var $dirty = … if ($composer.shouldExecute(…)) { … } } 46
  31. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(-1635445003) var $dirty = … if ($composer.shouldExecute(…)) { … } } 47 DurableFunctionKey Transformer Gene r ates k e y
  32. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(-1635445003) var $dirty = … if ($composer.shouldExecute(…)) { … } else { $composer.skipToGroupEnd() } } 48
  33. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(-1635445003) var $dirty = … if ($composer.shouldExecute(…)) { … } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 49
  34. @Composable fun A(x: Int, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(-1635445003) var $dirty = … if ($composer.shouldExecute(…)) { if ($default and 0b0001 != 0) x = 10 f(x, $composer, 0b0110) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 51 Stable
  35. StrongSkipping disabled @Composable fun A(x: UnstableType, $composer: Composer?, $changed: Int,

    …) { $composer.startRestartGroup(1096661310) f(x, $composer, 0) $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 52 Unstable
  36. What Stability means? Determines if Composable function is Skippable or

    not • ҆ఆʢStab l eʣ • $changed Λ࢖ͬͨߴ଎ͳ౳Ձ൑ఆ͕Մೳ • ঢ়ଶมԽΛ҆શʹ௥੻Ͱ͖Δ → εΩοϓՄೳ • ෆ҆ఆʢUnstab l eʣ • ࣮͔֬ͭߴ଎ͳൺֱखஈ͕ͳ͘ݪଇεΩοϓෆՄೳ • ྫ֎ɿSt r ong S k ipping Mode༗ޮ࣌͸ࢀরൺֱʹΑΓεΩοϓ͞ΕΔ 53
  37. @Composable fun A(x: UnstableType, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(1096661310) f(x, $composer, 0) $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 54 StrongSkipping disabled
  38. @Composable fun A(x: UnstableType, $composer: Composer?, $changed: Int, …) {

    $composer.startRestartGroup(1096661310) var $dirty = $changed if ($dirty and 0b0110 == 0) { $dirty = dirty or if ($composer.changedInstance(x)) 0b0100 else 0b0010 } if ($composer.shouldExecute(…)) { f(x, $composer, 0) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { $composer, $force -> A(x, $composer, $changed or 0b1) } } 55 StrongSkipping enabled
  39. Stability Inference StabilityInferencer in Stability.kt • Composab l eؔ਺ͷ֤ύϥϝʔλͷܕͷ҆ఆੑΛਪ࿦͢Δ •

    Ce r tain →ɹίϯύΠϧλΠϜͰ֬ఆ • R u ntime →ɹ࣮ߦ࣌ʹܾఆ • Un k nown →ɹෆ໌ • Pa r amete r →ɹܕҾ਺ʹґଘ • Combined → ෳ਺ͷStabi l it y ͕ෳ߹ͨ͠΋ͷ 57
  40. / / va l f : () -> Unit va

    l fu nction: F u nction <* > / / va l k f : (St r ing) -> Boo l ean = St r ing :: isEmpt y va l k F u nction: K F u nction <*> All Stable Function type? … ❌No ✅Yes Stable …
  41. va l st r ing: St r ing Stable String

    type? … ❌No ✅Yes Stable …
  42. Is primitive type? … ❌No ✅Yes Stable va l boo

    l ean: Boo l ean va l cha r : Cha r va l b y te: B y te va l sho r t: Sho r t va l int: Int va l fl oat: F l oat va l l ong: Long va l do u b l e: Do u b l e … All Stable
  43. @Stab l e c l ass A { … }

    @Imm u tab l e c l ass B { … } @Stab l e open c l ass C c l ass D : C() All Stable has StableMarker on itself or super type? … ❌No ✅Yes Stable …
  44. en u m A { B, C, D, } Is

    Enum or EnumEntry? … ❌No ✅Yes … All Stable Stable
  45. s y ntax = "p r oto3"; message Samp l

    e { boo l is_enab l ed = 1; } Is Protobuf type? … ❌No ✅Yes … Stable Stable p u b l ic f ina l c l ass Samp l e p r ivate const ru cto r ( va l name: St r ing = "", va l id: Long = 0L, ): com.goog l e.p r otob uf .Gene r ateMessageLite <Samp l e,Samp l e.B u i l de r > (DEFAULT_INSTANCE),
  46. // Stabi l it y In f e r ence

    r .stabi l it y O f (I r C l ass, …) va l dec l a r ation: I r C l ass i f ( canIn f e r Stabi l it y (dec l a r ation) | | dec l a r ation.isExte r na l Stab l eType() ) { … } Can infer stability or external stable type? … ❌No ✅Yes … Extra Logic
  47. object K nownStab l eConst r u cts { va

    l stab l eT y pes = mapO f ( Pai r :: c l ass.q u a l i f iedName !! to 0b11, … // G u ava "com.goog l e.common.co ll ect.Imm u tab l eList" to 0b1, … // K ot l inx imm u tab l e " k ot l inx.co ll ections.imm u tab l e.Imm u tab l eCo ll ection" to 0b1, " k ot l inx.co ll ections.imm u tab l e.Imm u tab l eList" to 0b1, … // Dagge r "dagge r .Laz y " to 0b1, // Co r o u tines Empt y Co r o u tineContext :: c l ass.q u a l i f iedName !! to 0, // Java t y pes BigIntege r:: c l ass.q u a l i f iedName !! to 0, BigDecima l:: c l ass.q u a l i f iedName !! to 0, ) Can infer stability or external stable type? ✅Yes Is known stable type? ✅Yes (maybe)Stable … ❌No
  48. ❌No // Sing l e c l ass com.examp l

    e.Exte r na l T y pe // Pac k age wi l dca r d com.examp l e.mode l s.** // Sing l e-segment wi l dca r d com.examp l e.*.data // Gene r ic pa r amete r inc lu sion com.examp l e.Containe r<*> // Gene r ic pa r amete r exc lu sion com.examp l e.W r appe r<* ,_> // Mixed gene r ic pa r amete r s com.examp l e.Comp l ex <* ,_, *> Is known stable type? Is external stable type? ✅Yes (maybe)Stable … ❌No https: // gith u b.com/s ky doves/compose-stabi l it y -in f e r ence? tab= r eadme-ov- f i l e#62-con f ig ur ation- f i l es stabi l it y _con f ig.con f
  49. For more information • compose-stabi l it y -in f

    e r ence b y s ky doves͞Μ • https: // gith u b.com/s ky doves/compose-stabi l it y -in f e r ence?tab= r eadme- ov- f i l e#27-stabi l it y -decision-t r ee • ίϯύΠϥϓϥάΠϯͷ࣮૷ʹج͍ͮͯΘ͔Γ΍͘͢੔ཧͨ͠ ϑϩʔνϟʔτ͕࡞ΒΕ͍ͯ·͢ 69
  50. Summary • Va lu e pa r amete r t

    r ans f o r mation • $compose r , $changed, $de f a ul t • $changed, $de f a ul t, $compose r .changed o r changedInstance → $di r t y → $compose r .sho ul dExec u te • Resta r t/Rep l ace/Movaba l e G r o u ps f o r node management • IR di ff e r ences when pa r amete r s a r e stab l e o r u nstab l e • Stabi l it y In f e r ence a l go r ithm 70