Skip to content

Commit

Permalink
FastProtocolBridge (#2106)
Browse files Browse the repository at this point in the history
* Update Zipline to 1.12.0

* FastProtocolBridge

This is a new implementation of ProtocolBridge that uses
Zipline's new asDynamicFunction() API. It's dramatically
faster - in one measurement we went from spending ~18% of
our samples on serialization to ~3% of our samples.

* Test for FastProtocolBridge

* Work around serialization bug for UInt

See Kotlin/kotlinx.serialization#2713

* Track Button.color property

* apiDump

* Track Button.color in ProtocolTest

* Track color in another test
  • Loading branch information
swankjesse committed Jun 14, 2024
1 parent 0e9b2ba commit 2b6027c
Show file tree
Hide file tree
Showing 30 changed files with 550 additions and 227 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ class DirectChangeListenerTest : AbstractChangeListenerTest() {
}

class ProtocolChangeListenerTest : AbstractChangeListenerTest() {
@OptIn(RedwoodCodegenApi::class)
override fun <T> CoroutineScope.launchComposition(
widgetSystem: TestSchemaWidgetSystem<WidgetValue>,
snapshot: () -> T,
Expand Down Expand Up @@ -101,7 +100,7 @@ abstract class AbstractChangeListenerTest {
c.setContent {
Button(text, onClick = null)
}
assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "modifier Modifier", "onEndChanges")
assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "color 0", "modifier Modifier", "onEndChanges")

text = "hello"
assertThat(c.awaitSnapshot()).containsExactly("text hello", "onEndChanges")
Expand All @@ -124,7 +123,7 @@ abstract class AbstractChangeListenerTest {
Button("hi", onClick = null)
Text(text)
}
assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "modifier Modifier", "onEndChanges")
assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "color 0", "modifier Modifier", "onEndChanges")

text = "hello"
assertThat(c.awaitSnapshot()).isEmpty()
Expand All @@ -146,7 +145,7 @@ abstract class AbstractChangeListenerTest {
c.setContent {
Button("hi", onClick = null, modifier = modifier)
}
assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "modifier Modifier", "onEndChanges")
assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "color 0", "modifier Modifier", "onEndChanges")

modifier = with(object : TestScope {}) {
Modifier.accessibilityDescription("hey")
Expand All @@ -171,7 +170,7 @@ abstract class AbstractChangeListenerTest {
c.setContent {
Button(text, onClick = null, modifier = modifier)
}
assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "modifier Modifier", "onEndChanges")
assertThat(c.awaitSnapshot()).containsExactly("text hi", "onClick false", "color 0", "modifier Modifier", "onEndChanges")

text = "hello"
modifier = with(object : TestScope {}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class ListeningButton :
changes += "onClick ${onClick != null}"
}

override fun color(color: UInt) {
changes += "color $color"
}

override fun onEndChanges() {
changes += "onEndChanges"
}
Expand Down
5 changes: 3 additions & 2 deletions redwood-protocol-guest/api/redwood-protocol-guest.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ public final class app/cash/redwood/protocol/guest/DefaultProtocolBridge : app/c
public fun appendModifierChange-z3jyS0k (ILjava/util/List;)V
public fun appendMove-HpxY78w (IIIII)V
public fun appendPropertyChange-DxQz5cw (IILkotlinx/serialization/KSerializer;Ljava/lang/Object;)V
public fun appendPropertyChange-M7EZMwg (III)V
public fun appendPropertyChange-e3iP1vo (IIZ)V
public fun appendRemove-HpxY78w (IIIILjava/util/List;)V
public fun emitChanges ()V
public fun getJson ()Lkotlinx/serialization/json/Json;
public fun getRoot ()Lapp/cash/redwood/protocol/guest/ProtocolWidgetChildren;
public synthetic fun getRoot ()Lapp/cash/redwood/widget/Widget$Children;
public fun getRoot ()Lapp/cash/redwood/widget/Widget$Children;
public fun getSynthesizeSubtreeRemoval ()Z
public fun getWidgetSystem ()Lapp/cash/redwood/widget/WidgetSystem;
public fun initChangesSink (Lapp/cash/redwood/protocol/ChangesSink;)V
Expand All @@ -28,6 +28,7 @@ public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge :
public abstract fun appendModifierChange-z3jyS0k (ILjava/util/List;)V
public abstract fun appendMove-HpxY78w (IIIII)V
public abstract fun appendPropertyChange-DxQz5cw (IILkotlinx/serialization/KSerializer;Ljava/lang/Object;)V
public abstract fun appendPropertyChange-M7EZMwg (III)V
public abstract fun appendPropertyChange-e3iP1vo (IIZ)V
public abstract fun appendRemove-HpxY78w (IIIILjava/util/List;)V
public static synthetic fun appendRemove-HpxY78w$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge;IIIILjava/util/List;ILjava/lang/Object;)V
Expand Down
10 changes: 3 additions & 7 deletions redwood-protocol-guest/api/redwood-protocol-guest.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ abstract interface app.cash.redwood.protocol.guest/ProtocolBridge : app.cash.red
abstract fun appendModifierChange(app.cash.redwood.protocol/Id, kotlin.collections/List<app.cash.redwood.protocol/ModifierElement>) // app.cash.redwood.protocol.guest/ProtocolBridge.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;kotlin.collections.List<app.cash.redwood.protocol.ModifierElement>){}[0]
abstract fun appendMove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.protocol.guest/ProtocolBridge.appendMove|appendMove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
abstract fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/Boolean) // app.cash.redwood.protocol.guest/ProtocolBridge.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.Boolean){}[0]
abstract fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/UInt) // app.cash.redwood.protocol.guest/ProtocolBridge.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.UInt){}[0]
abstract fun appendRemove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin.collections/List<app.cash.redwood.protocol/Id> = ...) // app.cash.redwood.protocol.guest/ProtocolBridge.appendRemove|appendRemove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.collections.List<app.cash.redwood.protocol.Id>){}[0]
abstract fun emitChanges() // app.cash.redwood.protocol.guest/ProtocolBridge.emitChanges|emitChanges(){}[0]
abstract fun initChangesSink(app.cash.redwood.protocol/ChangesSink) // app.cash.redwood.protocol.guest/ProtocolBridge.initChangesSink|initChangesSink(app.cash.redwood.protocol.ChangesSink){}[0]
Expand Down Expand Up @@ -56,6 +57,7 @@ final class app.cash.redwood.protocol.guest/DefaultProtocolBridge : app.cash.red
final fun appendModifierChange(app.cash.redwood.protocol/Id, kotlin.collections/List<app.cash.redwood.protocol/ModifierElement>) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;kotlin.collections.List<app.cash.redwood.protocol.ModifierElement>){}[0]
final fun appendMove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendMove|appendMove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/Boolean) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.Boolean){}[0]
final fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/UInt) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.UInt){}[0]
final fun appendRemove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin.collections/List<app.cash.redwood.protocol/Id>) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.appendRemove|appendRemove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.collections.List<app.cash.redwood.protocol.Id>){}[0]
final fun emitChanges() // app.cash.redwood.protocol.guest/DefaultProtocolBridge.emitChanges|emitChanges(){}[0]
final fun initChangesSink(app.cash.redwood.protocol/ChangesSink) // app.cash.redwood.protocol.guest/DefaultProtocolBridge.initChangesSink|initChangesSink(app.cash.redwood.protocol.ChangesSink){}[0]
Expand All @@ -66,7 +68,7 @@ final class app.cash.redwood.protocol.guest/DefaultProtocolBridge : app.cash.red
final val json // app.cash.redwood.protocol.guest/DefaultProtocolBridge.json|{}json[0]
final fun <get-json>(): kotlinx.serialization.json/Json // app.cash.redwood.protocol.guest/DefaultProtocolBridge.json.<get-json>|<get-json>(){}[0]
final val root // app.cash.redwood.protocol.guest/DefaultProtocolBridge.root|{}root[0]
final fun <get-root>(): app.cash.redwood.protocol.guest/ProtocolWidgetChildren // app.cash.redwood.protocol.guest/DefaultProtocolBridge.root.<get-root>|<get-root>(){}[0]
final fun <get-root>(): app.cash.redwood.widget/Widget.Children<kotlin/Unit> // app.cash.redwood.protocol.guest/DefaultProtocolBridge.root.<get-root>|<get-root>(){}[0]
final val synthesizeSubtreeRemoval // app.cash.redwood.protocol.guest/DefaultProtocolBridge.synthesizeSubtreeRemoval|{}synthesizeSubtreeRemoval[0]
final fun <get-synthesizeSubtreeRemoval>(): kotlin/Boolean // app.cash.redwood.protocol.guest/DefaultProtocolBridge.synthesizeSubtreeRemoval.<get-synthesizeSubtreeRemoval>|<get-synthesizeSubtreeRemoval>(){}[0]
final val widgetSystem // app.cash.redwood.protocol.guest/DefaultProtocolBridge.widgetSystem|{}widgetSystem[0]
Expand All @@ -88,9 +90,3 @@ final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_Defaul
final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_ProtocolWidgetChildren$stableprop // app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_ProtocolWidgetChildren$stableprop|#static{}app_cash_redwood_protocol_guest_ProtocolWidgetChildren$stableprop[0]
final val app.cash.redwood.protocol.guest/guestRedwoodVersion // app.cash.redwood.protocol.guest/guestRedwoodVersion|{}guestRedwoodVersion[0]
final fun <get-guestRedwoodVersion>(): app.cash.redwood.protocol/RedwoodVersion // app.cash.redwood.protocol.guest/guestRedwoodVersion.<get-guestRedwoodVersion>|<get-guestRedwoodVersion>(){}[0]
// Targets: [js]
final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsArray$stableprop // app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsArray$stableprop|#static{}app_cash_redwood_protocol_guest_JsArray$stableprop[0]
// Targets: [js]
final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsArrayList$stableprop // app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsArrayList$stableprop|#static{}app_cash_redwood_protocol_guest_JsArrayList$stableprop[0]
// Targets: [js]
final val app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsMap$stableprop // app.cash.redwood.protocol.guest/app_cash_redwood_protocol_guest_JsMap$stableprop|#static{}app_cash_redwood_protocol_guest_JsMap$stableprop[0]
9 changes: 0 additions & 9 deletions redwood-protocol-guest/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,6 @@ kotlin {
implementation projects.testApp.schema.protocolGuest
}
}
nonJsMain {
dependsOn(commonMain)
}
jvmMain {
dependsOn(nonJsMain)
}
nativeMain {
dependsOn(nonJsMain)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import app.cash.redwood.protocol.PropertyChange
import app.cash.redwood.protocol.PropertyTag
import app.cash.redwood.protocol.RedwoodVersion
import app.cash.redwood.protocol.WidgetTag
import app.cash.redwood.widget.Widget
import app.cash.redwood.widget.WidgetSystem
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonPrimitive

/** @suppress For generated code use only. */
@RedwoodCodegenApi
@OptIn(RedwoodCodegenApi::class)
public class DefaultProtocolBridge(
public override val json: Json = Json.Default,
hostVersion: RedwoodVersion,
Expand All @@ -50,7 +52,7 @@ public class DefaultProtocolBridge(
public override val widgetSystem: WidgetSystem<Unit> =
widgetSystemFactory.create(this, mismatchHandler)

public override val root: ProtocolWidgetChildren =
public override val root: Widget.Children<Unit> =
ProtocolWidgetChildren(Id.Root, ChildrenTag.Root, this)

public override val synthesizeSubtreeRemoval: Boolean = hostVersion < RedwoodVersion("0.10.0-SNAPSHOT")
Expand Down Expand Up @@ -100,6 +102,15 @@ public class DefaultProtocolBridge(
changes.add(PropertyChange(id, tag, JsonPrimitive(value)))
}

@OptIn(ExperimentalSerializationApi::class)
override fun appendPropertyChange(
id: Id,
tag: PropertyTag,
value: UInt,
) {
changes.add(PropertyChange(id, tag, JsonPrimitive(value)))
}

public override fun appendModifierChange(
id: Id,
elements: List<ModifierElement>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,16 @@ import kotlinx.serialization.json.Json
*
* This interface is for generated code use only.
*/
@RedwoodCodegenApi
public interface ProtocolBridge : EventSink {
@RedwoodCodegenApi
public val json: Json

/**
* Host versions prior to 0.10.0 contained a bug where they did not recursively remove widgets
* from the protocol map which leaked any child views of a removed node. We can work around this
* on the guest side by synthesizing removes for every node in the subtree.
*/
@RedwoodCodegenApi
public val synthesizeSubtreeRemoval: Boolean

/**
Expand All @@ -64,38 +65,59 @@ public interface ProtocolBridge : EventSink {

public fun emitChanges()

@RedwoodCodegenApi
public fun nextId(): Id

@RedwoodCodegenApi
public fun appendCreate(
id: Id,
tag: WidgetTag,
)

@RedwoodCodegenApi
public fun <T> appendPropertyChange(
id: Id,
tag: PropertyTag,
serializer: KSerializer<T>,
value: T,
)

@RedwoodCodegenApi
public fun appendPropertyChange(
id: Id,
tag: PropertyTag,
value: Boolean,
)

/**
* There's a bug in kotlinx.serialization where decodeFromDynamic() is broken for UInt values
* larger than MAX_INT. For example, 4294967295 is incorrectly encoded as -1. We work around that
* here by special casing that type.
*
* https://github.com/Kotlin/kotlinx.serialization/issues/2713
*/
@RedwoodCodegenApi
public fun appendPropertyChange(
id: Id,
tag: PropertyTag,
value: UInt,
)

@RedwoodCodegenApi
public fun appendModifierChange(
id: Id,
elements: List<ModifierElement>,
)

@RedwoodCodegenApi
public fun appendAdd(
id: Id,
tag: ChildrenTag,
index: Int,
child: ProtocolWidget,
)

@RedwoodCodegenApi
public fun appendMove(
id: Id,
tag: ChildrenTag,
Expand All @@ -104,6 +126,7 @@ public interface ProtocolBridge : EventSink {
count: Int,
)

@RedwoodCodegenApi
public fun appendRemove(
id: Id,
tag: ChildrenTag,
Expand All @@ -112,5 +135,6 @@ public interface ProtocolBridge : EventSink {
removedIds: List<Id> = listOf(),
)

@RedwoodCodegenApi
public fun removeWidget(id: Id)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.saveable.SaveableStateRegistry
import app.cash.redwood.RedwoodCodegenApi
import app.cash.redwood.compose.LocalWidgetVersion
import app.cash.redwood.compose.RedwoodComposition
import app.cash.redwood.ui.OnBackPressedDispatcher
Expand All @@ -31,7 +30,6 @@ import kotlinx.coroutines.flow.StateFlow
* @param scope A [CoroutineScope] whose [coroutineContext][kotlin.coroutines.CoroutineContext]
* must have a [MonotonicFrameClock] key which is being ticked.
*/
@OptIn(RedwoodCodegenApi::class)
@Suppress("FunctionName")
public fun ProtocolRedwoodComposition(
scope: CoroutineScope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
*/
package app.cash.redwood.protocol.guest

import app.cash.redwood.RedwoodCodegenApi
import app.cash.redwood.widget.WidgetSystem

@OptIn(RedwoodCodegenApi::class)
public interface ProtocolWidgetSystemFactory {
/** Create a new [WidgetSystem] connected to a host via [bridge]. */
public fun create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ class ProtocolTest {
PropertyChange(Id(1), PropertyTag(1), JsonPrimitive("hi")),
// onClick
PropertyChange(Id(1), PropertyTag(2), JsonPrimitive(false)),
// color
PropertyChange(Id(1), PropertyTag(3), JsonPrimitive(0u)),
ModifierChange(Id(1)),
ChildrenChange.Add(Id.Root, ChildrenTag.Root, Id(1), 0),
// Button
Expand All @@ -164,6 +166,8 @@ class ProtocolTest {
PropertyChange(Id(2), PropertyTag(1), JsonPrimitive("hi")),
// onClick
PropertyChange(Id(2), PropertyTag(2), JsonPrimitive(true)),
// color
PropertyChange(Id(2), PropertyTag(3), JsonPrimitive(0u)),
ModifierChange(Id(2)),
ChildrenChange.Add(Id.Root, ChildrenTag.Root, Id(2), 1),
// Button2
Expand Down Expand Up @@ -215,6 +219,8 @@ class ProtocolTest {
PropertyChange(Id(1), PropertyTag(1), JsonPrimitive("state: 0")),
// onClick
PropertyChange(Id(1), PropertyTag(2), JsonPrimitive(true)),
// color
PropertyChange(Id(1), PropertyTag(3), JsonPrimitive(0u)),
ModifierChange(Id(1)),
ChildrenChange.Add(Id.Root, ChildrenTag.Root, Id(1), 0),
),
Expand Down

This file was deleted.

Loading

0 comments on commit 2b6027c

Please sign in to comment.