Last active
June 2, 2023 12:41
-
-
Save wgjuher/896c16d08f75264c988ba80bb5231c68 to your computer and use it in GitHub Desktop.
This gist explain how compose compiler work under the hood
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val SLOTS_PER_INT = 10 | |
val BITS_PER_SLOT = 3 | |
val SameOrUnknown = ParamState.Same.bits or ParamState.Unknown.bits | |
println("Bits: ${valueOfBits(ParamState.Same.bits and SameOrUnknown)}") | |
println("base bit masks") | |
ParamState.values().forEach(::println) | |
println("forced masks (mask 'and' ForceUpdate(${SameOrUnknown.toBits()})) ") | |
ParamState.values() | |
.forEach { | |
val newValue = valueOfBits(bitsForSlot(SameOrUnknown, 0) and it.bitsForSlot(0)) | |
println("$it -> $newValue") | |
} | |
//initial compose values | |
val changed = ParamState.Different.bitsForSlot(0) | |
val composer = Composer() | |
print("invoke 'a' function with state \$changed: ${valueOfBits(changed)}", changed) | |
a(12, composer, changed) | |
//simplified decompiled 'a' function original in DecompiledComposeFunctions | |
fun a(x: Int, composer: Composer, changed: Int) { | |
composer.startRestartGroup("a") | |
var dirty = changed | |
val slotState = changed and ParamState.Mask.bitsForSlot(0) | |
print("check (changed 'and' mask)", slotState) | |
if (slotState == ParamState.Uncertain.bitsForSlot(0)) { | |
println("ask composer if 'a' is different") | |
dirty = changed or | |
(if (composer.changed(x)) ParamState.Different else ParamState.Same).bitsForSlot(0) | |
} | |
print("dirty: ${valueOfBits(dirty)}", dirty) | |
if (dirty and sameOrUnknownForced(0) == ParamState.Same.bitsForSlot(0) && composer.getSkipping()) { | |
composer.skipToGroupEnd() | |
} else { | |
println("invoke 'a' body") | |
b(x = x, y = 123, composer = composer, changed = | |
(ParamState.Mask.bitsForSlot(0) and dirty) or ParamState.Static.bitsForSlot(1) | |
) | |
} | |
composer.endRestartGroup() | |
} | |
//simplified decompiled 'b' function original in DecompiledComposeFunctions | |
fun b(x: Int, y: Int, composer: Composer, changed: Int) { | |
composer.startRestartGroup("b") | |
var dirty = changed | |
var slotState = changed and ParamState.Mask.bitsForSlot(0) | |
print("check changed 'and' mask for slot 0", slotState) | |
if (slotState == ParamState.Uncertain.bits) { | |
println("ask composer if 'a' is different") | |
dirty = changed or | |
(if (composer.changed(x)) ParamState.Different else ParamState.Same).bitsForSlot(0) | |
} | |
slotState = changed and ParamState.Mask.bitsForSlot(1) | |
print("check changed 'and' mask for slot 1", slotState) | |
if (slotState == ParamState.Uncertain.bits) { | |
println("ask composer if 'b' is different") | |
dirty = dirty or | |
(if (composer.changed(y, false)) ParamState.Different else ParamState.Same).bitsForSlot(1) | |
} | |
print("dirty: ${valueOfBits(dirty)}", dirty) | |
if (dirty and sameOrUnknownForced(1) == ParamState.Same.bitsForSlot(1) && composer.getSkipping()) { | |
composer.skipToGroupEnd() | |
} else { | |
println("invoke 'b' body") | |
} | |
composer.endRestartGroup() | |
} | |
//Helper functions | |
fun print(name: String, int: Int) { | |
println("$name bits ${int.toBits()}") | |
} | |
fun sameOrUnknownForced(slot: Int = 0): Int { | |
var result = bitsForSlot(SameOrUnknown, 0) | |
for (i in 1..slot) { | |
result = result or bitsForSlot(SameOrUnknown, slot) | |
} | |
return result or 0b1 //add 1 as smallest bit to force update | |
} | |
fun bitsForSlot(bits: Int, slot: Int): Int { | |
val realSlot = slot.rem(SLOTS_PER_INT) | |
return bits shl (realSlot * BITS_PER_SLOT + 1) | |
} | |
fun valueOfBits(bits: Int): String = bits.toParamState(bits.toBits().length / 3).toString() | |
fun ParamState.bitsForSlot(slot: Int) = bitsForSlot(bits, slot) | |
fun Int.toBits() = toUInt().toString(radix = 2) | |
fun Int.toParamState(slots: Int) = (0 until slots.coerceAtLeast(1)) | |
.map { slot -> getSlotState(this, slot) } | |
.mapNotNull { bits -> ParamState.values().firstOrNull { it.bits == bits } } | |
fun getSlotState(changed: Int, slot: Int) = | |
(changed and ParamState.Mask.bitsForSlot(slot)) shr (BITS_PER_SLOT * slot + 1) | |
//mock Composer - only print to console | |
class Composer { | |
private val stack = LinkedList<String>() | |
fun changed(param: Any, overrideReturn: Boolean = true): Boolean = overrideReturn | |
fun getSkipping(overrideReturn: Boolean = true) = overrideReturn | |
fun startRestartGroup(name: String) { | |
println("start restart '$name' group") | |
stack.push(name) | |
} | |
fun endRestartGroup() { | |
println("end restart '${stack.pop()}' group") | |
} | |
fun skipToGroupEnd() { | |
println("skip '${stack.peek()}' group") | |
} | |
} | |
//original masks | |
//https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt#163 | |
enum class ParamState(val bits: Int) { | |
Uncertain(0b000), | |
Same(0b001), | |
Different(0b010), | |
Static(0b011), | |
Unknown(0b100), | |
Mask(0b111); | |
override fun toString(): String { | |
return "$name(${bits.toUInt().toString(radix = 2)})" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Composable | |
public static final void A(final int x, @Nullable Composer $composer, final int $changed) { | |
$composer = $composer.startRestartGroup(-1673623075); | |
ComposerKt.sourceInformation($composer, "C(A)34@804L15:Stability.kt#p4r5jq"); | |
int $dirty = $changed; | |
if (($changed & 14) == 0) { | |
$dirty = $changed | ($composer.changed(x) ? 4 : 2); | |
} | |
if (($dirty & 11) == 2 && $composer.getSkipping()) { | |
$composer.skipToGroupEnd(); | |
} else { | |
B(x, LiveLiterals$StabilityKt.INSTANCE.Int$arg-1$call-B$fun-A(), $composer, 14 & $dirty); | |
} | |
ScopeUpdateScope var10000 = $composer.endRestartGroup(); | |
if (var10000 != null) { | |
var10000.updateScope((Function2)(new Function2() { | |
public final void invoke(@Nullable Composer $composer, int $force) { | |
StabilityKt.A(x, $composer, $changed | 1); | |
} | |
})); | |
} | |
} | |
@Composable | |
public static final void B(final int x, final int y, @Nullable Composer $composer, final int $changed) { | |
$composer = $composer.startRestartGroup(480008249); | |
ComposerKt.sourceInformation($composer, "C(B):Stability.kt#p4r5jq"); | |
int $dirty = $changed; | |
if (($changed & 14) == 0) { | |
$dirty = $changed | ($composer.changed(x) ? 4 : 2); | |
} | |
if (($changed & 112) == 0) { | |
$dirty |= $composer.changed(y) ? 32 : 16; | |
} | |
if (($dirty & 91) == 18 && $composer.getSkipping()) { | |
$composer.skipToGroupEnd(); | |
} else { | |
System.out.print(x + LiveLiterals$StabilityKt.INSTANCE.String$1$str$arg-0$call-print$fun-B() + y); | |
} | |
ScopeUpdateScope var10000 = $composer.endRestartGroup(); | |
if (var10000 != null) { | |
var10000.updateScope((Function2)(new Function2() { | |
public final void invoke(@Nullable Composer $composer, int $force) { | |
StabilityKt.B(x, y, $composer, $changed | 1); | |
} | |
})); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Composable | |
fun A(x: Int){ | |
B(x = x, y = 5) | |
} | |
@Composable | |
fun B(x: Int, y: Int){ | |
print("$x : $y") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment